Merge branch 'main' into test-case-verbosity
This commit is contained in:
commit
6bae32378e
|
@ -23,6 +23,11 @@ afc607cfd81458d4e4f3b1f3cf8cc931b933907e
|
||||||
5f95dce95602921a70bfbc7d8de2f7712c5e4505
|
5f95dce95602921a70bfbc7d8de2f7712c5e4505
|
||||||
# ran pyupgrade-docs again
|
# ran pyupgrade-docs again
|
||||||
75d0b899bbb56d6849e9d69d83a9426ed3f43f8b
|
75d0b899bbb56d6849e9d69d83a9426ed3f43f8b
|
||||||
|
|
||||||
# move argument parser to own file
|
# move argument parser to own file
|
||||||
c9df77cbd6a365dcb73c39618e4842711817e871
|
c9df77cbd6a365dcb73c39618e4842711817e871
|
||||||
|
# Replace reorder-python-imports by isort due to black incompatibility (#11896)
|
||||||
|
8b54596639f41dfac070030ef20394b9001fe63c
|
||||||
|
# Run blacken-docs with black's 2024's style
|
||||||
|
4546d5445aaefe6a03957db028c263521dfb5c4b
|
||||||
|
# Migration to ruff / ruff format
|
||||||
|
4588653b2497ed25976b7aaff225b889fb476756
|
|
@ -26,7 +26,7 @@ jobs:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v2.0.0
|
uses: hynek/build-and-inspect-python-package@v2.0.1
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
if: github.repository == 'pytest-dev/pytest'
|
if: github.repository == 'pytest-dev/pytest'
|
||||||
|
@ -72,6 +72,12 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Download Package
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Packages
|
||||||
|
path: dist
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- name: Build and Check Package
|
- name: Build and Check Package
|
||||||
uses: hynek/build-and-inspect-python-package@v2.0.0
|
uses: hynek/build-and-inspect-python-package@v2.0.1
|
||||||
|
|
||||||
build:
|
build:
|
||||||
needs: [package]
|
needs: [package]
|
||||||
|
@ -205,7 +205,7 @@ jobs:
|
||||||
|
|
||||||
- name: Upload coverage to Codecov
|
- name: Upload coverage to Codecov
|
||||||
if: "matrix.use_coverage"
|
if: "matrix.use_coverage"
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v4
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
fail_ci_if_error: true
|
fail_ci_if_error: true
|
||||||
|
|
|
@ -30,7 +30,7 @@ jobs:
|
||||||
python-version: "3.11"
|
python-version: "3.11"
|
||||||
cache: pip
|
cache: pip
|
||||||
- name: requests-cache
|
- name: requests-cache
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pytest-plugin-list/
|
path: ~/.cache/pytest-plugin-list/
|
||||||
key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well
|
key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well
|
||||||
|
@ -46,7 +46,7 @@ jobs:
|
||||||
run: python scripts/update-plugin-list.py
|
run: python scripts/update-plugin-list.py
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
|
uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50
|
||||||
with:
|
with:
|
||||||
commit-message: '[automated] Update plugin list'
|
commit-message: '[automated] Update plugin list'
|
||||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: 23.12.1
|
rev: "v0.2.2"
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: ruff
|
||||||
args: [--safe, --quiet]
|
args: ["--fix"]
|
||||||
- repo: https://github.com/asottile/blacken-docs
|
- id: ruff-format
|
||||||
rev: 1.16.0
|
|
||||||
hooks:
|
|
||||||
- id: blacken-docs
|
|
||||||
additional_dependencies: [black==23.7.0]
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v4.5.0
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -20,37 +16,11 @@ repos:
|
||||||
- id: debug-statements
|
- id: debug-statements
|
||||||
exclude: _pytest/(debugging|hookspec).py
|
exclude: _pytest/(debugging|hookspec).py
|
||||||
language_version: python3
|
language_version: python3
|
||||||
- repo: https://github.com/PyCQA/autoflake
|
- repo: https://github.com/adamchainz/blacken-docs
|
||||||
rev: v2.2.1
|
rev: 1.16.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: autoflake
|
- id: blacken-docs
|
||||||
name: autoflake
|
additional_dependencies: [black==24.1.1]
|
||||||
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
|
|
||||||
language: python
|
|
||||||
files: \.py$
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
|
||||||
rev: 7.0.0
|
|
||||||
hooks:
|
|
||||||
- id: flake8
|
|
||||||
language_version: python3
|
|
||||||
additional_dependencies:
|
|
||||||
- flake8-typing-imports==1.12.0
|
|
||||||
- flake8-docstrings==1.5.0
|
|
||||||
- repo: https://github.com/asottile/reorder-python-imports
|
|
||||||
rev: v3.12.0
|
|
||||||
hooks:
|
|
||||||
- id: reorder-python-imports
|
|
||||||
args: ['--application-directories=.:src', --py38-plus]
|
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
|
||||||
rev: v3.15.0
|
|
||||||
hooks:
|
|
||||||
- id: pyupgrade
|
|
||||||
args: [--py38-plus]
|
|
||||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
|
||||||
rev: v2.5.0
|
|
||||||
hooks:
|
|
||||||
- id: setup-cfg-fmt
|
|
||||||
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
|
||||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
rev: v1.10.0
|
rev: v1.10.0
|
||||||
hooks:
|
hooks:
|
||||||
|
@ -64,6 +34,7 @@ repos:
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- iniconfig>=1.1.0
|
- iniconfig>=1.1.0
|
||||||
- attrs>=19.2.0
|
- attrs>=19.2.0
|
||||||
|
- pluggy>=1.4.0
|
||||||
- packaging
|
- packaging
|
||||||
- tomli
|
- tomli
|
||||||
- types-pkg_resources
|
- types-pkg_resources
|
||||||
|
@ -71,6 +42,12 @@ repos:
|
||||||
# for mypy running on python>=3.11 since exceptiongroup is only a dependency
|
# for mypy running on python>=3.11 since exceptiongroup is only a dependency
|
||||||
# on <3.11
|
# on <3.11
|
||||||
- exceptiongroup>=1.0.0rc8
|
- exceptiongroup>=1.0.0rc8
|
||||||
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||||
|
rev: "1.7.0"
|
||||||
|
hooks:
|
||||||
|
- id: pyproject-fmt
|
||||||
|
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
|
||||||
|
additional_dependencies: ["tox>=4.9"]
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
- id: rst
|
- id: rst
|
||||||
|
|
7
AUTHORS
7
AUTHORS
|
@ -56,6 +56,7 @@ Babak Keyvani
|
||||||
Barney Gale
|
Barney Gale
|
||||||
Ben Brown
|
Ben Brown
|
||||||
Ben Gartner
|
Ben Gartner
|
||||||
|
Ben Leith
|
||||||
Ben Webb
|
Ben Webb
|
||||||
Benjamin Peterson
|
Benjamin Peterson
|
||||||
Benjamin Schubert
|
Benjamin Schubert
|
||||||
|
@ -93,6 +94,7 @@ Christopher Dignam
|
||||||
Christopher Gilling
|
Christopher Gilling
|
||||||
Claire Cecil
|
Claire Cecil
|
||||||
Claudio Madotto
|
Claudio Madotto
|
||||||
|
Clément M.T. Robert
|
||||||
CrazyMerlyn
|
CrazyMerlyn
|
||||||
Cristian Vera
|
Cristian Vera
|
||||||
Cyrus Maden
|
Cyrus Maden
|
||||||
|
@ -126,6 +128,8 @@ Edison Gustavo Muenz
|
||||||
Edoardo Batini
|
Edoardo Batini
|
||||||
Edson Tadeu M. Manoel
|
Edson Tadeu M. Manoel
|
||||||
Eduardo Schettino
|
Eduardo Schettino
|
||||||
|
Edward Haigh
|
||||||
|
Eero Vaher
|
||||||
Eli Boyarski
|
Eli Boyarski
|
||||||
Elizaveta Shashkova
|
Elizaveta Shashkova
|
||||||
Éloi Rivard
|
Éloi Rivard
|
||||||
|
@ -141,6 +145,7 @@ Evgeny Seliverstov
|
||||||
Fabian Sturm
|
Fabian Sturm
|
||||||
Fabien Zarifian
|
Fabien Zarifian
|
||||||
Fabio Zadrozny
|
Fabio Zadrozny
|
||||||
|
faph
|
||||||
Felix Hofstätter
|
Felix Hofstätter
|
||||||
Felix Nieuwenhuizen
|
Felix Nieuwenhuizen
|
||||||
Feng Ma
|
Feng Ma
|
||||||
|
@ -339,6 +344,7 @@ Ronny Pfannschmidt
|
||||||
Ross Lawley
|
Ross Lawley
|
||||||
Ruaridh Williamson
|
Ruaridh Williamson
|
||||||
Russel Winder
|
Russel Winder
|
||||||
|
Russell Martin
|
||||||
Ryan Puddephatt
|
Ryan Puddephatt
|
||||||
Ryan Wooden
|
Ryan Wooden
|
||||||
Sadra Barikbin
|
Sadra Barikbin
|
||||||
|
@ -413,6 +419,7 @@ Vivaan Verma
|
||||||
Vlad Dragos
|
Vlad Dragos
|
||||||
Vlad Radziuk
|
Vlad Radziuk
|
||||||
Vladyslav Rachek
|
Vladyslav Rachek
|
||||||
|
Volodymyr Kochetkov
|
||||||
Volodymyr Piskun
|
Volodymyr Piskun
|
||||||
Wei Lin
|
Wei Lin
|
||||||
Wil Cooley
|
Wil Cooley
|
||||||
|
|
|
@ -27,9 +27,6 @@
|
||||||
:target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main
|
:target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main
|
||||||
:alt: pre-commit.ci status
|
:alt: pre-commit.ci status
|
||||||
|
|
||||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
|
||||||
:target: https://github.com/psf/black
|
|
||||||
|
|
||||||
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
|
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
|
||||||
:target: https://www.codetriage.com/pytest-dev/pytest
|
:target: https://www.codetriage.com/pytest-dev/pytest
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,6 @@ members of the `contributors team`_ interested in receiving funding.
|
||||||
|
|
||||||
The current list of contributors receiving funding are:
|
The current list of contributors receiving funding are:
|
||||||
|
|
||||||
* `@asottile`_
|
|
||||||
* `@nicoddemus`_
|
* `@nicoddemus`_
|
||||||
* `@The-Compiler`_
|
* `@The-Compiler`_
|
||||||
|
|
||||||
|
@ -55,6 +54,5 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the
|
||||||
.. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members
|
.. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members
|
||||||
.. _`agreement`: https://tidelift.com/docs/lifting/agreement
|
.. _`agreement`: https://tidelift.com/docs/lifting/agreement
|
||||||
|
|
||||||
.. _`@asottile`: https://github.com/asottile
|
|
||||||
.. _`@nicoddemus`: https://github.com/nicoddemus
|
.. _`@nicoddemus`: https://github.com/nicoddemus
|
||||||
.. _`@The-Compiler`: https://github.com/The-Compiler
|
.. _`@The-Compiler`: https://github.com/The-Compiler
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import cProfile
|
import cProfile
|
||||||
import pytest # NOQA
|
|
||||||
import pstats
|
import pstats
|
||||||
|
|
||||||
|
import pytest # noqa: F401
|
||||||
|
|
||||||
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
|
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
|
||||||
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
|
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
|
||||||
p = pstats.Stats("prof")
|
p = pstats.Stats("prof")
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
# FastFilesCompleter 0.7383 1.0760
|
# FastFilesCompleter 0.7383 1.0760
|
||||||
import timeit
|
import timeit
|
||||||
|
|
||||||
|
|
||||||
imports = [
|
imports = [
|
||||||
"from argcomplete.completers import FilesCompleter as completer",
|
"from argcomplete.completers import FilesCompleter as completer",
|
||||||
"from _pytest._argcomplete import FastFilesCompleter as completer",
|
"from _pytest._argcomplete import FastFilesCompleter as completer",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
SKIP = True
|
SKIP = True
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
from unittest import TestCase # noqa: F401
|
from unittest import TestCase # noqa: F401
|
||||||
|
|
||||||
|
|
||||||
for i in range(15000):
|
for i in range(15000):
|
||||||
exec(
|
exec(
|
||||||
f"""
|
f"""
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
:func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`.
|
||||||
|
Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion).
|
||||||
|
While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing.
|
|
@ -1,5 +0,0 @@
|
||||||
Improvements to how ``-r`` for xfailures and xpasses:
|
|
||||||
|
|
||||||
* Report tracebacks for xfailures when ``-rx`` is set.
|
|
||||||
* Report captured output for xpasses when ``-rX`` is set.
|
|
||||||
* For xpasses, add ``-`` in summary between test name and reason, to match how xfail is displayed.
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
|
||||||
|
as the relative directory.
|
||||||
|
|
||||||
|
Previoulsy this would raise an :class:`AssertionError`.
|
|
@ -1 +0,0 @@
|
||||||
Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.
|
|
|
@ -1,2 +0,0 @@
|
||||||
Fixed ``IndexError: string index out of range`` crash in ``if highlighted[-1] == "\n" and source[-1] != "\n"``.
|
|
||||||
This bug was introduced in pytest 8.0.0rc1.
|
|
|
@ -0,0 +1 @@
|
||||||
|
Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail.
|
|
@ -1,2 +1,2 @@
|
||||||
Added the :func:`iterparents() <_pytest.nodes.Node.iterparents>` helper method on nodes.
|
Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes.
|
||||||
It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list.
|
It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12.
|
|
@ -0,0 +1 @@
|
||||||
|
Fix collection on Windows where initial paths contain the short version of a path (for example ``c:\PROGRA~1\tests``).
|
|
@ -0,0 +1 @@
|
||||||
|
Fix an ``IndexError`` crash raising from ``getstatementrange_ast``.
|
|
@ -0,0 +1 @@
|
||||||
|
In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``.
|
|
@ -0,0 +1,3 @@
|
||||||
|
Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``.
|
||||||
|
|
||||||
|
Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging.
|
|
@ -0,0 +1 @@
|
||||||
|
Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed.
|
|
@ -0,0 +1 @@
|
||||||
|
Fix the ``stacklevel`` used when warning about marks used on fixtures.
|
|
@ -0,0 +1 @@
|
||||||
|
Reverted a fix to `--maxfail` handling in pytest 8.0.0 because it caused a regression in pytest-xdist whereby session fixture teardowns may get executed multiple times when the max-fails is reached.
|
|
@ -5,11 +5,10 @@
|
||||||
<div id="searchbox" style="display: none" role="search">
|
<div id="searchbox" style="display: none" role="search">
|
||||||
<div class="searchformwrapper">
|
<div class="searchformwrapper">
|
||||||
<form class="search" action="{{ pathto('search') }}" method="get">
|
<form class="search" action="{{ pathto('search') }}" method="get">
|
||||||
<input type="text" name="q" aria-labelledby="searchlabel"
|
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||||
placeholder="Search"/>
|
|
||||||
<input type="submit" value="{{ _('Go') }}" />
|
<input type="submit" value="{{ _('Go') }}" />
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">$('#searchbox').show(0);</script>
|
<script>document.getElementById('searchbox').style.display = "block"</script>
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
|
|
|
@ -6,6 +6,9 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-8.0.1
|
||||||
|
release-8.0.0
|
||||||
|
release-8.0.0rc2
|
||||||
release-8.0.0rc1
|
release-8.0.0rc1
|
||||||
release-7.4.4
|
release-7.4.4
|
||||||
release-7.4.3
|
release-7.4.3
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
pytest-8.0.0
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
The pytest team is proud to announce the 8.0.0 release!
|
||||||
|
|
||||||
|
This release contains new features, improvements, bug fixes, and breaking changes, so users
|
||||||
|
are encouraged to take a look at the CHANGELOG carefully:
|
||||||
|
|
||||||
|
https://docs.pytest.org/en/stable/changelog.html
|
||||||
|
|
||||||
|
For complete documentation, please visit:
|
||||||
|
|
||||||
|
https://docs.pytest.org/en/stable/
|
||||||
|
|
||||||
|
As usual, you can upgrade from PyPI via:
|
||||||
|
|
||||||
|
pip install -U pytest
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,32 @@
|
||||||
|
pytest-8.0.0rc2
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
The pytest team is proud to announce the 8.0.0rc2 prerelease!
|
||||||
|
|
||||||
|
This is a prerelease, not intended for production use, but to test the upcoming features and improvements
|
||||||
|
in order to catch any major problems before the final version is released to the major public.
|
||||||
|
|
||||||
|
We appreciate your help testing this out before the final release, making sure to report any
|
||||||
|
regressions to our issue tracker:
|
||||||
|
|
||||||
|
https://github.com/pytest-dev/pytest/issues
|
||||||
|
|
||||||
|
When doing so, please include the string ``[prerelease]`` in the title.
|
||||||
|
|
||||||
|
You can upgrade from PyPI via:
|
||||||
|
|
||||||
|
pip install pytest==8.0.0rc2
|
||||||
|
|
||||||
|
Users are encouraged to take a look at the CHANGELOG carefully:
|
||||||
|
|
||||||
|
https://docs.pytest.org/en/release-8.0.0rc2/changelog.html
|
||||||
|
|
||||||
|
Thanks to all the contributors to this release:
|
||||||
|
|
||||||
|
* Ben Brown
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,21 @@
|
||||||
|
pytest-8.0.1
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 8.0.1 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Clément Robert
|
||||||
|
* Pierre Sassoulas
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
cachedir: .pytest_cache
|
cachedir: .pytest_cache
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 0 items
|
collected 0 items
|
||||||
cache -- .../_pytest/cacheprovider.py:526
|
cache -- .../_pytest/cacheprovider.py:527
|
||||||
Return a cache object that can persist state between testing sessions.
|
Return a cache object that can persist state between testing sessions.
|
||||||
|
|
||||||
cache.get(key, default)
|
cache.get(key, default)
|
||||||
|
@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
|
|
||||||
Values can be any object handled by the json stdlib module.
|
Values can be any object handled by the json stdlib module.
|
||||||
|
|
||||||
capsysbinary -- .../_pytest/capture.py:1008
|
capsysbinary -- .../_pytest/capture.py:1007
|
||||||
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||||
|
|
||||||
The captured output is made available via ``capsysbinary.readouterr()``
|
The captured output is made available via ``capsysbinary.readouterr()``
|
||||||
|
@ -43,7 +43,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_output(capsysbinary):
|
def test_output(capsysbinary):
|
||||||
|
@ -51,7 +50,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
captured = capsysbinary.readouterr()
|
captured = capsysbinary.readouterr()
|
||||||
assert captured.out == b"hello\n"
|
assert captured.out == b"hello\n"
|
||||||
|
|
||||||
capfd -- .../_pytest/capture.py:1036
|
capfd -- .../_pytest/capture.py:1034
|
||||||
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||||
|
|
||||||
The captured output is made available via ``capfd.readouterr()`` method
|
The captured output is made available via ``capfd.readouterr()`` method
|
||||||
|
@ -61,7 +60,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_system_echo(capfd):
|
def test_system_echo(capfd):
|
||||||
|
@ -69,7 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
captured = capfd.readouterr()
|
captured = capfd.readouterr()
|
||||||
assert captured.out == "hello\n"
|
assert captured.out == "hello\n"
|
||||||
|
|
||||||
capfdbinary -- .../_pytest/capture.py:1064
|
capfdbinary -- .../_pytest/capture.py:1061
|
||||||
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||||
|
|
||||||
The captured output is made available via ``capfd.readouterr()`` method
|
The captured output is made available via ``capfd.readouterr()`` method
|
||||||
|
@ -79,7 +77,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_system_echo(capfdbinary):
|
def test_system_echo(capfdbinary):
|
||||||
|
@ -97,7 +94,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_output(capsys):
|
def test_output(capsys):
|
||||||
|
@ -105,7 +101,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert captured.out == "hello\n"
|
assert captured.out == "hello\n"
|
||||||
|
|
||||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:743
|
doctest_namespace [session scope] -- .../_pytest/doctest.py:745
|
||||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||||
namespace of doctests.
|
namespace of doctests.
|
||||||
|
|
||||||
|
@ -119,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
|
|
||||||
For more details: :ref:`doctest_namespace`.
|
For more details: :ref:`doctest_namespace`.
|
||||||
|
|
||||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1365
|
pytestconfig [session scope] -- .../_pytest/fixtures.py:1354
|
||||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||||
object.
|
object.
|
||||||
|
|
||||||
|
@ -129,7 +125,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
if pytestconfig.getoption("verbose") > 0:
|
if pytestconfig.getoption("verbose") > 0:
|
||||||
...
|
...
|
||||||
|
|
||||||
record_property -- .../_pytest/junitxml.py:282
|
record_property -- .../_pytest/junitxml.py:283
|
||||||
Add extra properties to the calling test.
|
Add extra properties to the calling test.
|
||||||
|
|
||||||
User properties become part of the test report and are available to the
|
User properties become part of the test report and are available to the
|
||||||
|
@ -143,13 +139,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
def test_function(record_property):
|
def test_function(record_property):
|
||||||
record_property("example_key", 1)
|
record_property("example_key", 1)
|
||||||
|
|
||||||
record_xml_attribute -- .../_pytest/junitxml.py:305
|
record_xml_attribute -- .../_pytest/junitxml.py:306
|
||||||
Add extra xml attributes to the tag for the calling test.
|
Add extra xml attributes to the tag for the calling test.
|
||||||
|
|
||||||
The fixture is callable with ``name, value``. The value is
|
The fixture is callable with ``name, value``. The value is
|
||||||
automatically XML-encoded.
|
automatically XML-encoded.
|
||||||
|
|
||||||
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:343
|
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:344
|
||||||
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
|
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
|
||||||
|
|
||||||
This is suitable to writing global information regarding the entire test
|
This is suitable to writing global information regarding the entire test
|
||||||
|
@ -174,10 +170,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
||||||
:issue:`7767` for details.
|
:issue:`7767` for details.
|
||||||
|
|
||||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:300
|
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
|
||||||
Return a :class:`pytest.TempdirFactory` instance for the test session.
|
Return a :class:`pytest.TempdirFactory` instance for the test session.
|
||||||
|
|
||||||
tmpdir -- .../_pytest/legacypath.py:307
|
tmpdir -- .../_pytest/legacypath.py:309
|
||||||
Return a temporary directory path object which is unique to each test
|
Return a temporary directory path object which is unique to each test
|
||||||
function invocation, created as a sub directory of the base temporary
|
function invocation, created as a sub directory of the base temporary
|
||||||
directory.
|
directory.
|
||||||
|
@ -196,7 +192,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
|
|
||||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||||
|
|
||||||
caplog -- .../_pytest/logging.py:593
|
caplog -- .../_pytest/logging.py:594
|
||||||
Access and control log capturing.
|
Access and control log capturing.
|
||||||
|
|
||||||
Captured logs are available through the following properties/methods::
|
Captured logs are available through the following properties/methods::
|
||||||
|
@ -207,7 +203,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
||||||
* caplog.clear() -> clear captured records and formatted log output string
|
* caplog.clear() -> clear captured records and formatted log output string
|
||||||
|
|
||||||
monkeypatch -- .../_pytest/monkeypatch.py:30
|
monkeypatch -- .../_pytest/monkeypatch.py:32
|
||||||
A convenient fixture for monkey-patching.
|
A convenient fixture for monkey-patching.
|
||||||
|
|
||||||
The fixture provides these methods to modify objects, dictionaries, or
|
The fixture provides these methods to modify objects, dictionaries, or
|
||||||
|
@ -231,16 +227,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
To undo modifications done by the fixture in a contained scope,
|
To undo modifications done by the fixture in a contained scope,
|
||||||
use :meth:`context() <pytest.MonkeyPatch.context>`.
|
use :meth:`context() <pytest.MonkeyPatch.context>`.
|
||||||
|
|
||||||
recwarn -- .../_pytest/recwarn.py:30
|
recwarn -- .../_pytest/recwarn.py:32
|
||||||
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
|
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
|
||||||
|
|
||||||
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
||||||
on warning categories.
|
on warning categories.
|
||||||
|
|
||||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:239
|
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:241
|
||||||
Return a :class:`pytest.TempPathFactory` instance for the test session.
|
Return a :class:`pytest.TempPathFactory` instance for the test session.
|
||||||
|
|
||||||
tmp_path -- .../_pytest/tmpdir.py:254
|
tmp_path -- .../_pytest/tmpdir.py:256
|
||||||
Return a temporary directory path object which is unique to each test
|
Return a temporary directory path object which is unique to each test
|
||||||
function invocation, created as a sub directory of the base temporary
|
function invocation, created as a sub directory of the base temporary
|
||||||
directory.
|
directory.
|
||||||
|
|
|
@ -28,6 +28,75 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 8.0.1 (2024-02-16)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#11875 <https://github.com/pytest-dev/pytest/issues/11875>`_: Correctly handle errors from :func:`getpass.getuser` in Python 3.13.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11879 <https://github.com/pytest-dev/pytest/issues/11879>`_: Fix an edge case where ``ExceptionInfo._stringify_exception`` could crash :func:`pytest.raises`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11906 <https://github.com/pytest-dev/pytest/issues/11906>`_: Fix regression with :func:`pytest.warns` using custom warning subclasses which have more than one parameter in their `__init__`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11907 <https://github.com/pytest-dev/pytest/issues/11907>`_: Fix a regression in pytest 8.0.0 whereby calling :func:`pytest.skip` and similar control-flow exceptions within a :func:`pytest.warns()` block would get suppressed instead of propagating.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11929 <https://github.com/pytest-dev/pytest/issues/11929>`_: Fix a regression in pytest 8.0.0 whereby autouse fixtures defined in a module get ignored by the doctests in the module.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11937 <https://github.com/pytest-dev/pytest/issues/11937>`_: Fix a regression in pytest 8.0.0 whereby items would be collected in reverse order in some circumstances.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 8.0.0 (2024-01-27)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#11842 <https://github.com/pytest-dev/pytest/issues/11842>`_: Properly escape the ``reason`` of a :ref:`skip <pytest.mark.skip ref>` mark when writing JUnit XML files.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11861 <https://github.com/pytest-dev/pytest/issues/11861>`_: Avoid microsecond exceeds ``1_000_000`` when using ``log-date-format`` with ``%f`` specifier, which might cause the test suite to crash.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 8.0.0rc2 (2024-01-17)
|
||||||
|
============================
|
||||||
|
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#11233 <https://github.com/pytest-dev/pytest/issues/11233>`_: Improvements to ``-r`` for xfailures and xpasses:
|
||||||
|
|
||||||
|
* Report tracebacks for xfailures when ``-rx`` is set.
|
||||||
|
* Report captured output for xpasses when ``-rX`` is set.
|
||||||
|
* For xpasses, add ``-`` in summary between test name and reason, to match how xfail is displayed.
|
||||||
|
|
||||||
|
- `#11825 <https://github.com/pytest-dev/pytest/issues/11825>`_: The :hook:`pytest_plugin_registered` hook has a new ``plugin_name`` parameter containing the name by which ``plugin`` is registered.
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#11706 <https://github.com/pytest-dev/pytest/issues/11706>`_: Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.
|
||||||
|
|
||||||
|
NOTE: This change was reverted in pytest 8.0.2 to fix a `regression <https://github.com/pytest-dev/pytest-xdist/issues/1024>`_ it caused in pytest-xdist.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11758 <https://github.com/pytest-dev/pytest/issues/11758>`_: Fixed ``IndexError: string index out of range`` crash in ``if highlighted[-1] == "\n" and source[-1] != "\n"``.
|
||||||
|
This bug was introduced in pytest 8.0.0rc1.
|
||||||
|
|
||||||
|
|
||||||
|
- `#9765 <https://github.com/pytest-dev/pytest/issues/9765>`_, `#11816 <https://github.com/pytest-dev/pytest/issues/11816>`_: Fixed a frustrating bug that afflicted some users with the only error being ``assert mod not in mods``. The issue was caused by the fact that ``str(Path(mod))`` and ``mod.__file__`` don't necessarily produce the same string, and was being erroneously used interchangably in some places in the code.
|
||||||
|
|
||||||
|
This fix also broke the internal API of ``PytestPluginManager.consider_conftest`` by introducing a new parameter -- we mention this in case it is being used by external code, even if marked as *private*.
|
||||||
|
|
||||||
|
|
||||||
pytest 8.0.0rc1 (2023-12-30)
|
pytest 8.0.0rc1 (2023-12-30)
|
||||||
============================
|
============================
|
||||||
|
|
||||||
|
@ -223,6 +292,10 @@ These are breaking changes where deprecation was not possible.
|
||||||
therefore fail on the newly-re-emitted warnings.
|
therefore fail on the newly-re-emitted warnings.
|
||||||
|
|
||||||
|
|
||||||
|
- The internal ``FixtureManager.getfixtureclosure`` method has changed. Plugins which use this method or
|
||||||
|
which subclass ``FixtureManager`` and overwrite that method will need to adapt to the change.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Deprecations
|
Deprecations
|
||||||
------------
|
------------
|
||||||
|
|
|
@ -23,6 +23,7 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from _pytest import __version__ as version
|
from _pytest import __version__ as version
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import sphinx.application
|
import sphinx.application
|
||||||
|
|
||||||
|
@ -440,9 +441,10 @@ intersphinx_mapping = {
|
||||||
|
|
||||||
def configure_logging(app: "sphinx.application.Sphinx") -> None:
|
def configure_logging(app: "sphinx.application.Sphinx") -> None:
|
||||||
"""Configure Sphinx's WarningHandler to handle (expected) missing include."""
|
"""Configure Sphinx's WarningHandler to handle (expected) missing include."""
|
||||||
import sphinx.util.logging
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
import sphinx.util.logging
|
||||||
|
|
||||||
class WarnLogFilter(logging.Filter):
|
class WarnLogFilter(logging.Filter):
|
||||||
def filter(self, record: logging.LogRecord) -> bool:
|
def filter(self, record: logging.LogRecord) -> bool:
|
||||||
"""Ignore warnings about missing include with "only" directive.
|
"""Ignore warnings about missing include with "only" directive.
|
||||||
|
|
|
@ -33,13 +33,11 @@ have been available since years and should be used instead.
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.tryfirst
|
@pytest.mark.tryfirst
|
||||||
def pytest_runtest_call():
|
def pytest_runtest_call(): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
# or
|
# or
|
||||||
def pytest_runtest_call():
|
def pytest_runtest_call(): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
pytest_runtest_call.tryfirst = True
|
pytest_runtest_call.tryfirst = True
|
||||||
|
@ -49,8 +47,7 @@ should be changed to:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.hookimpl(tryfirst=True)
|
@pytest.hookimpl(tryfirst=True)
|
||||||
def pytest_runtest_call():
|
def pytest_runtest_call(): ...
|
||||||
...
|
|
||||||
|
|
||||||
Changed ``hookimpl`` attributes:
|
Changed ``hookimpl`` attributes:
|
||||||
|
|
||||||
|
@ -146,8 +143,7 @@ Applying a mark to a fixture function never had any effect, but it is a common u
|
||||||
|
|
||||||
@pytest.mark.usefixtures("clean_database")
|
@pytest.mark.usefixtures("clean_database")
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def user() -> User:
|
def user() -> User: ...
|
||||||
...
|
|
||||||
|
|
||||||
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
|
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
|
||||||
|
|
||||||
|
@ -308,11 +304,9 @@ they are in fact part of the ``nose`` support.
|
||||||
def teardown(self):
|
def teardown(self):
|
||||||
self.resource.close()
|
self.resource.close()
|
||||||
|
|
||||||
def test_foo(self):
|
def test_foo(self): ...
|
||||||
...
|
|
||||||
|
|
||||||
def test_bar(self):
|
def test_bar(self): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -327,11 +321,9 @@ Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`x
|
||||||
def teardown_method(self):
|
def teardown_method(self):
|
||||||
self.resource.close()
|
self.resource.close()
|
||||||
|
|
||||||
def test_foo(self):
|
def test_foo(self): ...
|
||||||
...
|
|
||||||
|
|
||||||
def test_bar(self):
|
def test_bar(self): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
This is easy to do in an entire code base by doing a simple find/replace.
|
This is easy to do in an entire code base by doing a simple find/replace.
|
||||||
|
@ -346,17 +338,14 @@ Code using `@with_setup <with-setup-nose>`_ such as this:
|
||||||
from nose.tools import with_setup
|
from nose.tools import with_setup
|
||||||
|
|
||||||
|
|
||||||
def setup_some_resource():
|
def setup_some_resource(): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def teardown_some_resource():
|
def teardown_some_resource(): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@with_setup(setup_some_resource, teardown_some_resource)
|
@with_setup(setup_some_resource, teardown_some_resource)
|
||||||
def test_foo():
|
def test_foo(): ...
|
||||||
...
|
|
||||||
|
|
||||||
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
|
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
|
||||||
|
|
||||||
|
@ -365,12 +354,10 @@ Will also need to be ported to a supported pytest style. One way to do it is usi
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def setup_some_resource():
|
def setup_some_resource(): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def teardown_some_resource():
|
def teardown_some_resource(): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -380,8 +367,7 @@ Will also need to be ported to a supported pytest style. One way to do it is usi
|
||||||
teardown_some_resource()
|
teardown_some_resource()
|
||||||
|
|
||||||
|
|
||||||
def test_foo(some_resource):
|
def test_foo(some_resource): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
||||||
|
@ -500,8 +486,7 @@ Implement the :hook:`pytest_load_initial_conftests` hook instead.
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None:
|
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
# becomes:
|
# becomes:
|
||||||
|
@ -509,8 +494,7 @@ Implement the :hook:`pytest_load_initial_conftests` hook instead.
|
||||||
|
|
||||||
def pytest_load_initial_conftests(
|
def pytest_load_initial_conftests(
|
||||||
early_config: Config, parser: Parser, args: List[str]
|
early_config: Config, parser: Parser, args: List[str]
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
Collection changes in pytest 8
|
Collection changes in pytest 8
|
||||||
|
@ -925,8 +909,7 @@ Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated
|
||||||
(50, 500),
|
(50, 500),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_foo(a, b):
|
def test_foo(a, b): ...
|
||||||
...
|
|
||||||
|
|
||||||
This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization
|
This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization
|
||||||
call.
|
call.
|
||||||
|
@ -949,8 +932,7 @@ To update the code, use ``pytest.param``:
|
||||||
(50, 500),
|
(50, 500),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_foo(a, b):
|
def test_foo(a, b): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
.. _pytest_funcarg__ prefix deprecated:
|
.. _pytest_funcarg__ prefix deprecated:
|
||||||
|
@ -1101,15 +1083,13 @@ This is just a matter of renaming the fixture as the API is the same:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_foo(record_xml_property):
|
def test_foo(record_xml_property): ...
|
||||||
...
|
|
||||||
|
|
||||||
Change to:
|
Change to:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_foo(record_property):
|
def test_foo(record_property): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
.. _passing command-line string to pytest.main deprecated:
|
.. _passing command-line string to pytest.main deprecated:
|
||||||
|
@ -1271,8 +1251,7 @@ Example of usage:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
class MySymbol:
|
class MySymbol: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_namespace():
|
def pytest_namespace():
|
||||||
|
|
|
@ -172,7 +172,7 @@ class TestRaises:
|
||||||
raise ValueError("demo error")
|
raise ValueError("demo error")
|
||||||
|
|
||||||
def test_tupleerror(self):
|
def test_tupleerror(self):
|
||||||
a, b = [1] # NOQA
|
a, b = [1] # noqa: F841
|
||||||
|
|
||||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||||
items = [1, 2, 3]
|
items = [1, 2, 3]
|
||||||
|
@ -180,7 +180,7 @@ class TestRaises:
|
||||||
a, b = items.pop()
|
a, b = items.pop()
|
||||||
|
|
||||||
def test_some_error(self):
|
def test_some_error(self):
|
||||||
if namenotexi: # NOQA
|
if namenotexi: # noqa: F821
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def func1(self):
|
def func1(self):
|
||||||
|
|
|
@ -2,6 +2,7 @@ import os.path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
mydir = os.path.dirname(__file__)
|
mydir = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import os.path
|
import os.path
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
|
||||||
failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py")
|
failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py")
|
||||||
pytest_plugins = ("pytester",)
|
pytest_plugins = ("pytester",)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""Module containing a parametrized tests testing cross-python serialization
|
"""Module containing a parametrized tests testing cross-python serialization
|
||||||
via the pickle module."""
|
via the pickle module."""
|
||||||
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import textwrap
|
import textwrap
|
||||||
|
@ -32,14 +33,12 @@ class Python:
|
||||||
dumpfile = self.picklefile.with_name("dump.py")
|
dumpfile = self.picklefile.with_name("dump.py")
|
||||||
dumpfile.write_text(
|
dumpfile.write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
r"""
|
rf"""
|
||||||
import pickle
|
import pickle
|
||||||
f = open({!r}, 'wb')
|
f = open({str(self.picklefile)!r}, 'wb')
|
||||||
s = pickle.dump({!r}, f, protocol=2)
|
s = pickle.dump({obj!r}, f, protocol=2)
|
||||||
f.close()
|
f.close()
|
||||||
""".format(
|
"""
|
||||||
str(self.picklefile), obj
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
subprocess.run((self.pythonpath, str(dumpfile)), check=True)
|
subprocess.run((self.pythonpath, str(dumpfile)), check=True)
|
||||||
|
@ -48,17 +47,15 @@ class Python:
|
||||||
loadfile = self.picklefile.with_name("load.py")
|
loadfile = self.picklefile.with_name("load.py")
|
||||||
loadfile.write_text(
|
loadfile.write_text(
|
||||||
textwrap.dedent(
|
textwrap.dedent(
|
||||||
r"""
|
rf"""
|
||||||
import pickle
|
import pickle
|
||||||
f = open({!r}, 'rb')
|
f = open({str(self.picklefile)!r}, 'rb')
|
||||||
obj = pickle.load(f)
|
obj = pickle.load(f)
|
||||||
f.close()
|
f.close()
|
||||||
res = eval({!r})
|
res = eval({expression!r})
|
||||||
if not res:
|
if not res:
|
||||||
raise SystemExit(1)
|
raise SystemExit(1)
|
||||||
""".format(
|
"""
|
||||||
str(self.picklefile), expression
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
print(loadfile)
|
print(loadfile)
|
||||||
|
|
|
@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 8 items
|
collected 8 items
|
||||||
|
|
||||||
<Dir parametrize.rst-189>
|
<Dir parametrize.rst-193>
|
||||||
<Module test_time.py>
|
<Module test_time.py>
|
||||||
<Function test_timedistance_v0[a0-b0-expected0]>
|
<Function test_timedistance_v0[a0-b0-expected0]>
|
||||||
<Function test_timedistance_v0[a1-b1-expected1]>
|
<Function test_timedistance_v0[a1-b1-expected1]>
|
||||||
|
@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 4 items
|
collected 4 items
|
||||||
|
|
||||||
<Dir parametrize.rst-189>
|
<Dir parametrize.rst-193>
|
||||||
<Module test_scenarios.py>
|
<Module test_scenarios.py>
|
||||||
<Class TestSampleWithScenarios>
|
<Class TestSampleWithScenarios>
|
||||||
<Function test_demo1[basic]>
|
<Function test_demo1[basic]>
|
||||||
|
@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
<Dir parametrize.rst-189>
|
<Dir parametrize.rst-193>
|
||||||
<Module test_backends.py>
|
<Module test_backends.py>
|
||||||
<Function test_db_initialized[d1]>
|
<Function test_db_initialized[d1]>
|
||||||
<Function test_db_initialized[d2]>
|
<Function test_db_initialized[d2]>
|
||||||
|
@ -505,8 +505,8 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||||
. $ pytest -rs -q multipython.py
|
. $ pytest -rs -q multipython.py
|
||||||
ssssssssssss...ssssssssssss [100%]
|
ssssssssssss...ssssssssssss [100%]
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
SKIPPED [12] multipython.py:68: 'python3.9' not found
|
SKIPPED [12] multipython.py:65: 'python3.9' not found
|
||||||
SKIPPED [12] multipython.py:68: 'python3.11' not found
|
SKIPPED [12] multipython.py:65: 'python3.11' not found
|
||||||
3 passed, 24 skipped in 0.12s
|
3 passed, 24 skipped in 0.12s
|
||||||
|
|
||||||
Parametrization of optional implementations/imports
|
Parametrization of optional implementations/imports
|
||||||
|
|
|
@ -152,7 +152,7 @@ The test collection would look like this:
|
||||||
configfile: pytest.ini
|
configfile: pytest.ini
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
<Dir pythoncollection.rst-190>
|
<Dir pythoncollection.rst-194>
|
||||||
<Module check_myapp.py>
|
<Module check_myapp.py>
|
||||||
<Class CheckMyApp>
|
<Class CheckMyApp>
|
||||||
<Function simple_check>
|
<Function simple_check>
|
||||||
|
@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
|
||||||
configfile: pytest.ini
|
configfile: pytest.ini
|
||||||
collected 3 items
|
collected 3 items
|
||||||
|
|
||||||
<Dir pythoncollection.rst-190>
|
<Dir pythoncollection.rst-194>
|
||||||
<Dir CWD>
|
<Dir CWD>
|
||||||
<Module pythoncollection.py>
|
<Module pythoncollection.py>
|
||||||
<Function test_function>
|
<Function test_function>
|
||||||
|
|
|
@ -660,6 +660,31 @@ If we run this:
|
||||||
E assert 0
|
E assert 0
|
||||||
|
|
||||||
test_step.py:11: AssertionError
|
test_step.py:11: AssertionError
|
||||||
|
================================ XFAILURES =================================
|
||||||
|
______________________ TestUserHandling.test_deletion ______________________
|
||||||
|
|
||||||
|
item = <Function test_deletion>
|
||||||
|
|
||||||
|
def pytest_runtest_setup(item):
|
||||||
|
if "incremental" in item.keywords:
|
||||||
|
# retrieve the class name of the test
|
||||||
|
cls_name = str(item.cls)
|
||||||
|
# check if a previous test has failed for this class
|
||||||
|
if cls_name in _test_failed_incremental:
|
||||||
|
# retrieve the index of the test (if parametrize is used in combination with incremental)
|
||||||
|
parametrize_index = (
|
||||||
|
tuple(item.callspec.indices.values())
|
||||||
|
if hasattr(item, "callspec")
|
||||||
|
else ()
|
||||||
|
)
|
||||||
|
# retrieve the name of the first test function to fail for this class name and index
|
||||||
|
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
|
||||||
|
# if name found, test has failed for the combination of class name & test name
|
||||||
|
if test_name is not None:
|
||||||
|
> pytest.xfail(f"previous test failed ({test_name})")
|
||||||
|
E _pytest.outcomes.XFailed: previous test failed (test_modification)
|
||||||
|
|
||||||
|
conftest.py:47: XFailed
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
|
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
|
||||||
================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================
|
================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
xfail = pytest.mark.xfail
|
xfail = pytest.mark.xfail
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -99,8 +99,7 @@ sets. pytest-2.3 introduces a decorator for use on the factory itself:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.fixture(params=["mysql", "pg"])
|
@pytest.fixture(params=["mysql", "pg"])
|
||||||
def db(request):
|
def db(request): ... # use request.param
|
||||||
... # use request.param
|
|
||||||
|
|
||||||
Here the factory will be invoked twice (with the respective "mysql"
|
Here the factory will be invoked twice (with the respective "mysql"
|
||||||
and "pg" values set as ``request.param`` attributes) and all of
|
and "pg" values set as ``request.param`` attributes) and all of
|
||||||
|
@ -141,8 +140,7 @@ argument:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def db(request):
|
def db(request): ...
|
||||||
...
|
|
||||||
|
|
||||||
The name under which the funcarg resource can be requested is ``db``.
|
The name under which the funcarg resource can be requested is ``db``.
|
||||||
|
|
||||||
|
@ -151,8 +149,7 @@ aka:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def pytest_funcarg__db(request):
|
def pytest_funcarg__db(request): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
But it is then not possible to define scoping and parametrization.
|
But it is then not possible to define scoping and parametrization.
|
||||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 8.0.0rc1
|
pytest 8.0.1
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|
|
@ -227,8 +227,7 @@ to use strings:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif("sys.version_info >= (3,3)")
|
@pytest.mark.skipif("sys.version_info >= (3,3)")
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
During test function setup the skipif condition is evaluated by calling
|
During test function setup the skipif condition is evaluated by calling
|
||||||
``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains
|
``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains
|
||||||
|
@ -262,8 +261,7 @@ configuration value which you might have added:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.skipif("not config.getvalue('db')")
|
@pytest.mark.skipif("not config.getvalue('db')")
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
The equivalent with "boolean conditions" is:
|
The equivalent with "boolean conditions" is:
|
||||||
|
|
||||||
|
|
|
@ -154,7 +154,7 @@ method to test for exceptions returned as part of an :class:`ExceptionGroup`:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_exception_in_group():
|
def test_exception_in_group():
|
||||||
with pytest.raises(RuntimeError) as excinfo:
|
with pytest.raises(ExceptionGroup) as excinfo:
|
||||||
raise ExceptionGroup(
|
raise ExceptionGroup(
|
||||||
"Group message",
|
"Group message",
|
||||||
[
|
[
|
||||||
|
@ -176,7 +176,7 @@ exception at a specific level; exceptions contained directly in the top
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_exception_in_group_at_given_depth():
|
def test_exception_in_group_at_given_depth():
|
||||||
with pytest.raises(RuntimeError) as excinfo:
|
with pytest.raises(ExceptionGroup) as excinfo:
|
||||||
raise ExceptionGroup(
|
raise ExceptionGroup(
|
||||||
"Group message",
|
"Group message",
|
||||||
[
|
[
|
||||||
|
|
|
@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 12 items
|
collected 12 items
|
||||||
|
|
||||||
<Dir fixtures.rst-208>
|
<Dir fixtures.rst-212>
|
||||||
<Module test_anothersmtp.py>
|
<Module test_anothersmtp.py>
|
||||||
<Function test_showhelo[smtp.gmail.com]>
|
<Function test_showhelo[smtp.gmail.com]>
|
||||||
<Function test_showhelo[mail.python.org]>
|
<Function test_showhelo[mail.python.org]>
|
||||||
|
@ -1721,8 +1721,7 @@ You can specify multiple fixtures like this:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.usefixtures("cleandir", "anotherfixture")
|
@pytest.mark.usefixtures("cleandir", "anotherfixture")
|
||||||
def test():
|
def test(): ...
|
||||||
...
|
|
||||||
|
|
||||||
and you may specify fixture usage at the test module level using :globalvar:`pytestmark`:
|
and you may specify fixture usage at the test module level using :globalvar:`pytestmark`:
|
||||||
|
|
||||||
|
@ -1750,8 +1749,7 @@ into an ini-file:
|
||||||
|
|
||||||
@pytest.mark.usefixtures("my_other_fixture")
|
@pytest.mark.usefixtures("my_other_fixture")
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def my_fixture_that_sadly_wont_use_my_other_fixture():
|
def my_fixture_that_sadly_wont_use_my_other_fixture(): ...
|
||||||
...
|
|
||||||
|
|
||||||
This generates a deprecation warning, and will become an error in Pytest 8.
|
This generates a deprecation warning, and will become an error in Pytest 8.
|
||||||
|
|
||||||
|
|
|
@ -206,8 +206,9 @@ option names are:
|
||||||
* ``log_cli_date_format``
|
* ``log_cli_date_format``
|
||||||
|
|
||||||
If you need to record the whole test suite logging calls to a file, you can pass
|
If you need to record the whole test suite logging calls to a file, you can pass
|
||||||
``--log-file=/path/to/log/file``. This log file is opened in write mode which
|
``--log-file=/path/to/log/file``. This log file is opened in write mode by default which
|
||||||
means that it will be overwritten at each run tests session.
|
means that it will be overwritten at each run tests session.
|
||||||
|
If you'd like the file opened in append mode instead, then you can pass ``--log-file-mode=a``.
|
||||||
Note that relative paths for the log-file location, whether passed on the CLI or declared in a
|
Note that relative paths for the log-file location, whether passed on the CLI or declared in a
|
||||||
config file, are always resolved relative to the current working directory.
|
config file, are always resolved relative to the current working directory.
|
||||||
|
|
||||||
|
@ -223,12 +224,13 @@ All of the log file options can also be set in the configuration INI file. The
|
||||||
option names are:
|
option names are:
|
||||||
|
|
||||||
* ``log_file``
|
* ``log_file``
|
||||||
|
* ``log_file_mode``
|
||||||
* ``log_file_level``
|
* ``log_file_level``
|
||||||
* ``log_file_format``
|
* ``log_file_format``
|
||||||
* ``log_file_date_format``
|
* ``log_file_date_format``
|
||||||
|
|
||||||
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
|
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
|
||||||
is considered **experimental**.
|
is considered **experimental**. Note that ``set_log_path()`` respects the ``log_file_mode`` option.
|
||||||
|
|
||||||
.. _log_colors:
|
.. _log_colors:
|
||||||
|
|
||||||
|
|
|
@ -406,10 +406,19 @@ Example:
|
||||||
E assert 0
|
E assert 0
|
||||||
|
|
||||||
test_example.py:14: AssertionError
|
test_example.py:14: AssertionError
|
||||||
|
================================ XFAILURES =================================
|
||||||
|
________________________________ test_xfail ________________________________
|
||||||
|
|
||||||
|
def test_xfail():
|
||||||
|
> pytest.xfail("xfailing this test")
|
||||||
|
E _pytest.outcomes.XFailed: xfailing this test
|
||||||
|
|
||||||
|
test_example.py:26: XFailed
|
||||||
|
================================= XPASSES ==================================
|
||||||
========================= short test summary info ==========================
|
========================= short test summary info ==========================
|
||||||
SKIPPED [1] test_example.py:22: skipping this test
|
SKIPPED [1] test_example.py:22: skipping this test
|
||||||
XFAIL test_example.py::test_xfail - reason: xfailing this test
|
XFAIL test_example.py::test_xfail - reason: xfailing this test
|
||||||
XPASS test_example.py::test_xpass always xfail
|
XPASS test_example.py::test_xpass - always xfail
|
||||||
ERROR test_example.py::test_error - assert 0
|
ERROR test_example.py::test_error - assert 0
|
||||||
FAILED test_example.py::test_fail - assert 0
|
FAILED test_example.py::test_fail - assert 0
|
||||||
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
|
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===
|
||||||
|
|
|
@ -47,8 +47,7 @@ which may be passed an optional ``reason``:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.skip(reason="no way of currently testing this")
|
@pytest.mark.skip(reason="no way of currently testing this")
|
||||||
def test_the_unknown():
|
def test_the_unknown(): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
Alternatively, it is also possible to skip imperatively during test execution or setup
|
Alternatively, it is also possible to skip imperatively during test execution or setup
|
||||||
|
@ -93,8 +92,7 @@ when run on an interpreter earlier than Python3.10:
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
|
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
If the condition evaluates to ``True`` during collection, the test function will be skipped,
|
If the condition evaluates to ``True`` during collection, the test function will be skipped,
|
||||||
with the specified reason appearing in the summary when using ``-rs``.
|
with the specified reason appearing in the summary when using ``-rs``.
|
||||||
|
@ -112,8 +110,7 @@ You can share ``skipif`` markers between modules. Consider this test module:
|
||||||
|
|
||||||
|
|
||||||
@minversion
|
@minversion
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
You can import the marker and reuse it in another test module:
|
You can import the marker and reuse it in another test module:
|
||||||
|
|
||||||
|
@ -124,8 +121,7 @@ You can import the marker and reuse it in another test module:
|
||||||
|
|
||||||
|
|
||||||
@minversion
|
@minversion
|
||||||
def test_anotherfunction():
|
def test_anotherfunction(): ...
|
||||||
...
|
|
||||||
|
|
||||||
For larger test suites it's usually a good idea to have one file
|
For larger test suites it's usually a good idea to have one file
|
||||||
where you define the markers which you then consistently apply
|
where you define the markers which you then consistently apply
|
||||||
|
@ -232,8 +228,7 @@ expect a test to fail:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.xfail
|
@pytest.mark.xfail
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
This test will run but no traceback will be reported when it fails. Instead, terminal
|
This test will run but no traceback will be reported when it fails. Instead, terminal
|
||||||
reporting will list it in the "expected to fail" (``XFAIL``) or "unexpectedly
|
reporting will list it in the "expected to fail" (``XFAIL``) or "unexpectedly
|
||||||
|
@ -275,8 +270,7 @@ that condition as the first parameter:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
|
@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
Note that you have to pass a reason as well (see the parameter description at
|
Note that you have to pass a reason as well (see the parameter description at
|
||||||
:ref:`pytest.mark.xfail ref`).
|
:ref:`pytest.mark.xfail ref`).
|
||||||
|
@ -289,8 +283,7 @@ You can specify the motive of an expected failure with the ``reason`` parameter:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.xfail(reason="known parser issue")
|
@pytest.mark.xfail(reason="known parser issue")
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
``raises`` parameter
|
``raises`` parameter
|
||||||
|
@ -302,8 +295,7 @@ a single exception, or a tuple of exceptions, in the ``raises`` argument.
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.xfail(raises=RuntimeError)
|
@pytest.mark.xfail(raises=RuntimeError)
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
Then the test will be reported as a regular failure if it fails with an
|
Then the test will be reported as a regular failure if it fails with an
|
||||||
exception not mentioned in ``raises``.
|
exception not mentioned in ``raises``.
|
||||||
|
@ -317,8 +309,7 @@ even executed, use the ``run`` parameter as ``False``:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.xfail(run=False)
|
@pytest.mark.xfail(run=False)
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
This is specially useful for xfailing tests that are crashing the interpreter and should be
|
This is specially useful for xfailing tests that are crashing the interpreter and should be
|
||||||
investigated later.
|
investigated later.
|
||||||
|
@ -334,8 +325,7 @@ You can change this by setting the ``strict`` keyword-only parameter to ``True``
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.xfail(strict=True)
|
@pytest.mark.xfail(strict=True)
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite.
|
This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite.
|
||||||
|
|
|
@ -8,9 +8,8 @@ How to use temporary directories and files in tests
|
||||||
The ``tmp_path`` fixture
|
The ``tmp_path`` fixture
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
You can use the ``tmp_path`` fixture which will
|
You can use the ``tmp_path`` fixture which will provide a temporary directory
|
||||||
provide a temporary directory unique to the current test,
|
unique to each test function.
|
||||||
created in the `base temporary directory`_.
|
|
||||||
|
|
||||||
``tmp_path`` is a :class:`pathlib.Path` object. Here is an example test usage:
|
``tmp_path`` is a :class:`pathlib.Path` object. Here is an example test usage:
|
||||||
|
|
||||||
|
@ -62,6 +61,11 @@ Running this would result in a passed test except for the last
|
||||||
FAILED test_tmp_path.py::test_create_file - assert 0
|
FAILED test_tmp_path.py::test_create_file - assert 0
|
||||||
============================ 1 failed in 0.12s =============================
|
============================ 1 failed in 0.12s =============================
|
||||||
|
|
||||||
|
By default, ``pytest`` retains the temporary directory for the last 3 ``pytest``
|
||||||
|
invocations. Concurrent invocations of the same test function are supported by
|
||||||
|
configuring the base temporary directory to be unique for each concurrent
|
||||||
|
run. See `temporary directory location and retention`_ for details.
|
||||||
|
|
||||||
.. _`tmp_path_factory example`:
|
.. _`tmp_path_factory example`:
|
||||||
|
|
||||||
The ``tmp_path_factory`` fixture
|
The ``tmp_path_factory`` fixture
|
||||||
|
@ -100,7 +104,7 @@ See :ref:`tmp_path_factory API <tmp_path_factory factory api>` for details.
|
||||||
.. _tmpdir:
|
.. _tmpdir:
|
||||||
|
|
||||||
The ``tmpdir`` and ``tmpdir_factory`` fixtures
|
The ``tmpdir`` and ``tmpdir_factory`` fixtures
|
||||||
---------------------------------------------------
|
----------------------------------------------
|
||||||
|
|
||||||
The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path``
|
The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path``
|
||||||
and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects
|
and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects
|
||||||
|
@ -124,10 +128,10 @@ See :fixture:`tmpdir <tmpdir>` :fixture:`tmpdir_factory <tmpdir_factory>`
|
||||||
API for details.
|
API for details.
|
||||||
|
|
||||||
|
|
||||||
.. _`base temporary directory`:
|
.. _`temporary directory location and retention`:
|
||||||
|
|
||||||
The default base temporary directory
|
Temporary directory location and retention
|
||||||
-----------------------------------------------
|
------------------------------------------
|
||||||
|
|
||||||
Temporary directories are by default created as sub-directories of
|
Temporary directories are by default created as sub-directories of
|
||||||
the system temporary directory. The base name will be ``pytest-NUM`` where
|
the system temporary directory. The base name will be ``pytest-NUM`` where
|
||||||
|
@ -152,7 +156,7 @@ You can override the default temporary directory setting like this:
|
||||||
for that purpose only.
|
for that purpose only.
|
||||||
|
|
||||||
When distributing tests on the local machine using ``pytest-xdist``, care is taken to
|
When distributing tests on the local machine using ``pytest-xdist``, care is taken to
|
||||||
automatically configure a basetemp directory for the sub processes such that all temporary
|
automatically configure a `basetemp` directory for the sub processes such that all temporary
|
||||||
data lands below a single per-test run basetemp directory.
|
data lands below a single per-test run temporary directory.
|
||||||
|
|
||||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||||
|
|
|
@ -46,24 +46,18 @@ Plugin discovery order at tool startup
|
||||||
|
|
||||||
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
|
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
|
||||||
|
|
||||||
6. by loading all :file:`conftest.py` files as inferred by the command line
|
6. by loading all "initial ":file:`conftest.py` files:
|
||||||
invocation:
|
|
||||||
|
|
||||||
- if no test paths are specified, use the current dir as a test path
|
- determine the test paths: specified on the command line, otherwise in
|
||||||
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
:confval:`testpaths` if defined and running from the rootdir, otherwise the
|
||||||
to the directory part of the first test path. After the ``conftest.py``
|
current dir
|
||||||
file is loaded, load all plugins specified in its
|
- for each test path, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||||
:globalvar:`pytest_plugins` variable if present.
|
to the directory part of the test path, if exist. Before a ``conftest.py``
|
||||||
|
file is loaded, load ``conftest.py`` files in all of its parent directories.
|
||||||
Note that pytest does not find ``conftest.py`` files in deeper nested
|
After a ``conftest.py`` file is loaded, recursively load all plugins specified
|
||||||
sub directories at tool startup. It is usually a good idea to keep
|
in its :globalvar:`pytest_plugins` variable if present.
|
||||||
your ``conftest.py`` file in the top level test or project root directory.
|
|
||||||
|
|
||||||
7. by recursively loading all plugins specified by the
|
|
||||||
:globalvar:`pytest_plugins` variable in ``conftest.py`` files.
|
|
||||||
|
|
||||||
|
|
||||||
.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/
|
|
||||||
.. _`conftest.py plugins`:
|
.. _`conftest.py plugins`:
|
||||||
.. _`localplugin`:
|
.. _`localplugin`:
|
||||||
.. _`local conftest plugins`:
|
.. _`local conftest plugins`:
|
||||||
|
@ -108,9 +102,9 @@ Here is how you might run it::
|
||||||
See also: :ref:`pythonpath`.
|
See also: :ref:`pythonpath`.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
Some hooks should be implemented only in plugins or conftest.py files situated at the
|
Some hooks cannot be implemented in conftest.py files which are not
|
||||||
tests root directory due to how pytest discovers plugins during startup,
|
:ref:`initial <pluginorder>` due to how pytest discovers plugins during
|
||||||
see the documentation of each hook for details.
|
startup. See the documentation of each hook for details.
|
||||||
|
|
||||||
Writing your own plugin
|
Writing your own plugin
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
:orphan:
|
:orphan:
|
||||||
|
|
||||||
.. sidebar:: Next Open Trainings
|
.. sidebar:: Next Open Trainings and Events
|
||||||
|
|
||||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
|
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
|
||||||
|
* **June 11th to 13th 2024**, Remote
|
||||||
|
* **March 4th to 6th 2025**, Leipzig, Germany / Remote
|
||||||
|
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, June 2024 (`date poll <https://nuudel.digitalcourage.de/2tEsEpRcwMNcAXVO>`_)
|
||||||
|
|
||||||
Also see :doc:`previous talks and blogposts <talks>`.
|
Also see :doc:`previous talks and blogposts <talks>`.
|
||||||
|
|
||||||
|
|
|
@ -177,13 +177,20 @@ Files will only be matched for configuration if:
|
||||||
* ``tox.ini``: contains a ``[pytest]`` section.
|
* ``tox.ini``: contains a ``[pytest]`` section.
|
||||||
* ``setup.cfg``: contains a ``[tool:pytest]`` section.
|
* ``setup.cfg``: contains a ``[tool:pytest]`` section.
|
||||||
|
|
||||||
|
Finally, a ``pyproject.toml`` file will be considered the ``configfile`` if no other match was found, in this case
|
||||||
|
even if it does not contain a ``[tool.pytest.ini_options]`` table (this was added in ``8.1``).
|
||||||
|
|
||||||
The files are considered in the order above. Options from multiple ``configfiles`` candidates
|
The files are considered in the order above. Options from multiple ``configfiles`` candidates
|
||||||
are never merged - the first match wins.
|
are never merged - the first match wins.
|
||||||
|
|
||||||
|
The configuration file also determines the value of the ``rootpath``.
|
||||||
|
|
||||||
The :class:`Config <pytest.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
|
The :class:`Config <pytest.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
|
||||||
will subsequently carry these attributes:
|
will subsequently carry these attributes:
|
||||||
|
|
||||||
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist.
|
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist. It is used as
|
||||||
|
a reference directory for constructing test addresses ("nodeids") and can be used also by plugins for storing
|
||||||
|
per-testrun information.
|
||||||
|
|
||||||
- :attr:`config.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None``
|
- :attr:`config.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None``
|
||||||
(it is named ``inipath`` for historical reasons).
|
(it is named ``inipath`` for historical reasons).
|
||||||
|
@ -193,9 +200,7 @@ will subsequently carry these attributes:
|
||||||
versions of the older ``config.rootdir`` and ``config.inifile``, which have type
|
versions of the older ``config.rootdir`` and ``config.inifile``, which have type
|
||||||
``py.path.local``, and still exist for backward compatibility.
|
``py.path.local``, and still exist for backward compatibility.
|
||||||
|
|
||||||
The ``rootdir`` is used as a reference directory for constructing test
|
|
||||||
addresses ("nodeids") and can be used also by plugins for storing
|
|
||||||
per-testrun information.
|
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -164,8 +164,7 @@ Add warning filters to marked test items.
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning")
|
@pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning")
|
||||||
def test_foo():
|
def test_foo(): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
.. _`pytest.mark.parametrize ref`:
|
.. _`pytest.mark.parametrize ref`:
|
||||||
|
@ -276,8 +275,7 @@ For example:
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
@pytest.mark.timeout(10, "slow", method="thread")
|
@pytest.mark.timeout(10, "slow", method="thread")
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
Will create and attach a :class:`Mark <pytest.Mark>` object to the collected
|
Will create and attach a :class:`Mark <pytest.Mark>` object to the collected
|
||||||
:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
|
:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
|
||||||
|
@ -294,8 +292,7 @@ Example for using multiple custom markers:
|
||||||
|
|
||||||
@pytest.mark.timeout(10, "slow", method="thread")
|
@pytest.mark.timeout(10, "slow", method="thread")
|
||||||
@pytest.mark.slow
|
@pytest.mark.slow
|
||||||
def test_function():
|
def test_function(): ...
|
||||||
...
|
|
||||||
|
|
||||||
When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers_with_node <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``.
|
When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers_with_node <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``.
|
||||||
|
|
||||||
|
@ -2044,7 +2041,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
failure
|
failure
|
||||||
--doctest-glob=pat Doctests file matching pattern, default: test*.txt
|
--doctest-glob=pat Doctests file matching pattern, default: test*.txt
|
||||||
--doctest-ignore-import-errors
|
--doctest-ignore-import-errors
|
||||||
Ignore doctest ImportErrors
|
Ignore doctest collection errors
|
||||||
--doctest-continue-on-failure
|
--doctest-continue-on-failure
|
||||||
For a given doctest, continue to run after the first
|
For a given doctest, continue to run after the first
|
||||||
failure
|
failure
|
||||||
|
|
|
@ -2,7 +2,7 @@ pallets-sphinx-themes
|
||||||
pluggy>=1.2.0
|
pluggy>=1.2.0
|
||||||
pygments-pytest>=2.3.0
|
pygments-pytest>=2.3.0
|
||||||
sphinx-removed-in>=0.2.0
|
sphinx-removed-in>=0.2.0
|
||||||
sphinx>=5,<8
|
sphinx>=7
|
||||||
sphinxcontrib-trio
|
sphinxcontrib-trio
|
||||||
sphinxcontrib-svg2pdfconverter
|
sphinxcontrib-svg2pdfconverter
|
||||||
# Pin packaging because it no longer handles 'latest' version, which
|
# Pin packaging because it no longer handles 'latest' version, which
|
||||||
|
|
|
@ -3,6 +3,7 @@ from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
|
||||||
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"
|
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"
|
||||||
|
|
||||||
|
|
||||||
|
|
190
pyproject.toml
190
pyproject.toml
|
@ -1,13 +1,178 @@
|
||||||
|
[project]
|
||||||
|
name = "pytest"
|
||||||
|
description = "pytest: simple powerful testing with Python"
|
||||||
|
readme = "README.rst"
|
||||||
|
keywords = [
|
||||||
|
"test",
|
||||||
|
"unittest",
|
||||||
|
]
|
||||||
|
license = {text = "MIT"}
|
||||||
|
authors = [
|
||||||
|
{name = "Holger Krekel"},
|
||||||
|
{name = "Bruno Oliveira"},
|
||||||
|
{name = "Ronny Pfannschmidt"},
|
||||||
|
{name = "Floris Bruynooghe"},
|
||||||
|
{name = "Brianna Laugher"},
|
||||||
|
{name = "Florian Bruhin"},
|
||||||
|
{name = "Others (See AUTHORS)"},
|
||||||
|
]
|
||||||
|
requires-python = ">=3.8"
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 6 - Mature",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: MacOS",
|
||||||
|
"Operating System :: Microsoft :: Windows",
|
||||||
|
"Operating System :: POSIX",
|
||||||
|
"Operating System :: Unix",
|
||||||
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Topic :: Software Development :: Libraries",
|
||||||
|
"Topic :: Software Development :: Testing",
|
||||||
|
"Topic :: Utilities",
|
||||||
|
]
|
||||||
|
dynamic = [
|
||||||
|
"version",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
'colorama; sys_platform == "win32"',
|
||||||
|
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
|
||||||
|
"iniconfig",
|
||||||
|
"packaging",
|
||||||
|
"pluggy<2.0,>=1.4",
|
||||||
|
'tomli>=1; python_version < "3.11"',
|
||||||
|
]
|
||||||
|
[project.optional-dependencies]
|
||||||
|
testing = [
|
||||||
|
"argcomplete",
|
||||||
|
"attrs>=19.2",
|
||||||
|
"hypothesis>=3.56",
|
||||||
|
"mock",
|
||||||
|
"pygments>=2.7.2",
|
||||||
|
"requests",
|
||||||
|
"setuptools",
|
||||||
|
"xmlschema",
|
||||||
|
]
|
||||||
|
[project.urls]
|
||||||
|
Changelog = "https://docs.pytest.org/en/stable/changelog.html"
|
||||||
|
Homepage = "https://docs.pytest.org/en/latest/"
|
||||||
|
Source = "https://github.com/pytest-dev/pytest"
|
||||||
|
Tracker = "https://github.com/pytest-dev/pytest/issues"
|
||||||
|
Twitter = "https://twitter.com/pytestdotorg"
|
||||||
|
[project.scripts]
|
||||||
|
"py.test" = "pytest:console_main"
|
||||||
|
pytest = "pytest:console_main"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
requires = [
|
requires = [
|
||||||
"setuptools>=45.0",
|
"setuptools>=61",
|
||||||
"setuptools-scm[toml]>=6.2.3",
|
"setuptools-scm[toml]>=6.2.3",
|
||||||
]
|
]
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
[tool.setuptools.package-data]
|
||||||
|
"_pytest" = ["py.typed"]
|
||||||
|
"pytest" = ["py.typed"]
|
||||||
|
|
||||||
[tool.setuptools_scm]
|
[tool.setuptools_scm]
|
||||||
write_to = "src/_pytest/_version.py"
|
write_to = "src/_pytest/_version.py"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
target-version = ['py38']
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
src = ["src"]
|
||||||
|
line-length = 88
|
||||||
|
|
||||||
|
[tool.ruff.format]
|
||||||
|
docstring-code-format = true
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = [
|
||||||
|
"B", # bugbear
|
||||||
|
"D", # pydocstyle
|
||||||
|
"E", # pycodestyle
|
||||||
|
"F", # pyflakes
|
||||||
|
"I", # isort
|
||||||
|
"PYI", # flake8-pyi
|
||||||
|
"UP", # pyupgrade
|
||||||
|
"RUF", # ruff
|
||||||
|
"W", # pycodestyle
|
||||||
|
"PIE", # flake8-pie
|
||||||
|
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
|
||||||
|
"PLE", # pylint error
|
||||||
|
"PLW", # pylint warning
|
||||||
|
"PLR1714", # Consider merging multiple comparisons
|
||||||
|
]
|
||||||
|
ignore = [
|
||||||
|
# bugbear ignore
|
||||||
|
"B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
|
||||||
|
"B007", # Loop control variable `i` not used within loop body
|
||||||
|
"B009", # Do not call `getattr` with a constant attribute value
|
||||||
|
"B010", # [*] Do not call `setattr` with a constant attribute value.
|
||||||
|
"B011", # Do not `assert False` (`python -O` removes these calls)
|
||||||
|
"B028", # No explicit `stacklevel` keyword argument found
|
||||||
|
# pycodestyle ignore
|
||||||
|
# pytest can do weird low-level things, and we usually know
|
||||||
|
# what we're doing when we use type(..) is ...
|
||||||
|
"E721", # Do not compare types, use `isinstance()`
|
||||||
|
# pydocstyle ignore
|
||||||
|
"D100", # Missing docstring in public module
|
||||||
|
"D101", # Missing docstring in public class
|
||||||
|
"D102", # Missing docstring in public method
|
||||||
|
"D103", # Missing docstring in public function
|
||||||
|
"D104", # Missing docstring in public package
|
||||||
|
"D105", # Missing docstring in magic method
|
||||||
|
"D106", # Missing docstring in public nested class
|
||||||
|
"D107", # Missing docstring in `__init__`
|
||||||
|
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
|
||||||
|
"D205", # 1 blank line required between summary line and description
|
||||||
|
"D400", # First line should end with a period
|
||||||
|
"D401", # First line of docstring should be in imperative mood
|
||||||
|
"D402", # First line should not be the function's signature
|
||||||
|
"D404", # First word of the docstring should not be "This"
|
||||||
|
"D415", # First line should end with a period, question mark, or exclamation point
|
||||||
|
# ruff ignore
|
||||||
|
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
|
||||||
|
# pylint ignore
|
||||||
|
"PLW0603", # Using the global statement
|
||||||
|
"PLW0120", # remove the else and dedent its contents
|
||||||
|
"PLW2901", # for loop variable overwritten by assignment target
|
||||||
|
"PLR5501", # Use `elif` instead of `else` then `if`
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.ruff.lint.pycodestyle]
|
||||||
|
# In order to be able to format for 88 char in ruff format
|
||||||
|
max-line-length = 120
|
||||||
|
|
||||||
|
[tool.ruff.lint.pydocstyle]
|
||||||
|
convention = "pep257"
|
||||||
|
|
||||||
|
[tool.ruff.lint.isort]
|
||||||
|
force-single-line = true
|
||||||
|
combine-as-imports = true
|
||||||
|
force-sort-within-sections = true
|
||||||
|
order-by-type = false
|
||||||
|
known-local-folder = ["pytest", "_pytest"]
|
||||||
|
lines-after-imports = 2
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
"src/_pytest/_py/**/*.py" = ["B", "PYI"]
|
||||||
|
"src/_pytest/_version.py" = ["I001"]
|
||||||
|
"testing/python/approx.py" = ["B015"]
|
||||||
|
|
||||||
|
[tool.check-wheel-contents]
|
||||||
|
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||||
|
# W009: Wheel contains multiple toplevel library entries
|
||||||
|
ignore = "W009"
|
||||||
|
|
||||||
|
[tool.pyproject-fmt]
|
||||||
|
indent = 4
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
minversion = "2.0"
|
minversion = "2.0"
|
||||||
addopts = "-rfEX -p pytester --strict-markers"
|
addopts = "-rfEX -p pytester --strict-markers"
|
||||||
|
@ -67,7 +232,6 @@ markers = [
|
||||||
"uses_pexpect",
|
"uses_pexpect",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
[tool.towncrier]
|
[tool.towncrier]
|
||||||
package = "pytest"
|
package = "pytest"
|
||||||
package_dir = "src"
|
package_dir = "src"
|
||||||
|
@ -116,10 +280,16 @@ template = "changelog/_template.rst"
|
||||||
name = "Trivial/Internal Changes"
|
name = "Trivial/Internal Changes"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[tool.black]
|
[tool.mypy]
|
||||||
target-version = ['py38']
|
mypy_path = ["src"]
|
||||||
|
check_untyped_defs = true
|
||||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
disallow_any_generics = true
|
||||||
[tool.check-wheel-contents]
|
disallow_untyped_defs = true
|
||||||
# W009: Wheel contains multiple toplevel library entries
|
ignore_missing_imports = true
|
||||||
ignore = "W009"
|
show_error_codes = true
|
||||||
|
strict_equality = true
|
||||||
|
warn_redundant_casts = true
|
||||||
|
warn_return_any = true
|
||||||
|
warn_unreachable = true
|
||||||
|
warn_unused_configs = true
|
||||||
|
no_implicit_reexport = true
|
||||||
|
|
|
@ -8,9 +8,9 @@ our CHANGELOG) into Markdown (which is required by GitHub Releases).
|
||||||
|
|
||||||
Requires Python3.6+.
|
Requires Python3.6+.
|
||||||
"""
|
"""
|
||||||
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
import pypandoc
|
import pypandoc
|
||||||
|
|
|
@ -14,8 +14,8 @@ After that, it will create a release using the `release` tox environment, and pu
|
||||||
`pytest bot <pytestbot@gmail.com>` commit author.
|
`pytest bot <pytestbot@gmail.com>` commit author.
|
||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
from subprocess import check_call
|
from subprocess import check_call
|
||||||
from subprocess import check_output
|
from subprocess import check_output
|
||||||
from subprocess import run
|
from subprocess import run
|
||||||
|
@ -79,7 +79,7 @@ def prepare_release_pr(
|
||||||
)
|
)
|
||||||
except InvalidFeatureRelease as e:
|
except InvalidFeatureRelease as e:
|
||||||
print(f"{Fore.RED}{e}")
|
print(f"{Fore.RED}{e}")
|
||||||
raise SystemExit(1)
|
raise SystemExit(1) from None
|
||||||
|
|
||||||
print(f"Version: {Fore.CYAN}{version}")
|
print(f"Version: {Fore.CYAN}{version}")
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,7 @@ def pre_release(
|
||||||
|
|
||||||
def changelog(version: str, write_out: bool = False) -> None:
|
def changelog(version: str, write_out: bool = False) -> None:
|
||||||
addopts = [] if write_out else ["--draft"]
|
addopts = [] if write_out else ["--draft"]
|
||||||
check_call(["towncrier", "--yes", "--version", version] + addopts)
|
check_call(["towncrier", "--yes", "--version", version, *addopts])
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# mypy: disallow-untyped-defs
|
# mypy: disallow-untyped-defs
|
||||||
import sys
|
|
||||||
from subprocess import call
|
from subprocess import call
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
|
|
|
@ -11,13 +11,13 @@ from typing import TypedDict
|
||||||
|
|
||||||
import packaging.version
|
import packaging.version
|
||||||
import platformdirs
|
import platformdirs
|
||||||
import tabulate
|
|
||||||
import wcwidth
|
|
||||||
from requests_cache import CachedResponse
|
from requests_cache import CachedResponse
|
||||||
from requests_cache import CachedSession
|
from requests_cache import CachedSession
|
||||||
from requests_cache import OriginalResponse
|
from requests_cache import OriginalResponse
|
||||||
from requests_cache import SQLiteCache
|
from requests_cache import SQLiteCache
|
||||||
|
import tabulate
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
import wcwidth
|
||||||
|
|
||||||
|
|
||||||
FILE_HEAD = r"""
|
FILE_HEAD = r"""
|
||||||
|
@ -29,7 +29,7 @@ Pytest Plugin List
|
||||||
==================
|
==================
|
||||||
|
|
||||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
It includes PyPI projects whose names begin with "pytest-" or "pytest_" and a handful of manually selected projects.
|
||||||
Packages classified as inactive are excluded.
|
Packages classified as inactive are excluded.
|
||||||
|
|
||||||
For detailed insights into how this list is generated,
|
For detailed insights into how this list is generated,
|
||||||
|
@ -61,6 +61,7 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
||||||
)
|
)
|
||||||
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||||
"logassert",
|
"logassert",
|
||||||
|
"logot",
|
||||||
"nuts",
|
"nuts",
|
||||||
"flask_fixture",
|
"flask_fixture",
|
||||||
}
|
}
|
||||||
|
@ -86,7 +87,6 @@ def project_response_with_refresh(
|
||||||
|
|
||||||
force refresh in case of last serial mismatch
|
force refresh in case of last serial mismatch
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = session.get(f"https://pypi.org/pypi/{name}/json")
|
response = session.get(f"https://pypi.org/pypi/{name}/json")
|
||||||
if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial:
|
if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial:
|
||||||
response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True)
|
response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True)
|
||||||
|
@ -110,7 +110,10 @@ def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
|
||||||
return {
|
return {
|
||||||
name: p["_last-serial"]
|
name: p["_last-serial"]
|
||||||
for p in response.json()["projects"]
|
for p in response.json()["projects"]
|
||||||
if (name := p["name"]).startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
if (
|
||||||
|
(name := p["name"]).startswith(("pytest-", "pytest_"))
|
||||||
|
or name in ADDITIONAL_PROJECTS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -185,7 +188,6 @@ def iter_plugins() -> Iterator[PluginInfo]:
|
||||||
|
|
||||||
def plugin_definitions(plugins: Iterable[PluginInfo]) -> Iterator[str]:
|
def plugin_definitions(plugins: Iterable[PluginInfo]) -> Iterator[str]:
|
||||||
"""Return RST for the plugin list that fits better on a vertical page."""
|
"""Return RST for the plugin list that fits better on a vertical page."""
|
||||||
|
|
||||||
for plugin in plugins:
|
for plugin in plugins:
|
||||||
yield dedent(
|
yield dedent(
|
||||||
f"""
|
f"""
|
||||||
|
@ -210,7 +212,7 @@ def main() -> None:
|
||||||
f.write(f"This list contains {len(plugins)} plugins.\n\n")
|
f.write(f"This list contains {len(plugins)} plugins.\n\n")
|
||||||
f.write(".. only:: not latex\n\n")
|
f.write(".. only:: not latex\n\n")
|
||||||
|
|
||||||
wcwidth # reference library that must exist for tabulate to work
|
_ = wcwidth # reference library that must exist for tabulate to work
|
||||||
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
|
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
|
||||||
f.write(indent(plugin_table, " "))
|
f.write(indent(plugin_table, " "))
|
||||||
f.write("\n\n")
|
f.write("\n\n")
|
||||||
|
|
104
setup.cfg
104
setup.cfg
|
@ -1,104 +0,0 @@
|
||||||
[metadata]
|
|
||||||
name = pytest
|
|
||||||
description = pytest: simple powerful testing with Python
|
|
||||||
long_description = file: README.rst
|
|
||||||
long_description_content_type = text/x-rst
|
|
||||||
url = https://docs.pytest.org/en/latest/
|
|
||||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
|
||||||
license = MIT
|
|
||||||
license_files = LICENSE
|
|
||||||
platforms = unix, linux, osx, cygwin, win32
|
|
||||||
classifiers =
|
|
||||||
Development Status :: 6 - Mature
|
|
||||||
Intended Audience :: Developers
|
|
||||||
License :: OSI Approved :: MIT License
|
|
||||||
Operating System :: MacOS :: MacOS X
|
|
||||||
Operating System :: Microsoft :: Windows
|
|
||||||
Operating System :: POSIX
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
Programming Language :: Python :: 3 :: Only
|
|
||||||
Programming Language :: Python :: 3.8
|
|
||||||
Programming Language :: Python :: 3.9
|
|
||||||
Programming Language :: Python :: 3.10
|
|
||||||
Programming Language :: Python :: 3.11
|
|
||||||
Programming Language :: Python :: 3.12
|
|
||||||
Topic :: Software Development :: Libraries
|
|
||||||
Topic :: Software Development :: Testing
|
|
||||||
Topic :: Utilities
|
|
||||||
keywords = test, unittest
|
|
||||||
project_urls =
|
|
||||||
Changelog=https://docs.pytest.org/en/stable/changelog.html
|
|
||||||
Twitter=https://twitter.com/pytestdotorg
|
|
||||||
Source=https://github.com/pytest-dev/pytest
|
|
||||||
Tracker=https://github.com/pytest-dev/pytest/issues
|
|
||||||
|
|
||||||
[options]
|
|
||||||
packages =
|
|
||||||
_pytest
|
|
||||||
_pytest._code
|
|
||||||
_pytest._io
|
|
||||||
_pytest._py
|
|
||||||
_pytest.assertion
|
|
||||||
_pytest.config
|
|
||||||
_pytest.mark
|
|
||||||
pytest
|
|
||||||
py_modules = py
|
|
||||||
install_requires =
|
|
||||||
iniconfig
|
|
||||||
packaging
|
|
||||||
pluggy>=1.3.0,<2.0
|
|
||||||
colorama;sys_platform=="win32"
|
|
||||||
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
|
||||||
tomli>=1.0.0;python_version<"3.11"
|
|
||||||
python_requires = >=3.8
|
|
||||||
package_dir =
|
|
||||||
=src
|
|
||||||
setup_requires =
|
|
||||||
setuptools
|
|
||||||
setuptools-scm>=6.0
|
|
||||||
zip_safe = no
|
|
||||||
|
|
||||||
[options.entry_points]
|
|
||||||
console_scripts =
|
|
||||||
pytest=pytest:console_main
|
|
||||||
py.test=pytest:console_main
|
|
||||||
|
|
||||||
[options.extras_require]
|
|
||||||
testing =
|
|
||||||
argcomplete
|
|
||||||
attrs>=19.2.0
|
|
||||||
hypothesis>=3.56
|
|
||||||
mock
|
|
||||||
pygments>=2.7.2
|
|
||||||
requests
|
|
||||||
setuptools
|
|
||||||
xmlschema
|
|
||||||
|
|
||||||
[options.package_data]
|
|
||||||
_pytest = py.typed
|
|
||||||
pytest = py.typed
|
|
||||||
|
|
||||||
[build_sphinx]
|
|
||||||
source_dir = doc/en/
|
|
||||||
build_dir = doc/build
|
|
||||||
all_files = 1
|
|
||||||
|
|
||||||
[check-manifest]
|
|
||||||
ignore =
|
|
||||||
src/_pytest/_version.py
|
|
||||||
|
|
||||||
[devpi:upload]
|
|
||||||
formats = sdist.tgz,bdist_wheel
|
|
||||||
|
|
||||||
[mypy]
|
|
||||||
mypy_path = src
|
|
||||||
check_untyped_defs = True
|
|
||||||
disallow_any_generics = True
|
|
||||||
ignore_missing_imports = True
|
|
||||||
show_error_codes = True
|
|
||||||
strict_equality = True
|
|
||||||
warn_redundant_casts = True
|
|
||||||
warn_return_any = True
|
|
||||||
warn_unreachable = True
|
|
||||||
warn_unused_configs = True
|
|
||||||
no_implicit_reexport = True
|
|
|
@ -1,7 +1,8 @@
|
||||||
__all__ = ["__version__", "version_tuple"]
|
__all__ = ["__version__", "version_tuple"]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ._version import version as __version__, version_tuple
|
from ._version import version as __version__
|
||||||
|
from ._version import version_tuple
|
||||||
except ImportError: # pragma: no cover
|
except ImportError: # pragma: no cover
|
||||||
# broken installation, we don't even try
|
# broken installation, we don't even try
|
||||||
# unknown only works because we do poor mans version compare
|
# unknown only works because we do poor mans version compare
|
||||||
|
|
|
@ -61,10 +61,11 @@ If things do not work right away:
|
||||||
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
||||||
global argcomplete script).
|
global argcomplete script).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
from glob import glob
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from glob import glob
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Python inspection/code generation API."""
|
"""Python inspection/code generation API."""
|
||||||
|
|
||||||
from .code import Code
|
from .code import Code
|
||||||
from .code import ExceptionInfo
|
from .code import ExceptionInfo
|
||||||
from .code import filter_traceback
|
from .code import filter_traceback
|
||||||
|
@ -9,6 +10,7 @@ from .code import TracebackEntry
|
||||||
from .source import getrawcode
|
from .source import getrawcode
|
||||||
from .source import Source
|
from .source import Source
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Code",
|
"Code",
|
||||||
"ExceptionInfo",
|
"ExceptionInfo",
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
import ast
|
import ast
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
from inspect import CO_VARARGS
|
from inspect import CO_VARARGS
|
||||||
from inspect import CO_VARKEYWORDS
|
from inspect import CO_VARKEYWORDS
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
from traceback import format_exception_only
|
from traceback import format_exception_only
|
||||||
from types import CodeType
|
from types import CodeType
|
||||||
from types import FrameType
|
from types import FrameType
|
||||||
|
@ -50,6 +51,7 @@ from _pytest.deprecated import check_ispytest
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import bestrelpath
|
from _pytest.pathlib import bestrelpath
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info[:2] < (3, 11):
|
if sys.version_info[:2] < (3, 11):
|
||||||
from exceptiongroup import BaseExceptionGroup
|
from exceptiongroup import BaseExceptionGroup
|
||||||
|
|
||||||
|
@ -486,9 +488,10 @@ class ExceptionInfo(Generic[E]):
|
||||||
|
|
||||||
.. versionadded:: 7.4
|
.. versionadded:: 7.4
|
||||||
"""
|
"""
|
||||||
assert (
|
assert exception.__traceback__, (
|
||||||
exception.__traceback__
|
"Exceptions passed to ExcInfo.from_exception(...)"
|
||||||
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
|
" must have a non-None __traceback__."
|
||||||
|
)
|
||||||
exc_info = (type(exception), exception, exception.__traceback__)
|
exc_info = (type(exception), exception, exception.__traceback__)
|
||||||
return cls.from_exc_info(exc_info, exprinfo)
|
return cls.from_exc_info(exc_info, exprinfo)
|
||||||
|
|
||||||
|
@ -587,9 +590,7 @@ class ExceptionInfo(Generic[E]):
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
if self._excinfo is None:
|
if self._excinfo is None:
|
||||||
return "<ExceptionInfo for raises contextmanager>"
|
return "<ExceptionInfo for raises contextmanager>"
|
||||||
return "<{} {} tblen={}>".format(
|
return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
|
||||||
self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
|
|
||||||
)
|
|
||||||
|
|
||||||
def exconly(self, tryshort: bool = False) -> str:
|
def exconly(self, tryshort: bool = False) -> str:
|
||||||
"""Return the exception as a string.
|
"""Return the exception as a string.
|
||||||
|
@ -698,10 +699,21 @@ class ExceptionInfo(Generic[E]):
|
||||||
return fmt.repr_excinfo(self)
|
return fmt.repr_excinfo(self)
|
||||||
|
|
||||||
def _stringify_exception(self, exc: BaseException) -> str:
|
def _stringify_exception(self, exc: BaseException) -> str:
|
||||||
|
try:
|
||||||
|
notes = getattr(exc, "__notes__", [])
|
||||||
|
except KeyError:
|
||||||
|
# Workaround for https://github.com/python/cpython/issues/98778 on
|
||||||
|
# Python <= 3.9, and some 3.10 and 3.11 patch versions.
|
||||||
|
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
|
||||||
|
if sys.version_info[:2] <= (3, 11) and isinstance(exc, HTTPError):
|
||||||
|
notes = []
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
return "\n".join(
|
return "\n".join(
|
||||||
[
|
[
|
||||||
str(exc),
|
str(exc),
|
||||||
*getattr(exc, "__notes__", []),
|
*notes,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1006,13 +1018,8 @@ class FormattedExcinfo:
|
||||||
extraline: Optional[str] = (
|
extraline: Optional[str] = (
|
||||||
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
|
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
|
||||||
" The following exception happened when comparing locals in the stack frame:\n"
|
" The following exception happened when comparing locals in the stack frame:\n"
|
||||||
" {exc_type}: {exc_msg}\n"
|
f" {type(e).__name__}: {e!s}\n"
|
||||||
" Displaying first and last {max_frames} stack frames out of {total}."
|
f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
|
||||||
).format(
|
|
||||||
exc_type=type(e).__name__,
|
|
||||||
exc_msg=str(e),
|
|
||||||
max_frames=max_frames,
|
|
||||||
total=len(traceback),
|
|
||||||
)
|
)
|
||||||
# Type ignored because adding two instances of a List subtype
|
# Type ignored because adding two instances of a List subtype
|
||||||
# currently incorrectly has type List instead of the subtype.
|
# currently incorrectly has type List instead of the subtype.
|
||||||
|
@ -1219,7 +1226,6 @@ class ReprEntry(TerminalRepr):
|
||||||
the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
|
the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
|
||||||
character, as doing so might break line continuations.
|
character, as doing so might break line continuations.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self.lines:
|
if not self.lines:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
import ast
|
import ast
|
||||||
|
from bisect import bisect_right
|
||||||
import inspect
|
import inspect
|
||||||
import textwrap
|
import textwrap
|
||||||
import tokenize
|
import tokenize
|
||||||
import types
|
import types
|
||||||
import warnings
|
|
||||||
from bisect import bisect_right
|
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -12,6 +12,7 @@ from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
class Source:
|
class Source:
|
||||||
|
@ -196,7 +197,9 @@ def getstatementrange_ast(
|
||||||
# by using the BlockFinder helper used which inspect.getsource() uses itself.
|
# by using the BlockFinder helper used which inspect.getsource() uses itself.
|
||||||
block_finder = inspect.BlockFinder()
|
block_finder = inspect.BlockFinder()
|
||||||
# If we start with an indented line, put blockfinder to "started" mode.
|
# If we start with an indented line, put blockfinder to "started" mode.
|
||||||
block_finder.started = source.lines[start][0].isspace()
|
block_finder.started = (
|
||||||
|
bool(source.lines[start]) and source.lines[start][0].isspace()
|
||||||
|
)
|
||||||
it = ((x + "\n") for x in source.lines[start:end])
|
it = ((x + "\n") for x in source.lines[start:end])
|
||||||
try:
|
try:
|
||||||
for tok in tokenize.generate_tokens(lambda: next(it)):
|
for tok in tokenize.generate_tokens(lambda: next(it)):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
# This module was imported from the cpython standard library
|
# This module was imported from the cpython standard library
|
||||||
# (https://github.com/python/cpython/) at commit
|
# (https://github.com/python/cpython/) at commit
|
||||||
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
|
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
|
||||||
|
@ -14,9 +15,9 @@
|
||||||
# useful, thank small children who sleep at night.
|
# useful, thank small children who sleep at night.
|
||||||
import collections as _collections
|
import collections as _collections
|
||||||
import dataclasses as _dataclasses
|
import dataclasses as _dataclasses
|
||||||
|
from io import StringIO as _StringIO
|
||||||
import re
|
import re
|
||||||
import types as _types
|
import types as _types
|
||||||
from io import StringIO as _StringIO
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
|
|
@ -19,8 +19,8 @@ def _format_repr_exception(exc: BaseException, obj: object) -> str:
|
||||||
raise
|
raise
|
||||||
except BaseException as exc:
|
except BaseException as exc:
|
||||||
exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})"
|
exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})"
|
||||||
return "<[{} raised in repr()] {} object at 0x{:x}>".format(
|
return (
|
||||||
exc_info, type(obj).__name__, id(obj)
|
f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -108,7 +108,6 @@ def saferepr(
|
||||||
This function is a wrapper around the Repr/reprlib functionality of the
|
This function is a wrapper around the Repr/reprlib functionality of the
|
||||||
stdlib.
|
stdlib.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return SafeRepr(maxsize, use_ascii).repr(obj)
|
return SafeRepr(maxsize, use_ascii).repr(obj)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""Helper functions for writing to terminals and files."""
|
"""Helper functions for writing to terminals and files."""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
@ -183,9 +184,7 @@ class TerminalWriter:
|
||||||
"""
|
"""
|
||||||
if indents and len(indents) != len(lines):
|
if indents and len(indents) != len(lines):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"indents size ({}) should have same size as lines ({})".format(
|
f"indents size ({len(indents)}) should have same size as lines ({len(lines)})"
|
||||||
len(indents), len(lines)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if not indents:
|
if not indents:
|
||||||
indents = [""] * len(lines)
|
indents = [""] * len(lines)
|
||||||
|
@ -233,17 +232,17 @@ class TerminalWriter:
|
||||||
# which may lead to the previous color being propagated to the
|
# which may lead to the previous color being propagated to the
|
||||||
# start of the expression, so reset first.
|
# start of the expression, so reset first.
|
||||||
return "\x1b[0m" + highlighted
|
return "\x1b[0m" + highlighted
|
||||||
except pygments.util.ClassNotFound:
|
except pygments.util.ClassNotFound as e:
|
||||||
raise UsageError(
|
raise UsageError(
|
||||||
"PYTEST_THEME environment variable had an invalid value: '{}'. "
|
"PYTEST_THEME environment variable had an invalid value: '{}'. "
|
||||||
"Only valid pygment styles are allowed.".format(
|
"Only valid pygment styles are allowed.".format(
|
||||||
os.getenv("PYTEST_THEME")
|
os.getenv("PYTEST_THEME")
|
||||||
)
|
)
|
||||||
)
|
) from e
|
||||||
except pygments.util.OptionError:
|
except pygments.util.OptionError as e:
|
||||||
raise UsageError(
|
raise UsageError(
|
||||||
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
|
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
|
||||||
"The only allowed values are 'dark' and 'light'.".format(
|
"The only allowed values are 'dark' and 'light'.".format(
|
||||||
os.getenv("PYTEST_THEME_MODE")
|
os.getenv("PYTEST_THEME_MODE")
|
||||||
)
|
)
|
||||||
)
|
) from e
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import unicodedata
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
|
import unicodedata
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(100)
|
@lru_cache(100)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
"""create errno-specific classes for IO or os calls."""
|
"""create errno-specific classes for IO or os calls."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
|
@ -8,6 +9,7 @@ from typing import Callable
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing_extensions import ParamSpec
|
from typing_extensions import ParamSpec
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""local path implementation."""
|
"""local path implementation."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import atexit
|
import atexit
|
||||||
|
from contextlib import contextmanager
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import posixpath
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
import warnings
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from os.path import abspath
|
from os.path import abspath
|
||||||
from os.path import dirname
|
from os.path import dirname
|
||||||
from os.path import exists
|
from os.path import exists
|
||||||
|
@ -19,18 +16,23 @@ from os.path import isdir
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from os.path import islink
|
from os.path import islink
|
||||||
from os.path import normpath
|
from os.path import normpath
|
||||||
|
import posixpath
|
||||||
from stat import S_ISDIR
|
from stat import S_ISDIR
|
||||||
from stat import S_ISLNK
|
from stat import S_ISLNK
|
||||||
from stat import S_ISREG
|
from stat import S_ISREG
|
||||||
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
import uuid
|
||||||
|
import warnings
|
||||||
|
|
||||||
from . import error
|
from . import error
|
||||||
|
|
||||||
|
|
||||||
# Moved from local.py.
|
# Moved from local.py.
|
||||||
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
||||||
|
|
||||||
|
@ -450,7 +452,7 @@ class LocalPath:
|
||||||
|
|
||||||
def ensure_dir(self, *args):
|
def ensure_dir(self, *args):
|
||||||
"""Ensure the path joined with args is a directory."""
|
"""Ensure the path joined with args is a directory."""
|
||||||
return self.ensure(*args, **{"dir": True})
|
return self.ensure(*args, dir=True)
|
||||||
|
|
||||||
def bestrelpath(self, dest):
|
def bestrelpath(self, dest):
|
||||||
"""Return a string which is a relative path from self
|
"""Return a string which is a relative path from self
|
||||||
|
@ -675,7 +677,7 @@ class LocalPath:
|
||||||
else:
|
else:
|
||||||
kw.setdefault("dirname", dirname)
|
kw.setdefault("dirname", dirname)
|
||||||
kw.setdefault("sep", self.sep)
|
kw.setdefault("sep", self.sep)
|
||||||
obj.strpath = normpath("%(dirname)s%(sep)s%(basename)s" % kw)
|
obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw))
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
def _getbyspec(self, spec: str) -> list[str]:
|
def _getbyspec(self, spec: str) -> list[str]:
|
||||||
|
@ -760,7 +762,10 @@ class LocalPath:
|
||||||
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
|
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
|
||||||
# Which seems incorrect, given io.open supports the given argument types.
|
# Which seems incorrect, given io.open supports the given argument types.
|
||||||
return error.checked_call(
|
return error.checked_call(
|
||||||
io.open, self.strpath, mode, encoding=encoding # type:ignore[arg-type]
|
io.open,
|
||||||
|
self.strpath,
|
||||||
|
mode,
|
||||||
|
encoding=encoding, # type:ignore[arg-type]
|
||||||
)
|
)
|
||||||
return error.checked_call(open, self.strpath, mode)
|
return error.checked_call(open, self.strpath, mode)
|
||||||
|
|
||||||
|
@ -779,11 +784,11 @@ class LocalPath:
|
||||||
|
|
||||||
valid checkers::
|
valid checkers::
|
||||||
|
|
||||||
file=1 # is a file
|
file = 1 # is a file
|
||||||
file=0 # is not a file (may not even exist)
|
file = 0 # is not a file (may not even exist)
|
||||||
dir=1 # is a dir
|
dir = 1 # is a dir
|
||||||
link=1 # is a link
|
link = 1 # is a link
|
||||||
exists=1 # exists
|
exists = 1 # exists
|
||||||
|
|
||||||
You can specify multiple checker definitions, for example::
|
You can specify multiple checker definitions, for example::
|
||||||
|
|
||||||
|
@ -1100,9 +1105,7 @@ class LocalPath:
|
||||||
modname = self.purebasename
|
modname = self.purebasename
|
||||||
spec = importlib.util.spec_from_file_location(modname, str(self))
|
spec = importlib.util.spec_from_file_location(modname, str(self))
|
||||||
if spec is None or spec.loader is None:
|
if spec is None or spec.loader is None:
|
||||||
raise ImportError(
|
raise ImportError(f"Can't find module {modname} at location {self!s}")
|
||||||
f"Can't find module {modname} at location {str(self)}"
|
|
||||||
)
|
|
||||||
mod = importlib.util.module_from_spec(spec)
|
mod = importlib.util.module_from_spec(spec)
|
||||||
spec.loader.exec_module(mod)
|
spec.loader.exec_module(mod)
|
||||||
return mod
|
return mod
|
||||||
|
@ -1167,7 +1170,8 @@ class LocalPath:
|
||||||
where the 'self' path points to executable.
|
where the 'self' path points to executable.
|
||||||
The process is directly invoked and not through a system shell.
|
The process is directly invoked and not through a system shell.
|
||||||
"""
|
"""
|
||||||
from subprocess import Popen, PIPE
|
from subprocess import PIPE
|
||||||
|
from subprocess import Popen
|
||||||
|
|
||||||
popen_opts.pop("stdout", None)
|
popen_opts.pop("stdout", None)
|
||||||
popen_opts.pop("stderr", None)
|
popen_opts.pop("stderr", None)
|
||||||
|
@ -1277,7 +1281,8 @@ class LocalPath:
|
||||||
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
|
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
|
||||||
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
|
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
|
||||||
path = error.checked_call(
|
path = error.checked_call(
|
||||||
tempfile.mkdtemp, dir=str(rootdir) # type:ignore[arg-type]
|
tempfile.mkdtemp,
|
||||||
|
dir=str(rootdir), # type:ignore[arg-type]
|
||||||
)
|
)
|
||||||
return cls(path)
|
return cls(path)
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Support for presenting detailed information in failing assertions."""
|
"""Support for presenting detailed information in failing assertions."""
|
||||||
import sys
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -15,6 +16,7 @@ from _pytest.config import hookimpl
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
|
|
||||||
|
@ -128,7 +130,6 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
|
||||||
reporting via the pytest_assertrepr_compare hook. This sets up this custom
|
reporting via the pytest_assertrepr_compare hook. This sets up this custom
|
||||||
comparison for the test.
|
comparison for the test.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ihook = item.ihook
|
ihook = item.ihook
|
||||||
|
|
||||||
def callbinrepr(op, left: object, right: object) -> Optional[str]:
|
def callbinrepr(op, left: object, right: object) -> Optional[str]:
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""Rewrite assertion AST to produce nice error messages."""
|
"""Rewrite assertion AST to produce nice error messages."""
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
|
from collections import defaultdict
|
||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
import importlib.abc
|
import importlib.abc
|
||||||
|
@ -9,13 +11,12 @@ import io
|
||||||
import itertools
|
import itertools
|
||||||
import marshal
|
import marshal
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from pathlib import PurePath
|
||||||
import struct
|
import struct
|
||||||
import sys
|
import sys
|
||||||
import tokenize
|
import tokenize
|
||||||
import types
|
import types
|
||||||
from collections import defaultdict
|
|
||||||
from pathlib import Path
|
|
||||||
from pathlib import PurePath
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
@ -33,15 +34,17 @@ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest._version import version
|
from _pytest._version import version
|
||||||
from _pytest.assertion import util
|
from _pytest.assertion import util
|
||||||
from _pytest.assertion.util import ( # noqa: F401
|
|
||||||
format_explanation as _format_explanation,
|
|
||||||
)
|
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.main import Session
|
from _pytest.main import Session
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
from _pytest.pathlib import fnmatch_ex
|
from _pytest.pathlib import fnmatch_ex
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
|
from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip
|
||||||
|
# fmt:on
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest.assertion import AssertionState
|
from _pytest.assertion import AssertionState
|
||||||
|
|
||||||
|
@ -858,9 +861,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
the expression is false.
|
the expression is false.
|
||||||
"""
|
"""
|
||||||
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
|
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
|
||||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||||
|
|
||||||
# TODO: This assert should not be needed.
|
# TODO: This assert should not be needed.
|
||||||
assert self.module_path is not None
|
assert self.module_path is not None
|
||||||
warnings.warn_explicit(
|
warnings.warn_explicit(
|
||||||
|
@ -921,7 +925,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
# If any hooks implement assert_pass hook
|
# If any hooks implement assert_pass hook
|
||||||
hook_impl_test = ast.If(
|
hook_impl_test = ast.If(
|
||||||
self.helper("_check_if_assertion_pass_impl"),
|
self.helper("_check_if_assertion_pass_impl"),
|
||||||
self.expl_stmts + [hook_call_pass],
|
[*self.expl_stmts, hook_call_pass],
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
statements_pass = [hook_impl_test]
|
statements_pass = [hook_impl_test]
|
||||||
|
@ -1002,7 +1006,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
if i:
|
if i:
|
||||||
fail_inner: List[ast.stmt] = []
|
fail_inner: List[ast.stmt] = []
|
||||||
# cond is set in a prior loop iteration below
|
# cond is set in a prior loop iteration below
|
||||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
|
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
|
||||||
self.expl_stmts = fail_inner
|
self.expl_stmts = fail_inner
|
||||||
# Check if the left operand is a ast.NamedExpr and the value has already been visited
|
# Check if the left operand is a ast.NamedExpr and the value has already been visited
|
||||||
if (
|
if (
|
||||||
|
@ -1016,9 +1020,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
pytest_temp = self.variable()
|
pytest_temp = self.variable()
|
||||||
self.variables_overwrite[self.scope][
|
self.variables_overwrite[self.scope][v.left.target.id] = v.left # type:ignore[assignment]
|
||||||
v.left.target.id
|
|
||||||
] = v.left # type:ignore[assignment]
|
|
||||||
v.left.target.id = pytest_temp
|
v.left.target.id = pytest_temp
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
res, expl = self.visit(v)
|
res, expl = self.visit(v)
|
||||||
|
@ -1062,9 +1064,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||||
self.scope, {}
|
self.scope, {}
|
||||||
):
|
):
|
||||||
arg = self.variables_overwrite[self.scope][
|
arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
|
||||||
arg.id
|
|
||||||
] # type:ignore[assignment]
|
|
||||||
res, expl = self.visit(arg)
|
res, expl = self.visit(arg)
|
||||||
arg_expls.append(expl)
|
arg_expls.append(expl)
|
||||||
new_args.append(res)
|
new_args.append(res)
|
||||||
|
@ -1072,9 +1072,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
if isinstance(
|
if isinstance(
|
||||||
keyword.value, ast.Name
|
keyword.value, ast.Name
|
||||||
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
||||||
keyword.value = self.variables_overwrite[self.scope][
|
keyword.value = self.variables_overwrite[self.scope][keyword.value.id] # type:ignore[assignment]
|
||||||
keyword.value.id
|
|
||||||
] # type:ignore[assignment]
|
|
||||||
res, expl = self.visit(keyword.value)
|
res, expl = self.visit(keyword.value)
|
||||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||||
if keyword.arg:
|
if keyword.arg:
|
||||||
|
@ -1111,13 +1109,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
if isinstance(
|
if isinstance(
|
||||||
comp.left, ast.Name
|
comp.left, ast.Name
|
||||||
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
|
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
|
||||||
comp.left = self.variables_overwrite[self.scope][
|
comp.left = self.variables_overwrite[self.scope][comp.left.id] # type:ignore[assignment]
|
||||||
comp.left.id
|
|
||||||
] # type:ignore[assignment]
|
|
||||||
if isinstance(comp.left, ast.NamedExpr):
|
if isinstance(comp.left, ast.NamedExpr):
|
||||||
self.variables_overwrite[self.scope][
|
self.variables_overwrite[self.scope][comp.left.target.id] = comp.left # type:ignore[assignment]
|
||||||
comp.left.target.id
|
|
||||||
] = comp.left # type:ignore[assignment]
|
|
||||||
left_res, left_expl = self.visit(comp.left)
|
left_res, left_expl = self.visit(comp.left)
|
||||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||||
left_expl = f"({left_expl})"
|
left_expl = f"({left_expl})"
|
||||||
|
@ -1135,9 +1129,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
and next_operand.target.id == left_res.id
|
and next_operand.target.id == left_res.id
|
||||||
):
|
):
|
||||||
next_operand.target.id = self.variable()
|
next_operand.target.id = self.variable()
|
||||||
self.variables_overwrite[self.scope][
|
self.variables_overwrite[self.scope][left_res.id] = next_operand # type:ignore[assignment]
|
||||||
left_res.id
|
|
||||||
] = next_operand # type:ignore[assignment]
|
|
||||||
next_res, next_expl = self.visit(next_operand)
|
next_res, next_expl = self.visit(next_operand)
|
||||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||||
next_expl = f"({next_expl})"
|
next_expl = f"({next_expl})"
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
Current default behaviour is to truncate assertion explanations at
|
Current default behaviour is to truncate assertion explanations at
|
||||||
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
|
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
@ -91,7 +92,8 @@ def _truncate_explanation(
|
||||||
else:
|
else:
|
||||||
# Add proper ellipsis when we were able to fit a full line exactly
|
# Add proper ellipsis when we were able to fit a full line exactly
|
||||||
truncated_explanation[-1] = "..."
|
truncated_explanation[-1] = "..."
|
||||||
return truncated_explanation + [
|
return [
|
||||||
|
*truncated_explanation,
|
||||||
"",
|
"",
|
||||||
f"...Full output truncated ({truncated_line_count} line"
|
f"...Full output truncated ({truncated_line_count} line"
|
||||||
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
|
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Utilities for assertion debugging."""
|
"""Utilities for assertion debugging."""
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import os
|
import os
|
||||||
|
@ -14,13 +15,14 @@ from typing import Protocol
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from unicodedata import normalize
|
from unicodedata import normalize
|
||||||
|
|
||||||
import _pytest._code
|
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
|
import _pytest._code
|
||||||
from _pytest._io.pprint import PrettyPrinter
|
from _pytest._io.pprint import PrettyPrinter
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
from _pytest._io.saferepr import saferepr_unlimited
|
from _pytest._io.saferepr import saferepr_unlimited
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
|
||||||
|
|
||||||
# The _reprcompare attribute on the util module is used by the new assertion
|
# The _reprcompare attribute on the util module is used by the new assertion
|
||||||
# interpretation code and assertion rewriter to detect this plugin was
|
# interpretation code and assertion rewriter to detect this plugin was
|
||||||
# loaded and in turn call the hooks defined here as part of the
|
# loaded and in turn call the hooks defined here as part of the
|
||||||
|
@ -231,8 +233,8 @@ def assertrepr_compare(
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if explanation[0] != "":
|
if explanation[0] != "":
|
||||||
explanation = [""] + explanation
|
explanation = ["", *explanation]
|
||||||
return [summary] + explanation
|
return [summary, *explanation]
|
||||||
|
|
||||||
|
|
||||||
def _compare_eq_any(
|
def _compare_eq_any(
|
||||||
|
@ -301,8 +303,8 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
|
||||||
if i > 42:
|
if i > 42:
|
||||||
i -= 10 # Provide some context
|
i -= 10 # Provide some context
|
||||||
explanation += [
|
explanation += [
|
||||||
"Skipping {} identical trailing "
|
f"Skipping {i} identical trailing "
|
||||||
"characters in diff, use -v to show".format(i)
|
"characters in diff, use -v to show"
|
||||||
]
|
]
|
||||||
left = left[:-i]
|
left = left[:-i]
|
||||||
right = right[:-i]
|
right = right[:-i]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Implementation of the cache provider."""
|
"""Implementation of the cache provider."""
|
||||||
# This plugin was not named "cache" to avoid conflicts with the external
|
# This plugin was not named "cache" to avoid conflicts with the external
|
||||||
# pytest-cache version.
|
# pytest-cache version.
|
||||||
|
@ -31,6 +32,7 @@ from _pytest.nodes import Directory
|
||||||
from _pytest.nodes import File
|
from _pytest.nodes import File
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
|
|
||||||
|
|
||||||
README_CONTENT = """\
|
README_CONTENT = """\
|
||||||
# pytest cache directory #
|
# pytest cache directory #
|
||||||
|
|
||||||
|
@ -111,6 +113,7 @@ class Cache:
|
||||||
"""
|
"""
|
||||||
check_ispytest(_ispytest)
|
check_ispytest(_ispytest)
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from _pytest.warning_types import PytestCacheWarning
|
from _pytest.warning_types import PytestCacheWarning
|
||||||
|
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
|
@ -366,15 +369,13 @@ class LFPlugin:
|
||||||
|
|
||||||
noun = "failure" if self._previously_failed_count == 1 else "failures"
|
noun = "failure" if self._previously_failed_count == 1 else "failures"
|
||||||
suffix = " first" if self.config.getoption("failedfirst") else ""
|
suffix = " first" if self.config.getoption("failedfirst") else ""
|
||||||
self._report_status = "rerun previous {count} {noun}{suffix}".format(
|
self._report_status = (
|
||||||
count=self._previously_failed_count, suffix=suffix, noun=noun
|
f"rerun previous {self._previously_failed_count} {noun}{suffix}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._skipped_files > 0:
|
if self._skipped_files > 0:
|
||||||
files_noun = "file" if self._skipped_files == 1 else "files"
|
files_noun = "file" if self._skipped_files == 1 else "files"
|
||||||
self._report_status += " (skipped {files} {files_noun})".format(
|
self._report_status += f" (skipped {self._skipped_files} {files_noun})"
|
||||||
files=self._skipped_files, files_noun=files_noun
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
self._report_status = "no previously failed tests, "
|
self._report_status = "no previously failed tests, "
|
||||||
if self.config.getoption("last_failed_no_failures") == "none":
|
if self.config.getoption("last_failed_no_failures") == "none":
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Per-test stdout/stderr capturing mechanism."""
|
"""Per-test stdout/stderr capturing mechanism."""
|
||||||
import abc
|
import abc
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import io
|
import io
|
||||||
|
from io import UnsupportedOperation
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from io import UnsupportedOperation
|
|
||||||
from tempfile import TemporaryFile
|
from tempfile import TemporaryFile
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -38,6 +39,7 @@ from _pytest.nodes import File
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
from _pytest.reports import CollectReport
|
from _pytest.reports import CollectReport
|
||||||
|
|
||||||
|
|
||||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -596,7 +598,8 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
||||||
else:
|
else:
|
||||||
|
|
||||||
class CaptureResult(
|
class CaptureResult(
|
||||||
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
|
||||||
|
Generic[AnyStr],
|
||||||
):
|
):
|
||||||
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
||||||
|
|
||||||
|
@ -789,9 +792,7 @@ class CaptureManager:
|
||||||
current_fixture = self._capture_fixture.request.fixturename
|
current_fixture = self._capture_fixture.request.fixturename
|
||||||
requested_fixture = capture_fixture.request.fixturename
|
requested_fixture = capture_fixture.request.fixturename
|
||||||
capture_fixture.request.raiseerror(
|
capture_fixture.request.raiseerror(
|
||||||
"cannot use {} and {} at the same time".format(
|
f"cannot use {requested_fixture} and {current_fixture} at the same time"
|
||||||
requested_fixture, current_fixture
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
self._capture_fixture = capture_fixture
|
self._capture_fixture = capture_fixture
|
||||||
|
|
||||||
|
@ -987,7 +988,6 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_output(capsys):
|
def test_output(capsys):
|
||||||
|
@ -1015,7 +1015,6 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
|
||||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_output(capsysbinary):
|
def test_output(capsysbinary):
|
||||||
|
@ -1043,7 +1042,6 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_system_echo(capfd):
|
def test_system_echo(capfd):
|
||||||
|
@ -1071,7 +1069,6 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
|
||||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
def test_system_echo(capfdbinary):
|
def test_system_echo(capfdbinary):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Python version compatibility code."""
|
"""Python version compatibility code."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
@ -5,20 +6,15 @@ import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from inspect import Parameter
|
from inspect import Parameter
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Final
|
from typing import Final
|
||||||
from typing import NoReturn
|
from typing import NoReturn
|
||||||
from typing import TypeVar
|
|
||||||
|
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
|
||||||
_S = TypeVar("_S")
|
|
||||||
|
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
|
@ -26,7 +22,7 @@ _S = TypeVar("_S")
|
||||||
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||||
class NotSetType(enum.Enum):
|
class NotSetType(enum.Enum):
|
||||||
token = 0
|
token = 0
|
||||||
NOTSET: Final = NotSetType.token # noqa: E305
|
NOTSET: Final = NotSetType.token
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
|
@ -176,25 +172,13 @@ _non_printable_ascii_translate_table.update(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _translate_non_printable(s: str) -> str:
|
|
||||||
return s.translate(_non_printable_ascii_translate_table)
|
|
||||||
|
|
||||||
|
|
||||||
STRING_TYPES = bytes, str
|
|
||||||
|
|
||||||
|
|
||||||
def _bytes_to_ascii(val: bytes) -> str:
|
|
||||||
return val.decode("ascii", "backslashreplace")
|
|
||||||
|
|
||||||
|
|
||||||
def ascii_escaped(val: bytes | str) -> str:
|
def ascii_escaped(val: bytes | str) -> str:
|
||||||
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
||||||
bytes objects into a sequence of escaped bytes:
|
bytes objects into a sequence of escaped bytes:
|
||||||
|
|
||||||
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
|
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
|
||||||
|
|
||||||
and escapes unicode objects into a sequence of escaped unicode
|
and escapes strings into a sequence of escaped unicode ids, e.g.:
|
||||||
ids, e.g.:
|
|
||||||
|
|
||||||
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
|
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
|
||||||
|
|
||||||
|
@ -205,10 +189,10 @@ def ascii_escaped(val: bytes | str) -> str:
|
||||||
a UTF-8 string.
|
a UTF-8 string.
|
||||||
"""
|
"""
|
||||||
if isinstance(val, bytes):
|
if isinstance(val, bytes):
|
||||||
ret = _bytes_to_ascii(val)
|
ret = val.decode("ascii", "backslashreplace")
|
||||||
else:
|
else:
|
||||||
ret = val.encode("unicode_escape").decode("ascii")
|
ret = val.encode("unicode_escape").decode("ascii")
|
||||||
return _translate_non_printable(ret)
|
return ret.translate(_non_printable_ascii_translate_table)
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
|
@ -243,9 +227,7 @@ def get_real_func(obj):
|
||||||
from _pytest._io.saferepr import saferepr
|
from _pytest._io.saferepr import saferepr
|
||||||
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
("could not find real function of {start}\nstopped at {current}").format(
|
f"could not find real function of {saferepr(start_obj)}\nstopped at {saferepr(obj)}"
|
||||||
start=saferepr(start_obj), current=saferepr(obj)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if isinstance(obj, functools.partial):
|
if isinstance(obj, functools.partial):
|
||||||
obj = obj.func
|
obj = obj.func
|
||||||
|
@ -307,7 +289,7 @@ def get_user_id() -> int | None:
|
||||||
# mypy follows the version and platform checking expectation of PEP 484:
|
# mypy follows the version and platform checking expectation of PEP 484:
|
||||||
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
||||||
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
||||||
if sys.platform == "win32" or sys.platform == "emscripten":
|
if sys.platform == "win32" or sys.platform == "emscripten": # noqa: PLR1714
|
||||||
# win32 does not have a getuid() function.
|
# win32 does not have a getuid() function.
|
||||||
# Emscripten has a return 0 stub.
|
# Emscripten has a return 0 stub.
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -1,23 +1,22 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Command line options, ini-file and conftest.py processing."""
|
"""Command line options, ini-file and conftest.py processing."""
|
||||||
import argparse
|
import argparse
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import copy
|
import copy
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
|
from functools import lru_cache
|
||||||
import glob
|
import glob
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
import types
|
|
||||||
import warnings
|
|
||||||
from functools import lru_cache
|
|
||||||
from pathlib import Path
|
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
import types
|
||||||
from types import FunctionType
|
from types import FunctionType
|
||||||
from types import TracebackType
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
@ -37,6 +36,7 @@ from typing import Tuple
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
import warnings
|
||||||
|
|
||||||
from pluggy import HookimplMarker
|
from pluggy import HookimplMarker
|
||||||
from pluggy import HookimplOpts
|
from pluggy import HookimplOpts
|
||||||
|
@ -44,15 +44,15 @@ from pluggy import HookspecMarker
|
||||||
from pluggy import HookspecOpts
|
from pluggy import HookspecOpts
|
||||||
from pluggy import PluginManager
|
from pluggy import PluginManager
|
||||||
|
|
||||||
import _pytest._code
|
|
||||||
import _pytest.deprecated
|
|
||||||
import _pytest.hookspec
|
|
||||||
from .exceptions import PrintHelp as PrintHelp
|
from .exceptions import PrintHelp as PrintHelp
|
||||||
from .exceptions import UsageError as UsageError
|
from .exceptions import UsageError as UsageError
|
||||||
from .findpaths import determine_setup
|
from .findpaths import determine_setup
|
||||||
|
import _pytest._code
|
||||||
from _pytest._code import ExceptionInfo
|
from _pytest._code import ExceptionInfo
|
||||||
from _pytest._code import filter_traceback
|
from _pytest._code import filter_traceback
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
|
import _pytest.deprecated
|
||||||
|
import _pytest.hookspec
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
|
@ -65,10 +65,12 @@ from _pytest.stash import Stash
|
||||||
from _pytest.warning_types import PytestConfigWarning
|
from _pytest.warning_types import PytestConfigWarning
|
||||||
from _pytest.warning_types import warn_explicit_for
|
from _pytest.warning_types import warn_explicit_for
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from .argparsing import Argument
|
||||||
|
from .argparsing import Parser
|
||||||
from _pytest._code.code import _TracebackStyle
|
from _pytest._code.code import _TracebackStyle
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
from .argparsing import Argument, Parser
|
|
||||||
|
|
||||||
|
|
||||||
_PluggyPlugin = object
|
_PluggyPlugin = object
|
||||||
|
@ -112,16 +114,14 @@ class ConftestImportFailure(Exception):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
path: Path,
|
path: Path,
|
||||||
excinfo: Tuple[Type[Exception], Exception, TracebackType],
|
*,
|
||||||
|
cause: Exception,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(path, excinfo)
|
|
||||||
self.path = path
|
self.path = path
|
||||||
self.excinfo = excinfo
|
self.cause = cause
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "{}: {} (from {})".format(
|
return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
|
||||||
self.excinfo[0].__name__, self.excinfo[1], self.path
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def filter_traceback_for_conftest_import_failure(
|
def filter_traceback_for_conftest_import_failure(
|
||||||
|
@ -152,7 +152,7 @@ def main(
|
||||||
try:
|
try:
|
||||||
config = _prepareconfig(args, plugins)
|
config = _prepareconfig(args, plugins)
|
||||||
except ConftestImportFailure as e:
|
except ConftestImportFailure as e:
|
||||||
exc_info = ExceptionInfo.from_exc_info(e.excinfo)
|
exc_info = ExceptionInfo.from_exception(e.cause)
|
||||||
tw = TerminalWriter(sys.stderr)
|
tw = TerminalWriter(sys.stderr)
|
||||||
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
|
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
|
||||||
exc_info.traceback = exc_info.traceback.filter(
|
exc_info.traceback = exc_info.traceback.filter(
|
||||||
|
@ -238,7 +238,8 @@ essential_plugins = (
|
||||||
"helpconfig", # Provides -p.
|
"helpconfig", # Provides -p.
|
||||||
)
|
)
|
||||||
|
|
||||||
default_plugins = essential_plugins + (
|
default_plugins = (
|
||||||
|
*essential_plugins,
|
||||||
"python",
|
"python",
|
||||||
"terminal",
|
"terminal",
|
||||||
"debugging",
|
"debugging",
|
||||||
|
@ -493,15 +494,19 @@ class PytestPluginManager(PluginManager):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
ret: Optional[str] = super().register(plugin, name)
|
plugin_name = super().register(plugin, name)
|
||||||
if ret:
|
if plugin_name is not None:
|
||||||
self.hook.pytest_plugin_registered.call_historic(
|
self.hook.pytest_plugin_registered.call_historic(
|
||||||
kwargs=dict(plugin=plugin, manager=self)
|
kwargs=dict(
|
||||||
|
plugin=plugin,
|
||||||
|
plugin_name=plugin_name,
|
||||||
|
manager=self,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(plugin, types.ModuleType):
|
if isinstance(plugin, types.ModuleType):
|
||||||
self.consider_module(plugin)
|
self.consider_module(plugin)
|
||||||
return ret
|
return plugin_name
|
||||||
|
|
||||||
def getplugin(self, name: str):
|
def getplugin(self, name: str):
|
||||||
# Support deprecated naming because plugins (xdist e.g.) use it.
|
# Support deprecated naming because plugins (xdist e.g.) use it.
|
||||||
|
@ -540,6 +545,7 @@ class PytestPluginManager(PluginManager):
|
||||||
noconftest: bool,
|
noconftest: bool,
|
||||||
rootpath: Path,
|
rootpath: Path,
|
||||||
confcutdir: Optional[Path],
|
confcutdir: Optional[Path],
|
||||||
|
invocation_dir: Path,
|
||||||
importmode: Union[ImportMode, str],
|
importmode: Union[ImportMode, str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Load initial conftest files given a preparsed "namespace".
|
"""Load initial conftest files given a preparsed "namespace".
|
||||||
|
@ -549,8 +555,9 @@ class PytestPluginManager(PluginManager):
|
||||||
All builtin and 3rd party plugins will have been loaded, however, so
|
All builtin and 3rd party plugins will have been loaded, however, so
|
||||||
common options will not confuse our logic here.
|
common options will not confuse our logic here.
|
||||||
"""
|
"""
|
||||||
current = Path.cwd()
|
self._confcutdir = (
|
||||||
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
|
absolutepath(invocation_dir / confcutdir) if confcutdir else None
|
||||||
|
)
|
||||||
self._noconftest = noconftest
|
self._noconftest = noconftest
|
||||||
self._using_pyargs = pyargs
|
self._using_pyargs = pyargs
|
||||||
foundanchor = False
|
foundanchor = False
|
||||||
|
@ -560,7 +567,7 @@ class PytestPluginManager(PluginManager):
|
||||||
i = path.find("::")
|
i = path.find("::")
|
||||||
if i != -1:
|
if i != -1:
|
||||||
path = path[:i]
|
path = path[:i]
|
||||||
anchor = absolutepath(current / path)
|
anchor = absolutepath(invocation_dir / path)
|
||||||
|
|
||||||
# Ensure we do not break if what appears to be an anchor
|
# Ensure we do not break if what appears to be an anchor
|
||||||
# is in fact a very long option (#10169, #11394).
|
# is in fact a very long option (#10169, #11394).
|
||||||
|
@ -568,15 +575,21 @@ class PytestPluginManager(PluginManager):
|
||||||
self._try_load_conftest(anchor, importmode, rootpath)
|
self._try_load_conftest(anchor, importmode, rootpath)
|
||||||
foundanchor = True
|
foundanchor = True
|
||||||
if not foundanchor:
|
if not foundanchor:
|
||||||
self._try_load_conftest(current, importmode, rootpath)
|
self._try_load_conftest(invocation_dir, importmode, rootpath)
|
||||||
|
|
||||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||||
"""Whether a path is within the confcutdir.
|
"""Whether to consider the given path to load conftests from."""
|
||||||
|
|
||||||
When false, should not load conftest.
|
|
||||||
"""
|
|
||||||
if self._confcutdir is None:
|
if self._confcutdir is None:
|
||||||
return True
|
return True
|
||||||
|
# The semantics here are literally:
|
||||||
|
# Do not load a conftest if it is found upwards from confcut dir.
|
||||||
|
# But this is *not* the same as:
|
||||||
|
# Load only conftests from confcutdir or below.
|
||||||
|
# At first glance they might seem the same thing, however we do support use cases where
|
||||||
|
# we want to load conftests that are not found in confcutdir or below, but are found
|
||||||
|
# in completely different directory hierarchies like packages installed
|
||||||
|
# in out-of-source trees.
|
||||||
|
# (see #9767 for a regression where the logic was inverted).
|
||||||
return path not in self._confcutdir.parents
|
return path not in self._confcutdir.parents
|
||||||
|
|
||||||
def _try_load_conftest(
|
def _try_load_conftest(
|
||||||
|
@ -602,9 +615,6 @@ class PytestPluginManager(PluginManager):
|
||||||
if directory in self._dirpath2confmods:
|
if directory in self._dirpath2confmods:
|
||||||
return
|
return
|
||||||
|
|
||||||
# XXX these days we may rather want to use config.rootpath
|
|
||||||
# and allow users to opt into looking into the rootdir parent
|
|
||||||
# directories instead of requiring to specify confcutdir.
|
|
||||||
clist = []
|
clist = []
|
||||||
for parent in reversed((directory, *directory.parents)):
|
for parent in reversed((directory, *directory.parents)):
|
||||||
if self._is_in_confcutdir(parent):
|
if self._is_in_confcutdir(parent):
|
||||||
|
@ -634,7 +644,8 @@ class PytestPluginManager(PluginManager):
|
||||||
def _importconftest(
|
def _importconftest(
|
||||||
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||||
) -> types.ModuleType:
|
) -> types.ModuleType:
|
||||||
existing = self.get_plugin(str(conftestpath))
|
conftestpath_plugin_name = str(conftestpath)
|
||||||
|
existing = self.get_plugin(conftestpath_plugin_name)
|
||||||
if existing is not None:
|
if existing is not None:
|
||||||
return cast(types.ModuleType, existing)
|
return cast(types.ModuleType, existing)
|
||||||
|
|
||||||
|
@ -653,8 +664,7 @@ class PytestPluginManager(PluginManager):
|
||||||
mod = import_path(conftestpath, mode=importmode, root=rootpath)
|
mod = import_path(conftestpath, mode=importmode, root=rootpath)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
assert e.__traceback__ is not None
|
assert e.__traceback__ is not None
|
||||||
exc_info = (type(e), e, e.__traceback__)
|
raise ConftestImportFailure(conftestpath, cause=e) from e
|
||||||
raise ConftestImportFailure(conftestpath, exc_info) from e
|
|
||||||
|
|
||||||
self._check_non_top_pytest_plugins(mod, conftestpath)
|
self._check_non_top_pytest_plugins(mod, conftestpath)
|
||||||
|
|
||||||
|
@ -663,10 +673,15 @@ class PytestPluginManager(PluginManager):
|
||||||
if dirpath in self._dirpath2confmods:
|
if dirpath in self._dirpath2confmods:
|
||||||
for path, mods in self._dirpath2confmods.items():
|
for path, mods in self._dirpath2confmods.items():
|
||||||
if dirpath in path.parents or path == dirpath:
|
if dirpath in path.parents or path == dirpath:
|
||||||
assert mod not in mods
|
if mod in mods:
|
||||||
|
raise AssertionError(
|
||||||
|
f"While trying to load conftest path {conftestpath!s}, "
|
||||||
|
f"found that the module {mod} is already loaded with path {mod.__file__}. "
|
||||||
|
"This is not supposed to happen. Please report this issue to pytest."
|
||||||
|
)
|
||||||
mods.append(mod)
|
mods.append(mod)
|
||||||
self.trace(f"loading conftestmodule {mod!r}")
|
self.trace(f"loading conftestmodule {mod!r}")
|
||||||
self.consider_conftest(mod)
|
self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
|
||||||
return mod
|
return mod
|
||||||
|
|
||||||
def _check_non_top_pytest_plugins(
|
def _check_non_top_pytest_plugins(
|
||||||
|
@ -737,18 +752,17 @@ class PytestPluginManager(PluginManager):
|
||||||
self.set_blocked("pytest_" + name)
|
self.set_blocked("pytest_" + name)
|
||||||
else:
|
else:
|
||||||
name = arg
|
name = arg
|
||||||
# Unblock the plugin. None indicates that it has been blocked.
|
# Unblock the plugin.
|
||||||
# There is no interface with pluggy for this.
|
self.unblock(name)
|
||||||
if self._name2plugin.get(name, -1) is None:
|
|
||||||
del self._name2plugin[name]
|
|
||||||
if not name.startswith("pytest_"):
|
if not name.startswith("pytest_"):
|
||||||
if self._name2plugin.get("pytest_" + name, -1) is None:
|
self.unblock("pytest_" + name)
|
||||||
del self._name2plugin["pytest_" + name]
|
|
||||||
self.import_plugin(arg, consider_entry_points=True)
|
self.import_plugin(arg, consider_entry_points=True)
|
||||||
|
|
||||||
def consider_conftest(self, conftestmodule: types.ModuleType) -> None:
|
def consider_conftest(
|
||||||
|
self, conftestmodule: types.ModuleType, registration_name: str
|
||||||
|
) -> None:
|
||||||
""":meta private:"""
|
""":meta private:"""
|
||||||
self.register(conftestmodule, name=conftestmodule.__file__)
|
self.register(conftestmodule, name=registration_name)
|
||||||
|
|
||||||
def consider_env(self) -> None:
|
def consider_env(self) -> None:
|
||||||
""":meta private:"""
|
""":meta private:"""
|
||||||
|
@ -804,7 +818,7 @@ class PytestPluginManager(PluginManager):
|
||||||
|
|
||||||
|
|
||||||
def _get_plugin_specs_as_list(
|
def _get_plugin_specs_as_list(
|
||||||
specs: Union[None, types.ModuleType, str, Sequence[str]]
|
specs: Union[None, types.ModuleType, str, Sequence[str]],
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""Parse a plugins specification into a list of plugin names."""
|
"""Parse a plugins specification into a list of plugin names."""
|
||||||
# None means empty.
|
# None means empty.
|
||||||
|
@ -965,7 +979,8 @@ class Config:
|
||||||
*,
|
*,
|
||||||
invocation_params: Optional[InvocationParams] = None,
|
invocation_params: Optional[InvocationParams] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
from .argparsing import Parser, FILE_OR_DIR
|
from .argparsing import FILE_OR_DIR
|
||||||
|
from .argparsing import Parser
|
||||||
|
|
||||||
if invocation_params is None:
|
if invocation_params is None:
|
||||||
invocation_params = self.InvocationParams(
|
invocation_params = self.InvocationParams(
|
||||||
|
@ -1160,6 +1175,7 @@ class Config:
|
||||||
noconftest=early_config.known_args_namespace.noconftest,
|
noconftest=early_config.known_args_namespace.noconftest,
|
||||||
rootpath=early_config.rootpath,
|
rootpath=early_config.rootpath,
|
||||||
confcutdir=early_config.known_args_namespace.confcutdir,
|
confcutdir=early_config.known_args_namespace.confcutdir,
|
||||||
|
invocation_dir=early_config.invocation_params.dir,
|
||||||
importmode=early_config.known_args_namespace.importmode,
|
importmode=early_config.known_args_namespace.importmode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1168,8 +1184,8 @@ class Config:
|
||||||
args, namespace=copy.copy(self.option)
|
args, namespace=copy.copy(self.option)
|
||||||
)
|
)
|
||||||
rootpath, inipath, inicfg = determine_setup(
|
rootpath, inipath, inicfg = determine_setup(
|
||||||
ns.inifilename,
|
inifile=ns.inifilename,
|
||||||
ns.file_or_dir + unknown_args,
|
args=ns.file_or_dir + unknown_args,
|
||||||
rootdir_cmd_arg=ns.rootdir or None,
|
rootdir_cmd_arg=ns.rootdir or None,
|
||||||
invocation_dir=self.invocation_params.dir,
|
invocation_dir=self.invocation_params.dir,
|
||||||
)
|
)
|
||||||
|
@ -1253,6 +1269,8 @@ class Config:
|
||||||
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
|
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
|
||||||
|
|
||||||
:param warn: Whether can issue warnings.
|
:param warn: Whether can issue warnings.
|
||||||
|
|
||||||
|
:returns: The args and the args source. Guaranteed to be non-empty.
|
||||||
"""
|
"""
|
||||||
if args:
|
if args:
|
||||||
source = Config.ArgsSource.ARGS
|
source = Config.ArgsSource.ARGS
|
||||||
|
@ -1361,12 +1379,7 @@ class Config:
|
||||||
|
|
||||||
if Version(minver) > Version(pytest.__version__):
|
if Version(minver) > Version(pytest.__version__):
|
||||||
raise pytest.UsageError(
|
raise pytest.UsageError(
|
||||||
"%s: 'minversion' requires pytest-%s, actual pytest-%s'"
|
f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
|
||||||
% (
|
|
||||||
self.inipath,
|
|
||||||
minver,
|
|
||||||
pytest.__version__,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _validate_config_options(self) -> None:
|
def _validate_config_options(self) -> None:
|
||||||
|
@ -1379,8 +1392,9 @@ class Config:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Imported lazily to improve start-up time.
|
# Imported lazily to improve start-up time.
|
||||||
|
from packaging.requirements import InvalidRequirement
|
||||||
|
from packaging.requirements import Requirement
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
from packaging.requirements import InvalidRequirement, Requirement
|
|
||||||
|
|
||||||
plugin_info = self.pluginmanager.list_plugin_distinfo()
|
plugin_info = self.pluginmanager.list_plugin_distinfo()
|
||||||
plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
|
plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
|
||||||
|
@ -1552,9 +1566,11 @@ class Config:
|
||||||
# in this case, we already have a list ready to use.
|
# in this case, we already have a list ready to use.
|
||||||
#
|
#
|
||||||
if type == "paths":
|
if type == "paths":
|
||||||
# TODO: This assert is probably not valid in all cases.
|
dp = (
|
||||||
assert self.inipath is not None
|
self.inipath.parent
|
||||||
dp = self.inipath.parent
|
if self.inipath is not None
|
||||||
|
else self.invocation_params.dir
|
||||||
|
)
|
||||||
input_values = shlex.split(value) if isinstance(value, str) else value
|
input_values = shlex.split(value) if isinstance(value, str) else value
|
||||||
return [dp / x for x in input_values]
|
return [dp / x for x in input_values]
|
||||||
elif type == "args":
|
elif type == "args":
|
||||||
|
@ -1600,9 +1616,7 @@ class Config:
|
||||||
key, user_ini_value = ini_config.split("=", 1)
|
key, user_ini_value = ini_config.split("=", 1)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise UsageError(
|
raise UsageError(
|
||||||
"-o/--override-ini expects option=value style (got: {!r}).".format(
|
f"-o/--override-ini expects option=value style (got: {ini_config!r})."
|
||||||
ini_config
|
|
||||||
)
|
|
||||||
) from e
|
) from e
|
||||||
else:
|
else:
|
||||||
if key == name:
|
if key == name:
|
||||||
|
@ -1662,7 +1676,6 @@ class Config:
|
||||||
can be used to explicitly use the global verbosity level.
|
can be used to explicitly use the global verbosity level.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
.. code-block:: ini
|
.. code-block:: ini
|
||||||
|
|
||||||
# content of pytest.ini
|
# content of pytest.ini
|
||||||
|
@ -1842,13 +1855,13 @@ def parse_warning_filter(
|
||||||
try:
|
try:
|
||||||
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
|
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||||
except warnings._OptionError as e:
|
except warnings._OptionError as e:
|
||||||
raise UsageError(error_template.format(error=str(e)))
|
raise UsageError(error_template.format(error=str(e))) from None
|
||||||
try:
|
try:
|
||||||
category: Type[Warning] = _resolve_warning_category(category_)
|
category: Type[Warning] = _resolve_warning_category(category_)
|
||||||
except Exception:
|
except Exception:
|
||||||
exc_info = ExceptionInfo.from_current()
|
exc_info = ExceptionInfo.from_current()
|
||||||
exception_text = exc_info.getrepr(style="native")
|
exception_text = exc_info.getrepr(style="native")
|
||||||
raise UsageError(error_template.format(error=exception_text))
|
raise UsageError(error_template.format(error=exception_text)) from None
|
||||||
if message and escape:
|
if message and escape:
|
||||||
message = re.escape(message)
|
message = re.escape(message)
|
||||||
if module and escape:
|
if module and escape:
|
||||||
|
@ -1861,7 +1874,7 @@ def parse_warning_filter(
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
raise UsageError(
|
raise UsageError(
|
||||||
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
|
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
|
||||||
)
|
) from None
|
||||||
else:
|
else:
|
||||||
lineno = 0
|
lineno = 0
|
||||||
return action, message, category, module, lineno
|
return action, message, category, module, lineno
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
import argparse
|
import argparse
|
||||||
|
from gettext import gettext
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from gettext import gettext
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
@ -20,6 +21,7 @@ import _pytest._io
|
||||||
from _pytest.config.exceptions import UsageError
|
from _pytest.config.exceptions import UsageError
|
||||||
from _pytest.deprecated import check_ispytest
|
from _pytest.deprecated import check_ispytest
|
||||||
|
|
||||||
|
|
||||||
FILE_OR_DIR = "file_or_dir"
|
FILE_OR_DIR = "file_or_dir"
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,7 +122,7 @@ class Parser:
|
||||||
from _pytest._argcomplete import filescompleter
|
from _pytest._argcomplete import filescompleter
|
||||||
|
|
||||||
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
|
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
|
||||||
groups = self._groups + [self._anonymous]
|
groups = [*self._groups, self._anonymous]
|
||||||
for group in groups:
|
for group in groups:
|
||||||
if group.options:
|
if group.options:
|
||||||
desc = group.description or group.name
|
desc = group.description or group.name
|
||||||
|
@ -196,9 +198,16 @@ class Parser:
|
||||||
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
|
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
|
||||||
* ``pathlist``: a list of ``py.path``, separated as in a shell
|
* ``pathlist``: a list of ``py.path``, separated as in a shell
|
||||||
|
|
||||||
|
For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file.
|
||||||
|
In case the execution is happening without an ini-file defined,
|
||||||
|
they will be considered relative to the current working directory (for example with ``--override-ini``).
|
||||||
|
|
||||||
.. versionadded:: 7.0
|
.. versionadded:: 7.0
|
||||||
The ``paths`` variable type.
|
The ``paths`` variable type.
|
||||||
|
|
||||||
|
.. versionadded:: 8.1
|
||||||
|
Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of an ini-file.
|
||||||
|
|
||||||
Defaults to ``string`` if ``None`` or not passed.
|
Defaults to ``string`` if ``None`` or not passed.
|
||||||
:param default:
|
:param default:
|
||||||
Default value if no ini-file option exists but is queried.
|
Default value if no ini-file option exists but is queried.
|
||||||
|
@ -215,7 +224,7 @@ class Parser:
|
||||||
|
|
||||||
|
|
||||||
def get_ini_default_for_type(
|
def get_ini_default_for_type(
|
||||||
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]]
|
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]],
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""
|
"""
|
||||||
Used by addini to get the default value for a given ini-option type, when
|
Used by addini to get the default value for a given ini-option type, when
|
||||||
|
@ -447,7 +456,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||||
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
|
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
|
||||||
if not arg_string:
|
if not arg_string:
|
||||||
return None
|
return None
|
||||||
if not arg_string[0] in self.prefix_chars:
|
if arg_string[0] not in self.prefix_chars:
|
||||||
return None
|
return None
|
||||||
if arg_string in self._option_string_actions:
|
if arg_string in self._option_string_actions:
|
||||||
action = self._option_string_actions[arg_string]
|
action = self._option_string_actions[arg_string]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -37,7 +37,6 @@ def load_config_dict_from_file(
|
||||||
|
|
||||||
Return None if the file does not contain valid pytest configuration.
|
Return None if the file does not contain valid pytest configuration.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Configuration from ini files are obtained from the [pytest] section, if present.
|
# Configuration from ini files are obtained from the [pytest] section, if present.
|
||||||
if filepath.suffix == ".ini":
|
if filepath.suffix == ".ini":
|
||||||
iniconfig = _parse_ini_config(filepath)
|
iniconfig = _parse_ini_config(filepath)
|
||||||
|
@ -87,6 +86,7 @@ def load_config_dict_from_file(
|
||||||
|
|
||||||
|
|
||||||
def locate_config(
|
def locate_config(
|
||||||
|
invocation_dir: Path,
|
||||||
args: Iterable[Path],
|
args: Iterable[Path],
|
||||||
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
|
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
|
||||||
"""Search in the list of arguments for a valid ini-file for pytest,
|
"""Search in the list of arguments for a valid ini-file for pytest,
|
||||||
|
@ -100,20 +100,28 @@ def locate_config(
|
||||||
]
|
]
|
||||||
args = [x for x in args if not str(x).startswith("-")]
|
args = [x for x in args if not str(x).startswith("-")]
|
||||||
if not args:
|
if not args:
|
||||||
args = [Path.cwd()]
|
args = [invocation_dir]
|
||||||
|
found_pyproject_toml: Optional[Path] = None
|
||||||
for arg in args:
|
for arg in args:
|
||||||
argpath = absolutepath(arg)
|
argpath = absolutepath(arg)
|
||||||
for base in (argpath, *argpath.parents):
|
for base in (argpath, *argpath.parents):
|
||||||
for config_name in config_names:
|
for config_name in config_names:
|
||||||
p = base / config_name
|
p = base / config_name
|
||||||
if p.is_file():
|
if p.is_file():
|
||||||
|
if p.name == "pyproject.toml" and found_pyproject_toml is None:
|
||||||
|
found_pyproject_toml = p
|
||||||
ini_config = load_config_dict_from_file(p)
|
ini_config = load_config_dict_from_file(p)
|
||||||
if ini_config is not None:
|
if ini_config is not None:
|
||||||
return base, p, ini_config
|
return base, p, ini_config
|
||||||
|
if found_pyproject_toml is not None:
|
||||||
|
return found_pyproject_toml.parent, found_pyproject_toml, {}
|
||||||
return None, None, {}
|
return None, None, {}
|
||||||
|
|
||||||
|
|
||||||
def get_common_ancestor(paths: Iterable[Path]) -> Path:
|
def get_common_ancestor(
|
||||||
|
invocation_dir: Path,
|
||||||
|
paths: Iterable[Path],
|
||||||
|
) -> Path:
|
||||||
common_ancestor: Optional[Path] = None
|
common_ancestor: Optional[Path] = None
|
||||||
for path in paths:
|
for path in paths:
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
|
@ -130,7 +138,7 @@ def get_common_ancestor(paths: Iterable[Path]) -> Path:
|
||||||
if shared is not None:
|
if shared is not None:
|
||||||
common_ancestor = shared
|
common_ancestor = shared
|
||||||
if common_ancestor is None:
|
if common_ancestor is None:
|
||||||
common_ancestor = Path.cwd()
|
common_ancestor = invocation_dir
|
||||||
elif common_ancestor.is_file():
|
elif common_ancestor.is_file():
|
||||||
common_ancestor = common_ancestor.parent
|
common_ancestor = common_ancestor.parent
|
||||||
return common_ancestor
|
return common_ancestor
|
||||||
|
@ -162,10 +170,11 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte
|
||||||
|
|
||||||
|
|
||||||
def determine_setup(
|
def determine_setup(
|
||||||
|
*,
|
||||||
inifile: Optional[str],
|
inifile: Optional[str],
|
||||||
args: Sequence[str],
|
args: Sequence[str],
|
||||||
rootdir_cmd_arg: Optional[str] = None,
|
rootdir_cmd_arg: Optional[str],
|
||||||
invocation_dir: Optional[Path] = None,
|
invocation_dir: Path,
|
||||||
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
|
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
|
||||||
"""Determine the rootdir, inifile and ini configuration values from the
|
"""Determine the rootdir, inifile and ini configuration values from the
|
||||||
command line arguments.
|
command line arguments.
|
||||||
|
@ -177,8 +186,7 @@ def determine_setup(
|
||||||
:param rootdir_cmd_arg:
|
:param rootdir_cmd_arg:
|
||||||
The `--rootdir` command line argument, if given.
|
The `--rootdir` command line argument, if given.
|
||||||
:param invocation_dir:
|
:param invocation_dir:
|
||||||
The working directory when pytest was invoked, if known.
|
The working directory when pytest was invoked.
|
||||||
If not known, the current working directory is used.
|
|
||||||
"""
|
"""
|
||||||
rootdir = None
|
rootdir = None
|
||||||
dirs = get_dirs_from_args(args)
|
dirs = get_dirs_from_args(args)
|
||||||
|
@ -189,8 +197,8 @@ def determine_setup(
|
||||||
if rootdir_cmd_arg is None:
|
if rootdir_cmd_arg is None:
|
||||||
rootdir = inipath_.parent
|
rootdir = inipath_.parent
|
||||||
else:
|
else:
|
||||||
ancestor = get_common_ancestor(dirs)
|
ancestor = get_common_ancestor(invocation_dir, dirs)
|
||||||
rootdir, inipath, inicfg = locate_config([ancestor])
|
rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor])
|
||||||
if rootdir is None and rootdir_cmd_arg is None:
|
if rootdir is None and rootdir_cmd_arg is None:
|
||||||
for possible_rootdir in (ancestor, *ancestor.parents):
|
for possible_rootdir in (ancestor, *ancestor.parents):
|
||||||
if (possible_rootdir / "setup.py").is_file():
|
if (possible_rootdir / "setup.py").is_file():
|
||||||
|
@ -198,22 +206,18 @@ def determine_setup(
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
if dirs != [ancestor]:
|
if dirs != [ancestor]:
|
||||||
rootdir, inipath, inicfg = locate_config(dirs)
|
rootdir, inipath, inicfg = locate_config(invocation_dir, dirs)
|
||||||
if rootdir is None:
|
if rootdir is None:
|
||||||
if invocation_dir is not None:
|
rootdir = get_common_ancestor(
|
||||||
cwd = invocation_dir
|
invocation_dir, [invocation_dir, ancestor]
|
||||||
else:
|
)
|
||||||
cwd = Path.cwd()
|
|
||||||
rootdir = get_common_ancestor([cwd, ancestor])
|
|
||||||
if is_fs_root(rootdir):
|
if is_fs_root(rootdir):
|
||||||
rootdir = ancestor
|
rootdir = ancestor
|
||||||
if rootdir_cmd_arg:
|
if rootdir_cmd_arg:
|
||||||
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
|
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
|
||||||
if not rootdir.is_dir():
|
if not rootdir.is_dir():
|
||||||
raise UsageError(
|
raise UsageError(
|
||||||
"Directory '{}' not found. Check your '--rootdir' option.".format(
|
f"Directory '{rootdir}' not found. Check your '--rootdir' option."
|
||||||
rootdir
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
assert rootdir is not None
|
assert rootdir is not None
|
||||||
return rootdir, inipath, inicfg or {}
|
return rootdir, inipath, inicfg or {}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Interactive debugging with PDB, the Python Debugger."""
|
"""Interactive debugging with PDB, the Python Debugger."""
|
||||||
import argparse
|
import argparse
|
||||||
import functools
|
import functools
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
import unittest
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
@ -13,6 +13,7 @@ from typing import Tuple
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
import unittest
|
||||||
|
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest._code import ExceptionInfo
|
from _pytest._code import ExceptionInfo
|
||||||
|
@ -25,6 +26,7 @@ from _pytest.config.exceptions import UsageError
|
||||||
from _pytest.nodes import Node
|
from _pytest.nodes import Node
|
||||||
from _pytest.reports import BaseReport
|
from _pytest.reports import BaseReport
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from _pytest.capture import CaptureManager
|
from _pytest.capture import CaptureManager
|
||||||
from _pytest.runner import CallInfo
|
from _pytest.runner import CallInfo
|
||||||
|
@ -263,8 +265,7 @@ class pytestPDB:
|
||||||
elif capturing:
|
elif capturing:
|
||||||
tw.sep(
|
tw.sep(
|
||||||
">",
|
">",
|
||||||
"PDB %s (IO-capturing turned off for %s)"
|
f"PDB {method} (IO-capturing turned off for {capturing})",
|
||||||
% (method, capturing),
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
tw.sep(">", f"PDB {method}")
|
tw.sep(">", f"PDB {method}")
|
||||||
|
@ -377,7 +378,8 @@ def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.Traceb
|
||||||
elif isinstance(excinfo.value, ConftestImportFailure):
|
elif isinstance(excinfo.value, ConftestImportFailure):
|
||||||
# A config.ConftestImportFailure is not useful for post_mortem.
|
# A config.ConftestImportFailure is not useful for post_mortem.
|
||||||
# Use the underlying exception instead:
|
# Use the underlying exception instead:
|
||||||
return excinfo.value.excinfo[2]
|
assert excinfo.value.cause.__traceback__ is not None
|
||||||
|
return excinfo.value.cause.__traceback__
|
||||||
else:
|
else:
|
||||||
assert excinfo._excinfo is not None
|
assert excinfo._excinfo is not None
|
||||||
return excinfo._excinfo[2]
|
return excinfo._excinfo[2]
|
||||||
|
|
|
@ -8,12 +8,14 @@ All constants defined in this module should be either instances of
|
||||||
:class:`PytestWarning`, or :class:`UnformattedWarning`
|
:class:`PytestWarning`, or :class:`UnformattedWarning`
|
||||||
in case of warnings which need to format their messages.
|
in case of warnings which need to format their messages.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from _pytest.warning_types import PytestDeprecationWarning
|
from _pytest.warning_types import PytestDeprecationWarning
|
||||||
from _pytest.warning_types import PytestRemovedIn9Warning
|
from _pytest.warning_types import PytestRemovedIn9Warning
|
||||||
from _pytest.warning_types import UnformattedWarning
|
from _pytest.warning_types import UnformattedWarning
|
||||||
|
|
||||||
|
|
||||||
# set of plugins which have been integrated into the core; we use this list to ignore
|
# set of plugins which have been integrated into the core; we use this list to ignore
|
||||||
# them during registration to avoid conflicts
|
# them during registration to avoid conflicts
|
||||||
DEPRECATED_EXTERNAL_PLUGINS = {
|
DEPRECATED_EXTERNAL_PLUGINS = {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Discover and run doctests in modules and test files."""
|
"""Discover and run doctests in modules and test files."""
|
||||||
import bdb
|
import bdb
|
||||||
|
from contextlib import contextmanager
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
import warnings
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
@ -23,6 +23,7 @@ from typing import Tuple
|
||||||
from typing import Type
|
from typing import Type
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
import warnings
|
||||||
|
|
||||||
from _pytest import outcomes
|
from _pytest import outcomes
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
|
@ -39,13 +40,14 @@ from _pytest.nodes import Item
|
||||||
from _pytest.outcomes import OutcomeException
|
from _pytest.outcomes import OutcomeException
|
||||||
from _pytest.outcomes import skip
|
from _pytest.outcomes import skip
|
||||||
from _pytest.pathlib import fnmatch_ex
|
from _pytest.pathlib import fnmatch_ex
|
||||||
from _pytest.pathlib import import_path
|
|
||||||
from _pytest.python import Module
|
from _pytest.python import Module
|
||||||
from _pytest.python_api import approx
|
from _pytest.python_api import approx
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import doctest
|
import doctest
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
DOCTEST_REPORT_CHOICE_NONE = "none"
|
DOCTEST_REPORT_CHOICE_NONE = "none"
|
||||||
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
||||||
|
@ -105,7 +107,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
"--doctest-ignore-import-errors",
|
"--doctest-ignore-import-errors",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
default=False,
|
default=False,
|
||||||
help="Ignore doctest ImportErrors",
|
help="Ignore doctest collection errors",
|
||||||
dest="doctest_ignore_import_errors",
|
dest="doctest_ignore_import_errors",
|
||||||
)
|
)
|
||||||
group.addoption(
|
group.addoption(
|
||||||
|
@ -132,11 +134,9 @@ def pytest_collect_file(
|
||||||
if config.option.doctestmodules and not any(
|
if config.option.doctestmodules and not any(
|
||||||
(_is_setup_py(file_path), _is_main_py(file_path))
|
(_is_setup_py(file_path), _is_main_py(file_path))
|
||||||
):
|
):
|
||||||
mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)
|
return DoctestModule.from_parent(parent, path=file_path)
|
||||||
return mod
|
|
||||||
elif _is_doctest(config, file_path, parent):
|
elif _is_doctest(config, file_path, parent):
|
||||||
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
|
return DoctestTextfile.from_parent(parent, path=file_path)
|
||||||
return txt
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@ -271,14 +271,14 @@ class DoctestItem(Item):
|
||||||
self._initrequest()
|
self._initrequest()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent( # type: ignore
|
def from_parent( # type: ignore[override]
|
||||||
cls,
|
cls,
|
||||||
parent: "Union[DoctestTextfile, DoctestModule]",
|
parent: "Union[DoctestTextfile, DoctestModule]",
|
||||||
*,
|
*,
|
||||||
name: str,
|
name: str,
|
||||||
runner: "doctest.DocTestRunner",
|
runner: "doctest.DocTestRunner",
|
||||||
dtest: "doctest.DocTest",
|
dtest: "doctest.DocTest",
|
||||||
):
|
) -> "Self":
|
||||||
# incompatible signature due to imposed limits on subclass
|
# incompatible signature due to imposed limits on subclass
|
||||||
"""The public named constructor."""
|
"""The public named constructor."""
|
||||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||||
|
@ -485,9 +485,9 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
|
||||||
return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
|
return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"Got %r when unwrapping %r. This is usually caused "
|
f"Got {e!r} when unwrapping {func!r}. This is usually caused "
|
||||||
"by a violation of Python's object protocol; see e.g. "
|
"by a violation of Python's object protocol; see e.g. "
|
||||||
"https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
|
"https://github.com/pytest-dev/pytest/issues/5080",
|
||||||
PytestWarning,
|
PytestWarning,
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
@ -558,24 +558,18 @@ class DoctestModule(Module):
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if self.path.name == "conftest.py":
|
|
||||||
module = self.config.pluginmanager._importconftest(
|
|
||||||
self.path,
|
|
||||||
self.config.getoption("importmode"),
|
|
||||||
rootpath=self.config.rootpath,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
try:
|
try:
|
||||||
module = import_path(
|
module = self.obj
|
||||||
self.path,
|
except Collector.CollectError:
|
||||||
root=self.config.rootpath,
|
|
||||||
mode=self.config.getoption("importmode"),
|
|
||||||
)
|
|
||||||
except ImportError:
|
|
||||||
if self.config.getvalue("doctest_ignore_import_errors"):
|
if self.config.getvalue("doctest_ignore_import_errors"):
|
||||||
skip("unable to import module %r" % self.path)
|
skip("unable to import module %r" % self.path)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
# While doctests currently don't support fixtures directly, we still
|
||||||
|
# need to pick up autouse fixtures.
|
||||||
|
self.session._fixturemanager.parsefactories(self)
|
||||||
|
|
||||||
# Uses internal doctest module parsing mechanism.
|
# Uses internal doctest module parsing mechanism.
|
||||||
finder = MockAwareDocTestFinder()
|
finder = MockAwareDocTestFinder()
|
||||||
optionflags = get_optionflags(self.config)
|
optionflags = get_optionflags(self.config)
|
||||||
|
|
|
@ -2,11 +2,11 @@ import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
import pytest
|
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.nodes import Item
|
from _pytest.nodes import Item
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
fault_handler_original_stderr_fd_key = StashKey[int]()
|
fault_handler_original_stderr_fd_key = StashKey[int]()
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
import abc
|
import abc
|
||||||
|
from collections import defaultdict
|
||||||
|
from collections import deque
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import warnings
|
|
||||||
from collections import defaultdict
|
|
||||||
from collections import deque
|
|
||||||
from contextlib import suppress
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
@ -30,6 +29,7 @@ from typing import Tuple
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
import warnings
|
||||||
|
|
||||||
import _pytest
|
import _pytest
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
|
@ -119,7 +119,7 @@ def get_scope_package(
|
||||||
) -> Optional[nodes.Node]:
|
) -> Optional[nodes.Node]:
|
||||||
from _pytest.python import Package
|
from _pytest.python import Package
|
||||||
|
|
||||||
for parent in node.iterparents():
|
for parent in node.iter_parents():
|
||||||
if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid:
|
if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid:
|
||||||
return parent
|
return parent
|
||||||
return node.session
|
return node.session
|
||||||
|
@ -168,16 +168,11 @@ def get_parametrized_fixture_keys(
|
||||||
the specified scope."""
|
the specified scope."""
|
||||||
assert scope is not Scope.Function
|
assert scope is not Scope.Function
|
||||||
try:
|
try:
|
||||||
callspec = item.callspec # type: ignore[attr-defined]
|
callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
return
|
||||||
else:
|
for argname in callspec.indices:
|
||||||
cs: CallSpec2 = callspec
|
if callspec._arg2scope[argname] != scope:
|
||||||
# cs.indices is random order of argnames. Need to
|
|
||||||
# sort this so that different calls to
|
|
||||||
# get_parametrized_fixture_keys will be deterministic.
|
|
||||||
for argname in sorted(cs.indices):
|
|
||||||
if cs._arg2scope[argname] != scope:
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
item_cls = None
|
item_cls = None
|
||||||
|
@ -193,7 +188,7 @@ def get_parametrized_fixture_keys(
|
||||||
else:
|
else:
|
||||||
assert_never(scope)
|
assert_never(scope)
|
||||||
|
|
||||||
param_index = cs.indices[argname]
|
param_index = callspec.indices[argname]
|
||||||
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
||||||
|
|
||||||
|
|
||||||
|
@ -582,7 +577,6 @@ class FixtureRequest(abc.ABC):
|
||||||
# (latter managed by fixturedef)
|
# (latter managed by fixturedef)
|
||||||
argname = fixturedef.argname
|
argname = fixturedef.argname
|
||||||
funcitem = self._pyfuncitem
|
funcitem = self._pyfuncitem
|
||||||
scope = fixturedef._scope
|
|
||||||
try:
|
try:
|
||||||
callspec = funcitem.callspec
|
callspec = funcitem.callspec
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
|
@ -590,24 +584,20 @@ class FixtureRequest(abc.ABC):
|
||||||
if callspec is not None and argname in callspec.params:
|
if callspec is not None and argname in callspec.params:
|
||||||
param = callspec.params[argname]
|
param = callspec.params[argname]
|
||||||
param_index = callspec.indices[argname]
|
param_index = callspec.indices[argname]
|
||||||
# If a parametrize invocation set a scope it will override
|
# The parametrize invocation scope overrides the fixture's scope.
|
||||||
# the static scope defined with the fixture function.
|
|
||||||
with suppress(KeyError):
|
|
||||||
scope = callspec._arg2scope[argname]
|
scope = callspec._arg2scope[argname]
|
||||||
else:
|
else:
|
||||||
param = NOTSET
|
param = NOTSET
|
||||||
param_index = 0
|
param_index = 0
|
||||||
|
scope = fixturedef._scope
|
||||||
|
|
||||||
has_params = fixturedef.params is not None
|
has_params = fixturedef.params is not None
|
||||||
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
||||||
if has_params and fixtures_not_supported:
|
if has_params and fixtures_not_supported:
|
||||||
msg = (
|
msg = (
|
||||||
"{name} does not support fixtures, maybe unittest.TestCase subclass?\n"
|
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
|
||||||
"Node id: {nodeid}\n"
|
f"Node id: {funcitem.nodeid}\n"
|
||||||
"Function type: {typename}"
|
f"Function type: {type(funcitem).__name__}"
|
||||||
).format(
|
|
||||||
name=funcitem.name,
|
|
||||||
nodeid=funcitem.nodeid,
|
|
||||||
typename=type(funcitem).__name__,
|
|
||||||
)
|
)
|
||||||
fail(msg, pytrace=False)
|
fail(msg, pytrace=False)
|
||||||
if has_params:
|
if has_params:
|
||||||
|
@ -740,9 +730,7 @@ class SubRequest(FixtureRequest):
|
||||||
if node is None and scope is Scope.Class:
|
if node is None and scope is Scope.Class:
|
||||||
# Fallback to function item itself.
|
# Fallback to function item itself.
|
||||||
node = self._pyfuncitem
|
node = self._pyfuncitem
|
||||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
assert node, f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}'
|
||||||
scope, self._pyfuncitem
|
|
||||||
)
|
|
||||||
return node
|
return node
|
||||||
|
|
||||||
def _check_scope(
|
def _check_scope(
|
||||||
|
@ -845,8 +833,8 @@ class FixtureLookupError(LookupError):
|
||||||
if faclist:
|
if faclist:
|
||||||
available.add(name)
|
available.add(name)
|
||||||
if self.argname in available:
|
if self.argname in available:
|
||||||
msg = " recursive dependency involving fixture '{}' detected".format(
|
msg = (
|
||||||
self.argname
|
f" recursive dependency involving fixture '{self.argname}' detected"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
msg = f"fixture '{self.argname}' not found"
|
msg = f"fixture '{self.argname}' not found"
|
||||||
|
@ -940,15 +928,13 @@ def _eval_scope_callable(
|
||||||
result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg]
|
result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"Error evaluating {} while defining fixture '{}'.\n"
|
f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n"
|
||||||
"Expected a function with the signature (*, fixture_name, config)".format(
|
"Expected a function with the signature (*, fixture_name, config)"
|
||||||
scope_callable, fixture_name
|
|
||||||
)
|
|
||||||
) from e
|
) from e
|
||||||
if not isinstance(result, str):
|
if not isinstance(result, str):
|
||||||
fail(
|
fail(
|
||||||
"Expected {} to return a 'str' while defining fixture '{}', but it returned:\n"
|
f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n"
|
||||||
"{!r}".format(scope_callable, fixture_name, result),
|
f"{result!r}",
|
||||||
pytrace=False,
|
pytrace=False,
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
@ -1090,9 +1076,7 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
return request.param_index if not hasattr(request, "param") else request.param
|
return request.param_index if not hasattr(request, "param") else request.param
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format(
|
return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"
|
||||||
self.argname, self.scope, self.baseid
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def resolve_fixture_function(
|
def resolve_fixture_function(
|
||||||
|
@ -1113,7 +1097,8 @@ def resolve_fixture_function(
|
||||||
# Handle the case where fixture is defined not in a test class, but some other class
|
# Handle the case where fixture is defined not in a test class, but some other class
|
||||||
# (for example a plugin class with a fixture), see #2270.
|
# (for example a plugin class with a fixture), see #2270.
|
||||||
if hasattr(fixturefunc, "__self__") and not isinstance(
|
if hasattr(fixturefunc, "__self__") and not isinstance(
|
||||||
request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr]
|
request.instance,
|
||||||
|
fixturefunc.__self__.__class__, # type: ignore[union-attr]
|
||||||
):
|
):
|
||||||
return fixturefunc
|
return fixturefunc
|
||||||
fixturefunc = getimfunc(fixturedef.func)
|
fixturefunc = getimfunc(fixturedef.func)
|
||||||
|
@ -1196,7 +1181,7 @@ class FixtureFunctionMarker:
|
||||||
|
|
||||||
if getattr(function, "_pytestfixturefunction", False):
|
if getattr(function, "_pytestfixturefunction", False):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"fixture is being applied more than once to the same function"
|
f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if hasattr(function, "pytestmark"):
|
if hasattr(function, "pytestmark"):
|
||||||
|
@ -1208,9 +1193,7 @@ class FixtureFunctionMarker:
|
||||||
if name == "request":
|
if name == "request":
|
||||||
location = getlocation(function)
|
location = getlocation(function)
|
||||||
fail(
|
fail(
|
||||||
"'request' is a reserved word for fixtures, use another name:\n {}".format(
|
f"'request' is a reserved word for fixtures, use another name:\n {location}",
|
||||||
location
|
|
||||||
),
|
|
||||||
pytrace=False,
|
pytrace=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1235,7 +1218,7 @@ def fixture(
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def fixture( # noqa: F811
|
def fixture(
|
||||||
fixture_function: None = ...,
|
fixture_function: None = ...,
|
||||||
*,
|
*,
|
||||||
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
|
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
|
||||||
|
@ -1249,7 +1232,7 @@ def fixture( # noqa: F811
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
def fixture( # noqa: F811
|
def fixture(
|
||||||
fixture_function: Optional[FixtureFunction] = None,
|
fixture_function: Optional[FixtureFunction] = None,
|
||||||
*,
|
*,
|
||||||
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
|
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
|
||||||
|
@ -1483,25 +1466,27 @@ class FixtureManager:
|
||||||
|
|
||||||
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||||
|
|
||||||
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None:
|
def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None:
|
||||||
nodeid = None
|
# Fixtures defined in conftest plugins are only visible to within the
|
||||||
|
# conftest's directory. This is unlike fixtures in non-conftest plugins
|
||||||
|
# which have global visibility. So for conftests, construct the base
|
||||||
|
# nodeid from the plugin name (which is the conftest path).
|
||||||
|
if plugin_name and plugin_name.endswith("conftest.py"):
|
||||||
|
# Note: we explicitly do *not* use `plugin.__file__` here -- The
|
||||||
|
# difference is that plugin_name has the correct capitalization on
|
||||||
|
# case-insensitive systems (Windows) and other normalization issues
|
||||||
|
# (issue #11816).
|
||||||
|
conftestpath = absolutepath(plugin_name)
|
||||||
try:
|
try:
|
||||||
p = absolutepath(plugin.__file__) # type: ignore[attr-defined]
|
nodeid = str(conftestpath.parent.relative_to(self.config.rootpath))
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
# Construct the base nodeid which is later used to check
|
|
||||||
# what fixtures are visible for particular tests (as denoted
|
|
||||||
# by their test id).
|
|
||||||
if p.name == "conftest.py":
|
|
||||||
try:
|
|
||||||
nodeid = str(p.parent.relative_to(self.config.rootpath))
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
nodeid = ""
|
nodeid = ""
|
||||||
if nodeid == ".":
|
if nodeid == ".":
|
||||||
nodeid = ""
|
nodeid = ""
|
||||||
if os.sep != nodes.SEP:
|
if os.sep != nodes.SEP:
|
||||||
nodeid = nodeid.replace(os.sep, nodes.SEP)
|
nodeid = nodeid.replace(os.sep, nodes.SEP)
|
||||||
|
else:
|
||||||
|
nodeid = None
|
||||||
|
|
||||||
self.parsefactories(plugin, nodeid)
|
self.parsefactories(plugin, nodeid)
|
||||||
|
|
||||||
|
@ -1681,7 +1666,7 @@ class FixtureManager:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def parsefactories( # noqa: F811
|
def parsefactories(
|
||||||
self,
|
self,
|
||||||
node_or_obj: object,
|
node_or_obj: object,
|
||||||
nodeid: Optional[str],
|
nodeid: Optional[str],
|
||||||
|
@ -1690,7 +1675,7 @@ class FixtureManager:
|
||||||
) -> None:
|
) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
def parsefactories( # noqa: F811
|
def parsefactories(
|
||||||
self,
|
self,
|
||||||
node_or_obj: Union[nodes.Node, object],
|
node_or_obj: Union[nodes.Node, object],
|
||||||
nodeid: Union[str, NotSetType, None] = NOTSET,
|
nodeid: Union[str, NotSetType, None] = NOTSET,
|
||||||
|
@ -1775,7 +1760,7 @@ class FixtureManager:
|
||||||
def _matchfactories(
|
def _matchfactories(
|
||||||
self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
|
self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
|
||||||
) -> Iterator[FixtureDef[Any]]:
|
) -> Iterator[FixtureDef[Any]]:
|
||||||
parentnodeids = {n.nodeid for n in node.iterparents()}
|
parentnodeids = {n.nodeid for n in node.iter_parents()}
|
||||||
for fixturedef in fixturedefs:
|
for fixturedef in fixturedefs:
|
||||||
if fixturedef.baseid in parentnodeids:
|
if fixturedef.baseid in parentnodeids:
|
||||||
yield fixturedef
|
yield fixturedef
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
"""Provides a function to report all internal modules for using freezing
|
"""Provides a function to report all internal modules for using freezing
|
||||||
tools."""
|
tools."""
|
||||||
|
|
||||||
import types
|
import types
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Version info, help messages, tracing configuration."""
|
"""Version info, help messages, tracing configuration."""
|
||||||
|
from argparse import Action
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from argparse import Action
|
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import pytest
|
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import PrintHelp
|
from _pytest.config import PrintHelp
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
class HelpAction(Action):
|
class HelpAction(Action):
|
||||||
|
@ -108,11 +109,11 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
|
||||||
path = config.option.debug
|
path = config.option.debug
|
||||||
debugfile = open(path, "w", encoding="utf-8")
|
debugfile = open(path, "w", encoding="utf-8")
|
||||||
debugfile.write(
|
debugfile.write(
|
||||||
"versions pytest-%s, "
|
"versions pytest-{}, "
|
||||||
"python-%s\ncwd=%s\nargs=%s\n\n"
|
"python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format(
|
||||||
% (
|
|
||||||
pytest.__version__,
|
pytest.__version__,
|
||||||
".".join(map(str, sys.version_info)),
|
".".join(map(str, sys.version_info)),
|
||||||
|
config.invocation_params.dir,
|
||||||
os.getcwd(),
|
os.getcwd(),
|
||||||
config.invocation_params.args,
|
config.invocation_params.args,
|
||||||
)
|
)
|
||||||
|
@ -135,9 +136,7 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
|
||||||
def showversion(config: Config) -> None:
|
def showversion(config: Config) -> None:
|
||||||
if config.option.version > 1:
|
if config.option.version > 1:
|
||||||
sys.stdout.write(
|
sys.stdout.write(
|
||||||
"This is pytest version {}, imported from {}\n".format(
|
f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n"
|
||||||
pytest.__version__, pytest.__file__
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
plugininfo = getpluginversioninfo(config)
|
plugininfo = getpluginversioninfo(config)
|
||||||
if plugininfo:
|
if plugininfo:
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Hook specifications for pytest plugins which are invoked by pytest itself
|
"""Hook specifications for pytest plugins which are invoked by pytest itself
|
||||||
and by builtin plugins."""
|
and by builtin plugins."""
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -13,17 +14,18 @@ from typing import Union
|
||||||
|
|
||||||
from pluggy import HookspecMarker
|
from pluggy import HookspecMarker
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import pdb
|
import pdb
|
||||||
import warnings
|
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
import warnings
|
||||||
|
|
||||||
from _pytest._code.code import ExceptionRepr
|
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
|
from _pytest._code.code import ExceptionRepr
|
||||||
|
from _pytest.config import _PluggyPlugin
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import PytestPluginManager
|
from _pytest.config import PytestPluginManager
|
||||||
from _pytest.config import _PluggyPlugin
|
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
from _pytest.fixtures import FixtureDef
|
from _pytest.fixtures import FixtureDef
|
||||||
from _pytest.fixtures import SubRequest
|
from _pytest.fixtures import SubRequest
|
||||||
|
@ -54,24 +56,41 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
|
||||||
"""Called at plugin registration time to allow adding new hooks via a call to
|
"""Called at plugin registration time to allow adding new hooks via a call to
|
||||||
:func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`.
|
:func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`.
|
||||||
|
|
||||||
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
|
:param pluginmanager: The pytest plugin manager.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with hook wrappers.
|
This hook is incompatible with hook wrappers.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
If a conftest plugin implements this hook, it will be called immediately
|
||||||
|
when the conftest is registered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@hookspec(historic=True)
|
@hookspec(historic=True)
|
||||||
def pytest_plugin_registered(
|
def pytest_plugin_registered(
|
||||||
plugin: "_PluggyPlugin", manager: "PytestPluginManager"
|
plugin: "_PluggyPlugin",
|
||||||
|
plugin_name: str,
|
||||||
|
manager: "PytestPluginManager",
|
||||||
) -> None:
|
) -> None:
|
||||||
"""A new pytest plugin got registered.
|
"""A new pytest plugin got registered.
|
||||||
|
|
||||||
:param plugin: The plugin module or instance.
|
:param plugin: The plugin module or instance.
|
||||||
:param pytest.PytestPluginManager manager: pytest plugin manager.
|
:param plugin_name: The name by which the plugin is registered.
|
||||||
|
:param manager: The pytest plugin manager.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with hook wrappers.
|
This hook is incompatible with hook wrappers.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
If a conftest plugin implements this hook, it will be called immediately
|
||||||
|
when the conftest is registered, once for each plugin registered thus far
|
||||||
|
(including itself!), and for all plugins thereafter when they are
|
||||||
|
registered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,19 +99,13 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||||
"""Register argparse-style options and ini-style config values,
|
"""Register argparse-style options and ini-style config values,
|
||||||
called once at the beginning of a test run.
|
called once at the beginning of a test run.
|
||||||
|
|
||||||
.. note::
|
:param parser:
|
||||||
|
|
||||||
This function should be implemented only in plugins or ``conftest.py``
|
|
||||||
files situated at the tests root directory due to how pytest
|
|
||||||
:ref:`discovers plugins during startup <pluginorder>`.
|
|
||||||
|
|
||||||
:param pytest.Parser parser:
|
|
||||||
To add command line options, call
|
To add command line options, call
|
||||||
:py:func:`parser.addoption(...) <pytest.Parser.addoption>`.
|
:py:func:`parser.addoption(...) <pytest.Parser.addoption>`.
|
||||||
To add ini-file values call :py:func:`parser.addini(...)
|
To add ini-file values call :py:func:`parser.addini(...)
|
||||||
<pytest.Parser.addini>`.
|
<pytest.Parser.addini>`.
|
||||||
|
|
||||||
:param pytest.PytestPluginManager pluginmanager:
|
:param pluginmanager:
|
||||||
The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
|
The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
|
||||||
or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks
|
or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks
|
||||||
to change how command line options are added.
|
to change how command line options are added.
|
||||||
|
@ -111,6 +124,14 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with hook wrappers.
|
This hook is incompatible with hook wrappers.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
If a conftest plugin implements this hook, it will be called immediately
|
||||||
|
when the conftest is registered.
|
||||||
|
|
||||||
|
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,16 +139,17 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||||
def pytest_configure(config: "Config") -> None:
|
def pytest_configure(config: "Config") -> None:
|
||||||
"""Allow plugins and conftest files to perform initial configuration.
|
"""Allow plugins and conftest files to perform initial configuration.
|
||||||
|
|
||||||
This hook is called for every plugin and initial conftest file
|
|
||||||
after command line options have been parsed.
|
|
||||||
|
|
||||||
After that, the hook is called for other conftest files as they are
|
|
||||||
imported.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook is incompatible with hook wrappers.
|
This hook is incompatible with hook wrappers.
|
||||||
|
|
||||||
:param pytest.Config config: The pytest config object.
|
:param config: The pytest config object.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
This hook is called for every :ref:`initial conftest <pluginorder>` file
|
||||||
|
after command line options have been parsed. After that, the hook is called
|
||||||
|
for other conftest files as they are registered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -146,40 +168,54 @@ def pytest_cmdline_parse(
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
This hook will only be called for plugin classes passed to the
|
This hook is only called for plugin classes passed to the
|
||||||
``plugins`` arg when using `pytest.main`_ to perform an in-process
|
``plugins`` arg when using `pytest.main`_ to perform an in-process
|
||||||
test run.
|
test run.
|
||||||
|
|
||||||
:param pluginmanager: The pytest plugin manager.
|
:param pluginmanager: The pytest plugin manager.
|
||||||
:param args: List of arguments passed on the command line.
|
:param args: List of arguments passed on the command line.
|
||||||
:returns: A pytest config object.
|
:returns: A pytest config object.
|
||||||
"""
|
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
This hook is not called for conftest files.
|
||||||
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
|
|
||||||
"""Called for performing the main command line action. The default
|
|
||||||
implementation will invoke the configure hooks and runtest_mainloop.
|
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
|
||||||
|
|
||||||
:param config: The pytest config object.
|
|
||||||
:returns: The exit code.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_load_initial_conftests(
|
def pytest_load_initial_conftests(
|
||||||
early_config: "Config", parser: "Parser", args: List[str]
|
early_config: "Config", parser: "Parser", args: List[str]
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Called to implement the loading of initial conftest files ahead
|
"""Called to implement the loading of :ref:`initial conftest files
|
||||||
of command line option parsing.
|
<pluginorder>` ahead of command line option parsing.
|
||||||
|
|
||||||
.. note::
|
|
||||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
|
||||||
|
|
||||||
:param early_config: The pytest config object.
|
:param early_config: The pytest config object.
|
||||||
:param args: Arguments passed on the command line.
|
:param args: Arguments passed on the command line.
|
||||||
:param parser: To add command line options.
|
:param parser: To add command line options.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
This hook is not called for conftest files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@hookspec(firstresult=True)
|
||||||
|
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
|
||||||
|
"""Called for performing the main command line action.
|
||||||
|
|
||||||
|
The default implementation will invoke the configure hooks and
|
||||||
|
:hook:`pytest_runtestloop`.
|
||||||
|
|
||||||
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
|
:param config: The pytest config object.
|
||||||
|
:returns: The exit code.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -222,6 +258,11 @@ def pytest_collection(session: "Session") -> Optional[object]:
|
||||||
counter (and returns `None`).
|
counter (and returns `None`).
|
||||||
|
|
||||||
:param session: The pytest session object.
|
:param session: The pytest session object.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -234,6 +275,11 @@ def pytest_collection_modifyitems(
|
||||||
:param session: The pytest session object.
|
:param session: The pytest session object.
|
||||||
:param config: The pytest config object.
|
:param config: The pytest config object.
|
||||||
:param items: List of item objects.
|
:param items: List of item objects.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest plugin can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -241,6 +287,11 @@ def pytest_collection_finish(session: "Session") -> None:
|
||||||
"""Called after collection has been performed and modified.
|
"""Called after collection has been performed and modified.
|
||||||
|
|
||||||
:param session: The pytest session object.
|
:param session: The pytest session object.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest plugin can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -263,6 +314,14 @@ def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[b
|
||||||
|
|
||||||
.. versionchanged:: 8.0.0
|
.. versionchanged:: 8.0.0
|
||||||
The ``path`` parameter has been removed.
|
The ``path`` parameter has been removed.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given collection path, only
|
||||||
|
conftest files in parent directories of the collection path are consulted
|
||||||
|
(if the path is a directory, its own conftest file is *not* consulted - a
|
||||||
|
directory cannot ignore itself!).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -284,6 +343,14 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
|
||||||
|
|
||||||
See :ref:`custom directory collectors` for a simple example of use of this
|
See :ref:`custom directory collectors` for a simple example of use of this
|
||||||
hook.
|
hook.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given collection path, only
|
||||||
|
conftest files in parent directories of the collection path are consulted
|
||||||
|
(if the path is a directory, its own conftest file is *not* consulted - a
|
||||||
|
directory cannot collect itself!).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -304,6 +371,12 @@ def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Colle
|
||||||
|
|
||||||
.. versionchanged:: 8.0.0
|
.. versionchanged:: 8.0.0
|
||||||
The ``path`` parameter was removed.
|
The ``path`` parameter was removed.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given file path, only
|
||||||
|
conftest files in parent directories of the file path are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -315,6 +388,13 @@ def pytest_collectstart(collector: "Collector") -> None:
|
||||||
|
|
||||||
:param collector:
|
:param collector:
|
||||||
The collector.
|
The collector.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given collector, only
|
||||||
|
conftest files in the collector's directory and its parent directories are
|
||||||
|
consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -323,6 +403,12 @@ def pytest_itemcollected(item: "Item") -> None:
|
||||||
|
|
||||||
:param item:
|
:param item:
|
||||||
The item.
|
The item.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in the item's directory and its parent directories are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -331,6 +417,13 @@ def pytest_collectreport(report: "CollectReport") -> None:
|
||||||
|
|
||||||
:param report:
|
:param report:
|
||||||
The collect report.
|
The collect report.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given collector, only
|
||||||
|
conftest files in the collector's directory and its parent directories are
|
||||||
|
consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -341,6 +434,11 @@ def pytest_deselected(items: Sequence["Item"]) -> None:
|
||||||
|
|
||||||
:param items:
|
:param items:
|
||||||
The items.
|
The items.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -353,6 +451,13 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
|
||||||
|
|
||||||
:param collector:
|
:param collector:
|
||||||
The collector.
|
The collector.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given collector, only
|
||||||
|
conftest files in the collector's directory and its parent directories are
|
||||||
|
consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -380,6 +485,13 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]
|
||||||
|
|
||||||
.. versionchanged:: 8.0.0
|
.. versionchanged:: 8.0.0
|
||||||
The ``path`` parameter has been removed in favor of ``module_path``.
|
The ``path`` parameter has been removed in favor of ``module_path``.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given parent collector,
|
||||||
|
only conftest files in the collector's directory and its parent directories
|
||||||
|
are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -399,6 +511,13 @@ def pytest_pycollect_makeitem(
|
||||||
The object.
|
The object.
|
||||||
:returns:
|
:returns:
|
||||||
The created items/collectors.
|
The created items/collectors.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given collector, only
|
||||||
|
conftest files in the collector's directory and its parent directories
|
||||||
|
are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -410,6 +529,13 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
|
||||||
|
|
||||||
:param pyfuncitem:
|
:param pyfuncitem:
|
||||||
The function item.
|
The function item.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only
|
||||||
|
conftest files in the item's directory and its parent directories
|
||||||
|
are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -418,6 +544,13 @@ def pytest_generate_tests(metafunc: "Metafunc") -> None:
|
||||||
|
|
||||||
:param metafunc:
|
:param metafunc:
|
||||||
The :class:`~pytest.Metafunc` helper for the test function.
|
The :class:`~pytest.Metafunc` helper for the test function.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given function definition,
|
||||||
|
only conftest files in the functions's directory and its parent directories
|
||||||
|
are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -435,7 +568,12 @@ def pytest_make_parametrize_id(
|
||||||
|
|
||||||
:param config: The pytest config object.
|
:param config: The pytest config object.
|
||||||
:param val: The parametrized value.
|
:param val: The parametrized value.
|
||||||
:param str argname: The automatic parameter name produced by pytest.
|
:param argname: The automatic parameter name produced by pytest.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -462,6 +600,11 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
The return value is not used, but only stops further processing.
|
The return value is not used, but only stops further processing.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -500,6 +643,11 @@ def pytest_runtest_protocol(
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
The return value is not used, but only stops further processing.
|
The return value is not used, but only stops further processing.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -514,6 +662,12 @@ def pytest_runtest_logstart(
|
||||||
:param location: A tuple of ``(filename, lineno, testname)``
|
:param location: A tuple of ``(filename, lineno, testname)``
|
||||||
where ``filename`` is a file path relative to ``config.rootpath``
|
where ``filename`` is a file path relative to ``config.rootpath``
|
||||||
and ``lineno`` is 0-based.
|
and ``lineno`` is 0-based.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in the item's directory and its parent directories are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -528,6 +682,12 @@ def pytest_runtest_logfinish(
|
||||||
:param location: A tuple of ``(filename, lineno, testname)``
|
:param location: A tuple of ``(filename, lineno, testname)``
|
||||||
where ``filename`` is a file path relative to ``config.rootpath``
|
where ``filename`` is a file path relative to ``config.rootpath``
|
||||||
and ``lineno`` is 0-based.
|
and ``lineno`` is 0-based.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in the item's directory and its parent directories are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -541,6 +701,12 @@ def pytest_runtest_setup(item: "Item") -> None:
|
||||||
|
|
||||||
:param item:
|
:param item:
|
||||||
The item.
|
The item.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in the item's directory and its parent directories are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -551,6 +717,12 @@ def pytest_runtest_call(item: "Item") -> None:
|
||||||
|
|
||||||
:param item:
|
:param item:
|
||||||
The item.
|
The item.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in the item's directory and its parent directories are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -569,6 +741,12 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
|
||||||
scheduled). This argument is used to perform exact teardowns, i.e.
|
scheduled). This argument is used to perform exact teardowns, i.e.
|
||||||
calling just enough finalizers so that nextitem only needs to call
|
calling just enough finalizers so that nextitem only needs to call
|
||||||
setup functions.
|
setup functions.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in the item's directory and its parent directories are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -585,6 +763,12 @@ def pytest_runtest_makereport(
|
||||||
:param call: The :class:`~pytest.CallInfo` for the phase.
|
:param call: The :class:`~pytest.CallInfo` for the phase.
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in the item's directory and its parent directories are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -593,6 +777,12 @@ def pytest_runtest_logreport(report: "TestReport") -> None:
|
||||||
of the setup, call and teardown runtest phases of an item.
|
of the setup, call and teardown runtest phases of an item.
|
||||||
|
|
||||||
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in the item's directory and its parent directories are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -606,6 +796,12 @@ def pytest_report_to_serializable(
|
||||||
|
|
||||||
:param config: The pytest config object.
|
:param config: The pytest config object.
|
||||||
:param report: The report.
|
:param report: The report.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. The exact details may depend
|
||||||
|
on the plugin which calls the hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -618,6 +814,12 @@ def pytest_report_from_serializable(
|
||||||
:hook:`pytest_report_to_serializable`.
|
:hook:`pytest_report_to_serializable`.
|
||||||
|
|
||||||
:param config: The pytest config object.
|
:param config: The pytest config object.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. The exact details may depend
|
||||||
|
on the plugin which calls the hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -645,6 +847,13 @@ def pytest_fixture_setup(
|
||||||
If the fixture function returns None, other implementations of
|
If the fixture function returns None, other implementations of
|
||||||
this hook function will continue to be called, according to the
|
this hook function will continue to be called, according to the
|
||||||
behavior of the :ref:`firstresult` option.
|
behavior of the :ref:`firstresult` option.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given fixture, only
|
||||||
|
conftest files in the fixture scope's directory and its parent directories
|
||||||
|
are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -659,6 +868,13 @@ def pytest_fixture_post_finalizer(
|
||||||
The fixture definition object.
|
The fixture definition object.
|
||||||
:param request:
|
:param request:
|
||||||
The fixture request object.
|
The fixture request object.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given fixture, only
|
||||||
|
conftest files in the fixture scope's directory and its parent directories
|
||||||
|
are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -672,6 +888,11 @@ def pytest_sessionstart(session: "Session") -> None:
|
||||||
and entering the run test loop.
|
and entering the run test loop.
|
||||||
|
|
||||||
:param session: The pytest session object.
|
:param session: The pytest session object.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -683,6 +904,11 @@ def pytest_sessionfinish(
|
||||||
|
|
||||||
:param session: The pytest session object.
|
:param session: The pytest session object.
|
||||||
:param exitstatus: The status which pytest will return to the system.
|
:param exitstatus: The status which pytest will return to the system.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -690,6 +916,11 @@ def pytest_unconfigure(config: "Config") -> None:
|
||||||
"""Called before test process is exited.
|
"""Called before test process is exited.
|
||||||
|
|
||||||
:param config: The pytest config object.
|
:param config: The pytest config object.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -712,6 +943,12 @@ def pytest_assertrepr_compare(
|
||||||
:param op: The operator, e.g. `"=="`, `"!="`, `"not in"`.
|
:param op: The operator, e.g. `"=="`, `"!="`, `"not in"`.
|
||||||
:param left: The left operand.
|
:param left: The left operand.
|
||||||
:param right: The right operand.
|
:param right: The right operand.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in the item's directory and its parent directories are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -740,6 +977,12 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
|
||||||
:param lineno: Line number of the assert statement.
|
:param lineno: Line number of the assert statement.
|
||||||
:param orig: String with the original assertion.
|
:param orig: String with the original assertion.
|
||||||
:param expl: String with the assert explanation.
|
:param expl: String with the assert explanation.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in the item's directory and its parent directories are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -764,18 +1007,17 @@ def pytest_report_header( # type:ignore[empty-body]
|
||||||
If you want to have your line(s) displayed first, use
|
If you want to have your line(s) displayed first, use
|
||||||
:ref:`trylast=True <plugin-hookorder>`.
|
:ref:`trylast=True <plugin-hookorder>`.
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This function should be implemented only in plugins or ``conftest.py``
|
|
||||||
files situated at the tests root directory due to how pytest
|
|
||||||
:ref:`discovers plugins during startup <pluginorder>`.
|
|
||||||
|
|
||||||
.. versionchanged:: 7.0.0
|
.. versionchanged:: 7.0.0
|
||||||
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
||||||
equivalent of the ``startdir`` parameter.
|
equivalent of the ``startdir`` parameter.
|
||||||
|
|
||||||
.. versionchanged:: 8.0.0
|
.. versionchanged:: 8.0.0
|
||||||
The ``startdir`` parameter has been removed.
|
The ``startdir`` parameter has been removed.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -809,6 +1051,11 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||||
|
|
||||||
.. versionchanged:: 8.0.0
|
.. versionchanged:: 8.0.0
|
||||||
The ``startdir`` parameter has been removed.
|
The ``startdir`` parameter has been removed.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest plugin can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -837,6 +1084,11 @@ def pytest_report_teststatus( # type:ignore[empty-body]
|
||||||
:returns: The test status.
|
:returns: The test status.
|
||||||
|
|
||||||
Stops at first non-None result, see :ref:`firstresult`.
|
Stops at first non-None result, see :ref:`firstresult`.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest plugin can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -853,6 +1105,11 @@ def pytest_terminal_summary(
|
||||||
|
|
||||||
.. versionadded:: 4.2
|
.. versionadded:: 4.2
|
||||||
The ``config`` parameter.
|
The ``config`` parameter.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest plugin can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -877,7 +1134,8 @@ def pytest_warning_recorded(
|
||||||
* ``"runtest"``: during test execution.
|
* ``"runtest"``: during test execution.
|
||||||
|
|
||||||
:param nodeid:
|
:param nodeid:
|
||||||
Full id of the item.
|
Full id of the item. Empty string for warnings that are not specific to
|
||||||
|
a particular node.
|
||||||
|
|
||||||
:param location:
|
:param location:
|
||||||
When available, holds information about the execution context of the captured
|
When available, holds information about the execution context of the captured
|
||||||
|
@ -885,6 +1143,13 @@ def pytest_warning_recorded(
|
||||||
when the execution context is at the module level.
|
when the execution context is at the module level.
|
||||||
|
|
||||||
.. versionadded:: 6.0
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. If the warning is specific to a
|
||||||
|
particular node, only conftest files in parent directories of the node are
|
||||||
|
consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -908,6 +1173,12 @@ def pytest_markeval_namespace( # type:ignore[empty-body]
|
||||||
|
|
||||||
:param config: The pytest config object.
|
:param config: The pytest config object.
|
||||||
:returns: A dictionary of additional globals to add.
|
:returns: A dictionary of additional globals to add.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given item, only conftest
|
||||||
|
files in parent directories of the item are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -927,6 +1198,11 @@ def pytest_internalerror(
|
||||||
|
|
||||||
:param excrepr: The exception repr object.
|
:param excrepr: The exception repr object.
|
||||||
:param excinfo: The exception info.
|
:param excinfo: The exception info.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest plugin can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -936,6 +1212,11 @@ def pytest_keyboard_interrupt(
|
||||||
"""Called for keyboard interrupt.
|
"""Called for keyboard interrupt.
|
||||||
|
|
||||||
:param excinfo: The exception info.
|
:param excinfo: The exception info.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest plugin can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -962,6 +1243,12 @@ def pytest_exception_interact(
|
||||||
The call information. Contains the exception.
|
The call information. Contains the exception.
|
||||||
:param report:
|
:param report:
|
||||||
The collection or test report.
|
The collection or test report.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest file can implement this hook. For a given node, only conftest
|
||||||
|
files in parent directories of the node are consulted.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -973,6 +1260,11 @@ def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
|
||||||
|
|
||||||
:param config: The pytest config object.
|
:param config: The pytest config object.
|
||||||
:param pdb: The Pdb instance.
|
:param pdb: The Pdb instance.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest plugin can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@ -984,4 +1276,9 @@ def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
|
||||||
|
|
||||||
:param config: The pytest config object.
|
:param config: The pytest config object.
|
||||||
:param pdb: The Pdb instance.
|
:param pdb: The Pdb instance.
|
||||||
|
|
||||||
|
Use in conftest plugins
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Any conftest plugin can implement this hook.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Report test results in JUnit-XML format, for use with Jenkins and build
|
"""Report test results in JUnit-XML format, for use with Jenkins and build
|
||||||
integration servers.
|
integration servers.
|
||||||
|
|
||||||
|
@ -6,12 +7,11 @@ Based on initial code from Ross Lawley.
|
||||||
Output conforms to
|
Output conforms to
|
||||||
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
||||||
"""
|
"""
|
||||||
|
from datetime import datetime
|
||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
from datetime import datetime
|
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -19,8 +19,8 @@ from typing import Match
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
import pytest
|
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
from _pytest import timing
|
from _pytest import timing
|
||||||
from _pytest._code.code import ExceptionRepr
|
from _pytest._code.code import ExceptionRepr
|
||||||
|
@ -32,6 +32,7 @@ from _pytest.fixtures import FixtureRequest
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
xml_key = StashKey["LogXML"]()
|
xml_key = StashKey["LogXML"]()
|
||||||
|
@ -248,7 +249,9 @@ class _NodeReporter:
|
||||||
skipreason = skipreason[9:]
|
skipreason = skipreason[9:]
|
||||||
details = f"{filename}:{lineno}: {skipreason}"
|
details = f"{filename}:{lineno}: {skipreason}"
|
||||||
|
|
||||||
skipped = ET.Element("skipped", type="pytest.skip", message=skipreason)
|
skipped = ET.Element(
|
||||||
|
"skipped", type="pytest.skip", message=bin_xml_escape(skipreason)
|
||||||
|
)
|
||||||
skipped.text = bin_xml_escape(details)
|
skipped.text = bin_xml_escape(details)
|
||||||
self.append(skipped)
|
self.append(skipped)
|
||||||
self.write_captured_output(report)
|
self.write_captured_output(report)
|
||||||
|
@ -271,9 +274,7 @@ def _warn_incompatibility_with_xunit2(
|
||||||
if xml is not None and xml.family not in ("xunit1", "legacy"):
|
if xml is not None and xml.family not in ("xunit1", "legacy"):
|
||||||
request.node.warn(
|
request.node.warn(
|
||||||
PytestWarning(
|
PytestWarning(
|
||||||
"{fixture_name} is incompatible with junit_family '{family}' (use 'legacy' or 'xunit1')".format(
|
f"{fixture_name} is incompatible with junit_family '{xml.family}' (use 'legacy' or 'xunit1')"
|
||||||
fixture_name=fixture_name, family=xml.family
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -365,7 +366,6 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
|
||||||
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
||||||
:issue:`7767` for details.
|
:issue:`7767` for details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
|
|
||||||
def record_func(name: str, value: object) -> None:
|
def record_func(name: str, value: object) -> None:
|
||||||
|
@ -375,7 +375,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
|
||||||
|
|
||||||
xml = request.config.stash.get(xml_key, None)
|
xml = request.config.stash.get(xml_key, None)
|
||||||
if xml is not None:
|
if xml is not None:
|
||||||
record_func = xml.add_global_property # noqa
|
record_func = xml.add_global_property
|
||||||
return record_func
|
return record_func
|
||||||
|
|
||||||
|
|
||||||
|
@ -624,7 +624,7 @@ class LogXML:
|
||||||
def update_testcase_duration(self, report: TestReport) -> None:
|
def update_testcase_duration(self, report: TestReport) -> None:
|
||||||
"""Accumulate total duration for nodeid from given report and update
|
"""Accumulate total duration for nodeid from given report and update
|
||||||
the Junit.testcase with the new total if already created."""
|
the Junit.testcase with the new total if already created."""
|
||||||
if self.report_duration == "total" or report.when == self.report_duration:
|
if self.report_duration in {"total", report.when}:
|
||||||
reporter = self.node_reporter(report)
|
reporter = self.node_reporter(report)
|
||||||
reporter.duration += getattr(report, "duration", 0.0)
|
reporter.duration += getattr(report, "duration", 0.0)
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Add backward compatibility support for the legacy py path type."""
|
"""Add backward compatibility support for the legacy py path type."""
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
|
||||||
from typing import Final
|
from typing import Final
|
||||||
from typing import final
|
from typing import final
|
||||||
from typing import List
|
from typing import List
|
||||||
|
@ -14,6 +15,7 @@ from typing import Union
|
||||||
from iniconfig import SectionWrapper
|
from iniconfig import SectionWrapper
|
||||||
|
|
||||||
import py
|
import py
|
||||||
|
|
||||||
from _pytest.cacheprovider import Cache
|
from _pytest.cacheprovider import Cache
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
|
@ -32,6 +34,7 @@ from _pytest.pytester import RunResult
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
from _pytest.tmpdir import TempPathFactory
|
from _pytest.tmpdir import TempPathFactory
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
import pexpect
|
import pexpect
|
||||||
|
|
||||||
|
@ -326,8 +329,8 @@ class LegacyTmpdirPlugin:
|
||||||
|
|
||||||
By default, a new base temporary directory is created each test session,
|
By default, a new base temporary directory is created each test session,
|
||||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
``--basetemp`` is used then it is cleared each session. See
|
||||||
temporary directory`.
|
:ref:`temporary directory location and retention`.
|
||||||
|
|
||||||
The returned object is a `legacy_path`_ object.
|
The returned object is a `legacy_path`_ object.
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
|
# mypy: allow-untyped-defs
|
||||||
"""Access and control log capturing."""
|
"""Access and control log capturing."""
|
||||||
import io
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from datetime import timezone
|
from datetime import timezone
|
||||||
|
import io
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
|
import logging
|
||||||
from logging import LogRecord
|
from logging import LogRecord
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import re
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
@ -43,6 +44,7 @@ from _pytest.main import Session
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
logging_StreamHandler = logging.StreamHandler[StringIO]
|
logging_StreamHandler = logging.StreamHandler[StringIO]
|
||||||
else:
|
else:
|
||||||
|
@ -71,7 +73,8 @@ class DatetimeFormatter(logging.Formatter):
|
||||||
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
|
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
|
||||||
# Construct `datetime.datetime` object from `struct_time`
|
# Construct `datetime.datetime` object from `struct_time`
|
||||||
# and msecs information from `record`
|
# and msecs information from `record`
|
||||||
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
|
# Using int() instead of round() to avoid it exceeding 1_000_000 and causing a ValueError (#11861).
|
||||||
|
dt = datetime(*ct[0:6], microsecond=int(record.msecs * 1000), tzinfo=tz)
|
||||||
return dt.strftime(datefmt)
|
return dt.strftime(datefmt)
|
||||||
# Use `logging.Formatter` for non-microsecond formats
|
# Use `logging.Formatter` for non-microsecond formats
|
||||||
return super().formatTime(record, datefmt)
|
return super().formatTime(record, datefmt)
|
||||||
|
@ -114,7 +117,6 @@ class ColoredLevelFormatter(DatetimeFormatter):
|
||||||
.. warning::
|
.. warning::
|
||||||
This is an experimental API.
|
This is an experimental API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
assert self._fmt is not None
|
assert self._fmt is not None
|
||||||
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
|
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
|
||||||
if not levelname_fmt_match:
|
if not levelname_fmt_match:
|
||||||
|
@ -181,7 +183,6 @@ class PercentStyleMultiline(logging.PercentStyle):
|
||||||
0 (auto-indent turned off) or
|
0 (auto-indent turned off) or
|
||||||
>0 (explicitly set indentation position).
|
>0 (explicitly set indentation position).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if auto_indent_option is None:
|
if auto_indent_option is None:
|
||||||
return 0
|
return 0
|
||||||
elif isinstance(auto_indent_option, bool):
|
elif isinstance(auto_indent_option, bool):
|
||||||
|
@ -297,6 +298,13 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
default=None,
|
default=None,
|
||||||
help="Path to a file when logging will be written to",
|
help="Path to a file when logging will be written to",
|
||||||
)
|
)
|
||||||
|
add_option_ini(
|
||||||
|
"--log-file-mode",
|
||||||
|
dest="log_file_mode",
|
||||||
|
default="w",
|
||||||
|
choices=["w", "a"],
|
||||||
|
help="Log file open mode",
|
||||||
|
)
|
||||||
add_option_ini(
|
add_option_ini(
|
||||||
"--log-file-level",
|
"--log-file-level",
|
||||||
dest="log_file_level",
|
dest="log_file_level",
|
||||||
|
@ -623,9 +631,9 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
# Python logging does not recognise this as a logging level
|
# Python logging does not recognise this as a logging level
|
||||||
raise UsageError(
|
raise UsageError(
|
||||||
"'{}' is not recognized as a logging level name for "
|
f"'{log_level}' is not recognized as a logging level name for "
|
||||||
"'{}'. Please consider passing the "
|
f"'{setting_name}'. Please consider passing the "
|
||||||
"logging level num instead.".format(log_level, setting_name)
|
"logging level num instead."
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@ -668,7 +676,10 @@ class LoggingPlugin:
|
||||||
if not os.path.isdir(directory):
|
if not os.path.isdir(directory):
|
||||||
os.makedirs(directory)
|
os.makedirs(directory)
|
||||||
|
|
||||||
self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
|
self.log_file_mode = get_option_ini(config, "log_file_mode") or "w"
|
||||||
|
self.log_file_handler = _FileHandler(
|
||||||
|
log_file, mode=self.log_file_mode, encoding="UTF-8"
|
||||||
|
)
|
||||||
log_file_format = get_option_ini(config, "log_file_format", "log_format")
|
log_file_format = get_option_ini(config, "log_file_format", "log_format")
|
||||||
log_file_date_format = get_option_ini(
|
log_file_date_format = get_option_ini(
|
||||||
config, "log_file_date_format", "log_date_format"
|
config, "log_file_date_format", "log_date_format"
|
||||||
|
@ -745,7 +756,7 @@ class LoggingPlugin:
|
||||||
fpath.parent.mkdir(exist_ok=True, parents=True)
|
fpath.parent.mkdir(exist_ok=True, parents=True)
|
||||||
|
|
||||||
# https://github.com/python/mypy/issues/11193
|
# https://github.com/python/mypy/issues/11193
|
||||||
stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment]
|
stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment]
|
||||||
old_stream = self.log_file_handler.setStream(stream)
|
old_stream = self.log_file_handler.setStream(stream)
|
||||||
if old_stream:
|
if old_stream:
|
||||||
old_stream.close()
|
old_stream.close()
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
"""Core implementation of the testing process: init, session, runtest loop."""
|
"""Core implementation of the testing process: init, session, runtest loop."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import fnmatch
|
import fnmatch
|
||||||
import functools
|
import functools
|
||||||
import importlib
|
import importlib
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import warnings
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
@ -21,12 +21,14 @@ from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
import warnings
|
||||||
|
|
||||||
import pluggy
|
import pluggy
|
||||||
|
|
||||||
import _pytest._code
|
|
||||||
from _pytest import nodes
|
from _pytest import nodes
|
||||||
|
import _pytest._code
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import directory_arg
|
from _pytest.config import directory_arg
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
@ -48,6 +50,10 @@ from _pytest.runner import SetupState
|
||||||
from _pytest.warning_types import PytestWarning
|
from _pytest.warning_types import PytestWarning
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: Parser) -> None:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
parser.addini(
|
parser.addini(
|
||||||
"norecursedirs",
|
"norecursedirs",
|
||||||
|
@ -376,6 +382,9 @@ def _in_venv(path: Path) -> bool:
|
||||||
|
|
||||||
|
|
||||||
def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
|
def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
|
||||||
|
if collection_path.name == "__pycache__":
|
||||||
|
return True
|
||||||
|
|
||||||
ignore_paths = config._getconftest_pathlist(
|
ignore_paths = config._getconftest_pathlist(
|
||||||
"collect_ignore", path=collection_path.parent
|
"collect_ignore", path=collection_path.parent
|
||||||
)
|
)
|
||||||
|
@ -487,16 +496,16 @@ class Dir(nodes.Directory):
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_parent( # type: ignore[override]
|
def from_parent( # type: ignore[override]
|
||||||
cls,
|
cls,
|
||||||
parent: nodes.Collector, # type: ignore[override]
|
parent: nodes.Collector,
|
||||||
*,
|
*,
|
||||||
path: Path,
|
path: Path,
|
||||||
) -> "Dir":
|
) -> "Self":
|
||||||
"""The public constructor.
|
"""The public constructor.
|
||||||
|
|
||||||
:param parent: The parent collector of this Dir.
|
:param parent: The parent collector of this Dir.
|
||||||
:param path: The directory's path.
|
:param path: The directory's path.
|
||||||
"""
|
"""
|
||||||
return super().from_parent(parent=parent, path=path) # type: ignore[no-any-return]
|
return super().from_parent(parent=parent, path=path)
|
||||||
|
|
||||||
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
||||||
config = self.config
|
config = self.config
|
||||||
|
@ -505,8 +514,6 @@ class Dir(nodes.Directory):
|
||||||
ihook = self.ihook
|
ihook = self.ihook
|
||||||
for direntry in scandir(self.path):
|
for direntry in scandir(self.path):
|
||||||
if direntry.is_dir():
|
if direntry.is_dir():
|
||||||
if direntry.name == "__pycache__":
|
|
||||||
continue
|
|
||||||
path = Path(direntry.path)
|
path = Path(direntry.path)
|
||||||
if not self.session.isinitpath(path, with_parents=True):
|
if not self.session.isinitpath(path, with_parents=True):
|
||||||
if ihook.pytest_ignore_collect(collection_path=path, config=config):
|
if ihook.pytest_ignore_collect(collection_path=path, config=config):
|
||||||
|
@ -724,12 +731,12 @@ class Session(nodes.Collector):
|
||||||
...
|
...
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def perform_collect( # noqa: F811
|
def perform_collect(
|
||||||
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
|
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
|
||||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
||||||
...
|
...
|
||||||
|
|
||||||
def perform_collect( # noqa: F811
|
def perform_collect(
|
||||||
self, args: Optional[Sequence[str]] = None, genitems: bool = True
|
self, args: Optional[Sequence[str]] = None, genitems: bool = True
|
||||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
||||||
"""Perform the collection phase for this session.
|
"""Perform the collection phase for this session.
|
||||||
|
@ -895,10 +902,14 @@ class Session(nodes.Collector):
|
||||||
|
|
||||||
# Prune this level.
|
# Prune this level.
|
||||||
any_matched_in_collector = False
|
any_matched_in_collector = False
|
||||||
for node in subnodes:
|
for node in reversed(subnodes):
|
||||||
# Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`.
|
# Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`.
|
||||||
if isinstance(matchparts[0], Path):
|
if isinstance(matchparts[0], Path):
|
||||||
is_match = node.path == matchparts[0]
|
is_match = node.path == matchparts[0]
|
||||||
|
if sys.platform == "win32" and not is_match:
|
||||||
|
# In case the file paths do not match, fallback to samefile() to
|
||||||
|
# account for short-paths on Windows (#11895).
|
||||||
|
is_match = os.path.samefile(node.path, matchparts[0])
|
||||||
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
|
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
|
||||||
else:
|
else:
|
||||||
# TODO: Remove parametrized workaround once collection structure contains
|
# TODO: Remove parametrized workaround once collection structure contains
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue