Merge upstream

This commit is contained in:
bowu 2023-07-19 08:13:56 +08:00
commit 5877bb8e8d
162 changed files with 2886 additions and 1737 deletions

View File

@ -13,39 +13,53 @@ on:
permissions: {} permissions: {}
jobs: jobs:
build:
deploy:
if: github.repository == 'pytest-dev/pytest'
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 30 timeout-minutes: 10
permissions:
contents: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
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@v1.5 uses: hynek/build-and-inspect-python-package@v1.5
deploy:
if: github.repository == 'pytest-dev/pytest'
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
id-token: write
steps:
- name: Download Package - name: Download Package
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: Packages name: Packages
path: dist path: dist
- name: Publish package to PyPI - name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@v1.8.8
with:
password: ${{ secrets.pypi_token }}
release-notes:
# todo: generate the content in the build job
# the goal being of using a github action script to push the release data
# after success instead of creating a complete python/tox env
needs: [deploy]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: "3.7" python-version: "3.11"
- name: Install tox - name: Install tox
run: | run: |

View File

@ -37,26 +37,26 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
name: [ name: [
"windows-py37",
"windows-py37-pluggy",
"windows-py38", "windows-py38",
"windows-py38-pluggy",
"windows-py39", "windows-py39",
"windows-py310", "windows-py310",
"windows-py311", "windows-py311",
"windows-py312",
"ubuntu-py37",
"ubuntu-py37-pluggy",
"ubuntu-py37-freeze",
"ubuntu-py38", "ubuntu-py38",
"ubuntu-py38-pluggy",
"ubuntu-py38-freeze",
"ubuntu-py39", "ubuntu-py39",
"ubuntu-py310", "ubuntu-py310",
"ubuntu-py311", "ubuntu-py311",
"ubuntu-py312",
"ubuntu-pypy3", "ubuntu-pypy3",
"macos-py37",
"macos-py38", "macos-py38",
"macos-py39", "macos-py39",
"macos-py310", "macos-py310",
"macos-py312",
"docs", "docs",
"doctesting", "doctesting",
@ -64,19 +64,15 @@ jobs:
] ]
include: include:
- name: "windows-py37"
python: "3.7"
os: windows-latest
tox_env: "py37-numpy"
- name: "windows-py37-pluggy"
python: "3.7"
os: windows-latest
tox_env: "py37-pluggymain-pylib-xdist"
- name: "windows-py38" - name: "windows-py38"
python: "3.8" python: "3.8"
os: windows-latest os: windows-latest
tox_env: "py38-unittestextras" tox_env: "py38-unittestextras"
use_coverage: true use_coverage: true
- name: "windows-py38-pluggy"
python: "3.8"
os: windows-latest
tox_env: "py38-pluggymain-pylib-xdist"
- name: "windows-py39" - name: "windows-py39"
python: "3.9" python: "3.9"
os: windows-latest os: windows-latest
@ -86,27 +82,27 @@ jobs:
os: windows-latest os: windows-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "windows-py311" - name: "windows-py311"
python: "3.11-dev" python: "3.11"
os: windows-latest os: windows-latest
tox_env: "py311" tox_env: "py311"
- name: "windows-py312"
python: "3.12-dev"
os: windows-latest
tox_env: "py312"
- name: "ubuntu-py37"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-lsof-numpy-pexpect"
use_coverage: true
- name: "ubuntu-py37-pluggy"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-pluggymain-pylib-xdist"
- name: "ubuntu-py37-freeze"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-freeze"
- name: "ubuntu-py38" - name: "ubuntu-py38"
python: "3.8" python: "3.8"
os: ubuntu-latest os: ubuntu-latest
tox_env: "py38-xdist" tox_env: "py38-lsof-numpy-pexpect"
use_coverage: true
- name: "ubuntu-py38-pluggy"
python: "3.8"
os: ubuntu-latest
tox_env: "py38-pluggymain-pylib-xdist"
- name: "ubuntu-py38-freeze"
python: "3.8"
os: ubuntu-latest
tox_env: "py38-freeze"
- name: "ubuntu-py39" - name: "ubuntu-py39"
python: "3.9" python: "3.9"
os: ubuntu-latest os: ubuntu-latest
@ -116,32 +112,37 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "ubuntu-py311" - name: "ubuntu-py311"
python: "3.11-dev" python: "3.11"
os: ubuntu-latest os: ubuntu-latest
tox_env: "py311" tox_env: "py311"
use_coverage: true use_coverage: true
- name: "ubuntu-py312"
python: "3.12-dev"
os: ubuntu-latest
tox_env: "py312"
use_coverage: true
- name: "ubuntu-pypy3" - name: "ubuntu-pypy3"
python: "pypy-3.7" python: "pypy-3.8"
os: ubuntu-latest os: ubuntu-latest
tox_env: "pypy3-xdist" tox_env: "pypy3-xdist"
- name: "macos-py37"
python: "3.7"
os: macos-latest
tox_env: "py37-xdist"
- name: "macos-py38" - name: "macos-py38"
python: "3.8" python: "3.8"
os: macos-latest os: macos-latest
tox_env: "py38-xdist" tox_env: "py38-xdist"
use_coverage: true
- name: "macos-py39" - name: "macos-py39"
python: "3.9" python: "3.9"
os: macos-latest os: macos-latest
tox_env: "py39-xdist" tox_env: "py39-xdist"
use_coverage: true
- name: "macos-py310" - name: "macos-py310"
python: "3.10" python: "3.10"
os: macos-latest os: macos-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "macos-py312"
python: "3.12-dev"
os: macos-latest
tox_env: "py312-xdist"
- name: "plugins" - name: "plugins"
python: "3.9" python: "3.9"
@ -149,11 +150,11 @@ jobs:
tox_env: "plugins" tox_env: "plugins"
- name: "docs" - name: "docs"
python: "3.7" python: "3.8"
os: ubuntu-latest os: ubuntu-latest
tox_env: "docs" tox_env: "docs"
- name: "doctesting" - name: "doctesting"
python: "3.7" python: "3.8"
os: ubuntu-latest os: ubuntu-latest
tox_env: "doctesting" tox_env: "doctesting"
use_coverage: true use_coverage: true
@ -168,6 +169,7 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
check-latest: ${{ endsWith(matrix.python, '-dev') }}
- name: Install dependencies - name: Install dependencies
run: | run: |

View File

@ -38,7 +38,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@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
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>'

View File

@ -1,14 +1,14 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.3.0 rev: 23.7.0
hooks: hooks:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs - repo: https://github.com/asottile/blacken-docs
rev: 1.13.0 rev: 1.15.0
hooks: hooks:
- id: blacken-docs - id: blacken-docs
additional_dependencies: [black==23.1.0] 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.4.0 rev: v4.4.0
hooks: hooks:
@ -21,7 +21,7 @@ repos:
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/PyCQA/autoflake
rev: v2.1.1 rev: v2.2.0
hooks: hooks:
- id: autoflake - id: autoflake
name: autoflake name: autoflake
@ -37,26 +37,26 @@ repos:
- flake8-typing-imports==1.12.0 - flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0 - flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder-python-imports - repo: https://github.com/asottile/reorder-python-imports
rev: v3.9.0 rev: v3.10.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: ['--application-directories=.:src', --py37-plus] args: ['--application-directories=.:src', --py38-plus]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.4.0 rev: v3.9.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py38-plus]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.2.0 rev: v2.4.0
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
args: ["--max-py-version=3.11", "--include-version-classifiers"] 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:
- id: python-use-type-annotations - id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.3.0 rev: v1.4.1
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/)

View File

@ -11,6 +11,7 @@ Adam Johnson
Adam Stewart Adam Stewart
Adam Uhlir Adam Uhlir
Ahn Ki-Wook Ahn Ki-Wook
Akhilesh Ramakrishnan
Akiomi Kamakura Akiomi Kamakura
Alan Velasco Alan Velasco
Alessio Izzo Alessio Izzo
@ -130,6 +131,7 @@ Eric Hunsberger
Eric Liu Eric Liu
Eric Siegerman Eric Siegerman
Erik Aronesty Erik Aronesty
Erik Hasse
Erik M. Bray Erik M. Bray
Evan Kepner Evan Kepner
Evgeny Seliverstov Evgeny Seliverstov
@ -167,6 +169,7 @@ Ian Bicking
Ian Lesperance Ian Lesperance
Ilya Konstantinov Ilya Konstantinov
Ionuț Turturică Ionuț Turturică
Isaac Virshup
Itxaso Aizpurua Itxaso Aizpurua
Iwan Briquemont Iwan Briquemont
Jaap Broekhuizen Jaap Broekhuizen
@ -262,6 +265,7 @@ Mickey Pashov
Mihai Capotă Mihai Capotă
Mike Hoyle (hoylemd) Mike Hoyle (hoylemd)
Mike Lundy Mike Lundy
Milan Lesnek
Miro Hrončok Miro Hrončok
Nathaniel Compton Nathaniel Compton
Nathaniel Waisbrot Nathaniel Waisbrot
@ -311,6 +315,7 @@ Raphael Pierzina
Rafal Semik Rafal Semik
Raquel Alegre Raquel Alegre
Ravi Chandra Ravi Chandra
Reagan Lee
Robert Holt Robert Holt
Roberto Aldera Roberto Aldera
Roberto Polli Roberto Polli
@ -371,6 +376,7 @@ Tomer Keren
Tony Narlock Tony Narlock
Tor Colvin Tor Colvin
Trevor Bekolay Trevor Bekolay
Tushar Sadhwani
Tyler Goodlet Tyler Goodlet
Tzu-ping Chung Tzu-ping Chung
Vasily Kuznetsov Vasily Kuznetsov
@ -378,6 +384,7 @@ Victor Maryama
Victor Rodriguez Victor Rodriguez
Victor Uriarte Victor Uriarte
Vidar T. Fauske Vidar T. Fauske
Vijay Arora
Virgil Dupras Virgil Dupras
Vitaly Lashmanov Vitaly Lashmanov
Vivaan Verma Vivaan Verma

View File

@ -201,7 +201,7 @@ Short version
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting. #. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
#. Tests are run using ``tox``:: #. Tests are run using ``tox``::
tox -e linting,py37 tox -e linting,py39
The test environments above are usually enough to cover most cases locally. The test environments above are usually enough to cover most cases locally.
@ -272,24 +272,24 @@ Here is a simple overview, with pytest-specific bits:
#. Run all the tests #. Run all the tests
You need to have Python 3.7 available in your system. Now You need to have Python 3.8 or later available in your system. Now
running tests is as simple as issuing this command:: running tests is as simple as issuing this command::
$ tox -e linting,py37 $ tox -e linting,py39
This command will run tests via the "tox" tool against Python 3.7 This command will run tests via the "tox" tool against Python 3.9
and also perform "lint" coding-style checks. and also perform "lint" coding-style checks.
#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming. #. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming.
You can pass different options to ``tox``. For example, to run tests on Python 3.7 and pass options to pytest You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest
(e.g. enter pdb on failure) to pytest you can do:: (e.g. enter pdb on failure) to pytest you can do::
$ tox -e py37 -- --pdb $ tox -e py39 -- --pdb
Or to only run tests in a particular test module on Python 3.7:: Or to only run tests in a particular test module on Python 3.9::
$ tox -e py37 -- testing/test_config.py $ tox -e py39 -- testing/test_config.py
When committing, ``pre-commit`` will re-format the files if necessary. When committing, ``pre-commit`` will re-format the files if necessary.

View File

@ -100,7 +100,7 @@ Features
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial), - Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box `nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
- Python 3.7+ or PyPy3 - Python 3.8+ or PyPy3
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community - Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community

View File

@ -1 +0,0 @@
Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.

View File

@ -0,0 +1,2 @@
Fixed that fake intermediate modules generated by ``--import-mode=importlib`` would not include the
child modules as attributes of the parent modules.

View File

@ -0,0 +1,2 @@
markers are now considered in the reverse mro order to ensure base class markers are considered first
this resolves a regression.

View File

@ -0,0 +1,2 @@
:meth:`pytest.WarningsRecorder.pop` will return the most-closely-matched warning in the list,
rather than the first warning which is an instance of the requested type.

View File

@ -0,0 +1 @@
Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.

View File

@ -0,0 +1,2 @@
Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
to be imported more than once, causing problems with modules that have import side effects.

View File

@ -1 +0,0 @@
Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.

View File

@ -1 +0,0 @@
Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.

View File

@ -1,2 +0,0 @@
Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.

View File

@ -1,5 +0,0 @@
When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
Previously, the last frame of the traceback was shown, even though it was hidden.

View File

@ -1,3 +0,0 @@
Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>` as a helper for that.

View File

@ -1 +0,0 @@
:confval:`testpaths` is now honored to load root ``conftests``.

View File

@ -1 +0,0 @@
Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.

View File

@ -1 +0,0 @@
The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.

View File

@ -1 +0,0 @@
Added underlying exception to cache provider path creation and write warning messages.

1
changelog/11011.doc.rst Normal file
View File

@ -0,0 +1 @@
Added a warning about modifying the root logger during tests when using ``caplog``.

View File

@ -1 +0,0 @@
Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.

View File

@ -1 +0,0 @@
Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.

View File

@ -1 +0,0 @@
Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.

View File

@ -1,3 +0,0 @@
When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir`.
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.

View File

@ -1 +0,0 @@
Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).

View File

@ -0,0 +1,6 @@
``pluggy>=1.2.0`` is now required.
pytest now uses "new-style" hook wrappers internally, available since pluggy 1.2.0.
See `pluggy's 1.2.0 changelog <https://pluggy.readthedocs.io/en/latest/changelog.html#pluggy-1-2-0-2023-06-21>`_ and the :ref:`updated docs <hookwrapper>` for details.
Plugins which want to use new-style wrappers can do so if they require this version of pytest or later.

View File

@ -0,0 +1 @@
- Prevent constants at the top of file from being detected as docstrings.

View File

@ -0,0 +1,2 @@
Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27
<https://devguide.python.org/versions/>`__.

View File

@ -0,0 +1,2 @@
The (internal) ``FixtureDef.cached_result`` type has changed.
Now the third item ``cached_result[2]``, when set, is an exception instance instead of an exception triplet.

View File

@ -0,0 +1 @@
If a test is skipped from inside an :ref:`xunit setup fixture <classic xunit>`, the test summary now shows the test location instead of the fixture location.

View File

@ -0,0 +1 @@
Allow :func:`pytest.raises` ``match`` argument to match against `PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``.

View File

@ -1 +0,0 @@
Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).

View File

@ -0,0 +1,3 @@
Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work.
This will become an error in the future.

View File

@ -0,0 +1,22 @@
**PytestRemovedIn8Warning deprecation warnings are now errors by default.**
Following our plan to remove deprecated features with as little disruption as
possible, all warnings of type ``PytestRemovedIn8Warning`` now generate errors
instead of warning messages by default.
**The affected features will be effectively removed in pytest 8.1**, so please consult the
:ref:`deprecations` section in the docs for directions on how to update existing code.
In the pytest ``8.0.X`` series, it is possible to change the errors back into warnings as a
stopgap measure by adding this to your ``pytest.ini`` file:
.. code-block:: ini
[pytest]
filterwarnings =
ignore::pytest.PytestRemovedIn8Warning
But this will stop working when pytest ``8.1`` is released.
**If you have concerns** about the removal of a specific feature, please add a
comment to :issue:`7363`.

View File

@ -0,0 +1 @@
:class:`~pytest.FixtureDef` is now exported as ``pytest.FixtureDef`` for typing purposes.

View File

@ -1,3 +0,0 @@
:func:`_pytest.logging.LogCaptureFixture.set_level` and :func:`_pytest.logging.LogCaptureFixture.at_level`
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
``logging.disable(LEVEL)``.

View File

@ -0,0 +1,5 @@
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
(unless :confval:`python_files` was changed to allow `__init__.py` file).
To collect the entire package, specify just the directory: `pytest pkg`.

View File

@ -0,0 +1 @@
``pytest.warns`` and similar functions now capture warnings when an exception is raised inside a ``with`` block.

View File

@ -0,0 +1,7 @@
:func:`pytest.warns <warns>` now re-emits unmatched warnings when the context
closes -- previously it would consume all warnings, hiding those that were not
matched by the function.
While this is a new feature, we decided to announce this as a breaking change
because many test suites are configured to error-out on warnings, and will
therefore fail on the newly-re-emitted warnings.

View File

@ -6,6 +6,8 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-7.4.0
release-7.3.2
release-7.3.1 release-7.3.1
release-7.3.0 release-7.3.0
release-7.2.2 release-7.2.2

View File

@ -0,0 +1,21 @@
pytest-7.3.2
=======================================
pytest 7.3.2 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:
* Adam J. Stewart
* Alessio Izzo
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team

View File

@ -0,0 +1,49 @@
pytest-7.4.0
=======================================
The pytest team is proud to announce the 7.4.0 release!
This release contains new features, improvements, and bug fixes,
the full list of changes is available in the changelog:
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:
* Adam J. Stewart
* Alessio Izzo
* Alex
* Alex Lambson
* Brian Larsen
* Bruno Oliveira
* Bryan Ricker
* Chris Mahoney
* Facundo Batista
* Florian Bruhin
* Jarrett Keifer
* Kenny Y
* Miro Hrončok
* Ran Benita
* Roberto Aldera
* Ronny Pfannschmidt
* Sergey Kim
* Stefanie Molin
* Vijay Arora
* Ville Skyttä
* Zac Hatfield-Dodds
* bzoracler
* leeyueh
* nondescryptid
* theirix
Happy testing,
The pytest Development Team

View File

@ -87,6 +87,7 @@ Released pytest versions support all Python versions that are actively maintaine
============== =================== ============== ===================
pytest version min. Python version pytest version min. Python version
============== =================== ============== ===================
8.0+ 3.8+
7.1+ 3.7+ 7.1+ 3.7+
6.2 - 7.0 3.6+ 6.2 - 7.0 3.6+
5.0 - 6.1 3.5+ 5.0 - 6.1 3.5+

View File

@ -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:510 cache -- .../_pytest/cacheprovider.py:528
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)
@ -119,7 +119,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:1360 pytestconfig [session scope] -- .../_pytest/fixtures.py:1353
Session-scoped fixture that returns the session's :class:`pytest.Config` Session-scoped fixture that returns the session's :class:`pytest.Config`
object. object.
@ -196,7 +196,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:498 caplog -- .../_pytest/logging.py:570
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 +207,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:29 monkeypatch -- .../_pytest/monkeypatch.py:30
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

View File

@ -28,6 +28,122 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
pytest 7.4.0 (2023-06-23)
=========================
Features
--------
- `#10901 <https://github.com/pytest-dev/pytest/issues/10901>`_: Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
Improvements
------------
- `#10872 <https://github.com/pytest-dev/pytest/issues/10872>`_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
- `#10907 <https://github.com/pytest-dev/pytest/issues/10907>`_: When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
Previously, the last frame of the traceback was shown, even though it was hidden.
- `#10940 <https://github.com/pytest-dev/pytest/issues/10940>`_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
Added ``TerminalReporter.wrap_write()`` as a helper for that.
- `#10991 <https://github.com/pytest-dev/pytest/issues/10991>`_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
- `#11005 <https://github.com/pytest-dev/pytest/issues/11005>`_: Added the underlying exception to the cache provider's path creation and write warning messages.
- `#11013 <https://github.com/pytest-dev/pytest/issues/11013>`_: Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
- `#11043 <https://github.com/pytest-dev/pytest/issues/11043>`_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir <rootdir>`.
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
- `#11081 <https://github.com/pytest-dev/pytest/issues/11081>`_: The :confval:`norecursedirs` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
If after updating to this version you see that your `norecursedirs` setting is not being respected,
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
Most likely, your hook returns `False` for paths it does not want to ignore,
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
- `#8711 <https://github.com/pytest-dev/pytest/issues/8711>`_: :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :func:`caplog.at_level() <pytest.LogCaptureFixture.at_level>`
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
``logging.disable(LEVEL)``.
Bug Fixes
---------
- `#10831 <https://github.com/pytest-dev/pytest/issues/10831>`_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
- `#11068 <https://github.com/pytest-dev/pytest/issues/11068>`_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
- `#11104 <https://github.com/pytest-dev/pytest/issues/11104>`_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
even when it was not utilized (e.g. when explicit paths were given on the command line).
Now the ``testpaths`` are only considered when they are in use.
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
- `#7781 <https://github.com/pytest-dev/pytest/issues/7781>`_: Fix writing non-encodable text to log file when using ``--debug``.
Improved Documentation
----------------------
- `#9146 <https://github.com/pytest-dev/pytest/issues/9146>`_: Improved documentation for :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>`.
Trivial/Internal Changes
------------------------
- `#11031 <https://github.com/pytest-dev/pytest/issues/11031>`_: Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
pytest 7.3.2 (2023-06-10)
=========================
Bug Fixes
---------
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
pytest 7.3.1 (2023-04-14) pytest 7.3.1 (2023-04-14)
========================= =========================

View File

@ -15,12 +15,10 @@
# #
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
# The short X.Y version. # The short X.Y version.
import ast
import os import os
import shutil import shutil
import sys import sys
from textwrap import dedent from textwrap import dedent
from typing import List
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from _pytest import __version__ as version from _pytest import __version__ as version
@ -451,25 +449,6 @@ def setup(app: "sphinx.application.Sphinx") -> None:
configure_logging(app) configure_logging(app)
# Make Sphinx mark classes with "final" when decorated with @final.
# We need this because we import final from pytest._compat, not from
# typing (for Python < 3.8 compat), so Sphinx doesn't detect it.
# To keep things simple we accept any `@final` decorator.
# Ref: https://github.com/pytest-dev/pytest/pull/7780
import sphinx.pycode.ast
import sphinx.pycode.parser
original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final
def patched_is_final(self, decorators: List[ast.expr]) -> bool:
if original_is_final(self, decorators):
return True
return any(
sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators
)
sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final
# legacypath.py monkey-patches pytest.Testdir in. Import the file so # legacypath.py monkey-patches pytest.Testdir in. Import the file so
# that autodoc can discover references to it. # that autodoc can discover references to it.
import _pytest.legacypath # noqa: F401 import _pytest.legacypath # noqa: F401

View File

@ -380,6 +380,25 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a ``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
deprecation warning is now raised. deprecation warning is now raised.
Applying a mark to a fixture function
-------------------------------------
.. deprecated:: 7.4
Applying a mark to a fixture function never had any effect, but it is a common user error.
.. code-block:: python
@pytest.mark.usefixtures("clean_database")
@pytest.fixture
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.
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
Backward compatibilities in ``Parser.addoption`` Backward compatibilities in ``Parser.addoption``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -467,12 +486,26 @@ The ``yield_fixture`` function/decorator
It has been so for a very long time, so can be search/replaced safely. It has been so for a very long time, so can be search/replaced safely.
Removed Features Removed Features and Breaking Changes
---------------- -------------------------------------
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
an appropriate period of deprecation has passed. an appropriate period of deprecation has passed.
Some breaking changes which could not be deprecated are also listed.
Collecting ``__init__.py`` files no longer collects package
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 8.0
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
(unless :confval:`python_files` was changed to allow `__init__.py` file).
To collect the entire package, specify just the directory: `pytest pkg`.
The ``pytest.collect`` module The ``pytest.collect`` module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -12,7 +12,7 @@ class YamlFile(pytest.File):
# We need a yaml parser, e.g. PyYAML. # We need a yaml parser, e.g. PyYAML.
import yaml import yaml
raw = yaml.safe_load(self.path.open()) raw = yaml.safe_load(self.path.open(encoding="utf-8"))
for name, spec in sorted(raw.items()): for name, spec in sorted(raw.items()):
yield YamlItem.from_parent(self, name=name, spec=spec) yield YamlItem.from_parent(self, name=name, spec=spec)

View File

@ -691,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory:
pass pass
@pytest.fixture(scope="session") @pytest.fixture(scope="package")
def db(): def db():
return DB() return DB()
@ -808,16 +808,15 @@ case we just write some information out to a ``failures`` file:
import pytest import pytest
@pytest.hookimpl(tryfirst=True, hookwrapper=True) @pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object # execute all other hooks to obtain the report object
outcome = yield rep = yield
rep = outcome.get_result()
# we only look at actual failing test calls, not setup/teardown # we only look at actual failing test calls, not setup/teardown
if rep.when == "call" and rep.failed: if rep.when == "call" and rep.failed:
mode = "a" if os.path.exists("failures") else "w" mode = "a" if os.path.exists("failures") else "w"
with open("failures", mode) as f: with open("failures", mode, encoding="utf-8") as f:
# let's also access a fixture for the fun of it # let's also access a fixture for the fun of it
if "tmp_path" in item.fixturenames: if "tmp_path" in item.fixturenames:
extra = " ({})".format(item.funcargs["tmp_path"]) extra = " ({})".format(item.funcargs["tmp_path"])
@ -826,6 +825,8 @@ case we just write some information out to a ``failures`` file:
f.write(rep.nodeid + extra + "\n") f.write(rep.nodeid + extra + "\n")
return rep
if you then have failing tests: if you then have failing tests:
@ -899,16 +900,17 @@ here is a little example implemented via a local plugin:
phase_report_key = StashKey[Dict[str, CollectReport]]() phase_report_key = StashKey[Dict[str, CollectReport]]()
@pytest.hookimpl(tryfirst=True, hookwrapper=True) @pytest.hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call): def pytest_runtest_makereport(item, call):
# execute all other hooks to obtain the report object # execute all other hooks to obtain the report object
outcome = yield rep = yield
rep = outcome.get_result()
# store test results for each phase of a call, which can # store test results for each phase of a call, which can
# be "setup", "call", "teardown" # be "setup", "call", "teardown"
item.stash.setdefault(phase_report_key, {})[rep.when] = rep item.stash.setdefault(phase_report_key, {})[rep.when] = rep
return rep
@pytest.fixture @pytest.fixture
def something(request): def something(request):

View File

@ -9,7 +9,7 @@ Get Started
Install ``pytest`` Install ``pytest``
---------------------------------------- ----------------------------------------
``pytest`` requires: Python 3.7+ or PyPy3. ``pytest`` requires: Python 3.8+ or PyPy3.
1. Run the following command in your command line: 1. Run the following command in your command line:
@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash .. code-block:: bash
$ pytest --version $ pytest --version
pytest 7.3.1 pytest 7.4.0
.. _`simpletest`: .. _`simpletest`:

View File

@ -135,10 +135,6 @@ Warning about unraisable exceptions and unhandled thread exceptions
.. versionadded:: 6.2 .. versionadded:: 6.2
.. note::
These features only work on Python>=3.8.
Unhandled exceptions are exceptions that are raised in a situation in which Unhandled exceptions are exceptions that are raised in a situation in which
they cannot propagate to a caller. The most common case is an exception raised they cannot propagate to a caller. The most common case is an exception raised
in a :meth:`__del__ <object.__del__>` implementation. in a :meth:`__del__ <object.__del__>` implementation.

View File

@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker:
class TestDirectoryInit: class TestDirectoryInit:
def test_cwd_starts_empty(self): def test_cwd_starts_empty(self):
assert os.listdir(os.getcwd()) == [] assert os.listdir(os.getcwd()) == []
with open("myfile", "w") as f: with open("myfile", "w", encoding="utf-8") as f:
f.write("hello") f.write("hello")
def test_cwd_again_starts_empty(self): def test_cwd_again_starts_empty(self):
@ -1752,8 +1752,7 @@ into an ini-file:
def my_fixture_that_sadly_wont_use_my_other_fixture(): def my_fixture_that_sadly_wont_use_my_other_fixture():
... ...
Currently this will not generate any error or warning, but this is intended This generates a deprecation warning, and will become an error in Pytest 8.
to be handled by :issue:`3664`.
.. _`override fixtures`: .. _`override fixtures`:

View File

@ -172,6 +172,13 @@ the records for the ``setup`` and ``call`` stages during teardown like so:
The full API is available at :class:`pytest.LogCaptureFixture`. The full API is available at :class:`pytest.LogCaptureFixture`.
.. warning::
The ``caplog`` fixture adds a handler to the root logger to capture logs. If the root logger is
modified during a test, for example with ``logging.config.dictConfig``, this handler may be
removed and cause no logs to be captured. To avoid this, ensure that any root logger configuration
only adds to the existing handlers.
.. _live_logs: .. _live_logs:

View File

@ -47,8 +47,7 @@ Unsupported idioms / known issues
- nose imports test modules with the same import path (e.g. - nose imports test modules with the same import path (e.g.
``tests.test_mode``) but different file system paths ``tests.test_mode``) but different file system paths
(e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``) (e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``)
by extending sys.path/import semantics. pytest does not do that by extending sys.path/import semantics. pytest does not do that. Note that
but there is discussion in :issue:`268` for adding some support. Note that
`nose2 choose to avoid this sys.path/import hackery <https://nose2.readthedocs.io/en/latest/differences.html#test-discovery-and-loading>`_. `nose2 choose to avoid this sys.path/import hackery <https://nose2.readthedocs.io/en/latest/differences.html#test-discovery-and-loading>`_.
If you place a conftest.py file in the root directory of your project If you place a conftest.py file in the root directory of your project
@ -66,16 +65,34 @@ Unsupported idioms / known issues
- no nose-configuration is recognized. - no nose-configuration is recognized.
- ``yield``-based methods are unsupported as of pytest 4.1.0. They are - ``yield``-based methods are
fundamentally incompatible with pytest because they don't support fixtures fundamentally incompatible with pytest because they don't support fixtures
properly since collection and test execution are separated. properly since collection and test execution are separated.
Here is a table comparing the default supported naming conventions for both
nose and pytest.
========= ========================== ======= =====
what default naming convention pytest nose
========= ========================== ======= =====
module ``test*.py``
module ``test_*.py`` ✅ ✅
module ``*_test.py``
module ``*_tests.py``
class ``*(unittest.TestCase)`` ✅ ✅
method ``test_*`` ✅ ✅
class ``Test*``
method ``test_*``
function ``test_*``
========= ========================== ======= =====
Migrating from nose to pytest Migrating from nose to pytest
------------------------------ ------------------------------
`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script `nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script
and pytest plugin to help convert Nose-based tests into pytest-based tests. and pytest plugin to help convert Nose-based tests into pytest-based tests.
Specifically, the script transforms nose.tools.assert_* function calls into Specifically, the script transforms ``nose.tools.assert_*`` function calls into
raw assert statements, while preserving format of original arguments raw assert statements, while preserving format of original arguments
as much as possible. as much as possible.

View File

@ -24,8 +24,8 @@ created in the `base temporary directory`_.
d = tmp_path / "sub" d = tmp_path / "sub"
d.mkdir() d.mkdir()
p = d / "hello.txt" p = d / "hello.txt"
p.write_text(CONTENT) p.write_text(CONTENT, encoding="utf-8")
assert p.read_text() == CONTENT assert p.read_text(encoding="utf-8") == CONTENT
assert len(list(tmp_path.iterdir())) == 1 assert len(list(tmp_path.iterdir())) == 1
assert 0 assert 0

View File

@ -207,10 +207,10 @@ creation of a per-test temporary directory:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def initdir(self, tmp_path, monkeypatch): def initdir(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory
tmp_path.joinpath("samplefile.ini").write_text("# testdata") tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8")
def test_method(self): def test_method(self):
with open("samplefile.ini") as f: with open("samplefile.ini", encoding="utf-8") as f:
s = f.read() s = f.read()
assert "testdata" in s assert "testdata" in s

View File

@ -173,7 +173,8 @@ You can invoke ``pytest`` from Python code directly:
this acts as if you would call "pytest" from the command line. this acts as if you would call "pytest" from the command line.
It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead. It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead.
You can pass in options and arguments: If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable.
You can pass in options and arguments explicitly:
.. code-block:: python .. code-block:: python

View File

@ -56,7 +56,7 @@ The remaining hook functions will not be called in this case.
.. _`hookwrapper`: .. _`hookwrapper`:
hookwrapper: executing around other hooks hook wrappers: executing around other hooks
------------------------------------------------- -------------------------------------------------
.. currentmodule:: _pytest.core .. currentmodule:: _pytest.core
@ -69,10 +69,8 @@ which yields exactly once. When pytest invokes hooks it first executes
hook wrappers and passes the same arguments as to the regular hooks. hook wrappers and passes the same arguments as to the regular hooks.
At the yield point of the hook wrapper pytest will execute the next hook At the yield point of the hook wrapper pytest will execute the next hook
implementations and return their result to the yield point in the form of implementations and return their result to the yield point, or will
a :py:class:`Result <pluggy._Result>` instance which encapsulates a result or propagate an exception if they raised.
exception info. The yield point itself will thus typically not raise
exceptions (unless there are bugs).
Here is an example definition of a hook wrapper: Here is an example definition of a hook wrapper:
@ -81,26 +79,35 @@ Here is an example definition of a hook wrapper:
import pytest import pytest
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(wrapper=True)
def pytest_pyfunc_call(pyfuncitem): def pytest_pyfunc_call(pyfuncitem):
do_something_before_next_hook_executes() do_something_before_next_hook_executes()
outcome = yield # If the outcome is an exception, will raise the exception.
# outcome.excinfo may be None or a (cls, val, tb) tuple res = yield
res = outcome.get_result() # will raise if outcome was exception new_res = post_process_result(res)
post_process_result(res) # Override the return value to the plugin system.
return new_res
outcome.force_result(new_res) # to override the return value to the plugin system The hook wrapper needs to return a result for the hook, or raise an exception.
Note that hook wrappers don't return results themselves, they merely In many cases, the wrapper only needs to perform tracing or other side effects
perform tracing or other side effects around the actual hook implementations. around the actual hook implementations, in which case it can return the result
If the result of the underlying hook is a mutable object, they may modify value of the ``yield``. The simplest (though useless) hook wrapper is
that result but it's probably better to avoid it. ``return (yield)``.
In other cases, the wrapper wants the adjust or adapt the result, in which case
it can return a new value. If the result of the underlying hook is a mutable
object, the wrapper may modify that result, but it's probably better to avoid it.
If the hook implementation failed with an exception, the wrapper can handle that
exception using a ``try-catch-finally`` around the ``yield``, by propagating it,
supressing it, or raising a different exception entirely.
For more information, consult the For more information, consult the
:ref:`pluggy documentation about hookwrappers <pluggy:hookwrappers>`. :ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.
.. _plugin-hookorder: .. _plugin-hookorder:
@ -130,11 +137,14 @@ after others, i.e. the position in the ``N``-sized list of functions:
# Plugin 3 # Plugin 3
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(wrapper=True)
def pytest_collection_modifyitems(items): def pytest_collection_modifyitems(items):
# will execute even before the tryfirst one above! # will execute even before the tryfirst one above!
outcome = yield try:
# will execute after all non-hookwrappers executed return (yield)
finally:
# will execute after all non-wrappers executed
...
Here is the order of execution: Here is the order of execution:
@ -149,12 +159,11 @@ Here is the order of execution:
Plugin1). Plugin1).
4. Plugin3's pytest_collection_modifyitems then executing the code after the yield 4. Plugin3's pytest_collection_modifyitems then executing the code after the yield
point. The yield receives a :py:class:`Result <pluggy._Result>` instance which encapsulates point. The yield receives the result from calling the non-wrappers, or raises
the result from calling the non-wrappers. Wrappers shall not modify the result. an exception if the non-wrappers raised.
It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
``hookwrapper=True`` in which case it will influence the ordering of hookwrappers in which case it will influence the ordering of hook wrappers among each other.
among each other.
Declaring new hooks Declaring new hooks

View File

@ -2,7 +2,9 @@
.. sidebar:: Next Open Trainings .. sidebar:: Next Open Trainings
- `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/Remote - `pytest tips and tricks for a better testsuite <https://ep2023.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2023 <https://ep2023.europython.eu/>`_, **July 18th** (3h), **Prague, Czech Republic / Remote**
- `pytest: Professionelles Testen (nicht nur) für Python <https://workshoptage.ch/workshops/2023/pytest-professionelles-testen-nicht-nur-fuer-python-2/>`_, at `Workshoptage 2023 <https://workshoptage.ch/>`_, **September 5th**, `OST <https://www.ost.ch/en>`_ Campus **Rapperswil, Switzerland**
- `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**
Also see :doc:`previous talks and blogposts <talks>`. Also see :doc:`previous talks and blogposts <talks>`.
@ -17,7 +19,7 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can
scale to support complex functional testing for applications and libraries. scale to support complex functional testing for applications and libraries.
``pytest`` requires: Python 3.7+ or PyPy3. ``pytest`` requires: Python 3.8+ or PyPy3.
**PyPI package name**: :pypi:`pytest` **PyPI package name**: :pypi:`pytest`
@ -76,7 +78,7 @@ Features
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box - Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
- Python 3.7+ or PyPy 3 - Python 3.8+ or PyPy 3
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community - Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community

File diff suppressed because it is too large Load Diff

View File

@ -783,18 +783,66 @@ reporting or interaction with exceptions:
.. autofunction:: pytest_leave_pdb .. autofunction:: pytest_leave_pdb
Objects Collection tree objects
------- -----------------------
Full reference to objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`. These are the collector and item classes (collectively called "nodes") which
make up the collection tree.
Node
~~~~
CallInfo .. autoclass:: _pytest.nodes.Node()
~~~~~~~~
.. autoclass:: pytest.CallInfo()
:members: :members:
Collector
~~~~~~~~~
.. autoclass:: pytest.Collector()
:members:
:show-inheritance:
Item
~~~~
.. autoclass:: pytest.Item()
:members:
:show-inheritance:
File
~~~~
.. autoclass:: pytest.File()
:members:
:show-inheritance:
FSCollector
~~~~~~~~~~~
.. autoclass:: _pytest.nodes.FSCollector()
:members:
:show-inheritance:
Session
~~~~~~~
.. autoclass:: pytest.Session()
:members:
:show-inheritance:
Package
~~~~~~~
.. autoclass:: pytest.Package()
:members:
:show-inheritance:
Module
~~~~~~
.. autoclass:: pytest.Module()
:members:
:show-inheritance:
Class Class
~~~~~ ~~~~~
@ -803,13 +851,34 @@ Class
:members: :members:
:show-inheritance: :show-inheritance:
Collector Function
~~~~~~~~~ ~~~~~~~~
.. autoclass:: pytest.Collector() .. autoclass:: pytest.Function()
:members: :members:
:show-inheritance: :show-inheritance:
FunctionDefinition
~~~~~~~~~~~~~~~~~~
.. autoclass:: _pytest.python.FunctionDefinition()
:members:
:show-inheritance:
Objects
-------
Objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`
or importable from ``pytest``.
CallInfo
~~~~~~~~
.. autoclass:: pytest.CallInfo()
:members:
CollectReport CollectReport
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
@ -837,46 +906,11 @@ ExitCode
.. autoclass:: pytest.ExitCode .. autoclass:: pytest.ExitCode
:members: :members:
File
~~~~
.. autoclass:: pytest.File()
:members:
:show-inheritance:
FixtureDef FixtureDef
~~~~~~~~~~ ~~~~~~~~~~
.. autoclass:: _pytest.fixtures.FixtureDef() .. autoclass:: pytest.FixtureDef()
:members:
:show-inheritance:
FSCollector
~~~~~~~~~~~
.. autoclass:: _pytest.nodes.FSCollector()
:members:
:show-inheritance:
Function
~~~~~~~~
.. autoclass:: pytest.Function()
:members:
:show-inheritance:
FunctionDefinition
~~~~~~~~~~~~~~~~~~
.. autoclass:: _pytest.python.FunctionDefinition()
:members:
:show-inheritance:
Item
~~~~
.. autoclass:: pytest.Item()
:members: :members:
:show-inheritance: :show-inheritance:
@ -907,19 +941,6 @@ Metafunc
.. autoclass:: pytest.Metafunc() .. autoclass:: pytest.Metafunc()
:members: :members:
Module
~~~~~~
.. autoclass:: pytest.Module()
:members:
:show-inheritance:
Node
~~~~
.. autoclass:: _pytest.nodes.Node()
:members:
Parser Parser
~~~~~~ ~~~~~~
@ -941,13 +962,6 @@ PytestPluginManager
:inherited-members: :inherited-members:
:show-inheritance: :show-inheritance:
Session
~~~~~~~
.. autoclass:: pytest.Session()
:members:
:show-inheritance:
TestReport TestReport
~~~~~~~~~~ ~~~~~~~~~~
@ -1153,6 +1167,9 @@ Custom warnings generated in some situations such as improper usage or deprecate
.. autoclass:: pytest.PytestRemovedIn8Warning .. autoclass:: pytest.PytestRemovedIn8Warning
:show-inheritance: :show-inheritance:
.. autoclass:: pytest.PytestRemovedIn9Warning
:show-inheritance:
.. autoclass:: pytest.PytestUnhandledCoroutineWarning .. autoclass:: pytest.PytestUnhandledCoroutineWarning
:show-inheritance: :show-inheritance:
@ -1703,6 +1720,11 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest] [pytest]
pythonpath = src1 src2 pythonpath = src1 src2
.. note::
``pythonpath`` does not affect some imports that happen very early,
most notably plugins loaded using the ``-p`` command line option.
.. confval:: required_plugins .. confval:: required_plugins
@ -1918,9 +1940,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
--strict-markers Markers not registered in the `markers` section of --strict-markers Markers not registered in the `markers` section of
the configuration file raise errors the configuration file raise errors
--strict (Deprecated) alias to --strict-markers --strict (Deprecated) alias to --strict-markers
-c, --config-file FILE -c FILE, --config-file=FILE
Load configuration from `FILE` instead of trying to Load configuration from `FILE` instead of trying to
locate one of the implicit configuration files locate one of the implicit configuration files.
--continue-on-collection-errors --continue-on-collection-errors
Force test execution even if collection errors occur Force test execution even if collection errors occur
--rootdir=ROOTDIR Define root directory for tests. Can be relative --rootdir=ROOTDIR Define root directory for tests. Can be relative

View File

@ -1,5 +1,5 @@
pallets-sphinx-themes pallets-sphinx-themes
pluggy>=1.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,<6 sphinx>=5,<6

View File

@ -113,7 +113,7 @@ template = "changelog/_template.rst"
showcontent = true showcontent = true
[tool.black] [tool.black]
target-version = ['py37'] target-version = ['py38']
# check-wheel-contents is executed by the build-and-inspect-python-package action. # check-wheel-contents is executed by the build-and-inspect-python-package action.
[tool.check-wheel-contents] [tool.check-wheel-contents]

View File

@ -7,7 +7,9 @@ def main():
Platform agnostic wrapper script for towncrier. Platform agnostic wrapper script for towncrier.
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs. Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
""" """
with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file: with open(
"doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8"
) as draft_file:
return call(("towncrier", "--draft"), stdout=draft_file) return call(("towncrier", "--draft"), stdout=draft_file)

View File

@ -17,7 +17,9 @@ Plugin List
=========== ===========
PyPI projects that match "pytest-\*" are considered plugins and are listed PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically. Packages classified as inactive are excluded. automatically together with a manually-maintained list in `the source
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
Packages classified as inactive are excluded.
.. The following conditional uses a different format for this list when .. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the creating a PDF, because otherwise the table gets far too wide for the
@ -33,6 +35,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
"Development Status :: 6 - Mature", "Development Status :: 6 - Mature",
"Development Status :: 7 - Inactive", "Development Status :: 7 - Inactive",
) )
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert",
}
def escape_rst(text: str) -> str: def escape_rst(text: str) -> str:
@ -52,18 +57,18 @@ def iter_plugins():
regex = r">([\d\w-]*)</a>" regex = r">([\d\w-]*)</a>"
response = requests.get("https://pypi.org/simple") response = requests.get("https://pypi.org/simple")
matches = list( match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
match plugin_names = [
for match in re.finditer(regex, response.text) name
if match.groups()[0].startswith("pytest-") for name in match_names
) if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
]
for match in tqdm(matches, smoothing=0): for name in tqdm(plugin_names, smoothing=0):
name = match.groups()[0]
response = requests.get(f"https://pypi.org/pypi/{name}/json") response = requests.get(f"https://pypi.org/pypi/{name}/json")
if response.status_code == 404: if response.status_code == 404:
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
# return 404 on the JSON API. Skip. # but return 404 on the JSON API. Skip.
continue continue
response.raise_for_status() response.raise_for_status()
info = response.json()["info"] info = response.json()["info"]

View File

@ -6,7 +6,7 @@ long_description_content_type = text/x-rst
url = https://docs.pytest.org/en/latest/ url = https://docs.pytest.org/en/latest/
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
license = MIT license = MIT
license_file = LICENSE license_files = LICENSE
platforms = unix, linux, osx, cygwin, win32 platforms = unix, linux, osx, cygwin, win32
classifiers = classifiers =
Development Status :: 6 - Mature Development Status :: 6 - Mature
@ -17,11 +17,11 @@ classifiers =
Operating System :: POSIX Operating System :: POSIX
Programming Language :: Python :: 3 Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Software Development :: Libraries Topic :: Software Development :: Libraries
Topic :: Software Development :: Testing Topic :: Software Development :: Testing
Topic :: Utilities Topic :: Utilities
@ -46,12 +46,11 @@ py_modules = py
install_requires = install_requires =
iniconfig iniconfig
packaging packaging
pluggy>=0.12,<2.0 pluggy>=1.2.0,<2.0
colorama;sys_platform=="win32" colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11" exceptiongroup>=1.0.0rc8;python_version<"3.11"
importlib-metadata>=0.12;python_version<"3.8"
tomli>=1.0.0;python_version<"3.11" tomli>=1.0.0;python_version<"3.11"
python_requires = >=3.7 python_requires = >=3.8
package_dir = package_dir =
=src =src
setup_requires = setup_requires =
@ -73,6 +72,7 @@ testing =
nose nose
pygments>=2.7.2 pygments>=2.7.2
requests requests
setuptools
xmlschema xmlschema
[options.package_data] [options.package_data]

View File

@ -17,18 +17,21 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import ClassVar from typing import ClassVar
from typing import Dict from typing import Dict
from typing import Final
from typing import final
from typing import Generic from typing import Generic
from typing import Iterable from typing import Iterable
from typing import List from typing import List
from typing import Literal
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
from typing import overload from typing import overload
from typing import Pattern from typing import Pattern
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
from typing import SupportsIndex
from typing import Tuple from typing import Tuple
from typing import Type from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
@ -42,22 +45,16 @@ from _pytest._code.source import Source
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.compat import final
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.deprecated import check_ispytest 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 TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal
from typing_extensions import SupportsIndex
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
if sys.version_info[:2] < (3, 11): if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup from exceptiongroup import BaseExceptionGroup
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
class Code: class Code:
"""Wrapper around Python code objects.""" """Wrapper around Python code objects."""
@ -396,11 +393,11 @@ class Traceback(List[TracebackEntry]):
def filter( def filter(
self, self,
# TODO(py38): change to positional only. excinfo_or_fn: Union[
_excinfo_or_fn: Union[
"ExceptionInfo[BaseException]", "ExceptionInfo[BaseException]",
Callable[[TracebackEntry], bool], Callable[[TracebackEntry], bool],
], ],
/,
) -> "Traceback": ) -> "Traceback":
"""Return a Traceback instance with certain items removed. """Return a Traceback instance with certain items removed.
@ -411,10 +408,10 @@ class Traceback(List[TracebackEntry]):
``TracebackEntry`` instance, and should return True when the item should ``TracebackEntry`` instance, and should return True when the item should
be added to the ``Traceback``, False when not. be added to the ``Traceback``, False when not.
""" """
if isinstance(_excinfo_or_fn, ExceptionInfo): if isinstance(excinfo_or_fn, ExceptionInfo):
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731 fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731
else: else:
fn = _excinfo_or_fn fn = excinfo_or_fn
return Traceback(filter(fn, self)) return Traceback(filter(fn, self))
def recursionindex(self) -> Optional[int]: def recursionindex(self) -> Optional[int]:
@ -633,7 +630,7 @@ class ExceptionInfo(Generic[E]):
def getrepr( def getrepr(
self, self,
showlocals: bool = False, showlocals: bool = False,
style: "_TracebackStyle" = "long", style: _TracebackStyle = "long",
abspath: bool = False, abspath: bool = False,
tbfilter: Union[ tbfilter: Union[
bool, Callable[["ExceptionInfo[BaseException]"], Traceback] bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
@ -707,7 +704,12 @@ class ExceptionInfo(Generic[E]):
If it matches `True` is returned, otherwise an `AssertionError` is raised. If it matches `True` is returned, otherwise an `AssertionError` is raised.
""" """
__tracebackhide__ = True __tracebackhide__ = True
value = str(self.value) value = "\n".join(
[
str(self.value),
*getattr(self.value, "__notes__", []),
]
)
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n str(exception): {value!r}" msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n str(exception): {value!r}"
if regexp == value: if regexp == value:
msg += "\n Did you mean to `re.escape()` the regex?" msg += "\n Did you mean to `re.escape()` the regex?"
@ -725,7 +727,7 @@ class FormattedExcinfo:
fail_marker: ClassVar = "E" fail_marker: ClassVar = "E"
showlocals: bool = False showlocals: bool = False
style: "_TracebackStyle" = "long" style: _TracebackStyle = "long"
abspath: bool = True abspath: bool = True
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
funcargs: bool = False funcargs: bool = False
@ -1090,7 +1092,7 @@ class ReprExceptionInfo(ExceptionRepr):
class ReprTraceback(TerminalRepr): class ReprTraceback(TerminalRepr):
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
extraline: Optional[str] extraline: Optional[str]
style: "_TracebackStyle" style: _TracebackStyle
entrysep: ClassVar = "_ " entrysep: ClassVar = "_ "
@ -1124,7 +1126,7 @@ class ReprTracebackNative(ReprTraceback):
class ReprEntryNative(TerminalRepr): class ReprEntryNative(TerminalRepr):
lines: Sequence[str] lines: Sequence[str]
style: ClassVar["_TracebackStyle"] = "native" style: ClassVar[_TracebackStyle] = "native"
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
tw.write("".join(self.lines)) tw.write("".join(self.lines))
@ -1136,7 +1138,7 @@ class ReprEntry(TerminalRepr):
reprfuncargs: Optional["ReprFuncArgs"] reprfuncargs: Optional["ReprFuncArgs"]
reprlocals: Optional["ReprLocals"] reprlocals: Optional["ReprLocals"]
reprfileloc: Optional["ReprFileLocation"] reprfileloc: Optional["ReprFileLocation"]
style: "_TracebackStyle" style: _TracebackStyle
def _write_entry_lines(self, tw: TerminalWriter) -> None: def _write_entry_lines(self, tw: TerminalWriter) -> None:
"""Write the source code portions of a list of traceback entries with syntax highlighting. """Write the source code portions of a list of traceback entries with syntax highlighting.

View File

@ -149,8 +149,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i
values: List[int] = [] values: List[int] = []
for x in ast.walk(node): for x in ast.walk(node):
if isinstance(x, (ast.stmt, ast.ExceptHandler)): if isinstance(x, (ast.stmt, ast.ExceptHandler)):
# Before Python 3.8, the lineno of a decorated class or function pointed at the decorator. # The lineno points to the class/def, so need to include the decorators.
# Since Python 3.8, the lineno points to the class/def, so need to include the decorators.
if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
for d in x.decorator_list: for d in x.decorator_list:
values.append(d.lineno - 1) values.append(d.lineno - 1)

View File

@ -2,12 +2,12 @@
import os import os
import shutil import shutil
import sys import sys
from typing import final
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
from typing import TextIO from typing import TextIO
from .wcwidth import wcswidth from .wcwidth import wcswidth
from _pytest.compat import final
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py. # This code was initially copied from py 1.8.1, file _io/terminalwriter.py.

View File

@ -25,14 +25,12 @@ from stat import S_ISREG
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 overload from typing import overload
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from . import error from . import error
if TYPE_CHECKING:
from typing import Literal
# 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")

View File

@ -112,8 +112,8 @@ def pytest_collection(session: "Session") -> None:
assertstate.hook.set_session(session) assertstate.hook.set_session(session)
@hookimpl(tryfirst=True, hookwrapper=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks. """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
The rewrite module will use util._reprcompare if it exists to use custom The rewrite module will use util._reprcompare if it exists to use custom
@ -162,8 +162,9 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
util._assertion_pass = call_assertion_pass_hook util._assertion_pass = call_assertion_pass_hook
yield try:
return (yield)
finally:
util._reprcompare, util._assertion_pass = saved_assert_hooks util._reprcompare, util._assertion_pass = saved_assert_hooks
util._config = None util._config = None

View File

@ -44,11 +44,6 @@ from _pytest.stash import StashKey
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.assertion import AssertionState from _pytest.assertion import AssertionState
if sys.version_info >= (3, 8):
namedExpr = ast.NamedExpr
else:
namedExpr = ast.Expr
assertstate_key = StashKey["AssertionState"]() assertstate_key = StashKey["AssertionState"]()
@ -680,9 +675,10 @@ class AssertionRewriter(ast.NodeVisitor):
if ( if (
expect_docstring expect_docstring
and isinstance(item, ast.Expr) and isinstance(item, ast.Expr)
and isinstance(item.value, ast.Str) and isinstance(item.value, ast.Constant)
and isinstance(item.value.value, str)
): ):
doc = item.value.s doc = item.value.value
if self.is_rewrite_disabled(doc): if self.is_rewrite_disabled(doc):
return return
expect_docstring = False expect_docstring = False
@ -814,7 +810,7 @@ class AssertionRewriter(ast.NodeVisitor):
current = self.stack.pop() current = self.stack.pop()
if self.stack: if self.stack:
self.explanation_specifiers = self.stack[-1] self.explanation_specifiers = self.stack[-1]
keys = [ast.Str(key) for key in current.keys()] keys = [ast.Constant(key) for key in current.keys()]
format_dict = ast.Dict(keys, list(current.values())) format_dict = ast.Dict(keys, list(current.values()))
form = ast.BinOp(expl_expr, ast.Mod(), format_dict) form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
name = "@py_format" + str(next(self.variable_counter)) name = "@py_format" + str(next(self.variable_counter))
@ -868,16 +864,16 @@ class AssertionRewriter(ast.NodeVisitor):
negation = ast.UnaryOp(ast.Not(), top_condition) negation = ast.UnaryOp(ast.Not(), top_condition)
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
msg = self.pop_format_context(ast.Str(explanation)) msg = self.pop_format_context(ast.Constant(explanation))
# Failed # Failed
if assert_.msg: if assert_.msg:
assertmsg = self.helper("_format_assertmsg", assert_.msg) assertmsg = self.helper("_format_assertmsg", assert_.msg)
gluestr = "\n>assert " gluestr = "\n>assert "
else: else:
assertmsg = ast.Str("") assertmsg = ast.Constant("")
gluestr = "assert " gluestr = "assert "
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg) err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg)
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
err_name = ast.Name("AssertionError", ast.Load()) err_name = ast.Name("AssertionError", ast.Load())
fmt = self.helper("_format_explanation", err_msg) fmt = self.helper("_format_explanation", err_msg)
@ -893,8 +889,8 @@ class AssertionRewriter(ast.NodeVisitor):
hook_call_pass = ast.Expr( hook_call_pass = ast.Expr(
self.helper( self.helper(
"_call_assertion_pass", "_call_assertion_pass",
ast.Num(assert_.lineno), ast.Constant(assert_.lineno),
ast.Str(orig), ast.Constant(orig),
fmt_pass, fmt_pass,
) )
) )
@ -913,7 +909,7 @@ class AssertionRewriter(ast.NodeVisitor):
variables = [ variables = [
ast.Name(name, ast.Store()) for name in self.format_variables ast.Name(name, ast.Store()) for name in self.format_variables
] ]
clear_format = ast.Assign(variables, ast.NameConstant(None)) clear_format = ast.Assign(variables, ast.Constant(None))
self.statements.append(clear_format) self.statements.append(clear_format)
else: # Original assertion rewriting else: # Original assertion rewriting
@ -924,9 +920,9 @@ class AssertionRewriter(ast.NodeVisitor):
assertmsg = self.helper("_format_assertmsg", assert_.msg) assertmsg = self.helper("_format_assertmsg", assert_.msg)
explanation = "\n>assert " + explanation explanation = "\n>assert " + explanation
else: else:
assertmsg = ast.Str("") assertmsg = ast.Constant("")
explanation = "assert " + explanation explanation = "assert " + explanation
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation))
msg = self.pop_format_context(template) msg = self.pop_format_context(template)
fmt = self.helper("_format_explanation", msg) fmt = self.helper("_format_explanation", msg)
err_name = ast.Name("AssertionError", ast.Load()) err_name = ast.Name("AssertionError", ast.Load())
@ -938,7 +934,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Clear temporary variables by setting them to None. # Clear temporary variables by setting them to None.
if self.variables: if self.variables:
variables = [ast.Name(name, ast.Store()) for name in self.variables] variables = [ast.Name(name, ast.Store()) for name in self.variables]
clear = ast.Assign(variables, ast.NameConstant(None)) clear = ast.Assign(variables, ast.Constant(None))
self.statements.append(clear) self.statements.append(clear)
# Fix locations (line numbers/column offsets). # Fix locations (line numbers/column offsets).
for stmt in self.statements: for stmt in self.statements:
@ -946,26 +942,26 @@ class AssertionRewriter(ast.NodeVisitor):
ast.copy_location(node, assert_) ast.copy_location(node, assert_)
return self.statements return self.statements
def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]: def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.NamedExpr, str]:
# This method handles the 'walrus operator' repr of the target # This method handles the 'walrus operator' repr of the target
# name if it's a local variable or _should_repr_global_name() # name if it's a local variable or _should_repr_global_name()
# thinks it's acceptable. # thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], []) locs = ast.Call(self.builtin("locals"), [], [])
target_id = name.target.id # type: ignore[attr-defined] target_id = name.target.id # type: ignore[attr-defined]
inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs]) inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name) dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), ast.Str(target_id)) expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
return name, self.explanation_param(expr) return name, self.explanation_param(expr)
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]: def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
# Display the repr of the name if it's a local variable or # Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable. # _should_repr_global_name() thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], []) locs = ast.Call(self.builtin("locals"), [], [])
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs]) inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name) dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
return name, self.explanation_param(expr) return name, self.explanation_param(expr)
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
@ -984,10 +980,10 @@ class AssertionRewriter(ast.NodeVisitor):
# 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
self.expl_stmts = fail_inner self.expl_stmts = fail_inner
# Check if the left operand is a 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 (
isinstance(v, ast.Compare) isinstance(v, ast.Compare)
and isinstance(v.left, namedExpr) and isinstance(v.left, ast.NamedExpr)
and v.left.target.id and v.left.target.id
in [ in [
ast_expr.id ast_expr.id
@ -1003,7 +999,7 @@ class AssertionRewriter(ast.NodeVisitor):
self.push_format_context() self.push_format_context()
res, expl = self.visit(v) res, expl = self.visit(v)
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
expl_format = self.pop_format_context(ast.Str(expl)) expl_format = self.pop_format_context(ast.Constant(expl))
call = ast.Call(app, [expl_format], []) call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call)) self.expl_stmts.append(ast.Expr(call))
if i < levels: if i < levels:
@ -1015,7 +1011,7 @@ class AssertionRewriter(ast.NodeVisitor):
self.statements = body = inner self.statements = body = inner
self.statements = save self.statements = save
self.expl_stmts = fail_save self.expl_stmts = fail_save
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or)) expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or))
expl = self.pop_format_context(expl_template) expl = self.pop_format_context(expl_template)
return ast.Name(res_var, ast.Load()), self.explanation_param(expl) return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
@ -1089,7 +1085,7 @@ class AssertionRewriter(ast.NodeVisitor):
comp.left = self.variables_overwrite[ comp.left = self.variables_overwrite[
comp.left.id comp.left.id
] # type:ignore[assignment] ] # type:ignore[assignment]
if isinstance(comp.left, namedExpr): if isinstance(comp.left, ast.NamedExpr):
self.variables_overwrite[ self.variables_overwrite[
comp.left.target.id comp.left.target.id
] = comp.left # type:ignore[assignment] ] = comp.left # type:ignore[assignment]
@ -1105,7 +1101,7 @@ class AssertionRewriter(ast.NodeVisitor):
results = [left_res] results = [left_res]
for i, op, next_operand in it: for i, op, next_operand in it:
if ( if (
isinstance(next_operand, namedExpr) isinstance(next_operand, ast.NamedExpr)
and isinstance(left_res, ast.Name) and isinstance(left_res, ast.Name)
and next_operand.target.id == left_res.id and next_operand.target.id == left_res.id
): ):
@ -1118,9 +1114,9 @@ class AssertionRewriter(ast.NodeVisitor):
next_expl = f"({next_expl})" next_expl = f"({next_expl})"
results.append(next_res) results.append(next_res)
sym = BINOP_MAP[op.__class__] sym = BINOP_MAP[op.__class__]
syms.append(ast.Str(sym)) syms.append(ast.Constant(sym))
expl = f"{left_expl} {sym} {next_expl}" expl = f"{left_expl} {sym} {next_expl}"
expls.append(ast.Str(expl)) expls.append(ast.Constant(expl))
res_expr = ast.Compare(left_res, [op], [next_res]) res_expr = ast.Compare(left_res, [op], [next_res])
self.statements.append(ast.Assign([store_names[i]], res_expr)) self.statements.append(ast.Assign([store_names[i]], res_expr))
left_res, left_expl = next_res, next_expl left_res, left_expl = next_res, next_expl
@ -1164,7 +1160,7 @@ def try_makedirs(cache_dir: Path) -> bool:
def get_cache_dir(file_path: Path) -> Path: def get_cache_dir(file_path: Path) -> Path:
"""Return the cache directory to write .pyc files for the given .py file path.""" """Return the cache directory to write .pyc files for the given .py file path."""
if sys.version_info >= (3, 8) and sys.pycache_prefix: if sys.pycache_prefix:
# given: # given:
# prefix = '/tmp/pycs' # prefix = '/tmp/pycs'
# path = '/home/user/proj/test_app.py' # path = '/home/user/proj/test_app.py'

View File

@ -6,6 +6,7 @@ import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
from typing import final
from typing import Generator from typing import Generator
from typing import Iterable from typing import Iterable
from typing import List from typing import List
@ -18,7 +19,6 @@ from .pathlib import rm_rf
from .reports import CollectReport from .reports import CollectReport
from _pytest import nodes from _pytest import nodes
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
@ -27,7 +27,7 @@ from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.main import Session from _pytest.main import Session
from _pytest.python import Module from _pytest.nodes import File
from _pytest.python import Package from _pytest.python import Package
from _pytest.reports import TestReport from _pytest.reports import TestReport
@ -217,12 +217,12 @@ class LFPluginCollWrapper:
self.lfplugin = lfplugin self.lfplugin = lfplugin
self._collected_at_least_one_failure = False self._collected_at_least_one_failure = False
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector): def pytest_make_collect_report(
self, collector: nodes.Collector
) -> Generator[None, CollectReport, CollectReport]:
res = yield
if isinstance(collector, (Session, Package)): if isinstance(collector, (Session, Package)):
out = yield
res: CollectReport = out.get_result()
# Sort any lf-paths to the beginning. # Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths lf_paths = self.lfplugin._last_failed_paths
@ -240,19 +240,16 @@ class LFPluginCollWrapper:
key=sort_key, key=sort_key,
reverse=True, reverse=True,
) )
return
elif isinstance(collector, Module): elif isinstance(collector, File):
if collector.path in self.lfplugin._last_failed_paths: if collector.path in self.lfplugin._last_failed_paths:
out = yield
res = out.get_result()
result = res.result result = res.result
lastfailed = self.lfplugin.lastfailed lastfailed = self.lfplugin.lastfailed
# Only filter with known failures. # Only filter with known failures.
if not self._collected_at_least_one_failure: if not self._collected_at_least_one_failure:
if not any(x.nodeid in lastfailed for x in result): if not any(x.nodeid in lastfailed for x in result):
return return res
self.lfplugin.config.pluginmanager.register( self.lfplugin.config.pluginmanager.register(
LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip" LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
) )
@ -268,8 +265,8 @@ class LFPluginCollWrapper:
# Keep all sub-collectors. # Keep all sub-collectors.
or isinstance(x, nodes.Collector) or isinstance(x, nodes.Collector)
] ]
return
yield return res
class LFPluginCollSkipfiles: class LFPluginCollSkipfiles:
@ -280,9 +277,9 @@ class LFPluginCollSkipfiles:
def pytest_make_collect_report( def pytest_make_collect_report(
self, collector: nodes.Collector self, collector: nodes.Collector
) -> Optional[CollectReport]: ) -> Optional[CollectReport]:
# Packages are Modules, but we only want to skip test-bearing Modules, # Packages are Files, but we only want to skip test-bearing Files,
# so don't filter Packages. # so don't filter Packages.
if isinstance(collector, Module) and not isinstance(collector, Package): if isinstance(collector, File) and not isinstance(collector, Package):
if collector.path not in self.lfplugin._last_failed_paths: if collector.path not in self.lfplugin._last_failed_paths:
self.lfplugin._skipped_files += 1 self.lfplugin._skipped_files += 1
@ -342,14 +339,14 @@ class LFPlugin:
else: else:
self.lastfailed[report.nodeid] = True self.lastfailed[report.nodeid] = True
@hookimpl(hookwrapper=True, tryfirst=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems( def pytest_collection_modifyitems(
self, config: Config, items: List[nodes.Item] self, config: Config, items: List[nodes.Item]
) -> Generator[None, None, None]: ) -> Generator[None, None, None]:
yield res = yield
if not self.active: if not self.active:
return return res
if self.lastfailed: if self.lastfailed:
previously_failed = [] previously_failed = []
@ -394,6 +391,8 @@ class LFPlugin:
else: else:
self._report_status += "not deselecting items." self._report_status += "not deselecting items."
return res
def pytest_sessionfinish(self, session: Session) -> None: def pytest_sessionfinish(self, session: Session) -> None:
config = self.config config = self.config
if config.getoption("cacheshow") or hasattr(config, "workerinput"): if config.getoption("cacheshow") or hasattr(config, "workerinput"):
@ -414,11 +413,11 @@ class NFPlugin:
assert config.cache is not None assert config.cache is not None
self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
@hookimpl(hookwrapper=True, tryfirst=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_collection_modifyitems( def pytest_collection_modifyitems(
self, items: List[nodes.Item] self, items: List[nodes.Item]
) -> Generator[None, None, None]: ) -> Generator[None, None, None]:
yield res = yield
if self.active: if self.active:
new_items: Dict[str, nodes.Item] = {} new_items: Dict[str, nodes.Item] = {}
@ -436,6 +435,8 @@ class NFPlugin:
else: else:
self.cached_nodeids.update(item.nodeid for item in items) self.cached_nodeids.update(item.nodeid for item in items)
return res
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return] return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]

View File

@ -11,11 +11,14 @@ from types import TracebackType
from typing import Any from typing import Any
from typing import AnyStr from typing import AnyStr
from typing import BinaryIO from typing import BinaryIO
from typing import Final
from typing import final
from typing import Generator from typing import Generator
from typing import Generic from typing import Generic
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List from typing import List
from typing import Literal
from typing import NamedTuple from typing import NamedTuple
from typing import Optional from typing import Optional
from typing import TextIO from typing import TextIO
@ -24,7 +27,6 @@ from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
from _pytest.compat import final
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
@ -34,12 +36,9 @@ from _pytest.fixtures import SubRequest
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.nodes import File from _pytest.nodes import File
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.reports import CollectReport
if TYPE_CHECKING: _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
from typing_extensions import Final
from typing_extensions import Literal
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
def pytest_addoption(parser: Parser) -> None: def pytest_addoption(parser: Parser) -> None:
@ -132,8 +131,8 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
sys.stderr = _reopen_stdio(sys.stderr, "wb") sys.stderr = _reopen_stdio(sys.stderr, "wb")
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_load_initial_conftests(early_config: Config): def pytest_load_initial_conftests(early_config: Config) -> Generator[None, None, None]:
ns = early_config.known_args_namespace ns = early_config.known_args_namespace
if ns.capture == "fd": if ns.capture == "fd":
_windowsconsoleio_workaround(sys.stdout) _windowsconsoleio_workaround(sys.stdout)
@ -147,12 +146,16 @@ def pytest_load_initial_conftests(early_config: Config):
# Finally trigger conftest loading but while capturing (issue #93). # Finally trigger conftest loading but while capturing (issue #93).
capman.start_global_capturing() capman.start_global_capturing()
outcome = yield try:
try:
yield
finally:
capman.suspend_global_capture() capman.suspend_global_capture()
if outcome.excinfo is not None: except BaseException:
out, err = capman.read_global_capture() out, err = capman.read_global_capture()
sys.stdout.write(out) sys.stdout.write(out)
sys.stderr.write(err) sys.stderr.write(err)
raise
# IO Helpers. # IO Helpers.
@ -687,7 +690,7 @@ class MultiCapture(Generic[AnyStr]):
return CaptureResult(out, err) # type: ignore[arg-type] return CaptureResult(out, err) # type: ignore[arg-type]
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
if method == "fd": if method == "fd":
return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
elif method == "sys": elif method == "sys":
@ -723,7 +726,7 @@ class CaptureManager:
needed to ensure the fixtures take precedence over the global capture. needed to ensure the fixtures take precedence over the global capture.
""" """
def __init__(self, method: "_CaptureMethod") -> None: def __init__(self, method: _CaptureMethod) -> None:
self._method: Final = method self._method: Final = method
self._global_capturing: Optional[MultiCapture[str]] = None self._global_capturing: Optional[MultiCapture[str]] = None
self._capture_fixture: Optional[CaptureFixture[Any]] = None self._capture_fixture: Optional[CaptureFixture[Any]] = None
@ -849,35 +852,39 @@ class CaptureManager:
# Hooks # Hooks
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_make_collect_report(self, collector: Collector): def pytest_make_collect_report(
self, collector: Collector
) -> Generator[None, CollectReport, CollectReport]:
if isinstance(collector, File): if isinstance(collector, File):
self.resume_global_capture() self.resume_global_capture()
outcome = yield try:
rep = yield
finally:
self.suspend_global_capture() self.suspend_global_capture()
out, err = self.read_global_capture() out, err = self.read_global_capture()
rep = outcome.get_result()
if out: if out:
rep.sections.append(("Captured stdout", out)) rep.sections.append(("Captured stdout", out))
if err: if err:
rep.sections.append(("Captured stderr", err)) rep.sections.append(("Captured stderr", err))
else: else:
yield rep = yield
return rep
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("setup", item): with self.item_capture("setup", item):
yield return (yield)
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("call", item): with self.item_capture("call", item):
yield return (yield)
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]: def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
with self.item_capture("teardown", item): with self.item_capture("teardown", item):
yield return (yield)
@hookimpl(tryfirst=True) @hookimpl(tryfirst=True)
def pytest_keyboard_interrupt(self) -> None: def pytest_keyboard_interrupt(self) -> None:

View File

@ -12,26 +12,12 @@ from inspect import signature
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Generic from typing import Final
from typing import NoReturn from typing import NoReturn
from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
import py import py
# fmt: off
# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351.
# If `overload` is imported from `compat` instead of from `typing`,
# Sphinx doesn't recognize it as `overload` and the API docs for
# overloaded functions look good again. But type checkers handle
# it fine.
# fmt: on
if True:
from typing import overload as overload
if TYPE_CHECKING:
from typing_extensions import Final
_T = TypeVar("_T") _T = TypeVar("_T")
_S = TypeVar("_S") _S = TypeVar("_S")
@ -58,17 +44,6 @@ class NotSetType(enum.Enum):
NOTSET: Final = NotSetType.token # noqa: E305 NOTSET: Final = NotSetType.token # noqa: E305
# fmt: on # fmt: on
if sys.version_info >= (3, 8):
import importlib.metadata
importlib_metadata = importlib.metadata
else:
import importlib_metadata as importlib_metadata # noqa: F401
def _format_args(func: Callable[..., Any]) -> str:
return str(signature(func))
def is_generator(func: object) -> bool: def is_generator(func: object) -> bool:
genfunc = inspect.isgeneratorfunction(func) genfunc = inspect.isgeneratorfunction(func)
@ -93,7 +68,7 @@ def is_async_function(func: object) -> bool:
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
def getlocation(function, curdir: str | None = None) -> str: def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
function = get_real_func(function) function = get_real_func(function)
fn = Path(inspect.getfile(function)) fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno lineno = function.__code__.co_firstlineno
@ -127,7 +102,7 @@ def num_mock_patch_args(function) -> int:
def getfuncargnames( def getfuncargnames(
function: Callable[..., Any], function: Callable[..., object],
*, *,
name: str = "", name: str = "",
is_method: bool = False, is_method: bool = False,
@ -338,47 +313,6 @@ def safe_isclass(obj: object) -> bool:
return False return False
if TYPE_CHECKING:
if sys.version_info >= (3, 8):
from typing import final as final
else:
from typing_extensions import final as final
elif sys.version_info >= (3, 8):
from typing import final as final
else:
def final(f):
return f
if sys.version_info >= (3, 8):
from functools import cached_property as cached_property
else:
class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__")
def __init__(self, func: Callable[[_S], _T]) -> None:
self.func = func
self.__doc__ = func.__doc__
@overload
def __get__(
self, instance: None, owner: type[_S] | None = ...
) -> cached_property[_S, _T]:
...
@overload
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
...
def __get__(self, instance, owner=None):
if instance is None:
return self
value = instance.__dict__[self.func.__name__] = self.func(instance)
return value
def get_user_id() -> int | None: def get_user_id() -> int | None:
"""Return the current user id, or None if we cannot get it reliably on the current platform.""" """Return the current user id, or None if we cannot get it reliably on the current platform."""
# win32 does not have a getuid() function. # win32 does not have a getuid() function.

View File

@ -5,6 +5,7 @@ import copy
import dataclasses import dataclasses
import enum import enum
import glob import glob
import importlib.metadata
import inspect import inspect
import os import os
import re import re
@ -21,6 +22,7 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import final
from typing import Generator from typing import Generator
from typing import IO from typing import IO
from typing import Iterable from typing import Iterable
@ -48,8 +50,6 @@ from .findpaths import determine_setup
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
from _pytest.compat import final
from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
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
@ -137,7 +137,9 @@ def main(
) -> Union[int, ExitCode]: ) -> Union[int, ExitCode]:
"""Perform an in-process test run. """Perform an in-process test run.
:param args: List of command line arguments. :param args:
List of command line arguments. If `None` or not given, defaults to reading
arguments directly from the process command line (:data:`sys.argv`).
:param plugins: List of plugin objects to be auto-registered during initialization. :param plugins: List of plugin objects to be auto-registered during initialization.
:returns: An exit code. :returns: An exit code.
@ -257,7 +259,8 @@ default_plugins = essential_plugins + (
"logging", "logging",
"reports", "reports",
"python_path", "python_path",
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), "unraisableexception",
"threadexception",
"faulthandler", "faulthandler",
) )
@ -527,9 +530,12 @@ class PytestPluginManager(PluginManager):
# #
def _set_initial_conftests( def _set_initial_conftests(
self, self,
namespace: argparse.Namespace, args: Sequence[Union[str, Path]],
pyargs: bool,
noconftest: bool,
rootpath: Path, rootpath: Path,
testpaths_ini: Sequence[str], confcutdir: Optional[Path],
importmode: Union[ImportMode, str],
) -> None: ) -> None:
"""Load initial conftest files given a preparsed "namespace". """Load initial conftest files given a preparsed "namespace".
@ -539,17 +545,12 @@ class PytestPluginManager(PluginManager):
common options will not confuse our logic here. common options will not confuse our logic here.
""" """
current = Path.cwd() current = Path.cwd()
self._confcutdir = ( self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
absolutepath(current / namespace.confcutdir) self._noconftest = noconftest
if namespace.confcutdir self._using_pyargs = pyargs
else None
)
self._noconftest = namespace.noconftest
self._using_pyargs = namespace.pyargs
testpaths = namespace.file_or_dir + testpaths_ini
foundanchor = False foundanchor = False
for testpath in testpaths: for intitial_path in args:
path = str(testpath) path = str(intitial_path)
# remove node-id syntax # remove node-id syntax
i = path.find("::") i = path.find("::")
if i != -1: if i != -1:
@ -563,10 +564,10 @@ class PytestPluginManager(PluginManager):
except OSError: # pragma: no cover except OSError: # pragma: no cover
anchor_exists = False anchor_exists = False
if anchor_exists: if anchor_exists:
self._try_load_conftest(anchor, namespace.importmode, rootpath) self._try_load_conftest(anchor, importmode, rootpath)
foundanchor = True foundanchor = True
if not foundanchor: if not foundanchor:
self._try_load_conftest(current, namespace.importmode, rootpath) self._try_load_conftest(current, 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 a path is within the confcutdir.
@ -1140,10 +1141,25 @@ class Config:
@hookimpl(trylast=True) @hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config: "Config") -> None: def pytest_load_initial_conftests(self, early_config: "Config") -> None:
self.pluginmanager._set_initial_conftests( # We haven't fully parsed the command line arguments yet, so
early_config.known_args_namespace, # early_config.args it not set yet. But we need it for
# discovering the initial conftests. So "pre-run" the logic here.
# It will be done for real in `parse()`.
args, args_source = early_config._decide_args(
args=early_config.known_args_namespace.file_or_dir,
pyargs=early_config.known_args_namespace.pyargs,
testpaths=early_config.getini("testpaths"),
invocation_dir=early_config.invocation_params.dir,
rootpath=early_config.rootpath, rootpath=early_config.rootpath,
testpaths_ini=self.getini("testpaths"), warn=False,
)
self.pluginmanager._set_initial_conftests(
args=args,
pyargs=early_config.known_args_namespace.pyargs,
noconftest=early_config.known_args_namespace.noconftest,
rootpath=early_config.rootpath,
confcutdir=early_config.known_args_namespace.confcutdir,
importmode=early_config.known_args_namespace.importmode,
) )
def _initini(self, args: Sequence[str]) -> None: def _initini(self, args: Sequence[str]) -> None:
@ -1203,7 +1219,7 @@ class Config:
package_files = ( package_files = (
str(file) str(file)
for dist in importlib_metadata.distributions() for dist in importlib.metadata.distributions()
if any(ep.group == "pytest11" for ep in dist.entry_points) if any(ep.group == "pytest11" for ep in dist.entry_points)
for file in dist.files or [] for file in dist.files or []
) )
@ -1223,6 +1239,49 @@ class Config:
return args return args
def _decide_args(
self,
*,
args: List[str],
pyargs: List[str],
testpaths: List[str],
invocation_dir: Path,
rootpath: Path,
warn: bool,
) -> Tuple[List[str], ArgsSource]:
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
:param warn: Whether can issue warnings.
"""
if args:
source = Config.ArgsSource.ARGS
result = args
else:
if invocation_dir == rootpath:
source = Config.ArgsSource.TESTPATHS
if pyargs:
result = testpaths
else:
result = []
for path in testpaths:
result.extend(sorted(glob.iglob(path, recursive=True)))
if testpaths and not result:
if warn:
warning_text = (
"No files were found in testpaths; "
"consider removing or adjusting your testpaths configuration. "
"Searching recursively from the current directory instead."
)
self.issue_config_time_warning(
PytestConfigWarning(warning_text), stacklevel=3
)
else:
result = []
if not result:
source = Config.ArgsSource.INCOVATION_DIR
result = [str(invocation_dir)]
return result, source
def _preparse(self, args: List[str], addopts: bool = True) -> None: def _preparse(self, args: List[str], addopts: bool = True) -> None:
if addopts: if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "") env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
@ -1282,11 +1341,13 @@ class Config:
else: else:
raise raise
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_collection(self) -> Generator[None, None, None]: def pytest_collection(self) -> Generator[None, object, object]:
# Validate invalid ini keys after collection is done so we take in account # Validate invalid ini keys after collection is done so we take in account
# options added by late-loading conftest files. # options added by late-loading conftest files.
yield try:
return (yield)
finally:
self._validate_config_options() self._validate_config_options()
def _checkversion(self) -> None: def _checkversion(self) -> None:
@ -1371,34 +1432,17 @@ class Config:
self.hook.pytest_cmdline_preparse(config=self, args=args) self.hook.pytest_cmdline_preparse(config=self, args=args)
self._parser.after_preparse = True # type: ignore self._parser.after_preparse = True # type: ignore
try: try:
source = Config.ArgsSource.ARGS
args = self._parser.parse_setoption( args = self._parser.parse_setoption(
args, self.option, namespace=self.option args, self.option, namespace=self.option
) )
if not args: self.args, self.args_source = self._decide_args(
if self.invocation_params.dir == self.rootpath: args=args,
source = Config.ArgsSource.TESTPATHS pyargs=self.known_args_namespace.pyargs,
testpaths: List[str] = self.getini("testpaths") testpaths=self.getini("testpaths"),
if self.known_args_namespace.pyargs: invocation_dir=self.invocation_params.dir,
args = testpaths rootpath=self.rootpath,
else: warn=True,
args = []
for path in testpaths:
args.extend(sorted(glob.iglob(path, recursive=True)))
if testpaths and not args:
warning_text = (
"No files were found in testpaths; "
"consider removing or adjusting your testpaths configuration. "
"Searching recursively from the current directory instead."
) )
self.issue_config_time_warning(
PytestConfigWarning(warning_text), stacklevel=3
)
if not args:
source = Config.ArgsSource.INCOVATION_DIR
args = [str(self.invocation_params.dir)]
self.args = args
self.args_source = source
except PrintHelp: except PrintHelp:
pass pass
@ -1406,7 +1450,7 @@ class Config:
"""Issue and handle a warning during the "configure" stage. """Issue and handle a warning during the "configure" stage.
During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item`` During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
function because it is not possible to have hookwrappers around ``pytest_configure``. function because it is not possible to have hook wrappers around ``pytest_configure``.
This function is mainly intended for plugins that need to issue warnings during This function is mainly intended for plugins that need to issue warnings during
``pytest_configure`` (or similar stages). ``pytest_configure`` (or similar stages).

View File

@ -7,26 +7,23 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import final
from typing import List from typing import List
from typing import Literal
from typing import Mapping from typing import Mapping
from typing import NoReturn from typing import NoReturn
from typing import Optional from typing import Optional
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 _pytest._io import _pytest._io
from _pytest.compat import final
from _pytest.config.exceptions import UsageError from _pytest.config.exceptions import UsageError
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
from _pytest.deprecated import ARGUMENT_TYPE_STR from _pytest.deprecated import ARGUMENT_TYPE_STR
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
if TYPE_CHECKING:
from typing_extensions import Literal
FILE_OR_DIR = "file_or_dir" FILE_OR_DIR = "file_or_dir"
@ -177,7 +174,7 @@ class Parser:
name: str, name: str,
help: str, help: str,
type: Optional[ type: Optional[
"Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']" Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
] = None, ] = None,
default: Any = None, default: Any = None,
) -> None: ) -> None:

View File

@ -1,4 +1,4 @@
from _pytest.compat import final from typing import final
@final @final

View File

@ -304,10 +304,10 @@ class PdbInvoke:
class PdbTrace: class PdbTrace:
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]: def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
wrap_pytest_function_for_tracing(pyfuncitem) wrap_pytest_function_for_tracing(pyfuncitem)
yield return (yield)
def wrap_pytest_function_for_tracing(pyfuncitem): def wrap_pytest_function_for_tracing(pyfuncitem):

View File

@ -122,6 +122,11 @@ HOOK_LEGACY_MARKING = UnformattedWarning(
"#configuring-hook-specs-impls-using-markers", "#configuring-hook-specs-impls-using-markers",
) )
MARKED_FIXTURE = PytestRemovedIn8Warning(
"Marks applied to fixtures have no effect\n"
"See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function"
)
# You want to make some `__init__` or function "private". # You want to make some `__init__` or function "private".
# #
# def my_private_function(some, args): # def my_private_function(some, args):

View File

@ -582,7 +582,7 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined] doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
node=doctest_item, func=func, cls=None, funcargs=False node=doctest_item, func=func, cls=None, funcargs=False
) )
fixture_request = FixtureRequest(doctest_item, _ispytest=True) fixture_request = FixtureRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
fixture_request._fillfixtures() fixture_request._fillfixtures()
return fixture_request return fixture_request

View File

@ -62,8 +62,8 @@ def get_timeout_config_value(config: Config) -> float:
return float(config.getini("faulthandler_timeout") or 0.0) return float(config.getini("faulthandler_timeout") or 0.0)
@pytest.hookimpl(hookwrapper=True, trylast=True) @pytest.hookimpl(wrapper=True, trylast=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
timeout = get_timeout_config_value(item.config) timeout = get_timeout_config_value(item.config)
if timeout > 0: if timeout > 0:
import faulthandler import faulthandler
@ -71,11 +71,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
stderr = item.config.stash[fault_handler_stderr_fd_key] stderr = item.config.stash[fault_handler_stderr_fd_key]
faulthandler.dump_traceback_later(timeout, file=stderr) faulthandler.dump_traceback_later(timeout, file=stderr)
try: try:
yield return (yield)
finally: finally:
faulthandler.cancel_dump_traceback_later() faulthandler.cancel_dump_traceback_later()
else: else:
yield return (yield)
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)

View File

@ -2,17 +2,17 @@ import dataclasses
import functools import functools
import inspect import inspect
import os import os
import sys
import warnings import warnings
from collections import defaultdict from collections import defaultdict
from collections import deque from collections import deque
from contextlib import suppress from contextlib import suppress
from pathlib import Path from pathlib import Path
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
from typing import Dict from typing import Dict
from typing import Final
from typing import final
from typing import Generator from typing import Generator
from typing import Generic from typing import Generic
from typing import Iterable from typing import Iterable
@ -21,10 +21,10 @@ from typing import List
from typing import MutableMapping from typing import MutableMapping
from typing import NoReturn from typing import NoReturn
from typing import Optional from typing import Optional
from typing import overload
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
from typing import Tuple from typing import Tuple
from typing import Type
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
@ -35,10 +35,8 @@ from _pytest._code import getfslineno
from _pytest._code.code import FormattedExcinfo from _pytest._code.code import FormattedExcinfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import _format_args
from _pytest.compat import _PytestWrapper from _pytest.compat import _PytestWrapper
from _pytest.compat import assert_never from _pytest.compat import assert_never
from _pytest.compat import final
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import get_real_method from _pytest.compat import get_real_method
from _pytest.compat import getfuncargnames from _pytest.compat import getfuncargnames
@ -47,12 +45,12 @@ from _pytest.compat import getlocation
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import NotSetType from _pytest.compat import NotSetType
from _pytest.compat import overload
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import MARKED_FIXTURE
from _pytest.deprecated import YIELD_FIXTURE from _pytest.deprecated import YIELD_FIXTURE
from _pytest.mark import Mark from _pytest.mark import Mark
from _pytest.mark import ParameterSet from _pytest.mark import ParameterSet
@ -62,6 +60,7 @@ from _pytest.outcomes import skip
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
from _pytest.scope import _ScopeName
from _pytest.scope import HIGH_SCOPES from _pytest.scope import HIGH_SCOPES
from _pytest.scope import Scope from _pytest.scope import Scope
from _pytest.stash import StashKey from _pytest.stash import StashKey
@ -70,9 +69,9 @@ from _pytest.stash import StashKey
if TYPE_CHECKING: if TYPE_CHECKING:
from typing import Deque from typing import Deque
from _pytest.scope import _ScopeName
from _pytest.main import Session from _pytest.main import Session
from _pytest.python import CallSpec2 from _pytest.python import CallSpec2
from _pytest.python import Function
from _pytest.python import Metafunc from _pytest.python import Metafunc
@ -97,8 +96,8 @@ _FixtureCachedResult = Union[
None, None,
# Cache key. # Cache key.
object, object,
# Exc info if raised. # Exception if raised.
Tuple[Type[BaseException], BaseException, TracebackType], BaseException,
], ],
] ]
@ -217,6 +216,7 @@ def add_funcarg_pseudo_fixture_def(
params=valuelist, params=valuelist,
unittest=False, unittest=False,
ids=None, ids=None,
_ispytest=True,
) )
arg2fixturedefs[argname] = [fixturedef] arg2fixturedefs[argname] = [fixturedef]
if name2pseudofixturedef is not None: if name2pseudofixturedef is not None:
@ -352,17 +352,35 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
return request.param return request.param
@dataclasses.dataclass @dataclasses.dataclass(frozen=True)
class FuncFixtureInfo: class FuncFixtureInfo:
"""Fixture-related information for a fixture-requesting item (e.g. test
function).
This is used to examine the fixtures which an item requests statically
(known during collection). This includes autouse fixtures, fixtures
requested by the `usefixtures` marker, fixtures requested in the function
parameters, and the transitive closure of these.
An item may also request fixtures dynamically (using `request.getfixturevalue`);
these are not reflected here.
"""
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs") __slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
# Original function argument names. # Fixture names that the item requests directly by function parameters.
argnames: Tuple[str, ...] argnames: Tuple[str, ...]
# Argnames that function immediately requires. These include argnames + # Fixture names that the item immediately requires. These include
# fixture names specified via usefixtures and via autouse=True in fixture # argnames + fixture names specified via usefixtures and via autouse=True in
# definitions. # fixture definitions.
initialnames: Tuple[str, ...] initialnames: Tuple[str, ...]
# The transitive closure of the fixture names that the item requires.
# Note: can't include dynamic dependencies (`request.getfixturevalue` calls).
names_closure: List[str] names_closure: List[str]
# A map from a fixture name in the transitive closure to the FixtureDefs
# matching the name which are applicable to this function.
# There may be multiple overriding fixtures with the same name. The
# sequence is ordered from furthest to closes to the function.
name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]] name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]]
def prune_dependency_tree(self) -> None: def prune_dependency_tree(self) -> None:
@ -401,17 +419,31 @@ class FixtureRequest:
indirectly. indirectly.
""" """
def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None: def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest) check_ispytest(_ispytest)
self._pyfuncitem = pyfuncitem
#: Fixture for which this request is being performed. #: Fixture for which this request is being performed.
self.fixturename: Optional[str] = None self.fixturename: Optional[str] = None
self._pyfuncitem = pyfuncitem
self._fixturemanager = pyfuncitem.session._fixturemanager
self._scope = Scope.Function self._scope = Scope.Function
self._fixture_defs: Dict[str, FixtureDef[Any]] = {} # The FixtureDefs for each fixture name requested by this item.
fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo # Starts from the statically-known fixturedefs resolved during
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() # collection. Dynamically requested fixtures (using
# `request.getfixturevalue("foo")`) are added dynamically.
self._arg2fixturedefs = pyfuncitem._fixtureinfo.name2fixturedefs.copy()
# A fixture may override another fixture with the same name, e.g. a fixture
# in a module can override a fixture in a conftest, a fixture in a class can
# override a fixture in the module, and so on.
# An overriding fixture can request its own name; in this case it gets
# the value of the fixture it overrides, one level up.
# The _arg2index state keeps the current depth in the overriding chain.
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
# furthest to closest, so we use negative indexing -1, -2, ... to go from
# last to first.
self._arg2index: Dict[str, int] = {} self._arg2index: Dict[str, int] = {}
self._fixturemanager: FixtureManager = pyfuncitem.session._fixturemanager # The evaluated argnames so far, mapping to the FixtureDef they resolved
# to.
self._fixture_defs: Dict[str, FixtureDef[Any]] = {}
# Notes on the type of `param`: # Notes on the type of `param`:
# -`request.param` is only defined in parametrized fixtures, and will raise # -`request.param` is only defined in parametrized fixtures, and will raise
# AttributeError otherwise. Python typing has no notion of "undefined", so # AttributeError otherwise. Python typing has no notion of "undefined", so
@ -423,7 +455,7 @@ class FixtureRequest:
self.param: Any self.param: Any
@property @property
def scope(self) -> "_ScopeName": def scope(self) -> _ScopeName:
"""Scope string, one of "function", "class", "module", "package", "session".""" """Scope string, one of "function", "class", "module", "package", "session"."""
return self._scope.value return self._scope.value
@ -464,12 +496,17 @@ class FixtureRequest:
assert self._pyfuncitem.parent is not None assert self._pyfuncitem.parent is not None
parentid = self._pyfuncitem.parent.nodeid parentid = self._pyfuncitem.parent.nodeid
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
# TODO: Fix this type ignore. Either add assert or adjust types. if fixturedefs is not None:
# Can this be None here? self._arg2fixturedefs[argname] = fixturedefs
self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment] # No fixtures defined with this name.
# fixturedefs list is immutable so we maintain a decreasing index. if fixturedefs is None:
raise FixtureLookupError(argname, self)
# The are no fixtures with this name applicable for the function.
if not fixturedefs:
raise FixtureLookupError(argname, self)
index = self._arg2index.get(argname, 0) - 1 index = self._arg2index.get(argname, 0) - 1
if fixturedefs is None or (-index > len(fixturedefs)): # The fixture requested its own name, but no remaining to override.
if -index > len(fixturedefs):
raise FixtureLookupError(argname, self) raise FixtureLookupError(argname, self)
self._arg2index[argname] = index self._arg2index[argname] = index
return fixturedefs[index] return fixturedefs[index]
@ -502,7 +539,7 @@ class FixtureRequest:
"""Instance (can be None) on which test function was collected.""" """Instance (can be None) on which test function was collected."""
# unittest support hack, see _pytest.unittest.TestCaseFunction. # unittest support hack, see _pytest.unittest.TestCaseFunction.
try: try:
return self._pyfuncitem._testcase return self._pyfuncitem._testcase # type: ignore[attr-defined]
except AttributeError: except AttributeError:
function = getattr(self, "function", None) function = getattr(self, "function", None)
return getattr(function, "__self__", None) return getattr(function, "__self__", None)
@ -512,15 +549,16 @@ class FixtureRequest:
"""Python module object where the test function was collected.""" """Python module object where the test function was collected."""
if self.scope not in ("function", "class", "module"): if self.scope not in ("function", "class", "module"):
raise AttributeError(f"module not available in {self.scope}-scoped context") raise AttributeError(f"module not available in {self.scope}-scoped context")
return self._pyfuncitem.getparent(_pytest.python.Module).obj mod = self._pyfuncitem.getparent(_pytest.python.Module)
assert mod is not None
return mod.obj
@property @property
def path(self) -> Path: def path(self) -> Path:
"""Path where the test function was collected.""" """Path where the test function was collected."""
if self.scope not in ("function", "class", "module", "package"): if self.scope not in ("function", "class", "module", "package"):
raise AttributeError(f"path not available in {self.scope}-scoped context") raise AttributeError(f"path not available in {self.scope}-scoped context")
# TODO: Remove ignore once _pyfuncitem is properly typed. return self._pyfuncitem.path
return self._pyfuncitem.path # type: ignore
@property @property
def keywords(self) -> MutableMapping[str, Any]: def keywords(self) -> MutableMapping[str, Any]:
@ -592,9 +630,8 @@ class FixtureRequest:
def _get_active_fixturedef( def _get_active_fixturedef(
self, argname: str self, argname: str
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]: ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
try: fixturedef = self._fixture_defs.get(argname)
return self._fixture_defs[argname] if fixturedef is None:
except KeyError:
try: try:
fixturedef = self._getnextfixturedef(argname) fixturedef = self._getnextfixturedef(argname)
except FixtureLookupError: except FixtureLookupError:
@ -602,8 +639,6 @@ class FixtureRequest:
cached_result = (self, [0], None) cached_result = (self, [0], None)
return PseudoFixtureDef(cached_result, Scope.Function) return PseudoFixtureDef(cached_result, Scope.Function)
raise raise
# Remove indent to prevent the python3 exception
# from leaking into the call.
self._compute_fixture_value(fixturedef) self._compute_fixture_value(fixturedef)
self._fixture_defs[argname] = fixturedef self._fixture_defs[argname] = fixturedef
return fixturedef return fixturedef
@ -698,7 +733,8 @@ class FixtureRequest:
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
) -> None: ) -> None:
# If fixture function failed it might have registered finalizers. # If fixture function failed it might have registered finalizers.
subrequest.node.addfinalizer(lambda: fixturedef.finish(request=subrequest)) finalizer = functools.partial(fixturedef.finish, request=subrequest)
subrequest.node.addfinalizer(finalizer)
def _check_scope( def _check_scope(
self, self,
@ -728,8 +764,10 @@ class FixtureRequest:
p = bestrelpath(session.path, fs) p = bestrelpath(session.path, fs)
else: else:
p = fs p = fs
args = _format_args(factory) lines.append(
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) "%s:%d: def %s%s"
% (p, lineno + 1, factory.__name__, inspect.signature(factory))
)
return lines return lines
def __repr__(self) -> str: def __repr__(self) -> str:
@ -825,7 +863,9 @@ class FixtureLookupError(LookupError):
if msg is None: if msg is None:
fm = self.request._fixturemanager fm = self.request._fixturemanager
available = set() available = set()
parentid = self.request._pyfuncitem.parent.nodeid parent = self.request._pyfuncitem.parent
assert parent is not None
parentid = parent.nodeid
for name, fixturedefs in fm._arg2fixturedefs.items(): for name, fixturedefs in fm._arg2fixturedefs.items():
faclist = list(fm._matchfactories(fixturedefs, parentid)) faclist = list(fm._matchfactories(fixturedefs, parentid))
if faclist: if faclist:
@ -916,10 +956,10 @@ def _teardown_yield_fixture(fixturefunc, it) -> None:
def _eval_scope_callable( def _eval_scope_callable(
scope_callable: "Callable[[str, Config], _ScopeName]", scope_callable: Callable[[str, Config], _ScopeName],
fixture_name: str, fixture_name: str,
config: Config, config: Config,
) -> "_ScopeName": ) -> _ScopeName:
try: try:
# Type ignored because there is no typing mechanism to specify # Type ignored because there is no typing mechanism to specify
# keyword arguments, currently. # keyword arguments, currently.
@ -942,7 +982,11 @@ def _eval_scope_callable(
@final @final
class FixtureDef(Generic[FixtureValue]): class FixtureDef(Generic[FixtureValue]):
"""A container for a fixture definition.""" """A container for a fixture definition.
Note: At this time, only explicitly documented fields and methods are
considered public stable API.
"""
def __init__( def __init__(
self, self,
@ -950,13 +994,16 @@ class FixtureDef(Generic[FixtureValue]):
baseid: Optional[str], baseid: Optional[str],
argname: str, argname: str,
func: "_FixtureFunc[FixtureValue]", func: "_FixtureFunc[FixtureValue]",
scope: Union[Scope, "_ScopeName", Callable[[str, Config], "_ScopeName"], None], scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None],
params: Optional[Sequence[object]], params: Optional[Sequence[object]],
unittest: bool = False, unittest: bool = False,
ids: Optional[ ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None, ] = None,
*,
_ispytest: bool = False,
) -> None: ) -> None:
check_ispytest(_ispytest)
self._fixturemanager = fixturemanager self._fixturemanager = fixturemanager
# The "base" node ID for the fixture. # The "base" node ID for the fixture.
# #
@ -972,15 +1019,15 @@ class FixtureDef(Generic[FixtureValue]):
# directory path relative to the rootdir. # directory path relative to the rootdir.
# #
# For other plugins, the baseid is the empty string (always matches). # For other plugins, the baseid is the empty string (always matches).
self.baseid = baseid or "" self.baseid: Final = baseid or ""
# Whether the fixture was found from a node or a conftest in the # Whether the fixture was found from a node or a conftest in the
# collection tree. Will be false for fixtures defined in non-conftest # collection tree. Will be false for fixtures defined in non-conftest
# plugins. # plugins.
self.has_location = baseid is not None self.has_location: Final = baseid is not None
# The fixture factory function. # The fixture factory function.
self.func = func self.func: Final = func
# The name by which the fixture may be requested. # The name by which the fixture may be requested.
self.argname = argname self.argname: Final = argname
if scope is None: if scope is None:
scope = Scope.Function scope = Scope.Function
elif callable(scope): elif callable(scope):
@ -989,26 +1036,24 @@ class FixtureDef(Generic[FixtureValue]):
scope = Scope.from_user( scope = Scope.from_user(
scope, descr=f"Fixture '{func.__name__}'", where=baseid scope, descr=f"Fixture '{func.__name__}'", where=baseid
) )
self._scope = scope self._scope: Final = scope
# If the fixture is directly parametrized, the parameter values. # If the fixture is directly parametrized, the parameter values.
self.params: Optional[Sequence[object]] = params self.params: Final = params
# If the fixture is directly parametrized, a tuple of explicit IDs to # If the fixture is directly parametrized, a tuple of explicit IDs to
# assign to the parameter values, or a callable to generate an ID given # assign to the parameter values, or a callable to generate an ID given
# a parameter value. # a parameter value.
self.ids = ids self.ids: Final = ids
# The names requested by the fixtures. # The names requested by the fixtures.
self.argnames = getfuncargnames(func, name=argname, is_method=unittest) self.argnames: Final = getfuncargnames(func, name=argname, is_method=unittest)
# Whether the fixture was collected from a unittest TestCase class. # Whether the fixture was collected from a unittest TestCase class.
# Note that it really only makes sense to define autouse fixtures in self.unittest: Final = unittest
# unittest TestCases.
self.unittest = unittest
# If the fixture was executed, the current value of the fixture. # If the fixture was executed, the current value of the fixture.
# Can change if the fixture is executed with different parameters. # Can change if the fixture is executed with different parameters.
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
self._finalizers: List[Callable[[], object]] = [] self._finalizers: Final[List[Callable[[], object]]] = []
@property @property
def scope(self) -> "_ScopeName": def scope(self) -> _ScopeName:
"""Scope string, one of "function", "class", "module", "package", "session".""" """Scope string, one of "function", "class", "module", "package", "session"."""
return self._scope.value return self._scope.value
@ -1036,7 +1081,7 @@ class FixtureDef(Generic[FixtureValue]):
# value and remove all finalizers because they may be bound methods # value and remove all finalizers because they may be bound methods
# which will keep instances alive. # which will keep instances alive.
self.cached_result = None self.cached_result = None
self._finalizers = [] self._finalizers.clear()
def execute(self, request: SubRequest) -> FixtureValue: def execute(self, request: SubRequest) -> FixtureValue:
# Get required arguments and register our own finish() # Get required arguments and register our own finish()
@ -1050,13 +1095,13 @@ class FixtureDef(Generic[FixtureValue]):
my_cache_key = self.cache_key(request) my_cache_key = self.cache_key(request)
if self.cached_result is not None: if self.cached_result is not None:
cache_key = self.cached_result[1]
# note: comparison with `==` can fail (or be expensive) for e.g. # note: comparison with `==` can fail (or be expensive) for e.g.
# numpy arrays (#6497). # numpy arrays (#6497).
cache_key = self.cached_result[1]
if my_cache_key is cache_key: if my_cache_key is cache_key:
if self.cached_result[2] is not None: if self.cached_result[2] is not None:
_, val, tb = self.cached_result[2] exc = self.cached_result[2]
raise val.with_traceback(tb) raise exc
else: else:
result = self.cached_result[0] result = self.cached_result[0]
return result return result
@ -1121,35 +1166,18 @@ def pytest_fixture_setup(
my_cache_key = fixturedef.cache_key(request) my_cache_key = fixturedef.cache_key(request)
try: try:
result = call_fixture_func(fixturefunc, request, kwargs) result = call_fixture_func(fixturefunc, request, kwargs)
except TEST_OUTCOME: except TEST_OUTCOME as e:
exc_info = sys.exc_info() if isinstance(e, skip.Exception):
assert exc_info[0] is not None # The test requested a fixture which caused a skip.
if isinstance( # Don't show the fixture as the skip location, as then the user
exc_info[1], skip.Exception # wouldn't know which test skipped.
) and not fixturefunc.__name__.startswith("xunit_setup"): e._use_item_location = True
exc_info[1]._use_item_location = True # type: ignore[attr-defined] fixturedef.cached_result = (None, my_cache_key, e)
fixturedef.cached_result = (None, my_cache_key, exc_info)
raise raise
fixturedef.cached_result = (result, my_cache_key, None) fixturedef.cached_result = (result, my_cache_key, None)
return result return result
def _ensure_immutable_ids(
ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]]
) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]:
if ids is None:
return None
if callable(ids):
return ids
return tuple(ids)
def _params_converter(
params: Optional[Iterable[object]],
) -> Optional[Tuple[object, ...]]:
return tuple(params) if params is not None else None
def wrap_function_to_error_out_if_called_directly( def wrap_function_to_error_out_if_called_directly(
function: FixtureFunction, function: FixtureFunction,
fixture_marker: "FixtureFunctionMarker", fixture_marker: "FixtureFunctionMarker",
@ -1199,6 +1227,9 @@ class FixtureFunctionMarker:
"fixture is being applied more than once to the same function" "fixture is being applied more than once to the same function"
) )
if hasattr(function, "pytestmark"):
warnings.warn(MARKED_FIXTURE, stacklevel=2)
function = wrap_function_to_error_out_if_called_directly(function, self) function = wrap_function_to_error_out_if_called_directly(function, self)
name = self.name or function.__name__ name = self.name or function.__name__
@ -1410,10 +1441,14 @@ class FixtureManager:
def __init__(self, session: "Session") -> None: def __init__(self, session: "Session") -> None:
self.session = session self.session = session
self.config: Config = session.config self.config: Config = session.config
self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {} # Maps a fixture name (argname) to all of the FixtureDefs in the test
self._holderobjseen: Set[object] = set() # suite/plugins defined with this name. Populated by parsefactories().
# TODO: The order of the FixtureDefs list of each arg is significant,
# explain.
self._arg2fixturedefs: Final[Dict[str, List[FixtureDef[Any]]]] = {}
self._holderobjseen: Final[Set[object]] = set()
# A mapping from a nodeid to a list of autouse fixtures it defines. # A mapping from a nodeid to a list of autouse fixtures it defines.
self._nodeid_autousenames: Dict[str, List[str]] = { self._nodeid_autousenames: Final[Dict[str, List[str]]] = {
"": self.config.getini("usefixtures"), "": self.config.getini("usefixtures"),
} }
session.config.pluginmanager.register(self, "funcmanage") session.config.pluginmanager.register(self, "funcmanage")
@ -1438,8 +1473,26 @@ class FixtureManager:
return parametrize_argnames return parametrize_argnames
def getfixtureinfo( def getfixtureinfo(
self, node: nodes.Node, func, cls, funcargs: bool = True self,
node: nodes.Item,
func: Callable[..., object],
cls: Optional[type],
funcargs: bool = True,
) -> FuncFixtureInfo: ) -> FuncFixtureInfo:
"""Calculate the :class:`FuncFixtureInfo` for an item.
If ``funcargs`` is false, or if the item sets an attribute
``nofuncargs = True``, then ``func`` is not examined at all.
:param node:
The item requesting the fixtures.
:param func:
The item's function.
:param cls:
If the function is a method, the method's class.
:param funcargs:
Whether to look into func's parameters as fixture requests.
"""
if funcargs and not getattr(node, "nofuncargs", False): if funcargs and not getattr(node, "nofuncargs", False):
argnames = getfuncargnames(func, name=node.name, cls=cls) argnames = getfuncargnames(func, name=node.name, cls=cls)
else: else:
@ -1449,8 +1502,7 @@ class FixtureManager:
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
) )
initialnames = usefixtures + argnames initialnames = usefixtures + argnames
fm = node.session._fixturemanager initialnames, names_closure, arg2fixturedefs = self.getfixtureclosure(
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
initialnames, node, ignore_args=self._get_direct_parametrize_args(node) initialnames, node, ignore_args=self._get_direct_parametrize_args(node)
) )
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
@ -1465,7 +1517,7 @@ class FixtureManager:
# Construct the base nodeid which is later used to check # Construct the base nodeid which is later used to check
# what fixtures are visible for particular tests (as denoted # what fixtures are visible for particular tests (as denoted
# by their test id). # by their test id).
if p.name.startswith("conftest.py"): if p.name == "conftest.py":
try: try:
nodeid = str(p.parent.relative_to(self.config.rootpath)) nodeid = str(p.parent.relative_to(self.config.rootpath))
except ValueError: except ValueError:
@ -1671,6 +1723,7 @@ class FixtureManager:
params=marker.params, params=marker.params,
unittest=unittest, unittest=unittest,
ids=marker.ids, ids=marker.ids,
_ispytest=True,
) )
faclist = self._arg2fixturedefs.setdefault(name, []) faclist = self._arg2fixturedefs.setdefault(name, [])
@ -1692,11 +1745,16 @@ class FixtureManager:
def getfixturedefs( def getfixturedefs(
self, argname: str, nodeid: str self, argname: str, nodeid: str
) -> Optional[Sequence[FixtureDef[Any]]]: ) -> Optional[Sequence[FixtureDef[Any]]]:
"""Get a list of fixtures which are applicable to the given node id. """Get FixtureDefs for a fixture name which are applicable
to a given node.
:param str argname: Name of the fixture to search for. Returns None if there are no fixtures at all defined with the given
:param str nodeid: Full node id of the requesting test. name. (This is different from the case in which there are fixtures
:rtype: Sequence[FixtureDef] with the given name, but none applicable to the node. In this case,
an empty result is returned).
:param argname: Name of the fixture to search for.
:param nodeid: Full node id of the requesting test.
""" """
try: try:
fixturedefs = self._arg2fixturedefs[argname] fixturedefs = self._arg2fixturedefs[argname]

View File

@ -2,6 +2,7 @@
import os import os
import sys import sys
from argparse import Action from argparse import Action
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
@ -97,15 +98,14 @@ def pytest_addoption(parser: Parser) -> None:
) )
@pytest.hookimpl(hookwrapper=True) @pytest.hookimpl(wrapper=True)
def pytest_cmdline_parse(): def pytest_cmdline_parse() -> Generator[None, Config, Config]:
outcome = yield config = yield
config: Config = outcome.get_result()
if config.option.debug: if config.option.debug:
# --debug | --debug <file.log> was provided. # --debug | --debug <file.log> was provided.
path = config.option.debug path = config.option.debug
debugfile = open(path, "w") debugfile = open(path, "w", encoding="utf-8")
debugfile.write( debugfile.write(
"versions pytest-%s, " "versions pytest-%s, "
"python-%s\ncwd=%s\nargs=%s\n\n" "python-%s\ncwd=%s\nargs=%s\n\n"
@ -128,6 +128,8 @@ def pytest_cmdline_parse():
config.add_cleanup(unset_tracing) config.add_cleanup(unset_tracing)
return config
def showversion(config: Config) -> None: def showversion(config: Config) -> None:
if config.option.version > 1: if config.option.version > 1:

View File

@ -18,7 +18,7 @@ from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
if TYPE_CHECKING: if TYPE_CHECKING:
import pdb import pdb
import warnings import warnings
from typing_extensions import Literal from typing import Literal
from _pytest._code.code import ExceptionRepr from _pytest._code.code import ExceptionRepr
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
@ -60,7 +60,7 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager. :param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
.. note:: .. note::
This hook is incompatible with ``hookwrapper=True``. This hook is incompatible with hook wrappers.
""" """
@ -74,7 +74,7 @@ def pytest_plugin_registered(
:param pytest.PytestPluginManager manager: pytest plugin manager. :param pytest.PytestPluginManager manager: pytest plugin manager.
.. note:: .. note::
This hook is incompatible with ``hookwrapper=True``. This hook is incompatible with hook wrappers.
""" """
@ -113,7 +113,7 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
attribute or can be retrieved as the ``pytestconfig`` fixture. attribute or can be retrieved as the ``pytestconfig`` fixture.
.. note:: .. note::
This hook is incompatible with ``hookwrapper=True``. This hook is incompatible with hook wrappers.
""" """
@ -128,7 +128,7 @@ def pytest_configure(config: "Config") -> None:
imported. imported.
.. note:: .. note::
This hook is incompatible with ``hookwrapper=True``. This hook is incompatible with hook wrappers.
:param pytest.Config config: The pytest config object. :param pytest.Config config: The pytest config object.
""" """

View File

@ -3,6 +3,8 @@ import dataclasses
import shlex import shlex
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from typing import Final
from typing import final
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -11,7 +13,6 @@ from typing import Union
from iniconfig import SectionWrapper from iniconfig import SectionWrapper
from _pytest.cacheprovider import Cache from _pytest.cacheprovider import Cache
from _pytest.compat import final
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path from _pytest.compat import legacy_path
from _pytest.config import Config from _pytest.config import Config
@ -32,8 +33,6 @@ from _pytest.terminal import TerminalReporter
from _pytest.tmpdir import TempPathFactory from _pytest.tmpdir import TempPathFactory
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Final
import pexpect import pexpect

View File

@ -13,8 +13,10 @@ from logging import LogRecord
from pathlib import Path from pathlib import Path
from typing import AbstractSet from typing import AbstractSet
from typing import Dict from typing import Dict
from typing import final
from typing import Generator from typing import Generator
from typing import List from typing import List
from typing import Literal
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
@ -25,7 +27,6 @@ from typing import Union
from _pytest import nodes from _pytest import nodes
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.capture import CaptureManager from _pytest.capture import CaptureManager
from _pytest.compat import final
from _pytest.config import _strtobool from _pytest.config import _strtobool
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import create_terminal_writer from _pytest.config import create_terminal_writer
@ -41,8 +42,6 @@ from _pytest.terminal import TerminalReporter
if TYPE_CHECKING: if TYPE_CHECKING:
logging_StreamHandler = logging.StreamHandler[StringIO] logging_StreamHandler = logging.StreamHandler[StringIO]
from typing_extensions import Literal
else: else:
logging_StreamHandler = logging.StreamHandler logging_StreamHandler = logging.StreamHandler
@ -515,7 +514,9 @@ class LogCaptureFixture:
return original_disable_level return original_disable_level
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None: def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
"""Set the level of a logger for the duration of a test. """Set the threshold level of a logger for the duration of a test.
Logging messages which are less severe than this level will not be captured.
.. versionchanged:: 3.4 .. versionchanged:: 3.4
The levels of the loggers changed by this function will be The levels of the loggers changed by this function will be
@ -736,27 +737,26 @@ class LoggingPlugin:
return True return True
@hookimpl(hookwrapper=True, tryfirst=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_sessionstart(self) -> Generator[None, None, None]: def pytest_sessionstart(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("sessionstart") self.log_cli_handler.set_when("sessionstart")
with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level): with catching_logs(self.log_file_handler, level=self.log_file_level):
yield return (yield)
@hookimpl(hookwrapper=True, tryfirst=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_collection(self) -> Generator[None, None, None]: def pytest_collection(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("collection") self.log_cli_handler.set_when("collection")
with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level): with catching_logs(self.log_file_handler, level=self.log_file_level):
yield return (yield)
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]: def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]:
if session.config.option.collectonly: if session.config.option.collectonly:
yield return (yield)
return
if self._log_cli_enabled() and self._config.getoption("verbose") < 1: if self._log_cli_enabled() and self._config.getoption("verbose") < 1:
# The verbose flag is needed to avoid messy test progress output. # The verbose flag is needed to avoid messy test progress output.
@ -764,7 +764,7 @@ class LoggingPlugin:
with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level): with catching_logs(self.log_file_handler, level=self.log_file_level):
yield # Run all the tests. return (yield) # Run all the tests.
@hookimpl @hookimpl
def pytest_runtest_logstart(self) -> None: def pytest_runtest_logstart(self) -> None:
@ -789,12 +789,13 @@ class LoggingPlugin:
item.stash[caplog_records_key][when] = caplog_handler.records item.stash[caplog_records_key][when] = caplog_handler.records
item.stash[caplog_handler_key] = caplog_handler item.stash[caplog_handler_key] = caplog_handler
try:
yield yield
finally:
log = report_handler.stream.getvalue().strip() log = report_handler.stream.getvalue().strip()
item.add_report_section(when, "log", log) item.add_report_section(when, "log", log)
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("setup") self.log_cli_handler.set_when("setup")
@ -802,17 +803,19 @@ class LoggingPlugin:
item.stash[caplog_records_key] = empty item.stash[caplog_records_key] = empty
yield from self._runtest_for(item, "setup") yield from self._runtest_for(item, "setup")
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]: def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("call") self.log_cli_handler.set_when("call")
yield from self._runtest_for(item, "call") yield from self._runtest_for(item, "call")
@hookimpl(hookwrapper=True) @hookimpl(wrapper=True)
def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]: def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]:
self.log_cli_handler.set_when("teardown") self.log_cli_handler.set_when("teardown")
try:
yield from self._runtest_for(item, "teardown") yield from self._runtest_for(item, "teardown")
finally:
del item.stash[caplog_records_key] del item.stash[caplog_records_key]
del item.stash[caplog_handler_key] del item.stash[caplog_handler_key]
@ -820,13 +823,13 @@ class LoggingPlugin:
def pytest_runtest_logfinish(self) -> None: def pytest_runtest_logfinish(self) -> None:
self.log_cli_handler.set_when("finish") self.log_cli_handler.set_when("finish")
@hookimpl(hookwrapper=True, tryfirst=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_sessionfinish(self) -> Generator[None, None, None]: def pytest_sessionfinish(self) -> Generator[None, None, None]:
self.log_cli_handler.set_when("sessionfinish") self.log_cli_handler.set_when("sessionfinish")
with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_cli_handler, level=self.log_cli_level):
with catching_logs(self.log_file_handler, level=self.log_file_level): with catching_logs(self.log_file_handler, level=self.log_file_level):
yield return (yield)
@hookimpl @hookimpl
def pytest_unconfigure(self) -> None: def pytest_unconfigure(self) -> None:

View File

@ -9,21 +9,21 @@ import sys
from pathlib import Path from pathlib import Path
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import final
from typing import FrozenSet from typing import FrozenSet
from typing import Iterator from typing import Iterator
from typing import List from typing import List
from typing import Literal
from typing import Optional from typing import Optional
from typing import overload
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
from typing import Tuple from typing import Tuple
from typing import Type from typing import Type
from typing import TYPE_CHECKING
from typing import Union from typing import Union
import _pytest._code import _pytest._code
from _pytest import nodes from _pytest import nodes
from _pytest.compat import final
from _pytest.compat import overload
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
@ -43,10 +43,6 @@ from _pytest.runner import collect_one_node
from _pytest.runner import SetupState from _pytest.runner import SetupState
if TYPE_CHECKING:
from typing_extensions import Literal
def pytest_addoption(parser: Parser) -> None: def pytest_addoption(parser: Parser) -> None:
parser.addini( parser.addini(
"norecursedirs", "norecursedirs",
@ -400,6 +396,12 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
allow_in_venv = config.getoption("collect_in_virtualenv") allow_in_venv = config.getoption("collect_in_virtualenv")
if not allow_in_venv and _in_venv(collection_path): if not allow_in_venv and _in_venv(collection_path):
return True return True
if collection_path.is_dir():
norecursepatterns = config.getini("norecursedirs")
if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
return True
return None return None
@ -456,6 +458,11 @@ class _bestrelpath_cache(Dict[Path, str]):
@final @final
class Session(nodes.FSCollector): class Session(nodes.FSCollector):
"""The root of the collection tree.
``Session`` collects the initial paths given as arguments to pytest.
"""
Interrupted = Interrupted Interrupted = Interrupted
Failed = Failed Failed = Failed
# Set on the session by runner.pytest_sessionstart. # Set on the session by runner.pytest_sessionstart.
@ -563,9 +570,6 @@ class Session(nodes.FSCollector):
ihook = self.gethookproxy(fspath.parent) ihook = self.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True return True
def _collectfile( def _collectfile(
@ -686,8 +690,8 @@ class Session(nodes.FSCollector):
# are not collected more than once. # are not collected more than once.
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {} matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
# Dirnames of pkgs with dunder-init files. # Directories of pkgs with dunder-init files.
pkg_roots: Dict[str, Package] = {} pkg_roots: Dict[Path, Package] = {}
for argpath, names in self._initial_parts: for argpath, names in self._initial_parts:
self.trace("processing argument", (argpath, names)) self.trace("processing argument", (argpath, names))
@ -708,7 +712,7 @@ class Session(nodes.FSCollector):
col = self._collectfile(pkginit, handle_dupes=False) col = self._collectfile(pkginit, handle_dupes=False)
if col: if col:
if isinstance(col[0], Package): if isinstance(col[0], Package):
pkg_roots[str(parent)] = col[0] pkg_roots[parent] = col[0]
node_cache1[col[0].path] = [col[0]] node_cache1[col[0].path] = [col[0]]
# If it's a directory argument, recurse and look for any Subpackages. # If it's a directory argument, recurse and look for any Subpackages.
@ -717,7 +721,7 @@ class Session(nodes.FSCollector):
assert not names, f"invalid arg {(argpath, names)!r}" assert not names, f"invalid arg {(argpath, names)!r}"
seen_dirs: Set[Path] = set() seen_dirs: Set[Path] = set()
for direntry in visit(str(argpath), self._recurse): for direntry in visit(argpath, self._recurse):
if not direntry.is_file(): if not direntry.is_file():
continue continue
@ -732,8 +736,8 @@ class Session(nodes.FSCollector):
for x in self._collectfile(pkginit): for x in self._collectfile(pkginit):
yield x yield x
if isinstance(x, Package): if isinstance(x, Package):
pkg_roots[str(dirpath)] = x pkg_roots[dirpath] = x
if str(dirpath) in pkg_roots: if dirpath in pkg_roots:
# Do not collect packages here. # Do not collect packages here.
continue continue
@ -750,7 +754,7 @@ class Session(nodes.FSCollector):
if argpath in node_cache1: if argpath in node_cache1:
col = node_cache1[argpath] col = node_cache1[argpath]
else: else:
collect_root = pkg_roots.get(str(argpath.parent), self) collect_root = pkg_roots.get(argpath.parent, self)
col = collect_root._collectfile(argpath, handle_dupes=False) col = collect_root._collectfile(argpath, handle_dupes=False)
if col: if col:
node_cache1[argpath] = col node_cache1[argpath] = col

View File

@ -26,7 +26,6 @@ from typing import NoReturn
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
__all__ = [ __all__ = [
"Expression", "Expression",
"ParseError", "ParseError",
@ -132,7 +131,7 @@ IDENT_PREFIX = "$"
def expression(s: Scanner) -> ast.Expression: def expression(s: Scanner) -> ast.Expression:
if s.accept(TokenType.EOF): if s.accept(TokenType.EOF):
ret: ast.expr = ast.NameConstant(False) ret: ast.expr = ast.Constant(False)
else: else:
ret = expr(s) ret = expr(s)
s.accept(TokenType.EOF, reject=True) s.accept(TokenType.EOF, reject=True)

View File

@ -5,6 +5,7 @@ import warnings
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Collection from typing import Collection
from typing import final
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List from typing import List
@ -23,11 +24,11 @@ from typing import Union
from .._code import getfslineno from .._code import getfslineno
from ..compat import ascii_escaped from ..compat import ascii_escaped
from ..compat import final
from ..compat import NOTSET from ..compat import NOTSET
from ..compat import NotSetType from ..compat import NotSetType
from _pytest.config import Config from _pytest.config import Config
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import MARKED_FIXTURE
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestUnknownMarkWarning
@ -373,7 +374,9 @@ def get_unpacked_marks(
if not consider_mro: if not consider_mro:
mark_lists = [obj.__dict__.get("pytestmark", [])] mark_lists = [obj.__dict__.get("pytestmark", [])]
else: else:
mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__] mark_lists = [
x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
]
mark_list = [] mark_list = []
for item in mark_lists: for item in mark_lists:
if isinstance(item, list): if isinstance(item, list):
@ -412,6 +415,12 @@ def store_mark(obj, mark: Mark) -> None:
This is used to implement the Mark declarations/decorators correctly. This is used to implement the Mark declarations/decorators correctly.
""" """
assert isinstance(mark, Mark), mark assert isinstance(mark, Mark), mark
from ..fixtures import getfixturemarker
if getfixturemarker(obj) is not None:
warnings.warn(MARKED_FIXTURE, stacklevel=2)
# Always reassign name to avoid updating pytestmark in a reference that # Always reassign name to avoid updating pytestmark in a reference that
# was only borrowed. # was only borrowed.
obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark] obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]

View File

@ -5,6 +5,7 @@ import sys
import warnings import warnings
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any from typing import Any
from typing import final
from typing import Generator from typing import Generator
from typing import List from typing import List
from typing import Mapping from typing import Mapping
@ -15,7 +16,6 @@ from typing import Tuple
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
from _pytest.compat import final
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning

View File

@ -1,5 +1,6 @@
import os import os
import warnings import warnings
from functools import cached_property
from inspect import signature from inspect import signature
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@ -23,7 +24,6 @@ from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback from _pytest._code.code import Traceback
from _pytest.compat import cached_property
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ConftestImportFailure from _pytest.config import ConftestImportFailure
@ -157,10 +157,11 @@ class NodeMeta(type):
class Node(metaclass=NodeMeta): class Node(metaclass=NodeMeta):
"""Base class for Collector and Item, the components of the test r"""Base class of :class:`Collector` and :class:`Item`, the components of
collection tree. the test collection tree.
Collector subclasses have children; Items are leaf nodes. ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the
leaf nodes.
""" """
# Implemented in the legacypath plugin. # Implemented in the legacypath plugin.
@ -525,15 +526,17 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
class Collector(Node): class Collector(Node):
"""Collector instances create children through collect() and thus """Base class of all collectors.
iteratively build a tree."""
Collector create children through `collect()` and thus iteratively build
the collection tree.
"""
class CollectError(Exception): class CollectError(Exception):
"""An error during collection, contains a custom message.""" """An error during collection, contains a custom message."""
def collect(self) -> Iterable[Union["Item", "Collector"]]: def collect(self) -> Iterable[Union["Item", "Collector"]]:
"""Return a list of children (items and collectors) for this """Collect children (items and collectors) for this collector."""
collection node."""
raise NotImplementedError("abstract") raise NotImplementedError("abstract")
# TODO: This omits the style= parameter which breaks Liskov Substitution. # TODO: This omits the style= parameter which breaks Liskov Substitution.
@ -577,6 +580,8 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[
class FSCollector(Collector): class FSCollector(Collector):
"""Base class for filesystem collectors."""
def __init__( def __init__(
self, self,
fspath: Optional[LEGACY_PATH] = None, fspath: Optional[LEGACY_PATH] = None,
@ -660,7 +665,7 @@ class File(FSCollector):
class Item(Node): class Item(Node):
"""A basic test invocation item. """Base class of all test invocation items.
Note that for a single function there might be multiple test invocation items. Note that for a single function there might be multiple test invocation items.
""" """

View File

@ -7,23 +7,12 @@ from typing import Callable
from typing import cast from typing import cast
from typing import NoReturn from typing import NoReturn
from typing import Optional from typing import Optional
from typing import Protocol
from typing import Type from typing import Type
from typing import TypeVar from typing import TypeVar
from _pytest.deprecated import KEYWORD_MSG_ARG from _pytest.deprecated import KEYWORD_MSG_ARG
TYPE_CHECKING = False # Avoid circular import through compat.
if TYPE_CHECKING:
from typing_extensions import Protocol
else:
# typing.Protocol is only available starting from Python 3.8. It is also
# available from typing_extensions, but we don't want a runtime dependency
# on that. So use a dummy runtime implementation.
from typing import Generic
Protocol = Generic
class OutcomeException(BaseException): class OutcomeException(BaseException):
"""OutcomeException and its subclass instances indicate and contain info """OutcomeException and its subclass instances indicate and contain info

View File

@ -523,6 +523,8 @@ def import_path(
if mode is ImportMode.importlib: if mode is ImportMode.importlib:
module_name = module_name_from_path(path, root) module_name = module_name_from_path(path, root)
with contextlib.suppress(KeyError):
return sys.modules[module_name]
for meta_importer in sys.meta_path: for meta_importer in sys.meta_path:
spec = meta_importer.find_spec(module_name, [str(path.parent)]) spec = meta_importer.find_spec(module_name, [str(path.parent)])
@ -633,6 +635,9 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
otherwise "src.tests.test_foo" is not importable by ``__import__``. otherwise "src.tests.test_foo" is not importable by ``__import__``.
""" """
module_parts = module_name.split(".") module_parts = module_name.split(".")
child_module: Union[ModuleType, None] = None
module: Union[ModuleType, None] = None
child_name: str = ""
while module_name: while module_name:
if module_name not in modules: if module_name not in modules:
try: try:
@ -642,13 +647,22 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
# ourselves to fall back to creating a dummy module. # ourselves to fall back to creating a dummy module.
if not sys.meta_path: if not sys.meta_path:
raise ModuleNotFoundError raise ModuleNotFoundError
importlib.import_module(module_name) module = importlib.import_module(module_name)
except ModuleNotFoundError: except ModuleNotFoundError:
module = ModuleType( module = ModuleType(
module_name, module_name,
doc="Empty module created by pytest's importmode=importlib.", doc="Empty module created by pytest's importmode=importlib.",
) )
else:
module = modules[module_name]
if child_module:
# Add child attribute to the parent that can reference the child
# modules.
if not hasattr(module, child_name):
setattr(module, child_name, child_module)
modules[module_name] = module modules[module_name] = module
# Keep track of the child module while moving up the tree.
child_module, child_name = module, module_name.rpartition(".")[-1]
module_parts.pop(-1) module_parts.pop(-1)
module_name = ".".join(module_parts) module_name = ".".join(module_parts)
@ -755,21 +769,3 @@ def bestrelpath(directory: Path, dest: Path) -> str:
# Forward from base to dest. # Forward from base to dest.
*reldest.parts, *reldest.parts,
) )
# Originates from py. path.local.copy(), with siginficant trims and adjustments.
# TODO(py38): Replace with shutil.copytree(..., symlinks=True, dirs_exist_ok=True)
def copytree(source: Path, target: Path) -> None:
"""Recursively copy a source directory to target."""
assert source.is_dir()
for entry in visit(source, recurse=lambda entry: not entry.is_symlink()):
x = Path(entry)
relpath = x.relative_to(source)
newx = target / relpath
newx.parent.mkdir(exist_ok=True)
if x.is_symlink():
newx.symlink_to(os.readlink(x))
elif x.is_file():
shutil.copyfile(x, newx)
elif x.is_dir():
newx.mkdir(exist_ok=True)

View File

@ -6,6 +6,7 @@ import collections.abc
import contextlib import contextlib
import gc import gc
import importlib import importlib
import locale
import os import os
import platform import platform
import re import re
@ -19,10 +20,13 @@ 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
from typing import Final
from typing import final
from typing import Generator from typing import Generator
from typing import IO from typing import IO
from typing import Iterable from typing import Iterable
from typing import List from typing import List
from typing import Literal
from typing import Optional from typing import Optional
from typing import overload from typing import overload
from typing import Sequence from typing import Sequence
@ -39,7 +43,6 @@ from iniconfig import SectionWrapper
from _pytest import timing from _pytest import timing
from _pytest._code import Source from _pytest._code import Source
from _pytest.capture import _get_multicapture from _pytest.capture import _get_multicapture
from _pytest.compat import final
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import NotSetType from _pytest.compat import NotSetType
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -60,18 +63,13 @@ from _pytest.outcomes import fail
from _pytest.outcomes import importorskip from _pytest.outcomes import importorskip
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
from _pytest.pathlib import copytree
from _pytest.pathlib import make_numbered_dir from _pytest.pathlib import make_numbered_dir
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
from _pytest.reports import TestReport from _pytest.reports import TestReport
from _pytest.tmpdir import TempPathFactory from _pytest.tmpdir import TempPathFactory
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal
import pexpect import pexpect
@ -129,6 +127,7 @@ class LsofFdLeakChecker:
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
check=True, check=True,
text=True, text=True,
encoding=locale.getpreferredencoding(False),
).stdout ).stdout
def isopen(line: str) -> bool: def isopen(line: str) -> bool:
@ -161,10 +160,12 @@ class LsofFdLeakChecker:
else: else:
return True return True
@hookimpl(hookwrapper=True, tryfirst=True) @hookimpl(wrapper=True, tryfirst=True)
def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object]:
lines1 = self.get_open_files() lines1 = self.get_open_files()
yield try:
return (yield)
finally:
if hasattr(sys, "pypy_version_info"): if hasattr(sys, "pypy_version_info"):
gc.collect() gc.collect()
lines2 = self.get_open_files() lines2 = self.get_open_files()
@ -971,7 +972,7 @@ class Pytester:
example_path = example_dir.joinpath(name) example_path = example_dir.joinpath(name)
if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file():
copytree(example_path, self.path) shutil.copytree(example_path, self.path, symlinks=True, dirs_exist_ok=True)
return self.path return self.path
elif example_path.is_file(): elif example_path.is_file():
result = self.path.joinpath(example_path.name) result = self.path.joinpath(example_path.name)

View File

@ -15,17 +15,18 @@ 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
from typing import final
from typing import Generator from typing import Generator
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List from typing import List
from typing import Literal
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
from typing import Pattern from typing import Pattern
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
from typing import Tuple from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union from typing import Union
import _pytest import _pytest
@ -40,7 +41,6 @@ from _pytest._io import TerminalWriter
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped from _pytest.compat import ascii_escaped
from _pytest.compat import assert_never from _pytest.compat import assert_never
from _pytest.compat import final
from _pytest.compat import get_default_arg_names from _pytest.compat import get_default_arg_names
from _pytest.compat import get_real_func from _pytest.compat import get_real_func
from _pytest.compat import getimfunc from _pytest.compat import getimfunc
@ -75,16 +75,12 @@ from _pytest.pathlib import import_path
from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import ImportPathMismatchError
from _pytest.pathlib import parts from _pytest.pathlib import parts
from _pytest.pathlib import visit from _pytest.pathlib import visit
from _pytest.scope import _ScopeName
from _pytest.scope import Scope from _pytest.scope import Scope
from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestCollectionWarning
from _pytest.warning_types import PytestReturnNotNoneWarning from _pytest.warning_types import PytestReturnNotNoneWarning
from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning
if TYPE_CHECKING:
from typing_extensions import Literal
from _pytest.scope import _ScopeName
_PYTEST_DIR = Path(_pytest.__file__).parent _PYTEST_DIR = Path(_pytest.__file__).parent
@ -522,7 +518,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
class Module(nodes.File, PyCollector): class Module(nodes.File, PyCollector):
"""Collector for test classes and functions.""" """Collector for test classes and functions in a Python module."""
def _getobj(self): def _getobj(self):
return self._importtestmodule() return self._importtestmodule()
@ -659,6 +655,9 @@ class Module(nodes.File, PyCollector):
class Package(Module): class Package(Module):
"""Collector for files and directories in a Python packages -- directories
with an `__init__.py` file."""
def __init__( def __init__(
self, self,
fspath: Optional[LEGACY_PATH], fspath: Optional[LEGACY_PATH],
@ -706,9 +705,6 @@ class Package(Module):
ihook = self.session.gethookproxy(fspath.parent) ihook = self.session.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True return True
def _collectfile( def _collectfile(
@ -739,7 +735,9 @@ class Package(Module):
this_path = self.path.parent this_path = self.path.parent
# Always collect the __init__ first. # Always collect the __init__ first.
if path_matches_patterns(self.path, self.config.getini("python_files")): if self.session.isinitpath(self.path) or path_matches_patterns(
self.path, self.config.getini("python_files")
):
yield Module.from_parent(self, path=self.path) yield Module.from_parent(self, path=self.path)
pkg_prefixes: Set[Path] = set() pkg_prefixes: Set[Path] = set()
@ -791,7 +789,7 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[o
class Class(PyCollector): class Class(PyCollector):
"""Collector for test methods.""" """Collector for test methods (and nested classes) in a Python class."""
@classmethod @classmethod
def from_parent(cls, parent, *, name, obj=None, **kw): def from_parent(cls, parent, *, name, obj=None, **kw):
@ -1234,7 +1232,7 @@ class Metafunc:
ids: Optional[ ids: Optional[
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]] Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
] = None, ] = None,
scope: "Optional[_ScopeName]" = None, scope: Optional[_ScopeName] = None,
*, *,
_param_mark: Optional[Mark] = None, _param_mark: Optional[Mark] = None,
) -> None: ) -> None:
@ -1676,7 +1674,7 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
class Function(PyobjMixin, nodes.Item): class Function(PyobjMixin, nodes.Item):
"""An Item responsible for setting up and executing a Python test function. """Item responsible for setting up and executing a Python test function.
:param name: :param name:
The full function name, including any decorations like those The full function name, including any decorations like those
@ -1833,10 +1831,8 @@ class Function(PyobjMixin, nodes.Item):
class FunctionDefinition(Function): class FunctionDefinition(Function):
""" """This class is a stop gap solution until we evolve to have actual function
This class is a step gap solution until we evolve to have actual function definition nodes definition nodes and manage to get rid of ``metafunc``."""
and manage to get rid of ``metafunc``.
"""
def runtest(self) -> None: def runtest(self) -> None:
raise RuntimeError("function definitions are not supposed to be run as tests") raise RuntimeError("function definitions are not supposed to be run as tests")

View File

@ -9,9 +9,11 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import ContextManager from typing import ContextManager
from typing import final
from typing import List from typing import List
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
from typing import overload
from typing import Pattern from typing import Pattern
from typing import Sequence from typing import Sequence
from typing import Tuple from typing import Tuple
@ -20,17 +22,14 @@ from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import _pytest._code
from _pytest.compat import STRING_TYPES
from _pytest.outcomes import fail
if TYPE_CHECKING: if TYPE_CHECKING:
from numpy import ndarray from numpy import ndarray
import _pytest._code
from _pytest.compat import final
from _pytest.compat import STRING_TYPES
from _pytest.compat import overload
from _pytest.outcomes import fail
def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: def _non_numeric_type_error(value, at: Optional[str]) -> TypeError:
at_str = f" at {at}" if at else "" at_str = f" at {at}" if at else ""
return TypeError( return TypeError(
@ -266,6 +265,7 @@ class ApproxMapping(ApproxBase):
approx_side_as_map.items(), other_side.values() approx_side_as_map.items(), other_side.values()
): ):
if approx_value != other_value: if approx_value != other_value:
if approx_value.expected is not None and other_value is not None:
max_abs_diff = max( max_abs_diff = max(
max_abs_diff, abs(approx_value.expected - other_value) max_abs_diff, abs(approx_value.expected - other_value)
) )
@ -843,6 +843,14 @@ def raises( # noqa: F811
>>> with pytest.raises(ValueError, match=r'must be \d+$'): >>> with pytest.raises(ValueError, match=r'must be \d+$'):
... raise ValueError("value must be 42") ... raise ValueError("value must be 42")
The ``match`` argument searches the formatted exception string, which includes any
`PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``:
>>> with pytest.raises(ValueError, match=r'had a note added'): # doctest: +SKIP
... e = ValueError("value must be 42")
... e.add_note("had a note added")
... raise e
The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the
details of the captured exception:: details of the captured exception::

Some files were not shown because too many files have changed in this diff Show More