Compare commits
119 Commits
8.1.2
...
8.3.0.dev0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9eecdc7dae | ||
|
|
79ca819e59 | ||
|
|
835765c9d3 | ||
|
|
7e7503c0b0 | ||
|
|
882c4da2f3 | ||
|
|
2e8fb9f140 | ||
|
|
acf2971f46 | ||
|
|
3c77aec1da | ||
|
|
d217d68cde | ||
|
|
530be28575 | ||
|
|
d208c1d4a5 | ||
|
|
80ca255d42 | ||
|
|
1a332802ff | ||
|
|
93c2cdf6d6 | ||
|
|
4eb8b6d525 | ||
|
|
fafab1dbfd | ||
|
|
5cffef7f07 | ||
|
|
a830a3e98d | ||
|
|
1d322ffb48 | ||
|
|
63d985c6d3 | ||
|
|
3617ef1c79 | ||
|
|
58136c5376 | ||
|
|
042625957a | ||
|
|
6fb474a3eb | ||
|
|
ff806b239e | ||
|
|
ad95d59d61 | ||
|
|
48b6d18834 | ||
|
|
58844247f7 | ||
|
|
b56b294f54 | ||
|
|
6e248818b9 | ||
|
|
0644e467fb | ||
|
|
5f6e825a26 | ||
|
|
089116bdff | ||
|
|
1f001cd105 | ||
|
|
99890636bf | ||
|
|
17fc20af78 | ||
|
|
1ca59dfd6b | ||
|
|
e06c337bd8 | ||
|
|
a02cc0ad1b | ||
|
|
74f05648d5 | ||
|
|
f75dd87eb7 | ||
|
|
5acc3f86ac | ||
|
|
4e3dd21506 | ||
|
|
2e65f4e3ac | ||
|
|
cc588d1a1a | ||
|
|
b63bea9523 | ||
|
|
a10ddad774 | ||
|
|
1188ea2608 | ||
|
|
aa5324560b | ||
|
|
381593ccf0 | ||
|
|
9d6b5f8772 | ||
|
|
44895289c7 | ||
|
|
1125296b53 | ||
|
|
bd9b62161a | ||
|
|
e64efd8653 | ||
|
|
2dd58e827e | ||
|
|
12e061e2e8 | ||
|
|
5d345f6d7f | ||
|
|
2cba2237cd | ||
|
|
e7b048e92d | ||
|
|
4a8cefbf64 | ||
|
|
909759de2d | ||
|
|
85ff5d1b54 | ||
|
|
cfc4cf074e | ||
|
|
aeae91e27d | ||
|
|
3eb16b34be | ||
|
|
c650e3a94f | ||
|
|
470edc5884 | ||
|
|
07a08028de | ||
|
|
42d3960fea | ||
|
|
e7bf216516 | ||
|
|
532782a228 | ||
|
|
bf8b088bbe | ||
|
|
027464975f | ||
|
|
6be5c9d88c | ||
|
|
2607fe8b47 | ||
|
|
2e69f31444 | ||
|
|
70c11582aa | ||
|
|
c203f1615c | ||
|
|
bec0e9caf8 | ||
|
|
2e5da5d2fb | ||
|
|
c0532dda18 | ||
|
|
14437788f0 | ||
|
|
7eaaf370bb | ||
|
|
6c9e107681 | ||
|
|
122b43439c | ||
|
|
a29ea1bc32 | ||
|
|
0a442a9599 | ||
|
|
520ff29c07 | ||
|
|
b777b05c0e | ||
|
|
b90da34a86 | ||
|
|
0dc0360351 | ||
|
|
140c777590 | ||
|
|
774f0c44e6 | ||
|
|
006058f1f9 | ||
|
|
437eb86edd | ||
|
|
9033d4d3ff | ||
|
|
2ccc73be9a | ||
|
|
ff551b7685 | ||
|
|
f5de111357 | ||
|
|
71671f60b5 | ||
|
|
a9d1f55a0f | ||
|
|
83887c066b | ||
|
|
4a8bd5f2e2 | ||
|
|
51595d9000 | ||
|
|
1a5e0eb71d | ||
|
|
221097517b | ||
|
|
dacee1f11d | ||
|
|
303cd0d48a | ||
|
|
86945f9a1f | ||
|
|
03e54712dd | ||
|
|
18c0e5075f | ||
|
|
6ee02a3e6c | ||
|
|
0951cbc6e4 | ||
|
|
37b3401091 | ||
|
|
13558b9f53 | ||
|
|
23bdc643c9 | ||
|
|
71849cc05c | ||
|
|
e410705561 |
6
.github/workflows/deploy.yml
vendored
6
.github/workflows/deploy.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.1
|
||||
uses: hynek/build-and-inspect-python-package@v2.4.0
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
path: dist
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.11
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.14
|
||||
|
||||
- name: Push tag
|
||||
run: |
|
||||
@@ -94,7 +94,7 @@ jobs:
|
||||
tox -e generate-gh-release-notes -- ${{ github.event.inputs.version }} scripts/latest-release-notes.md
|
||||
|
||||
- name: Publish GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
body_path: scripts/latest-release-notes.md
|
||||
files: dist/*
|
||||
|
||||
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
days-before-issue-close: 7
|
||||
only-labels: "status: needs information"
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
|
||||
stale-issue-message: "This issue is stale because it has the `status: needs information` label and requested follow-up information was not provided for 14 days."
|
||||
close-issue-message: "This issue was closed because it has the `status: needs information` label and follow-up information has not been provided for 7 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.1
|
||||
uses: hynek/build-and-inspect-python-package@v2.4.0
|
||||
|
||||
build:
|
||||
needs: [package]
|
||||
|
||||
2
.github/workflows/update-plugin-list.yml
vendored
2
.github/workflows/update-plugin-list.yml
vendored
@@ -46,7 +46,7 @@ jobs:
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50
|
||||
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -51,6 +51,7 @@ coverage.xml
|
||||
.settings
|
||||
.vscode
|
||||
__pycache__/
|
||||
.python-version
|
||||
|
||||
# generated by pip
|
||||
pip-wheel-metadata/
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: "v0.2.2"
|
||||
rev: "v0.4.1"
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
- id: ruff-format
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -26,7 +26,7 @@ repos:
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
rev: v1.9.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/|scripts/)
|
||||
@@ -34,7 +34,7 @@ repos:
|
||||
additional_dependencies:
|
||||
- iniconfig>=1.1.0
|
||||
- attrs>=19.2.0
|
||||
- pluggy>=1.4.0
|
||||
- pluggy>=1.5.0
|
||||
- packaging
|
||||
- tomli
|
||||
- types-pkg_resources
|
||||
@@ -43,13 +43,20 @@ repos:
|
||||
# on <3.11
|
||||
- exceptiongroup>=1.0.0rc8
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: "1.7.0"
|
||||
rev: "1.8.0"
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
|
||||
additional_dependencies: ["tox>=4.9"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: pylint
|
||||
language: system
|
||||
types: [python]
|
||||
args: ["-rn", "-sn", "--fail-on=I"]
|
||||
stages: [manual]
|
||||
- id: rst
|
||||
name: rst
|
||||
entry: rst-lint --encoding utf-8
|
||||
|
||||
6
AUTHORS
6
AUTHORS
@@ -101,6 +101,7 @@ Cyrus Maden
|
||||
Damian Skrzypczak
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Miller
|
||||
Daniel Nuri
|
||||
Daniel Sánchez Castelló
|
||||
Daniel Valenzuela Zenteno
|
||||
@@ -118,6 +119,7 @@ Daw-Ran Liou
|
||||
Debi Mishra
|
||||
Denis Kirisov
|
||||
Denivy Braiam Rück
|
||||
Dheeraj C K
|
||||
Dhiren Serai
|
||||
Diego Russo
|
||||
Dmitry Dygalo
|
||||
@@ -137,6 +139,7 @@ Endre Galaczi
|
||||
Eric Hunsberger
|
||||
Eric Liu
|
||||
Eric Siegerman
|
||||
Eric Yuan
|
||||
Erik Aronesty
|
||||
Erik Hasse
|
||||
Erik M. Bray
|
||||
@@ -235,6 +238,7 @@ Kyle Altendorf
|
||||
Lawrence Mitchell
|
||||
Lee Kamentsky
|
||||
Lev Maximov
|
||||
Levon Saldamli
|
||||
Lewis Cowles
|
||||
Llandy Riveron Del Risco
|
||||
Loic Esteve
|
||||
@@ -318,6 +322,7 @@ Pierre Sassoulas
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Piotr Helm
|
||||
Poulami Sau
|
||||
Prakhar Gurunani
|
||||
Prashant Anand
|
||||
Prashant Sharma
|
||||
@@ -431,6 +436,7 @@ Xixi Zhao
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Yannick Péroux
|
||||
Yao Xiao
|
||||
Yoav Caspi
|
||||
Yuliang Shao
|
||||
Yusuke Kadowaki
|
||||
|
||||
@@ -297,12 +297,12 @@ Here is a simple overview, with pytest-specific bits:
|
||||
When committing, ``pre-commit`` will re-format the files if necessary.
|
||||
|
||||
#. If instead of using ``tox`` you prefer to run the tests directly, then we suggest to create a virtual environment and use
|
||||
an editable install with the ``testing`` extra::
|
||||
an editable install with the ``dev`` extra::
|
||||
|
||||
$ python3 -m venv .venv
|
||||
$ source .venv/bin/activate # Linux
|
||||
$ .venv/Scripts/activate.bat # Windows
|
||||
$ pip install -e ".[testing]"
|
||||
$ pip install -e ".[dev]"
|
||||
|
||||
Afterwards, you can edit the files and run pytest normally::
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ The current list of contributors receiving funding are:
|
||||
|
||||
* `@nicoddemus`_
|
||||
* `@The-Compiler`_
|
||||
* `@RonnyPfannschmidt`_
|
||||
|
||||
Contributors interested in receiving a part of the funds just need to submit a PR adding their
|
||||
name to the list. Contributors that want to stop receiving the funds should also submit a PR
|
||||
@@ -56,3 +57,4 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the
|
||||
|
||||
.. _`@nicoddemus`: https://github.com/nicoddemus
|
||||
.. _`@The-Compiler`: https://github.com/The-Compiler
|
||||
.. _`@RonnyPfannschmidt`: https://github.com/RonnyPfannschmidt
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
:func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`.
|
||||
Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion).
|
||||
While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing.
|
||||
@@ -1,4 +0,0 @@
|
||||
When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
|
||||
as the relative directory.
|
||||
|
||||
Previoulsy this would raise an :class:`AssertionError`.
|
||||
@@ -1,3 +0,0 @@
|
||||
Added the new :confval:`consider_namespace_packages` configuration option, defaulting to ``False``.
|
||||
|
||||
If set to ``True``, pytest will attempt to identify modules that are part of `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__ when importing modules.
|
||||
@@ -1,3 +0,0 @@
|
||||
:ref:`--import-mode=importlib <import-mode-importlib>` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails.
|
||||
|
||||
This means that installed packages will be imported under their canonical name if possible first, for example ``app.core.models``, instead of having the module name always be derived from their path (for example ``.env310.lib.site_packages.app.core.models``).
|
||||
@@ -1,2 +0,0 @@
|
||||
Added the new :confval:`verbosity_test_cases` configuration option for fine-grained control of test execution verbosity.
|
||||
See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.
|
||||
@@ -1,7 +0,0 @@
|
||||
Some changes were made to private functions which may affect plugins which access them:
|
||||
|
||||
- ``FixtureManager._getautousenames()`` now takes a ``Node`` itself instead of the nodeid.
|
||||
- ``FixtureManager.getfixturedefs()`` now takes the ``Node`` itself instead of the nodeid.
|
||||
- The ``_pytest.nodes.iterparentnodeids()`` function is removed without replacement.
|
||||
Prefer to traverse the node hierarchy itself instead.
|
||||
If you really need to, copy the function from the previous pytest release.
|
||||
@@ -1 +0,0 @@
|
||||
Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail.
|
||||
@@ -1,2 +0,0 @@
|
||||
Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes.
|
||||
It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list.
|
||||
@@ -1 +0,0 @@
|
||||
Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12.
|
||||
@@ -1,3 +0,0 @@
|
||||
Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using ``--pyargs``.
|
||||
|
||||
This change improves the collection tree for tests specified using ``--pyargs``, see :pull:`12043` for a comparison with pytest 8.0 and <8.
|
||||
@@ -1 +0,0 @@
|
||||
In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``.
|
||||
@@ -1,3 +0,0 @@
|
||||
Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``.
|
||||
|
||||
Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed.
|
||||
@@ -1 +0,0 @@
|
||||
Fix the ``stacklevel`` used when warning about marks used on fixtures.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed a regression in ``8.0.2`` where tests created using :fixture:`tmp_path` have been collected multiple times in CI under Windows.
|
||||
@@ -1,2 +0,0 @@
|
||||
When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group.
|
||||
Previously, only the first exception was reported.
|
||||
@@ -6,6 +6,10 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-8.2.0
|
||||
release-8.1.2
|
||||
release-8.1.1
|
||||
release-8.1.0
|
||||
release-8.0.2
|
||||
release-8.0.1
|
||||
release-8.0.0
|
||||
|
||||
54
doc/en/announce/release-8.1.0.rst
Normal file
54
doc/en/announce/release-8.1.0.rst
Normal file
@@ -0,0 +1,54 @@
|
||||
pytest-8.1.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 8.1.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:
|
||||
|
||||
* Ben Brown
|
||||
* Ben Leith
|
||||
* Bruno Oliveira
|
||||
* Clément Robert
|
||||
* Dave Hall
|
||||
* Dương Quốc Khánh
|
||||
* Eero Vaher
|
||||
* Eric Larson
|
||||
* Fabian Sturm
|
||||
* Faisal Fawad
|
||||
* Florian Bruhin
|
||||
* Franck Charras
|
||||
* Joachim B Haga
|
||||
* John Litborn
|
||||
* Loïc Estève
|
||||
* Marc Bresson
|
||||
* Patrick Lannigan
|
||||
* Pierre Sassoulas
|
||||
* Ran Benita
|
||||
* Reagan Lee
|
||||
* Ronny Pfannschmidt
|
||||
* Russell Martin
|
||||
* clee2000
|
||||
* donghui
|
||||
* faph
|
||||
* jakkdl
|
||||
* mrbean-bremen
|
||||
* robotherapist
|
||||
* whysage
|
||||
* woutdenolf
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-8.1.1.rst
Normal file
18
doc/en/announce/release-8.1.1.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-8.1.1
|
||||
=======================================
|
||||
|
||||
pytest 8.1.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-8.1.2.rst
Normal file
18
doc/en/announce/release-8.1.2.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-8.1.2
|
||||
=======================================
|
||||
|
||||
pytest 8.1.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:
|
||||
|
||||
* Bruno Oliveira
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
43
doc/en/announce/release-8.2.0.rst
Normal file
43
doc/en/announce/release-8.2.0.rst
Normal file
@@ -0,0 +1,43 @@
|
||||
pytest-8.2.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 8.2.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:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Daniel Miller
|
||||
* Florian Bruhin
|
||||
* HolyMagician03-UMich
|
||||
* John Litborn
|
||||
* Levon Saldamli
|
||||
* Linghao Zhang
|
||||
* Manuel López-Ibáñez
|
||||
* Pierre Sassoulas
|
||||
* Ran Benita
|
||||
* Ronny Pfannschmidt
|
||||
* Sebastian Meyer
|
||||
* Shekhar verma
|
||||
* Tamir Duberstein
|
||||
* Tobias Stoeckmann
|
||||
* dj
|
||||
* jakkdl
|
||||
* poulami-sau
|
||||
* tserg
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
cachedir: .pytest_cache
|
||||
rootdir: /home/sweet/project
|
||||
collected 0 items
|
||||
cache -- .../_pytest/cacheprovider.py:527
|
||||
cache -- .../_pytest/cacheprovider.py:542
|
||||
Return a cache object that can persist state between testing sessions.
|
||||
|
||||
cache.get(key, default)
|
||||
@@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
Values can be any object handled by the json stdlib module.
|
||||
|
||||
capsysbinary -- .../_pytest/capture.py:1007
|
||||
capsysbinary -- .../_pytest/capture.py:1003
|
||||
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsysbinary.readouterr()``
|
||||
@@ -50,7 +50,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsysbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capfd -- .../_pytest/capture.py:1034
|
||||
capfd -- .../_pytest/capture.py:1030
|
||||
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
@@ -67,7 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capfd.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
capfdbinary -- .../_pytest/capture.py:1061
|
||||
capfdbinary -- .../_pytest/capture.py:1057
|
||||
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
@@ -84,7 +84,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capfdbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capsys -- .../_pytest/capture.py:980
|
||||
capsys -- .../_pytest/capture.py:976
|
||||
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsys.readouterr()`` method
|
||||
@@ -101,7 +101,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:745
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:738
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
@@ -115,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1354
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1335
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
@@ -125,7 +125,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
if pytestconfig.getoption("verbose") > 0:
|
||||
...
|
||||
|
||||
record_property -- .../_pytest/junitxml.py:283
|
||||
record_property -- .../_pytest/junitxml.py:284
|
||||
Add extra properties to the calling test.
|
||||
|
||||
User properties become part of the test report and are available to the
|
||||
@@ -139,13 +139,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
def test_function(record_property):
|
||||
record_property("example_key", 1)
|
||||
|
||||
record_xml_attribute -- .../_pytest/junitxml.py:306
|
||||
record_xml_attribute -- .../_pytest/junitxml.py:307
|
||||
Add extra xml attributes to the tag for the calling test.
|
||||
|
||||
The fixture is callable with ``name, value``. The value is
|
||||
automatically XML-encoded.
|
||||
|
||||
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:344
|
||||
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:345
|
||||
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
|
||||
|
||||
This is suitable to writing global information regarding the entire test
|
||||
@@ -170,18 +170,18 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
||||
:issue:`7767` for details.
|
||||
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:303
|
||||
Return a :class:`pytest.TempdirFactory` instance for the test session.
|
||||
|
||||
tmpdir -- .../_pytest/legacypath.py:309
|
||||
tmpdir -- .../_pytest/legacypath.py:310
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
``--basetemp`` is used then it is cleared each session. See
|
||||
:ref:`temporary directory location and retention`.
|
||||
|
||||
The returned object is a `legacy_path`_ object.
|
||||
|
||||
@@ -192,7 +192,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
caplog -- .../_pytest/logging.py:594
|
||||
caplog -- .../_pytest/logging.py:602
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following properties/methods::
|
||||
@@ -203,7 +203,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
||||
* caplog.clear() -> clear captured records and formatted log output string
|
||||
|
||||
monkeypatch -- .../_pytest/monkeypatch.py:32
|
||||
monkeypatch -- .../_pytest/monkeypatch.py:33
|
||||
A convenient fixture for monkey-patching.
|
||||
|
||||
The fixture provides these methods to modify objects, dictionaries, or
|
||||
@@ -233,10 +233,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
||||
on warning categories.
|
||||
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:241
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:242
|
||||
Return a :class:`pytest.TempPathFactory` instance for the test session.
|
||||
|
||||
tmp_path -- .../_pytest/tmpdir.py:256
|
||||
tmp_path -- .../_pytest/tmpdir.py:257
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
@@ -245,8 +245,8 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
and old bases are removed after 3 sessions, to aid in debugging.
|
||||
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See
|
||||
:ref:`temporary directory location and retention`.
|
||||
|
||||
The returned object is a :class:`pathlib.Path` object.
|
||||
|
||||
|
||||
@@ -28,6 +28,217 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 8.2.0 (2024-04-27)
|
||||
=========================
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: A deprecation warning is now raised when implementations of one of the following hooks request a deprecated ``py.path.local`` parameter instead of the ``pathlib.Path`` parameter which replaced it:
|
||||
|
||||
- :hook:`pytest_ignore_collect` - the ``path`` parameter - use ``collection_path`` instead.
|
||||
- :hook:`pytest_collect_file` - the ``path`` parameter - use ``file_path`` instead.
|
||||
- :hook:`pytest_pycollect_makemodule` - the ``path`` parameter - use ``module_path`` instead.
|
||||
- :hook:`pytest_report_header` - the ``startdir`` parameter - use ``start_path`` instead.
|
||||
- :hook:`pytest_report_collectionfinish` - the ``startdir`` parameter - use ``start_path`` instead.
|
||||
|
||||
The replacement parameters are available since pytest 7.0.0.
|
||||
The old parameters will be removed in pytest 9.0.0.
|
||||
|
||||
See :ref:`legacy-path-hooks-deprecated` for more details.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#11871 <https://github.com/pytest-dev/pytest/issues/11871>`_: Added support for reading command line arguments from a file using the prefix character ``@``, like e.g.: ``pytest @tests.txt``. The file must have one argument per line.
|
||||
|
||||
See :ref:`Read arguments from file <args-from-file>` for details.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#11523 <https://github.com/pytest-dev/pytest/issues/11523>`_: :func:`pytest.importorskip` will now issue a warning if the module could be found, but raised :class:`ImportError` instead of :class:`ModuleNotFoundError`.
|
||||
|
||||
The warning can be suppressed by passing ``exc_type=ImportError`` to :func:`pytest.importorskip`.
|
||||
|
||||
See :ref:`import-or-skip-import-error` for details.
|
||||
|
||||
|
||||
- `#11728 <https://github.com/pytest-dev/pytest/issues/11728>`_: For ``unittest``-based tests, exceptions during class cleanup (as raised by functions registered with :meth:`TestCase.addClassCleanup <unittest.TestCase.addClassCleanup>`) are now reported instead of silently failing.
|
||||
|
||||
|
||||
- `#11777 <https://github.com/pytest-dev/pytest/issues/11777>`_: Text is no longer truncated in the ``short test summary info`` section when ``-vv`` is given.
|
||||
|
||||
|
||||
- `#12112 <https://github.com/pytest-dev/pytest/issues/12112>`_: Improved namespace packages detection when :confval:`consider_namespace_packages` is enabled, covering more situations (like editable installs).
|
||||
|
||||
|
||||
- `#9502 <https://github.com/pytest-dev/pytest/issues/9502>`_: Added :envvar:`PYTEST_VERSION` environment variable which is defined at the start of the pytest session and undefined afterwards. It contains the value of ``pytest.__version__``, and among other things can be used to easily check if code is running from within a pytest run.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#12065 <https://github.com/pytest-dev/pytest/issues/12065>`_: Fixed a regression in pytest 8.0.0 where test classes containing ``setup_method`` and tests using ``@staticmethod`` or ``@classmethod`` would crash with ``AttributeError: 'NoneType' object has no attribute 'setup_method'``.
|
||||
|
||||
Now the :attr:`request.instance <pytest.FixtureRequest.instance>` attribute of tests using ``@staticmethod`` and ``@classmethod`` is no longer ``None``, but a fresh instance of the class, like in non-static methods.
|
||||
Previously it was ``None``, and all fixtures of such tests would share a single ``self``.
|
||||
|
||||
|
||||
- `#12135 <https://github.com/pytest-dev/pytest/issues/12135>`_: Fixed issue where fixtures adding their finalizer multiple times to fixtures they request would cause unreliable and non-intuitive teardown ordering in some instances.
|
||||
|
||||
|
||||
- `#12194 <https://github.com/pytest-dev/pytest/issues/12194>`_: Fixed a bug with ``--importmode=importlib`` and ``--doctest-modules`` where child modules did not appear as attributes in parent modules.
|
||||
|
||||
|
||||
- `#1489 <https://github.com/pytest-dev/pytest/issues/1489>`_: Fixed some instances where teardown of higher-scoped fixtures was not happening in the reverse order they were initialized in.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: ``pluggy>=1.5.0`` is now required.
|
||||
|
||||
|
||||
- `#12167 <https://github.com/pytest-dev/pytest/issues/12167>`_: :ref:`cache <cache>`: create supporting files (``CACHEDIR.TAG``, ``.gitignore``, etc.) in a temporary directory to provide atomic semantics.
|
||||
|
||||
|
||||
pytest 8.1.2 (2024-04-26)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#12114 <https://github.com/pytest-dev/pytest/issues/12114>`_: Fixed error in :func:`pytest.approx` when used with `numpy` arrays and comparing with other types.
|
||||
|
||||
|
||||
pytest 8.1.1 (2024-03-08)
|
||||
=========================
|
||||
|
||||
.. note::
|
||||
|
||||
This release is not a usual bug fix release -- it contains features and improvements, being a follow up
|
||||
to ``8.1.0``, which has been yanked from PyPI.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: Added the new :confval:`consider_namespace_packages` configuration option, defaulting to ``False``.
|
||||
|
||||
If set to ``True``, pytest will attempt to identify modules that are part of `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__ when importing modules.
|
||||
|
||||
|
||||
- `#11653 <https://github.com/pytest-dev/pytest/issues/11653>`_: Added the new :confval:`verbosity_test_cases` configuration option for fine-grained control of test execution verbosity.
|
||||
See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10865 <https://github.com/pytest-dev/pytest/issues/10865>`_: :func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`.
|
||||
Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion).
|
||||
While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing.
|
||||
|
||||
|
||||
- `#11311 <https://github.com/pytest-dev/pytest/issues/11311>`_: When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
|
||||
as the relative directory.
|
||||
|
||||
Previoulsy this would raise an :class:`AssertionError`.
|
||||
|
||||
|
||||
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: :ref:`--import-mode=importlib <import-mode-importlib>` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails.
|
||||
|
||||
This means that installed packages will be imported under their canonical name if possible first, for example ``app.core.models``, instead of having the module name always be derived from their path (for example ``.env310.lib.site_packages.app.core.models``).
|
||||
|
||||
|
||||
- `#11801 <https://github.com/pytest-dev/pytest/issues/11801>`_: Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes.
|
||||
It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list.
|
||||
|
||||
|
||||
- `#11850 <https://github.com/pytest-dev/pytest/issues/11850>`_: Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12.
|
||||
|
||||
|
||||
- `#11962 <https://github.com/pytest-dev/pytest/issues/11962>`_: In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``.
|
||||
|
||||
|
||||
- `#11978 <https://github.com/pytest-dev/pytest/issues/11978>`_: Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``.
|
||||
|
||||
Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging.
|
||||
|
||||
|
||||
- `#12047 <https://github.com/pytest-dev/pytest/issues/12047>`_: When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group.
|
||||
Previously, only the first exception was reported.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: Fixed regression where ``--importmode=importlib`` would import non-test modules more than once.
|
||||
|
||||
|
||||
- `#11904 <https://github.com/pytest-dev/pytest/issues/11904>`_: Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using ``--pyargs``.
|
||||
|
||||
This change improves the collection tree for tests specified using ``--pyargs``, see :pull:`12043` for a comparison with pytest 8.0 and <8.
|
||||
|
||||
|
||||
- `#12011 <https://github.com/pytest-dev/pytest/issues/12011>`_: Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed.
|
||||
|
||||
|
||||
- `#12014 <https://github.com/pytest-dev/pytest/issues/12014>`_: Fix the ``stacklevel`` used when warning about marks used on fixtures.
|
||||
|
||||
|
||||
- `#12039 <https://github.com/pytest-dev/pytest/issues/12039>`_: Fixed a regression in ``8.0.2`` where tests created using :fixture:`tmp_path` have been collected multiple times in CI under Windows.
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#11790 <https://github.com/pytest-dev/pytest/issues/11790>`_: Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#11785 <https://github.com/pytest-dev/pytest/issues/11785>`_: Some changes were made to private functions which may affect plugins which access them:
|
||||
|
||||
- ``FixtureManager._getautousenames()`` now takes a ``Node`` itself instead of the nodeid.
|
||||
- ``FixtureManager.getfixturedefs()`` now takes the ``Node`` itself instead of the nodeid.
|
||||
- The ``_pytest.nodes.iterparentnodeids()`` function is removed without replacement.
|
||||
Prefer to traverse the node hierarchy itself instead.
|
||||
If you really need to, copy the function from the previous pytest release.
|
||||
|
||||
|
||||
- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: Delayed the deprecation of the following features to ``9.0.0``:
|
||||
|
||||
* :ref:`node-ctor-fspath-deprecation`.
|
||||
* :ref:`legacy-path-hooks-deprecated`.
|
||||
|
||||
It was discovered after ``8.1.0`` was released that the warnings about the impeding removal were not being displayed, so the team decided to revert the removal.
|
||||
|
||||
This is the reason for ``8.1.0`` being yanked.
|
||||
|
||||
|
||||
pytest 8.1.0 (YANKED)
|
||||
=====================
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
This release has been **yanked**: it broke some plugins without the proper warning period, due to
|
||||
some warnings not showing up as expected.
|
||||
|
||||
See `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`__.
|
||||
|
||||
|
||||
pytest 8.0.2 (2024-02-24)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -200,6 +200,7 @@ nitpick_ignore = [
|
||||
("py:class", "_tracing.TagTracerSub"),
|
||||
("py:class", "warnings.WarningMessage"),
|
||||
# Undocumented type aliases
|
||||
("py:class", "LEGACY_PATH"),
|
||||
("py:class", "_PluggyPlugin"),
|
||||
# TypeVars
|
||||
("py:class", "_pytest._code.code.E"),
|
||||
@@ -394,7 +395,7 @@ epub_copyright = "2013, holger krekel et alii"
|
||||
# The format is a list of tuples containing the path and title.
|
||||
# epub_pre_files = []
|
||||
|
||||
# HTML files shat should be inserted after the pages created by sphinx.
|
||||
# HTML files that should be inserted after the pages created by sphinx.
|
||||
# The format is a list of tuples containing the path and title.
|
||||
# epub_post_files = []
|
||||
|
||||
|
||||
@@ -19,7 +19,80 @@ Below is a complete list of all pytest features which are considered deprecated.
|
||||
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
|
||||
|
||||
|
||||
.. _legacy-path-hooks-deprecated:
|
||||
.. _import-or-skip-import-error:
|
||||
|
||||
``pytest.importorskip`` default behavior regarding :class:`ImportError`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 8.2
|
||||
|
||||
Traditionally :func:`pytest.importorskip` will capture :class:`ImportError`, with the original intent being to skip
|
||||
tests where a dependent module is not installed, for example testing with different dependencies.
|
||||
|
||||
However some packages might be installed in the system, but are not importable due to
|
||||
some other issue, for example, a compilation error or a broken installation. In those cases :func:`pytest.importorskip`
|
||||
would still silently skip the test, but more often than not users would like to see the unexpected
|
||||
error so the underlying issue can be fixed.
|
||||
|
||||
In ``8.2`` the ``exc_type`` parameter has been added, giving users the ability of passing :class:`ModuleNotFoundError`
|
||||
to skip tests only if the module cannot really be found, and not because of some other error.
|
||||
|
||||
Catching only :class:`ModuleNotFoundError` by default (and letting other errors propagate) would be the best solution,
|
||||
however for backward compatibility, pytest will keep the existing behavior but raise an warning if:
|
||||
|
||||
1. The captured exception is of type :class:`ImportError`, and:
|
||||
2. The user does not pass ``exc_type`` explicitly.
|
||||
|
||||
If the import attempt raises :class:`ModuleNotFoundError` (the usual case), then the module is skipped and no
|
||||
warning is emitted.
|
||||
|
||||
This way, the usual cases will keep working the same way, while unexpected errors will now issue a warning, with
|
||||
users being able to supress the warning by passing ``exc_type=ImportError`` explicitly.
|
||||
|
||||
In ``9.0``, the warning will turn into an error, and in ``9.1`` :func:`pytest.importorskip` will only capture
|
||||
:class:`ModuleNotFoundError` by default and no warnings will be issued anymore -- but users can still capture
|
||||
:class:`ImportError` by passing it to ``exc_type``.
|
||||
|
||||
|
||||
.. _node-ctor-fspath-deprecation:
|
||||
|
||||
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
|
||||
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
|
||||
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
|
||||
is now deprecated.
|
||||
|
||||
Plugins which construct nodes should pass the ``path`` argument, of type
|
||||
:class:`pathlib.Path`, instead of the ``fspath`` argument.
|
||||
|
||||
Plugins which implement custom items and collectors are encouraged to replace
|
||||
``fspath`` parameters (``py.path.local``) with ``path`` parameters
|
||||
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
|
||||
|
||||
If possible, plugins with custom items should use :ref:`cooperative
|
||||
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
|
||||
arguments they only pass on to the superclass.
|
||||
|
||||
.. note::
|
||||
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
|
||||
new attribute being ``path``) is **the opposite** of the situation for
|
||||
hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
|
||||
argument being ``path``).
|
||||
|
||||
This is an unfortunate artifact due to historical reasons, which should be
|
||||
resolved in future versions as we slowly get rid of the :pypi:`py`
|
||||
dependency (see :issue:`9283` for a longer discussion).
|
||||
|
||||
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
|
||||
which still is expected to return a ``py.path.local`` object, nodes still have
|
||||
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
|
||||
no matter what argument was used in the constructor. We expect to deprecate the
|
||||
``fspath`` attribute in a future release.
|
||||
|
||||
|
||||
Configuring hook specs/impls using markers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -62,6 +135,33 @@ Changed ``hookwrapper`` attributes:
|
||||
* ``historic``
|
||||
|
||||
|
||||
.. _legacy-path-hooks-deprecated:
|
||||
|
||||
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
|
||||
|
||||
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
|
||||
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
|
||||
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
|
||||
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
|
||||
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
|
||||
|
||||
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
|
||||
|
||||
.. note::
|
||||
The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
|
||||
:ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
|
||||
being ``path``) is **the opposite** of the situation for hooks (the old
|
||||
argument being ``path``).
|
||||
|
||||
This is an unfortunate artifact due to historical reasons, which should be
|
||||
resolved in future versions as we slowly get rid of the :pypi:`py`
|
||||
dependency (see :issue:`9283` for a longer discussion).
|
||||
|
||||
Directly constructing internal classes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -208,73 +308,6 @@ an appropriate period of deprecation has passed.
|
||||
|
||||
Some breaking changes which could not be deprecated are also listed.
|
||||
|
||||
.. _node-ctor-fspath-deprecation:
|
||||
|
||||
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
|
||||
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
|
||||
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
|
||||
is now deprecated.
|
||||
|
||||
Plugins which construct nodes should pass the ``path`` argument, of type
|
||||
:class:`pathlib.Path`, instead of the ``fspath`` argument.
|
||||
|
||||
Plugins which implement custom items and collectors are encouraged to replace
|
||||
``fspath`` parameters (``py.path.local``) with ``path`` parameters
|
||||
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
|
||||
|
||||
If possible, plugins with custom items should use :ref:`cooperative
|
||||
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
|
||||
arguments they only pass on to the superclass.
|
||||
|
||||
.. note::
|
||||
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
|
||||
new attribute being ``path``) is **the opposite** of the situation for
|
||||
hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
|
||||
argument being ``path``).
|
||||
|
||||
This is an unfortunate artifact due to historical reasons, which should be
|
||||
resolved in future versions as we slowly get rid of the :pypi:`py`
|
||||
dependency (see :issue:`9283` for a longer discussion).
|
||||
|
||||
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
|
||||
which still is expected to return a ``py.path.local`` object, nodes still have
|
||||
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
|
||||
no matter what argument was used in the constructor. We expect to deprecate the
|
||||
``fspath`` attribute in a future release.
|
||||
|
||||
|
||||
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
|
||||
|
||||
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
|
||||
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
|
||||
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
|
||||
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
|
||||
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
|
||||
|
||||
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
|
||||
|
||||
.. note::
|
||||
The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
|
||||
:ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
|
||||
being ``path``) is **the opposite** of the situation for hooks (the old
|
||||
argument being ``path``).
|
||||
|
||||
This is an unfortunate artifact due to historical reasons, which should be
|
||||
resolved in future versions as we slowly get rid of the :pypi:`py`
|
||||
dependency (see :issue:`9283` for a longer discussion).
|
||||
|
||||
|
||||
.. _nose-deprecation:
|
||||
|
||||
Support for tests written for nose
|
||||
|
||||
@@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
|
||||
rootdir: /home/sweet/project
|
||||
collected 8 items
|
||||
|
||||
<Dir parametrize.rst-194>
|
||||
<Dir parametrize.rst-198>
|
||||
<Module test_time.py>
|
||||
<Function test_timedistance_v0[a0-b0-expected0]>
|
||||
<Function test_timedistance_v0[a1-b1-expected1]>
|
||||
@@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
|
||||
rootdir: /home/sweet/project
|
||||
collected 4 items
|
||||
|
||||
<Dir parametrize.rst-194>
|
||||
<Dir parametrize.rst-198>
|
||||
<Module test_scenarios.py>
|
||||
<Class TestSampleWithScenarios>
|
||||
<Function test_demo1[basic]>
|
||||
@@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
|
||||
rootdir: /home/sweet/project
|
||||
collected 2 items
|
||||
|
||||
<Dir parametrize.rst-194>
|
||||
<Dir parametrize.rst-198>
|
||||
<Module test_backends.py>
|
||||
<Function test_db_initialized[d1]>
|
||||
<Function test_db_initialized[d2]>
|
||||
|
||||
@@ -152,7 +152,7 @@ The test collection would look like this:
|
||||
configfile: pytest.ini
|
||||
collected 2 items
|
||||
|
||||
<Dir pythoncollection.rst-195>
|
||||
<Dir pythoncollection.rst-199>
|
||||
<Module check_myapp.py>
|
||||
<Class CheckMyApp>
|
||||
<Function simple_check>
|
||||
@@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
|
||||
configfile: pytest.ini
|
||||
collected 3 items
|
||||
|
||||
<Dir pythoncollection.rst-195>
|
||||
<Dir pythoncollection.rst-199>
|
||||
<Dir CWD>
|
||||
<Module pythoncollection.py>
|
||||
<Function test_function>
|
||||
|
||||
@@ -445,7 +445,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
||||
|
||||
def test_tupleerror(self):
|
||||
> a, b = [1] # NOQA
|
||||
> a, b = [1] # noqa: F841
|
||||
E ValueError: not enough values to unpack (expected 2, got 1)
|
||||
|
||||
failure_demo.py:175: ValueError
|
||||
@@ -467,7 +467,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0022>
|
||||
|
||||
def test_some_error(self):
|
||||
> if namenotexi: # NOQA
|
||||
> if namenotexi: # noqa: F821
|
||||
E NameError: name 'namenotexi' is not defined
|
||||
|
||||
failure_demo.py:183: NameError
|
||||
|
||||
@@ -52,10 +52,9 @@ Plugins
|
||||
|
||||
Rerunning any failed tests can mitigate the negative effects of flaky tests by giving them additional chances to pass, so that the overall build does not fail. Several pytest plugins support this:
|
||||
|
||||
* `flaky <https://github.com/box/flaky>`_
|
||||
* `pytest-flakefinder <https://github.com/dropbox/pytest-flakefinder>`_ - `blog post <https://blogs.dropbox.com/tech/2016/03/open-sourcing-pytest-tools/>`_
|
||||
* `pytest-rerunfailures <https://github.com/pytest-dev/pytest-rerunfailures>`_
|
||||
* `pytest-replay <https://github.com/ESSS/pytest-replay>`_: This plugin helps to reproduce locally crashes or flaky tests observed during CI runs.
|
||||
* `pytest-flakefinder <https://github.com/dropbox/pytest-flakefinder>`_ - `blog post <https://blogs.dropbox.com/tech/2016/03/open-sourcing-pytest-tools/>`_
|
||||
|
||||
Plugins to deliberately randomize tests can help expose tests with state problems:
|
||||
|
||||
@@ -106,7 +105,7 @@ This is a limited list, please submit an issue or pull request to expand it!
|
||||
* Gao, Zebao, Yalan Liang, Myra B. Cohen, Atif M. Memon, and Zhen Wang. "Making system user interactive tests repeatable: When and what should we control?." In *Software Engineering (ICSE), 2015 IEEE/ACM 37th IEEE International Conference on*, vol. 1, pp. 55-65. IEEE, 2015. `PDF <http://www.cs.umd.edu/~atif/pubs/gao-icse15.pdf>`__
|
||||
* Palomba, Fabio, and Andy Zaidman. "Does refactoring of test smells induce fixing flaky tests?." In *Software Maintenance and Evolution (ICSME), 2017 IEEE International Conference on*, pp. 1-12. IEEE, 2017. `PDF in Google Drive <https://drive.google.com/file/d/10HdcCQiuQVgW3yYUJD-TSTq1NbYEprl0/view>`__
|
||||
* Bell, Jonathan, Owolabi Legunsen, Michael Hilton, Lamyaa Eloussi, Tifany Yung, and Darko Marinov. "DeFlaker: Automatically detecting flaky tests." In *Proceedings of the 2018 International Conference on Software Engineering*. 2018. `PDF <https://www.jonbell.net/icse18-deflaker.pdf>`__
|
||||
|
||||
* Dutta, Saikat and Shi, August and Choudhary, Rutvik and Zhang, Zhekun and Jain, Aryaman and Misailovic, Sasa. "Detecting flaky tests in probabilistic and machine learning applications." In *Proceedings of the 29th ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA)*, pp. 211-224. ACM, 2020. `PDF <https://www.cs.cornell.edu/~saikatd/papers/flash-issta20.pdf>`__
|
||||
|
||||
Resources
|
||||
^^^^^^^^^
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 8.0.2
|
||||
pytest 8.2.0
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -224,6 +224,7 @@ place the objects you want to appear in the doctest namespace:
|
||||
.. code-block:: python
|
||||
|
||||
# content of conftest.py
|
||||
import pytest
|
||||
import numpy
|
||||
|
||||
|
||||
|
||||
@@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
|
||||
rootdir: /home/sweet/project
|
||||
collected 12 items
|
||||
|
||||
<Dir fixtures.rst-213>
|
||||
<Dir fixtures.rst-217>
|
||||
<Module test_anothersmtp.py>
|
||||
<Function test_showhelo[smtp.gmail.com]>
|
||||
<Function test_showhelo[mail.python.org]>
|
||||
|
||||
@@ -294,9 +294,47 @@ Now if we increase verbosity even more:
|
||||
|
||||
test_verbosity_example.py:19: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
|
||||
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
|
||||
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
|
||||
FAILED test_verbosity_example.py::test_words_fail - AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi']
|
||||
|
||||
At index 2 diff: 'grapes' != 'orange'
|
||||
|
||||
Full diff:
|
||||
[
|
||||
'banana',
|
||||
'apple',
|
||||
- 'orange',
|
||||
? ^ ^^
|
||||
+ 'grapes',
|
||||
? ^ ^ +
|
||||
'melon',
|
||||
'kiwi',
|
||||
]
|
||||
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
|
||||
|
||||
Common items:
|
||||
{'0': 0}
|
||||
Left contains 4 more items:
|
||||
{'1': 1, '2': 2, '3': 3, '4': 4}
|
||||
Right contains 4 more items:
|
||||
{'10': 10, '20': 20, '30': 30, '40': 40}
|
||||
|
||||
Full diff:
|
||||
{
|
||||
'0': 0,
|
||||
- '10': 10,
|
||||
? - -
|
||||
+ '1': 1,
|
||||
- '20': 20,
|
||||
? - -
|
||||
+ '2': 2,
|
||||
- '30': 30,
|
||||
? - -
|
||||
+ '3': 3,
|
||||
- '40': 40,
|
||||
? - -
|
||||
+ '4': 4,
|
||||
}
|
||||
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
|
||||
======================= 3 failed, 1 passed in 0.12s ========================
|
||||
|
||||
Notice now that:
|
||||
|
||||
@@ -17,7 +17,8 @@ in the current directory and its subdirectories. More generally, pytest follows
|
||||
Specifying which tests to run
|
||||
------------------------------
|
||||
|
||||
Pytest supports several ways to run and select tests from the command-line.
|
||||
Pytest supports several ways to run and select tests from the command-line or from a file
|
||||
(see below for :ref:`reading arguments from file <args-from-file>`).
|
||||
|
||||
**Run tests in a module**
|
||||
|
||||
@@ -91,6 +92,28 @@ For more information see :ref:`marks <mark>`.
|
||||
|
||||
This will import ``pkg.testing`` and use its filesystem location to find and run tests from.
|
||||
|
||||
.. _args-from-file:
|
||||
|
||||
**Read arguments from file**
|
||||
|
||||
.. versionadded:: 8.2
|
||||
|
||||
All of the above can be read from a file using the ``@`` prefix:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest @tests_to_run.txt
|
||||
|
||||
where ``tests_to_run.txt`` contains an entry per line, e.g.:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
tests/test_file.py
|
||||
tests/test_mod.py::test_func[x1,y2]
|
||||
tests/test_mod.py::TestClass
|
||||
-m slow
|
||||
|
||||
This file can also be generated using ``pytest --collect-only -q`` and modified as needed.
|
||||
|
||||
Getting help on version, option names, environment variables
|
||||
--------------------------------------------------------------
|
||||
|
||||
@@ -100,7 +100,7 @@ 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.
|
||||
suppressing it, or raising a different exception entirely.
|
||||
|
||||
For more information, consult the
|
||||
:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
|
||||
* **June 11th to 13th 2024**, Remote
|
||||
* **March 4th to 6th 2025**, Leipzig, Germany / Remote
|
||||
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, June 2024 (`date poll <https://nuudel.digitalcourage.de/2tEsEpRcwMNcAXVO>`_)
|
||||
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, **June 17th -- 22nd 2024**
|
||||
- pytest tips and tricks for a better testsuite, `Europython 2024 <https://ep2024.europython.eu/>`_, **July 8th -- 14th 2024** (3h), Prague
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
@@ -87,7 +88,7 @@ Features
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
* :ref:`Get started <get-started>` - install pytest and grasp its basics just twenty minutes
|
||||
* :ref:`Get started <get-started>` - install pytest and grasp its basics in just twenty minutes
|
||||
* :ref:`How-to guides <how-to>` - step-by-step guides, covering a vast range of use-cases and needs
|
||||
* :ref:`Reference guides <reference>` - includes the complete pytest API reference, lists of plugins and more
|
||||
* :ref:`Explanation <explanation>` - background, discussion of key topics, answers to higher-level questions
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -136,7 +136,7 @@ pytest.freeze_includes
|
||||
Marks
|
||||
-----
|
||||
|
||||
Marks can be used apply meta data to *test functions* (but not fixtures), which can then be accessed by
|
||||
Marks can be used to apply metadata to *test functions* (but not fixtures), which can then be accessed by
|
||||
fixtures or plugins.
|
||||
|
||||
|
||||
@@ -1117,6 +1117,11 @@ When set (regardless of value), pytest acknowledges that is running in a CI proc
|
||||
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given
|
||||
by the user, see :ref:`adding default options` for more information.
|
||||
|
||||
.. envvar:: PYTEST_VERSION
|
||||
|
||||
This environment variable is defined at the start of the pytest session and is undefined afterwards.
|
||||
It contains the value of ``pytest.__version__``, and among other things can be used to easily check if a code is running from within a pytest run.
|
||||
|
||||
.. envvar:: PYTEST_CURRENT_TEST
|
||||
|
||||
This is not meant to be set by users, but is set by pytest internally with the name of the current test so other
|
||||
@@ -1279,8 +1284,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__
|
||||
when collecting Python modules. Default is ``False``.
|
||||
|
||||
Set to ``True`` if you are testing namespace packages installed into a virtual environment and it is important for
|
||||
your packages to be imported using their full namespace package name.
|
||||
Set to ``True`` if the package you are testing is part of a namespace package.
|
||||
|
||||
Only `native namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages>`__
|
||||
are supported, with no plans to support `legacy namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#legacy-namespace-packages>`__.
|
||||
@@ -2103,6 +2107,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
--log-cli-date-format=LOG_CLI_DATE_FORMAT
|
||||
Log date format used by the logging module
|
||||
--log-file=LOG_FILE Path to a file when logging will be written to
|
||||
--log-file-mode={w,a}
|
||||
Log file open mode
|
||||
--log-file-level=LOG_FILE_LEVEL
|
||||
Log file logging level
|
||||
--log-file-format=LOG_FILE_FORMAT
|
||||
@@ -2128,6 +2134,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
Each line specifies a pattern for
|
||||
warnings.filterwarnings. Processed after
|
||||
-W/--pythonwarnings.
|
||||
consider_namespace_packages (bool):
|
||||
Consider namespace packages when resolving module
|
||||
names during import
|
||||
usefixtures (args): List of default fixtures to be used with this
|
||||
project
|
||||
python_files (args): Glob-style file patterns for Python test module
|
||||
@@ -2146,6 +2155,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
progress information ("progress" (percentage) |
|
||||
"count" | "progress-even-when-capture-no" (forces
|
||||
progress even when capture=no)
|
||||
verbosity_test_cases (string):
|
||||
Specify a verbosity level for test case execution,
|
||||
overriding the main level. Higher levels will
|
||||
provide more detailed information about each test
|
||||
case executed.
|
||||
xfail_strict (bool): Default for the strict parameter of xfail markers
|
||||
when not given explicitly (default: False)
|
||||
tmp_path_retention_count (string):
|
||||
@@ -2193,6 +2207,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
log_cli_date_format (string):
|
||||
Default value for --log-cli-date-format
|
||||
log_file (string): Default value for --log-file
|
||||
log_file_mode (string):
|
||||
Default value for --log-file-mode
|
||||
log_file_level (string):
|
||||
Default value for --log-file-level
|
||||
log_file_format (string):
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pallets-sphinx-themes
|
||||
pluggy>=1.2.0
|
||||
pluggy>=1.5.0
|
||||
pygments-pytest>=2.3.0
|
||||
sphinx-removed-in>=0.2.0
|
||||
sphinx>=7
|
||||
|
||||
119
pyproject.toml
119
pyproject.toml
@@ -43,11 +43,11 @@ dependencies = [
|
||||
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
|
||||
"iniconfig",
|
||||
"packaging",
|
||||
"pluggy<2.0,>=1.4",
|
||||
"pluggy<2.0,>=1.5",
|
||||
'tomli>=1; python_version < "3.11"',
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
testing = [
|
||||
dev = [
|
||||
"argcomplete",
|
||||
"attrs>=19.2",
|
||||
"hypothesis>=3.56",
|
||||
@@ -165,6 +165,119 @@ lines-after-imports = 2
|
||||
"src/_pytest/_version.py" = ["I001"]
|
||||
"testing/python/approx.py" = ["B015"]
|
||||
|
||||
[tool.pylint.main]
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length = 120
|
||||
disable= [
|
||||
"abstract-method",
|
||||
"arguments-differ",
|
||||
"arguments-renamed",
|
||||
"assigning-non-slot",
|
||||
"attribute-defined-outside-init",
|
||||
"bad-classmethod-argument",
|
||||
"bad-mcs-method-argument",
|
||||
"broad-exception-caught",
|
||||
"broad-exception-raised",
|
||||
"cell-var-from-loop",
|
||||
"comparison-of-constants",
|
||||
"comparison-with-callable",
|
||||
"comparison-with-itself",
|
||||
"condition-evals-to-constant",
|
||||
"consider-using-dict-items",
|
||||
"consider-using-enumerate",
|
||||
"consider-using-from-import",
|
||||
"consider-using-f-string",
|
||||
"consider-using-in",
|
||||
"consider-using-sys-exit",
|
||||
"consider-using-ternary",
|
||||
"consider-using-with",
|
||||
"cyclic-import",
|
||||
"disallowed-name",
|
||||
"duplicate-code",
|
||||
"eval-used",
|
||||
"exec-used",
|
||||
"expression-not-assigned",
|
||||
"fixme",
|
||||
"global-statement",
|
||||
"implicit-str-concat",
|
||||
"import-error",
|
||||
"import-outside-toplevel",
|
||||
"inconsistent-return-statements",
|
||||
"invalid-bool-returned",
|
||||
"invalid-name",
|
||||
"invalid-repr-returned",
|
||||
"invalid-str-returned",
|
||||
"keyword-arg-before-vararg",
|
||||
"line-too-long",
|
||||
"method-hidden",
|
||||
"misplaced-bare-raise",
|
||||
"missing-docstring",
|
||||
"missing-timeout",
|
||||
"multiple-statements",
|
||||
"no-else-break",
|
||||
"no-else-continue",
|
||||
"no-else-raise",
|
||||
"no-else-return",
|
||||
"no-member",
|
||||
"no-name-in-module",
|
||||
"no-self-argument",
|
||||
"not-an-iterable",
|
||||
"not-callable",
|
||||
"pointless-exception-statement",
|
||||
"pointless-statement",
|
||||
"pointless-string-statement",
|
||||
"protected-access",
|
||||
"raise-missing-from",
|
||||
"redefined-argument-from-local",
|
||||
"redefined-builtin",
|
||||
"redefined-outer-name",
|
||||
"reimported",
|
||||
"simplifiable-condition",
|
||||
"simplifiable-if-expression",
|
||||
"singleton-comparison",
|
||||
"superfluous-parens",
|
||||
"super-init-not-called",
|
||||
"too-few-public-methods",
|
||||
"too-many-ancestors",
|
||||
"too-many-arguments",
|
||||
"too-many-branches",
|
||||
"too-many-function-args",
|
||||
"too-many-instance-attributes",
|
||||
"too-many-lines",
|
||||
"too-many-locals",
|
||||
"too-many-nested-blocks",
|
||||
"too-many-public-methods",
|
||||
"too-many-return-statements",
|
||||
"too-many-statements",
|
||||
"try-except-raise",
|
||||
"typevar-name-incorrect-variance",
|
||||
"unbalanced-tuple-unpacking",
|
||||
"undefined-loop-variable",
|
||||
"undefined-variable",
|
||||
"unexpected-keyword-arg",
|
||||
"unidiomatic-typecheck",
|
||||
"unnecessary-comprehension",
|
||||
"unnecessary-dunder-call",
|
||||
"unnecessary-lambda",
|
||||
"unnecessary-lambda-assignment",
|
||||
"unpacking-non-sequence",
|
||||
"unspecified-encoding",
|
||||
"unsubscriptable-object",
|
||||
"unused-argument",
|
||||
"unused-import",
|
||||
"unused-variable",
|
||||
"used-before-assignment",
|
||||
"use-dict-literal",
|
||||
"use-implicit-booleaness-not-comparison",
|
||||
"use-implicit-booleaness-not-len",
|
||||
"useless-else-on-loop",
|
||||
"useless-import-alias",
|
||||
"useless-return",
|
||||
"use-maxsplit-arg",
|
||||
"using-constant-test",
|
||||
"wrong-import-order",
|
||||
]
|
||||
|
||||
[tool.check-wheel-contents]
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
# W009: Wheel contains multiple toplevel library entries
|
||||
@@ -281,6 +394,7 @@ template = "changelog/_template.rst"
|
||||
showcontent = true
|
||||
|
||||
[tool.mypy]
|
||||
files = ["src", "testing", "scripts"]
|
||||
mypy_path = ["src"]
|
||||
check_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
@@ -293,3 +407,4 @@ warn_return_any = true
|
||||
warn_unreachable = true
|
||||
warn_unused_configs = true
|
||||
no_implicit_reexport = true
|
||||
warn_unused_ignores = true
|
||||
|
||||
@@ -8,6 +8,7 @@ our CHANGELOG) into Markdown (which is required by GitHub Releases).
|
||||
|
||||
Requires Python3.6+.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
@@ -13,6 +13,7 @@ After that, it will create a release using the `release` tox environment, and pu
|
||||
**Token**: currently the token from the GitHub Actions is used, pushed with
|
||||
`pytest bot <pytestbot@gmail.com>` commit author.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: disallow-untyped-defs
|
||||
"""Invoke development tasks."""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -7,4 +7,4 @@ except ImportError: # pragma: no cover
|
||||
# broken installation, we don't even try
|
||||
# unknown only works because we do poor mans version compare
|
||||
__version__ = "unknown"
|
||||
version_tuple = (0, 0, "unknown") # type:ignore[assignment]
|
||||
version_tuple = (0, 0, "unknown")
|
||||
|
||||
@@ -52,7 +52,7 @@ from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||
@@ -279,9 +279,9 @@ class TracebackEntry:
|
||||
|
||||
Mostly for internal use.
|
||||
"""
|
||||
tbh: Union[
|
||||
bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]
|
||||
] = False
|
||||
tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = (
|
||||
False
|
||||
)
|
||||
for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
|
||||
# in normal cases, f_locals and f_globals are dictionaries
|
||||
# however via `exec(...)` / `eval(...)` they can be other types
|
||||
@@ -378,12 +378,10 @@ class Traceback(List[TracebackEntry]):
|
||||
return self
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: "SupportsIndex") -> TracebackEntry:
|
||||
...
|
||||
def __getitem__(self, key: "SupportsIndex") -> TracebackEntry: ...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> "Traceback":
|
||||
...
|
||||
def __getitem__(self, key: slice) -> "Traceback": ...
|
||||
|
||||
def __getitem__(
|
||||
self, key: Union["SupportsIndex", slice]
|
||||
@@ -705,7 +703,7 @@ class ExceptionInfo(Generic[E]):
|
||||
# Workaround for https://github.com/python/cpython/issues/98778 on
|
||||
# Python <= 3.9, and some 3.10 and 3.11 patch versions.
|
||||
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
|
||||
if sys.version_info[:2] <= (3, 11) and isinstance(exc, HTTPError):
|
||||
if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
|
||||
notes = []
|
||||
else:
|
||||
raise
|
||||
@@ -788,6 +786,8 @@ class ExceptionInfo(Generic[E]):
|
||||
If `None`, will search for a matching exception at any nesting depth.
|
||||
If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
|
||||
the exceptions contained within the topmost exception group).
|
||||
|
||||
.. versionadded:: 8.0
|
||||
"""
|
||||
msg = "Captured exception is not an instance of `BaseExceptionGroup`"
|
||||
assert isinstance(self.value, BaseExceptionGroup), msg
|
||||
@@ -1051,13 +1051,13 @@ class FormattedExcinfo:
|
||||
# full support for exception groups added to ExceptionInfo.
|
||||
# See https://github.com/pytest-dev/pytest/issues/9159
|
||||
if isinstance(e, BaseExceptionGroup):
|
||||
reprtraceback: Union[
|
||||
ReprTracebackNative, ReprTraceback
|
||||
] = ReprTracebackNative(
|
||||
traceback.format_exception(
|
||||
type(excinfo_.value),
|
||||
excinfo_.value,
|
||||
excinfo_.traceback[0]._rawentry,
|
||||
reprtraceback: Union[ReprTracebackNative, ReprTraceback] = (
|
||||
ReprTracebackNative(
|
||||
traceback.format_exception(
|
||||
type(excinfo_.value),
|
||||
excinfo_.value,
|
||||
excinfo_.traceback[0]._rawentry,
|
||||
)
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -1348,7 +1348,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
|
||||
# in 6ec13a2b9. It ("place_as") appears to be something very custom.
|
||||
obj = get_real_func(obj)
|
||||
if hasattr(obj, "place_as"):
|
||||
obj = obj.place_as # type: ignore[attr-defined]
|
||||
obj = obj.place_as
|
||||
|
||||
try:
|
||||
code = Code.from_function(obj)
|
||||
|
||||
@@ -47,12 +47,10 @@ class Source:
|
||||
__hash__ = None # type: ignore
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: int) -> str:
|
||||
...
|
||||
def __getitem__(self, key: int) -> str: ...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> "Source":
|
||||
...
|
||||
def __getitem__(self, key: slice) -> "Source": ...
|
||||
|
||||
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:
|
||||
if isinstance(key, int):
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import TextIO
|
||||
|
||||
from ..compat import assert_never
|
||||
from .wcwidth import wcswidth
|
||||
|
||||
|
||||
@@ -209,6 +210,8 @@ class TerminalWriter:
|
||||
from pygments.lexers.python import PythonLexer as Lexer
|
||||
elif lexer == "diff":
|
||||
from pygments.lexers.diff import DiffLexer as Lexer
|
||||
else:
|
||||
assert_never(lexer)
|
||||
from pygments import highlight
|
||||
import pygments.util
|
||||
except ImportError:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""local path implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
@@ -205,12 +206,10 @@ class Stat:
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
...
|
||||
def size(self) -> int: ...
|
||||
|
||||
@property
|
||||
def mtime(self) -> float:
|
||||
...
|
||||
def mtime(self) -> float: ...
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self._osstatresult, "st_" + name)
|
||||
@@ -225,7 +224,7 @@ class Stat:
|
||||
raise NotImplementedError("XXX win32")
|
||||
import pwd
|
||||
|
||||
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined]
|
||||
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore]
|
||||
return entry[0]
|
||||
|
||||
@property
|
||||
@@ -235,7 +234,7 @@ class Stat:
|
||||
raise NotImplementedError("XXX win32")
|
||||
import grp
|
||||
|
||||
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined]
|
||||
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore]
|
||||
return entry[0]
|
||||
|
||||
def isdir(self):
|
||||
@@ -253,7 +252,7 @@ def getuserid(user):
|
||||
import pwd
|
||||
|
||||
if not isinstance(user, int):
|
||||
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
|
||||
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore]
|
||||
return user
|
||||
|
||||
|
||||
@@ -261,7 +260,7 @@ def getgroupid(group):
|
||||
import grp
|
||||
|
||||
if not isinstance(group, int):
|
||||
group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
|
||||
group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore]
|
||||
return group
|
||||
|
||||
|
||||
@@ -318,7 +317,7 @@ class LocalPath:
|
||||
def readlink(self) -> str:
|
||||
"""Return value of a symbolic link."""
|
||||
# https://github.com/python/mypy/issues/12278
|
||||
return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value]
|
||||
return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore]
|
||||
|
||||
def mklinkto(self, oldname):
|
||||
"""Posix style hard link to another name."""
|
||||
@@ -757,15 +756,11 @@ class LocalPath:
|
||||
if ensure:
|
||||
self.dirpath().ensure(dir=1)
|
||||
if encoding:
|
||||
# Using type ignore here because of this error:
|
||||
# error: Argument 1 has incompatible type overloaded function;
|
||||
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
|
||||
# Which seems incorrect, given io.open supports the given argument types.
|
||||
return error.checked_call(
|
||||
io.open,
|
||||
self.strpath,
|
||||
mode,
|
||||
encoding=encoding, # type:ignore[arg-type]
|
||||
encoding=encoding,
|
||||
)
|
||||
return error.checked_call(open, self.strpath, mode)
|
||||
|
||||
@@ -966,12 +961,10 @@ class LocalPath:
|
||||
return p
|
||||
|
||||
@overload
|
||||
def stat(self, raising: Literal[True] = ...) -> Stat:
|
||||
...
|
||||
def stat(self, raising: Literal[True] = ...) -> Stat: ...
|
||||
|
||||
@overload
|
||||
def stat(self, raising: Literal[False]) -> Stat | None:
|
||||
...
|
||||
def stat(self, raising: Literal[False]) -> Stat | None: ...
|
||||
|
||||
def stat(self, raising: bool = True) -> Stat | None:
|
||||
"""Return an os.stat() tuple."""
|
||||
@@ -1277,13 +1270,7 @@ class LocalPath:
|
||||
|
||||
if rootdir is None:
|
||||
rootdir = cls.get_temproot()
|
||||
# Using type ignore here because of this error:
|
||||
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
|
||||
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
|
||||
path = error.checked_call(
|
||||
tempfile.mkdtemp,
|
||||
dir=str(rootdir), # type:ignore[arg-type]
|
||||
)
|
||||
path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir))
|
||||
return cls(path)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Support for presenting detailed information in failing assertions."""
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
|
||||
@@ -289,15 +289,13 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
else:
|
||||
from importlib.abc import TraversableResources
|
||||
|
||||
def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore
|
||||
def get_resource_reader(self, name: str) -> TraversableResources:
|
||||
if sys.version_info < (3, 11):
|
||||
from importlib.readers import FileReader
|
||||
else:
|
||||
from importlib.resources.readers import FileReader
|
||||
|
||||
return FileReader( # type:ignore[no-any-return]
|
||||
types.SimpleNamespace(path=self._rewritten_names[name])
|
||||
)
|
||||
return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
|
||||
|
||||
|
||||
def _write_pyc_fp(
|
||||
@@ -672,9 +670,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
self.enable_assertion_pass_hook = False
|
||||
self.source = source
|
||||
self.scope: tuple[ast.AST, ...] = ()
|
||||
self.variables_overwrite: defaultdict[
|
||||
tuple[ast.AST, ...], Dict[str, str]
|
||||
] = defaultdict(dict)
|
||||
self.variables_overwrite: defaultdict[tuple[ast.AST, ...], Dict[str, str]] = (
|
||||
defaultdict(dict)
|
||||
)
|
||||
|
||||
def run(self, mod: ast.Module) -> None:
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
@@ -975,7 +973,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# name if it's a local variable or _should_repr_global_name()
|
||||
# thinks it's acceptable.
|
||||
locs = ast.Call(self.builtin("locals"), [], [])
|
||||
target_id = name.target.id # type: ignore[attr-defined]
|
||||
target_id = name.target.id
|
||||
inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
|
||||
dorepr = self.helper("_should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Utilities for assertion debugging."""
|
||||
|
||||
import collections.abc
|
||||
import os
|
||||
import pprint
|
||||
@@ -222,10 +223,9 @@ def assertrepr_compare(
|
||||
except outcomes.Exit:
|
||||
raise
|
||||
except Exception:
|
||||
repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
|
||||
explanation = [
|
||||
"(pytest_assertion plugin: representation of details failed: {}.".format(
|
||||
_pytest._code.ExceptionInfo.from_current()._getreprcrash()
|
||||
),
|
||||
f"(pytest_assertion plugin: representation of details failed: {repr_crash}.",
|
||||
" Probably an object has a faulty __repr__.)",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Implementation of the cache provider."""
|
||||
|
||||
# This plugin was not named "cache" to avoid conflicts with the external
|
||||
# pytest-cache version.
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import Generator
|
||||
@@ -122,6 +124,10 @@ class Cache:
|
||||
stacklevel=3,
|
||||
)
|
||||
|
||||
def _mkdir(self, path: Path) -> None:
|
||||
self._ensure_cache_dir_and_supporting_files()
|
||||
path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
def mkdir(self, name: str) -> Path:
|
||||
"""Return a directory path object with the given name.
|
||||
|
||||
@@ -140,7 +146,7 @@ class Cache:
|
||||
if len(path.parts) > 1:
|
||||
raise ValueError("name is not allowed to contain path separators")
|
||||
res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
|
||||
res.mkdir(exist_ok=True, parents=True)
|
||||
self._mkdir(res)
|
||||
return res
|
||||
|
||||
def _getvaluepath(self, key: str) -> Path:
|
||||
@@ -177,19 +183,13 @@ class Cache:
|
||||
"""
|
||||
path = self._getvaluepath(key)
|
||||
try:
|
||||
if path.parent.is_dir():
|
||||
cache_dir_exists_already = True
|
||||
else:
|
||||
cache_dir_exists_already = self._cachedir.exists()
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
self._mkdir(path.parent)
|
||||
except OSError as exc:
|
||||
self.warn(
|
||||
f"could not create cache path {path}: {exc}",
|
||||
_ispytest=True,
|
||||
)
|
||||
return
|
||||
if not cache_dir_exists_already:
|
||||
self._ensure_supporting_files()
|
||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||
try:
|
||||
f = path.open("w", encoding="UTF-8")
|
||||
@@ -202,17 +202,32 @@ class Cache:
|
||||
with f:
|
||||
f.write(data)
|
||||
|
||||
def _ensure_supporting_files(self) -> None:
|
||||
"""Create supporting files in the cache dir that are not really part of the cache."""
|
||||
readme_path = self._cachedir / "README.md"
|
||||
readme_path.write_text(README_CONTENT, encoding="UTF-8")
|
||||
def _ensure_cache_dir_and_supporting_files(self) -> None:
|
||||
"""Create the cache dir and its supporting files."""
|
||||
if self._cachedir.is_dir():
|
||||
return
|
||||
|
||||
gitignore_path = self._cachedir.joinpath(".gitignore")
|
||||
msg = "# Created by pytest automatically.\n*\n"
|
||||
gitignore_path.write_text(msg, encoding="UTF-8")
|
||||
self._cachedir.parent.mkdir(parents=True, exist_ok=True)
|
||||
with tempfile.TemporaryDirectory(
|
||||
prefix="pytest-cache-files-",
|
||||
dir=self._cachedir.parent,
|
||||
) as newpath:
|
||||
path = Path(newpath)
|
||||
with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f:
|
||||
f.write(README_CONTENT)
|
||||
with open(path.joinpath(".gitignore"), "xt", encoding="UTF-8") as f:
|
||||
f.write("# Created by pytest automatically.\n*\n")
|
||||
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
|
||||
f.write(CACHEDIR_TAG_CONTENT)
|
||||
|
||||
cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG")
|
||||
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
|
||||
path.rename(self._cachedir)
|
||||
# Create a directory in place of the one we just moved so that `TemporaryDirectory`'s
|
||||
# cleanup doesn't complain.
|
||||
#
|
||||
# TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. See
|
||||
# https://github.com/python/cpython/issues/74168. Note that passing delete=False would
|
||||
# do the wrong thing in case of errors and isn't supported until python 3.12.
|
||||
path.mkdir()
|
||||
|
||||
|
||||
class LFPluginCollWrapper:
|
||||
@@ -432,7 +447,7 @@ class NFPlugin:
|
||||
return res
|
||||
|
||||
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)
|
||||
|
||||
def pytest_sessionfinish(self) -> None:
|
||||
config = self.config
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Per-test stdout/stderr capturing mechanism."""
|
||||
|
||||
import abc
|
||||
import collections
|
||||
import contextlib
|
||||
@@ -105,17 +106,16 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
return
|
||||
|
||||
# Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
|
||||
if not hasattr(stream, "buffer"): # type: ignore[unreachable]
|
||||
if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore]
|
||||
return
|
||||
|
||||
buffered = hasattr(stream.buffer, "raw")
|
||||
raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]
|
||||
raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer
|
||||
|
||||
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined]
|
||||
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore]
|
||||
return
|
||||
|
||||
def _reopen_stdio(f, mode):
|
||||
if not buffered and mode[0] == "w":
|
||||
if not hasattr(stream.buffer, "raw") and mode[0] == "w":
|
||||
buffering = 0
|
||||
else:
|
||||
buffering = -1
|
||||
@@ -482,12 +482,9 @@ class FDCaptureBase(CaptureBase[AnyStr]):
|
||||
self._state = "initialized"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format(
|
||||
self.__class__.__name__,
|
||||
self.targetfd,
|
||||
self.targetfd_save,
|
||||
self._state,
|
||||
self.tmpfile,
|
||||
return (
|
||||
f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} "
|
||||
f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
|
||||
)
|
||||
|
||||
def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
|
||||
@@ -621,12 +618,9 @@ class MultiCapture(Generic[AnyStr]):
|
||||
self.err: Optional[CaptureBase[AnyStr]] = err
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
|
||||
self.out,
|
||||
self.err,
|
||||
self.in_,
|
||||
self._state,
|
||||
self._in_suspended,
|
||||
return (
|
||||
f"<MultiCapture out={self.out!r} err={self.err!r} in_={self.in_!r} "
|
||||
f"_state={self._state!r} _in_suspended={self._in_suspended!r}>"
|
||||
)
|
||||
|
||||
def start_capturing(self) -> None:
|
||||
@@ -735,8 +729,9 @@ class CaptureManager:
|
||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
|
||||
self._method, self._global_capturing, self._capture_fixture
|
||||
return (
|
||||
f"<CaptureManager _method={self._method!r} _global_capturing={self._global_capturing!r} "
|
||||
f"_capture_fixture={self._capture_fixture!r}>"
|
||||
)
|
||||
|
||||
def is_capturing(self) -> Union[str, bool]:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Python version compatibility code."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
@@ -16,6 +17,22 @@ from typing import Callable
|
||||
from typing import Final
|
||||
from typing import NoReturn
|
||||
|
||||
import py
|
||||
|
||||
|
||||
#: constant to prepare valuing pylib path replacements/lazy proxies later on
|
||||
# intended for removal in pytest 8.0 or 9.0
|
||||
|
||||
# fmt: off
|
||||
# intentional space to create a fake difference for the verification
|
||||
LEGACY_PATH = py.path. local
|
||||
# fmt: on
|
||||
|
||||
|
||||
def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
|
||||
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
|
||||
return LEGACY_PATH(path)
|
||||
|
||||
|
||||
# fmt: off
|
||||
# Singleton type for NOTSET, as described in:
|
||||
@@ -86,7 +103,6 @@ def getfuncargnames(
|
||||
function: Callable[..., object],
|
||||
*,
|
||||
name: str = "",
|
||||
is_method: bool = False,
|
||||
cls: type | None = None,
|
||||
) -> tuple[str, ...]:
|
||||
"""Return the names of a function's mandatory arguments.
|
||||
@@ -97,9 +113,8 @@ def getfuncargnames(
|
||||
* Aren't bound with functools.partial.
|
||||
* Aren't replaced with mocks.
|
||||
|
||||
The is_method and cls arguments indicate that the function should
|
||||
be treated as a bound method even though it's not unless, only in
|
||||
the case of cls, the function is a static method.
|
||||
The cls arguments indicate that the function should be treated as a bound
|
||||
method even though it's not unless the function is a static method.
|
||||
|
||||
The name parameter should be the original name in which the function was collected.
|
||||
"""
|
||||
@@ -137,7 +152,7 @@ def getfuncargnames(
|
||||
# If this function should be treated as a bound method even though
|
||||
# it's passed as an unbound method or function, remove the first
|
||||
# parameter name.
|
||||
if is_method or (
|
||||
if (
|
||||
# Not using `getattr` because we don't want to resolve the staticmethod.
|
||||
# Not using `cls.__dict__` because we want to check the entire MRO.
|
||||
cls
|
||||
@@ -289,7 +304,7 @@ def get_user_id() -> int | None:
|
||||
# mypy follows the version and platform checking expectation of PEP 484:
|
||||
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
||||
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
||||
if sys.platform == "win32" or sys.platform == "emscripten": # noqa: PLR1714
|
||||
if sys.platform == "win32" or sys.platform == "emscripten":
|
||||
# win32 does not have a getuid() function.
|
||||
# Emscripten has a return 0 stub.
|
||||
return None
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Command line options, ini-file and conftest.py processing."""
|
||||
|
||||
import argparse
|
||||
import collections.abc
|
||||
import copy
|
||||
@@ -38,15 +39,18 @@ from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
import pluggy
|
||||
from pluggy import HookimplMarker
|
||||
from pluggy import HookimplOpts
|
||||
from pluggy import HookspecMarker
|
||||
from pluggy import HookspecOpts
|
||||
from pluggy import PluginManager
|
||||
|
||||
from .compat import PathAwareHookProxy
|
||||
from .exceptions import PrintHelp as PrintHelp
|
||||
from .exceptions import UsageError as UsageError
|
||||
from .findpaths import determine_setup
|
||||
from _pytest import __version__
|
||||
import _pytest._code
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import filter_traceback
|
||||
@@ -148,7 +152,9 @@ def main(
|
||||
|
||||
:returns: An exit code.
|
||||
"""
|
||||
old_pytest_version = os.environ.get("PYTEST_VERSION")
|
||||
try:
|
||||
os.environ["PYTEST_VERSION"] = __version__
|
||||
try:
|
||||
config = _prepareconfig(args, plugins)
|
||||
except ConftestImportFailure as e:
|
||||
@@ -183,6 +189,11 @@ def main(
|
||||
for msg in e.args:
|
||||
tw.line(f"ERROR: {msg}\n", red=True)
|
||||
return ExitCode.USAGE_ERROR
|
||||
finally:
|
||||
if old_pytest_version is None:
|
||||
os.environ.pop("PYTEST_VERSION", None)
|
||||
else:
|
||||
os.environ["PYTEST_VERSION"] = old_pytest_version
|
||||
|
||||
|
||||
def console_main() -> int:
|
||||
@@ -1068,7 +1079,7 @@ class Config:
|
||||
self._store = self.stash
|
||||
|
||||
self.trace = self.pluginmanager.trace.root.get("config")
|
||||
self.hook = self.pluginmanager.hook # type: ignore[assignment]
|
||||
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
|
||||
self._inicache: Dict[str, Any] = {}
|
||||
self._override_ini: Sequence[str] = ()
|
||||
self._opt2dest: Dict[str, str] = {}
|
||||
|
||||
@@ -415,6 +415,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
add_help=False,
|
||||
formatter_class=DropShorterLongHelpFormatter,
|
||||
allow_abbrev=False,
|
||||
fromfile_prefix_chars="@",
|
||||
)
|
||||
# extra_info is a dict of (param -> value) to display if there's
|
||||
# an usage error to provide more contextual information to the user.
|
||||
@@ -425,8 +426,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
msg = f"{self.prog}: error: {message}"
|
||||
|
||||
if hasattr(self._parser, "_config_source_hint"):
|
||||
# Type ignored because the attribute is set dynamically.
|
||||
msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore
|
||||
msg = f"{msg} ({self._parser._config_source_hint})"
|
||||
|
||||
raise UsageError(self.format_usage() + msg)
|
||||
|
||||
@@ -448,7 +448,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
|
||||
return parsed
|
||||
|
||||
if sys.version_info[:2] < (3, 9): # pragma: no cover
|
||||
if sys.version_info < (3, 9): # pragma: no cover
|
||||
# Backport of https://github.com/python/cpython/pull/14316 so we can
|
||||
# disable long --argument abbreviations without breaking short flags.
|
||||
def _parse_optional(
|
||||
|
||||
85
src/_pytest/config/compat.py
Normal file
85
src/_pytest/config/compat.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Mapping
|
||||
import warnings
|
||||
|
||||
import pluggy
|
||||
|
||||
from ..compat import LEGACY_PATH
|
||||
from ..compat import legacy_path
|
||||
from ..deprecated import HOOK_LEGACY_PATH_ARG
|
||||
|
||||
|
||||
# hookname: (Path, LEGACY_PATH)
|
||||
imply_paths_hooks: Mapping[str, tuple[str, str]] = {
|
||||
"pytest_ignore_collect": ("collection_path", "path"),
|
||||
"pytest_collect_file": ("file_path", "path"),
|
||||
"pytest_pycollect_makemodule": ("module_path", "path"),
|
||||
"pytest_report_header": ("start_path", "startdir"),
|
||||
"pytest_report_collectionfinish": ("start_path", "startdir"),
|
||||
}
|
||||
|
||||
|
||||
def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
|
||||
if Path(fspath) != path:
|
||||
raise ValueError(
|
||||
f"Path({fspath!r}) != {path!r}\n"
|
||||
"if both path and fspath are given they need to be equal"
|
||||
)
|
||||
|
||||
|
||||
class PathAwareHookProxy:
|
||||
"""
|
||||
this helper wraps around hook callers
|
||||
until pluggy supports fixingcalls, this one will do
|
||||
|
||||
it currently doesn't return full hook caller proxies for fixed hooks,
|
||||
this may have to be changed later depending on bugs
|
||||
"""
|
||||
|
||||
def __init__(self, hook_relay: pluggy.HookRelay) -> None:
|
||||
self._hook_relay = hook_relay
|
||||
|
||||
def __dir__(self) -> list[str]:
|
||||
return dir(self._hook_relay)
|
||||
|
||||
def __getattr__(self, key: str) -> pluggy.HookCaller:
|
||||
hook: pluggy.HookCaller = getattr(self._hook_relay, key)
|
||||
if key not in imply_paths_hooks:
|
||||
self.__dict__[key] = hook
|
||||
return hook
|
||||
else:
|
||||
path_var, fspath_var = imply_paths_hooks[key]
|
||||
|
||||
@functools.wraps(hook)
|
||||
def fixed_hook(**kw: Any) -> Any:
|
||||
path_value: Path | None = kw.pop(path_var, None)
|
||||
fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
|
||||
if fspath_value is not None:
|
||||
warnings.warn(
|
||||
HOOK_LEGACY_PATH_ARG.format(
|
||||
pylib_path_arg=fspath_var, pathlib_path_arg=path_var
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
if path_value is not None:
|
||||
if fspath_value is not None:
|
||||
_check_path(path_value, fspath_value)
|
||||
else:
|
||||
fspath_value = legacy_path(path_value)
|
||||
else:
|
||||
assert fspath_value is not None
|
||||
path_value = Path(fspath_value)
|
||||
|
||||
kw[path_var] = path_value
|
||||
kw[fspath_var] = fspath_value
|
||||
return hook(**kw)
|
||||
|
||||
fixed_hook.name = hook.name # type: ignore[attr-defined]
|
||||
fixed_hook.spec = hook.spec # type: ignore[attr-defined]
|
||||
fixed_hook.__name__ = key
|
||||
self.__dict__[key] = fixed_hook
|
||||
return fixed_hook # type: ignore[return-value]
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Interactive debugging with PDB, the Python Debugger."""
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import sys
|
||||
@@ -154,9 +155,7 @@ class pytestPDB:
|
||||
def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
|
||||
import _pytest.config
|
||||
|
||||
# Type ignored because mypy doesn't support "dynamic"
|
||||
# inheritance like this.
|
||||
class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc]
|
||||
class PytestPdbWrapper(pdb_cls):
|
||||
_pytest_capman = capman
|
||||
_continued = False
|
||||
|
||||
|
||||
@@ -36,6 +36,21 @@ YIELD_FIXTURE = PytestDeprecationWarning(
|
||||
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
|
||||
|
||||
|
||||
HOOK_LEGACY_PATH_ARG = UnformattedWarning(
|
||||
PytestRemovedIn9Warning,
|
||||
"The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n"
|
||||
"see https://docs.pytest.org/en/latest/deprecations.html"
|
||||
"#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
|
||||
)
|
||||
|
||||
NODE_CTOR_FSPATH_ARG = UnformattedWarning(
|
||||
PytestRemovedIn9Warning,
|
||||
"The (fspath: py.path.local) argument to {node_type_name} is deprecated. "
|
||||
"Please use the (path: pathlib.Path) argument instead.\n"
|
||||
"See https://docs.pytest.org/en/latest/deprecations.html"
|
||||
"#fspath-argument-for-node-constructors-replaced-with-pathlib-path",
|
||||
)
|
||||
|
||||
HOOK_LEGACY_MARKING = UnformattedWarning(
|
||||
PytestDeprecationWarning,
|
||||
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Discover and run doctests in modules and test files."""
|
||||
|
||||
import bdb
|
||||
from contextlib import contextmanager
|
||||
import functools
|
||||
|
||||
@@ -35,6 +35,7 @@ import warnings
|
||||
import _pytest
|
||||
from _pytest import nodes
|
||||
from _pytest._code import getfslineno
|
||||
from _pytest._code import Source
|
||||
from _pytest._code.code import FormattedExcinfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._io import TerminalWriter
|
||||
@@ -51,6 +52,7 @@ from _pytest.compat import NotSetType
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.config import _PluggyPlugin
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import MARKED_FIXTURE
|
||||
@@ -68,7 +70,7 @@ from _pytest.scope import HIGH_SCOPES
|
||||
from _pytest.scope import Scope
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
|
||||
@@ -388,6 +390,14 @@ class FixtureRequest(abc.ABC):
|
||||
"""Scope string, one of "function", "class", "module", "package", "session"."""
|
||||
return self._scope.value
|
||||
|
||||
@abc.abstractmethod
|
||||
def _check_scope(
|
||||
self,
|
||||
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
|
||||
requested_scope: Scope,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def fixturenames(self) -> List[str]:
|
||||
"""Names of all active fixtures in this request."""
|
||||
@@ -401,45 +411,10 @@ class FixtureRequest(abc.ABC):
|
||||
"""Underlying collection node (depends on current request scope)."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||
if fixturedefs is None:
|
||||
# We arrive here because of a dynamic call to
|
||||
# getfixturevalue(argname) usage which was naturally
|
||||
# not known at parsing/collection time.
|
||||
fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
|
||||
if fixturedefs is not None:
|
||||
self._arg2fixturedefs[argname] = fixturedefs
|
||||
# No fixtures defined with this name.
|
||||
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)
|
||||
|
||||
# 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 (possibly indirectly);
|
||||
# in this case it gets the value of the fixture it overrides, one level
|
||||
# up.
|
||||
# Check how many `argname`s deep we are, and take the next one.
|
||||
# `fixturedefs` is sorted from furthest to closest, so use negative
|
||||
# indexing to go in reverse.
|
||||
index = -1
|
||||
for request in self._iter_chain():
|
||||
if request.fixturename == argname:
|
||||
index -= 1
|
||||
# If already consumed all of the available levels, fail.
|
||||
if -index > len(fixturedefs):
|
||||
raise FixtureLookupError(argname, self)
|
||||
|
||||
return fixturedefs[index]
|
||||
|
||||
@property
|
||||
def config(self) -> Config:
|
||||
"""The pytest config object associated with this request."""
|
||||
return self._pyfuncitem.config # type: ignore[no-any-return]
|
||||
return self._pyfuncitem.config
|
||||
|
||||
@property
|
||||
def function(self):
|
||||
@@ -462,12 +437,9 @@ class FixtureRequest(abc.ABC):
|
||||
@property
|
||||
def instance(self):
|
||||
"""Instance (can be None) on which test function was collected."""
|
||||
# unittest support hack, see _pytest.unittest.TestCaseFunction.
|
||||
try:
|
||||
return self._pyfuncitem._testcase # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
function = getattr(self, "function", None)
|
||||
return getattr(function, "__self__", None)
|
||||
if self.scope != "function":
|
||||
return None
|
||||
return getattr(self._pyfuncitem, "instance", None)
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
@@ -494,7 +466,7 @@ class FixtureRequest(abc.ABC):
|
||||
@property
|
||||
def session(self) -> "Session":
|
||||
"""Pytest session object."""
|
||||
return self._pyfuncitem.session # type: ignore[no-any-return]
|
||||
return self._pyfuncitem.session
|
||||
|
||||
@abc.abstractmethod
|
||||
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||
@@ -538,6 +510,11 @@ class FixtureRequest(abc.ABC):
|
||||
:raises pytest.FixtureLookupError:
|
||||
If the given fixture could not be found.
|
||||
"""
|
||||
# Note that in addition to the use case described in the docstring,
|
||||
# getfixturevalue() is also called by pytest itself during item and fixture
|
||||
# setup to evaluate the fixtures that are requested statically
|
||||
# (using function parameters, autouse, etc).
|
||||
|
||||
fixturedef = self._get_active_fixturedef(argname)
|
||||
assert fixturedef.cached_result is not None, (
|
||||
f'The fixture value for "{argname}" is not available. '
|
||||
@@ -558,38 +535,53 @@ class FixtureRequest(abc.ABC):
|
||||
def _get_active_fixturedef(
|
||||
self, argname: str
|
||||
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
|
||||
if argname == "request":
|
||||
cached_result = (self, [0], None)
|
||||
return PseudoFixtureDef(cached_result, Scope.Function)
|
||||
|
||||
# If we already finished computing a fixture by this name in this item,
|
||||
# return it.
|
||||
fixturedef = self._fixture_defs.get(argname)
|
||||
if fixturedef is None:
|
||||
try:
|
||||
fixturedef = self._getnextfixturedef(argname)
|
||||
except FixtureLookupError:
|
||||
if argname == "request":
|
||||
cached_result = (self, [0], None)
|
||||
return PseudoFixtureDef(cached_result, Scope.Function)
|
||||
raise
|
||||
self._compute_fixture_value(fixturedef)
|
||||
self._fixture_defs[argname] = fixturedef
|
||||
return fixturedef
|
||||
if fixturedef is not None:
|
||||
self._check_scope(fixturedef, fixturedef._scope)
|
||||
return fixturedef
|
||||
|
||||
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
|
||||
values = [request._fixturedef for request in self._iter_chain()]
|
||||
values.reverse()
|
||||
return values
|
||||
# Find the appropriate fixturedef.
|
||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||
if fixturedefs is None:
|
||||
# We arrive here because of a dynamic call to
|
||||
# getfixturevalue(argname) which was naturally
|
||||
# not known at parsing/collection time.
|
||||
fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
|
||||
if fixturedefs is not None:
|
||||
self._arg2fixturedefs[argname] = fixturedefs
|
||||
# No fixtures defined with this name.
|
||||
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)
|
||||
# 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 (possibly indirectly);
|
||||
# in this case it gets the value of the fixture it overrides, one level
|
||||
# up.
|
||||
# Check how many `argname`s deep we are, and take the next one.
|
||||
# `fixturedefs` is sorted from furthest to closest, so use negative
|
||||
# indexing to go in reverse.
|
||||
index = -1
|
||||
for request in self._iter_chain():
|
||||
if request.fixturename == argname:
|
||||
index -= 1
|
||||
# If already consumed all of the available levels, fail.
|
||||
if -index > len(fixturedefs):
|
||||
raise FixtureLookupError(argname, self)
|
||||
fixturedef = fixturedefs[index]
|
||||
|
||||
def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
|
||||
"""Create a SubRequest based on "self" and call the execute method
|
||||
of the given FixtureDef object.
|
||||
|
||||
This will force the FixtureDef object to throw away any previous
|
||||
results and compute a new fixture value, which will be stored into
|
||||
the FixtureDef object itself.
|
||||
"""
|
||||
# prepare a subrequest object before calling fixture function
|
||||
# (latter managed by fixturedef)
|
||||
argname = fixturedef.argname
|
||||
funcitem = self._pyfuncitem
|
||||
# Prepare a SubRequest object for calling the fixture.
|
||||
try:
|
||||
callspec = funcitem.callspec
|
||||
callspec = self._pyfuncitem.callspec
|
||||
except AttributeError:
|
||||
callspec = None
|
||||
if callspec is not None and argname in callspec.params:
|
||||
@@ -601,59 +593,55 @@ class FixtureRequest(abc.ABC):
|
||||
param = NOTSET
|
||||
param_index = 0
|
||||
scope = fixturedef._scope
|
||||
|
||||
has_params = fixturedef.params is not None
|
||||
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
||||
if has_params and fixtures_not_supported:
|
||||
msg = (
|
||||
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
|
||||
f"Node id: {funcitem.nodeid}\n"
|
||||
f"Function type: {type(funcitem).__name__}"
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
if has_params:
|
||||
frame = inspect.stack()[3]
|
||||
frameinfo = inspect.getframeinfo(frame[0])
|
||||
source_path = absolutepath(frameinfo.filename)
|
||||
source_lineno = frameinfo.lineno
|
||||
try:
|
||||
source_path_str = str(
|
||||
source_path.relative_to(funcitem.config.rootpath)
|
||||
)
|
||||
except ValueError:
|
||||
source_path_str = str(source_path)
|
||||
msg = (
|
||||
"The requested fixture has no parameter defined for test:\n"
|
||||
" {}\n\n"
|
||||
"Requested fixture '{}' defined in:\n{}"
|
||||
"\n\nRequested here:\n{}:{}".format(
|
||||
funcitem.nodeid,
|
||||
fixturedef.argname,
|
||||
getlocation(fixturedef.func, funcitem.config.rootpath),
|
||||
source_path_str,
|
||||
source_lineno,
|
||||
)
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
self._check_fixturedef_without_param(fixturedef)
|
||||
self._check_scope(fixturedef, scope)
|
||||
subrequest = SubRequest(
|
||||
self, scope, param, param_index, fixturedef, _ispytest=True
|
||||
)
|
||||
|
||||
# Check if a higher-level scoped fixture accesses a lower level one.
|
||||
subrequest._check_scope(argname, self._scope, scope)
|
||||
try:
|
||||
# Call the fixture function.
|
||||
fixturedef.execute(request=subrequest)
|
||||
finally:
|
||||
self._schedule_finalizers(fixturedef, subrequest)
|
||||
# Make sure the fixture value is cached, running it if it isn't
|
||||
fixturedef.execute(request=subrequest)
|
||||
|
||||
def _schedule_finalizers(
|
||||
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
|
||||
) -> None:
|
||||
# If fixture function failed it might have registered finalizers.
|
||||
finalizer = functools.partial(fixturedef.finish, request=subrequest)
|
||||
subrequest.node.addfinalizer(finalizer)
|
||||
self._fixture_defs[argname] = fixturedef
|
||||
return fixturedef
|
||||
|
||||
def _check_fixturedef_without_param(self, fixturedef: "FixtureDef[object]") -> None:
|
||||
"""Check that this request is allowed to execute this fixturedef without
|
||||
a param."""
|
||||
funcitem = self._pyfuncitem
|
||||
has_params = fixturedef.params is not None
|
||||
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
||||
if has_params and fixtures_not_supported:
|
||||
msg = (
|
||||
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
|
||||
f"Node id: {funcitem.nodeid}\n"
|
||||
f"Function type: {type(funcitem).__name__}"
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
if has_params:
|
||||
frame = inspect.stack()[3]
|
||||
frameinfo = inspect.getframeinfo(frame[0])
|
||||
source_path = absolutepath(frameinfo.filename)
|
||||
source_lineno = frameinfo.lineno
|
||||
try:
|
||||
source_path_str = str(source_path.relative_to(funcitem.config.rootpath))
|
||||
except ValueError:
|
||||
source_path_str = str(source_path)
|
||||
location = getlocation(fixturedef.func, funcitem.config.rootpath)
|
||||
msg = (
|
||||
"The requested fixture has no parameter defined for test:\n"
|
||||
f" {funcitem.nodeid}\n\n"
|
||||
f"Requested fixture '{fixturedef.argname}' defined in:\n"
|
||||
f"{location}\n\n"
|
||||
f"Requested here:\n"
|
||||
f"{source_path_str}:{source_lineno}"
|
||||
)
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
|
||||
values = [request._fixturedef for request in self._iter_chain()]
|
||||
values.reverse()
|
||||
return values
|
||||
|
||||
|
||||
@final
|
||||
@@ -673,6 +661,14 @@ class TopRequest(FixtureRequest):
|
||||
def _scope(self) -> Scope:
|
||||
return Scope.Function
|
||||
|
||||
def _check_scope(
|
||||
self,
|
||||
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
|
||||
requested_scope: Scope,
|
||||
) -> None:
|
||||
# TopRequest always has function scope so always valid.
|
||||
pass
|
||||
|
||||
@property
|
||||
def node(self):
|
||||
return self._pyfuncitem
|
||||
@@ -744,56 +740,38 @@ class SubRequest(FixtureRequest):
|
||||
|
||||
def _check_scope(
|
||||
self,
|
||||
argname: str,
|
||||
invoking_scope: Scope,
|
||||
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
|
||||
requested_scope: Scope,
|
||||
) -> None:
|
||||
if argname == "request":
|
||||
if isinstance(requested_fixturedef, PseudoFixtureDef):
|
||||
return
|
||||
if invoking_scope > requested_scope:
|
||||
if self._scope > requested_scope:
|
||||
# Try to report something helpful.
|
||||
text = "\n".join(self._factorytraceback())
|
||||
argname = requested_fixturedef.argname
|
||||
fixture_stack = "\n".join(
|
||||
self._format_fixturedef_line(fixturedef)
|
||||
for fixturedef in self._get_fixturestack()
|
||||
)
|
||||
requested_fixture = self._format_fixturedef_line(requested_fixturedef)
|
||||
fail(
|
||||
f"ScopeMismatch: You tried to access the {requested_scope.value} scoped "
|
||||
f"fixture {argname} with a {invoking_scope.value} scoped request object, "
|
||||
f"involved factories:\n{text}",
|
||||
f"fixture {argname} with a {self._scope.value} scoped request object. "
|
||||
f"Requesting fixture stack:\n{fixture_stack}\n"
|
||||
f"Requested fixture:\n{requested_fixture}",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
def _factorytraceback(self) -> List[str]:
|
||||
lines = []
|
||||
for fixturedef in self._get_fixturestack():
|
||||
factory = fixturedef.func
|
||||
fs, lineno = getfslineno(factory)
|
||||
if isinstance(fs, Path):
|
||||
session: Session = self._pyfuncitem.session
|
||||
p = bestrelpath(session.path, fs)
|
||||
else:
|
||||
p = fs
|
||||
lines.append(
|
||||
"%s:%d: def %s%s"
|
||||
% (p, lineno + 1, factory.__name__, inspect.signature(factory))
|
||||
)
|
||||
return lines
|
||||
def _format_fixturedef_line(self, fixturedef: "FixtureDef[object]") -> str:
|
||||
factory = fixturedef.func
|
||||
path, lineno = getfslineno(factory)
|
||||
if isinstance(path, Path):
|
||||
path = bestrelpath(self._pyfuncitem.session.path, path)
|
||||
signature = inspect.signature(factory)
|
||||
return f"{path}:{lineno + 1}: def {factory.__name__}{signature}"
|
||||
|
||||
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||
self._fixturedef.addfinalizer(finalizer)
|
||||
|
||||
def _schedule_finalizers(
|
||||
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
|
||||
) -> None:
|
||||
# If the executing fixturedef was not explicitly requested in the argument list (via
|
||||
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
|
||||
# first.
|
||||
if (
|
||||
fixturedef.argname not in self._fixture_defs
|
||||
and fixturedef.argname not in self._pyfuncitem.fixturenames
|
||||
):
|
||||
fixturedef.addfinalizer(
|
||||
functools.partial(self._fixturedef.finish, request=self)
|
||||
)
|
||||
super()._schedule_finalizers(fixturedef, subrequest)
|
||||
|
||||
|
||||
@final
|
||||
class FixtureLookupError(LookupError):
|
||||
@@ -887,13 +865,6 @@ class FixtureLookupErrorRepr(TerminalRepr):
|
||||
tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1))
|
||||
|
||||
|
||||
def fail_fixturefunc(fixturefunc, msg: str) -> NoReturn:
|
||||
fs, lineno = getfslineno(fixturefunc)
|
||||
location = f"{fs}:{lineno + 1}"
|
||||
source = _pytest._code.Source(fixturefunc)
|
||||
fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False)
|
||||
|
||||
|
||||
def call_fixture_func(
|
||||
fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs
|
||||
) -> FixtureValue:
|
||||
@@ -923,7 +894,13 @@ def _teardown_yield_fixture(fixturefunc, it) -> None:
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
fail_fixturefunc(fixturefunc, "fixture function has more than one 'yield'")
|
||||
fs, lineno = getfslineno(fixturefunc)
|
||||
fail(
|
||||
f"fixture function has more than one 'yield':\n\n"
|
||||
f"{Source(fixturefunc).indent()}\n"
|
||||
f"{fs}:{lineno + 1}",
|
||||
pytrace=False,
|
||||
)
|
||||
|
||||
|
||||
def _eval_scope_callable(
|
||||
@@ -965,7 +942,6 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
func: "_FixtureFunc[FixtureValue]",
|
||||
scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None],
|
||||
params: Optional[Sequence[object]],
|
||||
unittest: bool = False,
|
||||
ids: Optional[
|
||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||
] = None,
|
||||
@@ -1011,9 +987,7 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
# a parameter value.
|
||||
self.ids: Final = ids
|
||||
# The names requested by the fixtures.
|
||||
self.argnames: Final = getfuncargnames(func, name=argname, is_method=unittest)
|
||||
# Whether the fixture was collected from a unittest TestCase class.
|
||||
self.unittest: Final = unittest
|
||||
self.argnames: Final = getfuncargnames(func, name=argname)
|
||||
# If the fixture was executed, the current value of the fixture.
|
||||
# Can change if the fixture is executed with different parameters.
|
||||
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
|
||||
@@ -1049,15 +1023,25 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
raise BaseExceptionGroup(msg, exceptions[::-1])
|
||||
|
||||
def execute(self, request: SubRequest) -> FixtureValue:
|
||||
# Get required arguments and register our own finish()
|
||||
# with their finalization.
|
||||
"""Return the value of this fixture, executing it if not cached."""
|
||||
# Ensure that the dependent fixtures requested by this fixture are loaded.
|
||||
# This needs to be done before checking if we have a cached value, since
|
||||
# if a dependent fixture has their cache invalidated, e.g. due to
|
||||
# parametrization, they finalize themselves and fixtures depending on it
|
||||
# (which will likely include this fixture) setting `self.cached_result = None`.
|
||||
# See #4871
|
||||
requested_fixtures_that_should_finalize_us = []
|
||||
for argname in self.argnames:
|
||||
fixturedef = request._get_active_fixturedef(argname)
|
||||
if argname != "request":
|
||||
# PseudoFixtureDef is only for "request".
|
||||
assert isinstance(fixturedef, FixtureDef)
|
||||
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
|
||||
# Saves requested fixtures in a list so we later can add our finalizer
|
||||
# to them, ensuring that if a requested fixture gets torn down we get torn
|
||||
# down first. This is generally handled by SetupState, but still currently
|
||||
# needed when this fixture is not parametrized but depends on a parametrized
|
||||
# fixture.
|
||||
if not isinstance(fixturedef, PseudoFixtureDef):
|
||||
requested_fixtures_that_should_finalize_us.append(fixturedef)
|
||||
|
||||
# Check for (and return) cached value/exception.
|
||||
my_cache_key = self.cache_key(request)
|
||||
if self.cached_result is not None:
|
||||
cache_key = self.cached_result[1]
|
||||
@@ -1075,12 +1059,26 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
self.finish(request)
|
||||
assert self.cached_result is None
|
||||
|
||||
# Add finalizer to requested fixtures we saved previously.
|
||||
# We make sure to do this after checking for cached value to avoid
|
||||
# adding our finalizer multiple times. (#12135)
|
||||
finalizer = functools.partial(self.finish, request=request)
|
||||
for parent_fixture in requested_fixtures_that_should_finalize_us:
|
||||
parent_fixture.addfinalizer(finalizer)
|
||||
|
||||
ihook = request.node.ihook
|
||||
result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
try:
|
||||
# Setup the fixture, run the code in it, and cache the value
|
||||
# in self.cached_result
|
||||
result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
finally:
|
||||
# schedule our finalizer, even if the setup failed
|
||||
request.node.addfinalizer(finalizer)
|
||||
|
||||
return result
|
||||
|
||||
def cache_key(self, request: SubRequest) -> object:
|
||||
return request.param_index if not hasattr(request, "param") else request.param
|
||||
return getattr(request, "param", None)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"
|
||||
@@ -1090,27 +1088,23 @@ def resolve_fixture_function(
|
||||
fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
|
||||
) -> "_FixtureFunc[FixtureValue]":
|
||||
"""Get the actual callable that can be called to obtain the fixture
|
||||
value, dealing with unittest-specific instances and bound methods."""
|
||||
value."""
|
||||
fixturefunc = fixturedef.func
|
||||
if fixturedef.unittest:
|
||||
if request.instance is not None:
|
||||
# Bind the unbound method to the TestCase instance.
|
||||
fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr]
|
||||
else:
|
||||
# The fixture function needs to be bound to the actual
|
||||
# request.instance so that code working with "fixturedef" behaves
|
||||
# as expected.
|
||||
if request.instance is not None:
|
||||
# Handle the case where fixture is defined not in a test class, but some other class
|
||||
# (for example a plugin class with a fixture), see #2270.
|
||||
if hasattr(fixturefunc, "__self__") and not isinstance(
|
||||
request.instance,
|
||||
fixturefunc.__self__.__class__, # type: ignore[union-attr]
|
||||
):
|
||||
return fixturefunc
|
||||
fixturefunc = getimfunc(fixturedef.func)
|
||||
if fixturefunc != fixturedef.func:
|
||||
fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr]
|
||||
# The fixture function needs to be bound to the actual
|
||||
# request.instance so that code working with "fixturedef" behaves
|
||||
# as expected.
|
||||
instance = request.instance
|
||||
if instance is not None:
|
||||
# Handle the case where fixture is defined not in a test class, but some other class
|
||||
# (for example a plugin class with a fixture), see #2270.
|
||||
if hasattr(fixturefunc, "__self__") and not isinstance(
|
||||
instance,
|
||||
fixturefunc.__self__.__class__,
|
||||
):
|
||||
return fixturefunc
|
||||
fixturefunc = getimfunc(fixturedef.func)
|
||||
if fixturefunc != fixturedef.func:
|
||||
fixturefunc = fixturefunc.__get__(instance)
|
||||
return fixturefunc
|
||||
|
||||
|
||||
@@ -1120,11 +1114,7 @@ def pytest_fixture_setup(
|
||||
"""Execution of fixture setup."""
|
||||
kwargs = {}
|
||||
for argname in fixturedef.argnames:
|
||||
fixdef = request._get_active_fixturedef(argname)
|
||||
assert fixdef.cached_result is not None
|
||||
result, arg_cache_key, exc = fixdef.cached_result
|
||||
request._check_scope(argname, request._scope, fixdef._scope)
|
||||
kwargs[argname] = result
|
||||
kwargs[argname] = request.getfixturevalue(argname)
|
||||
|
||||
fixturefunc = resolve_fixture_function(fixturedef, request)
|
||||
my_cache_key = fixturedef.cache_key(request)
|
||||
@@ -1148,12 +1138,13 @@ def wrap_function_to_error_out_if_called_directly(
|
||||
) -> FixtureFunction:
|
||||
"""Wrap the given fixture function so we can raise an error about it being called directly,
|
||||
instead of used as an argument in a test function."""
|
||||
name = fixture_marker.name or function.__name__
|
||||
message = (
|
||||
'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
|
||||
f'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
|
||||
"but are created automatically when test functions request them as parameters.\n"
|
||||
"See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n"
|
||||
"https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code."
|
||||
).format(name=fixture_marker.name or function.__name__)
|
||||
)
|
||||
|
||||
@functools.wraps(function)
|
||||
def result(*args, **kwargs):
|
||||
@@ -1220,8 +1211,7 @@ def fixture(
|
||||
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
] = ...,
|
||||
name: Optional[str] = ...,
|
||||
) -> FixtureFunction:
|
||||
...
|
||||
) -> FixtureFunction: ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -1235,8 +1225,7 @@ def fixture(
|
||||
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
||||
] = ...,
|
||||
name: Optional[str] = None,
|
||||
) -> FixtureFunctionMarker:
|
||||
...
|
||||
) -> FixtureFunctionMarker: ...
|
||||
|
||||
|
||||
def fixture(
|
||||
@@ -1364,6 +1353,33 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
default=[],
|
||||
help="List of default fixtures to be used with this project",
|
||||
)
|
||||
group = parser.getgroup("general")
|
||||
group.addoption(
|
||||
"--fixtures",
|
||||
"--funcargs",
|
||||
action="store_true",
|
||||
dest="showfixtures",
|
||||
default=False,
|
||||
help="Show available fixtures, sorted by plugin appearance "
|
||||
"(fixtures with leading '_' are only shown with '-v')",
|
||||
)
|
||||
group.addoption(
|
||||
"--fixtures-per-test",
|
||||
action="store_true",
|
||||
dest="show_fixtures_per_test",
|
||||
default=False,
|
||||
help="Show fixtures per test",
|
||||
)
|
||||
|
||||
|
||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
if config.option.showfixtures:
|
||||
showfixtures(config)
|
||||
return 0
|
||||
if config.option.show_fixtures_per_test:
|
||||
show_fixtures_per_test(config)
|
||||
return 0
|
||||
return None
|
||||
|
||||
|
||||
def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
|
||||
@@ -1614,7 +1630,6 @@ class FixtureManager:
|
||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||
] = None,
|
||||
autouse: bool = False,
|
||||
unittest: bool = False,
|
||||
) -> None:
|
||||
"""Register a fixture
|
||||
|
||||
@@ -1635,8 +1650,6 @@ class FixtureManager:
|
||||
The fixture's IDs.
|
||||
:param autouse:
|
||||
Whether this is an autouse fixture.
|
||||
:param unittest:
|
||||
Set this if this is a unittest fixture.
|
||||
"""
|
||||
fixture_def = FixtureDef(
|
||||
config=self.config,
|
||||
@@ -1645,7 +1658,6 @@ class FixtureManager:
|
||||
func=func,
|
||||
scope=scope,
|
||||
params=params,
|
||||
unittest=unittest,
|
||||
ids=ids,
|
||||
_ispytest=True,
|
||||
)
|
||||
@@ -1667,8 +1679,6 @@ class FixtureManager:
|
||||
def parsefactories(
|
||||
self,
|
||||
node_or_obj: nodes.Node,
|
||||
*,
|
||||
unittest: bool = ...,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -1677,8 +1687,6 @@ class FixtureManager:
|
||||
self,
|
||||
node_or_obj: object,
|
||||
nodeid: Optional[str],
|
||||
*,
|
||||
unittest: bool = ...,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@@ -1686,8 +1694,6 @@ class FixtureManager:
|
||||
self,
|
||||
node_or_obj: Union[nodes.Node, object],
|
||||
nodeid: Union[str, NotSetType, None] = NOTSET,
|
||||
*,
|
||||
unittest: bool = False,
|
||||
) -> None:
|
||||
"""Collect fixtures from a collection node or object.
|
||||
|
||||
@@ -1739,7 +1745,6 @@ class FixtureManager:
|
||||
func=func,
|
||||
scope=marker.scope,
|
||||
params=marker.params,
|
||||
unittest=unittest,
|
||||
ids=marker.ids,
|
||||
autouse=marker.autouse,
|
||||
)
|
||||
@@ -1771,3 +1776,137 @@ class FixtureManager:
|
||||
for fixturedef in fixturedefs:
|
||||
if fixturedef.baseid in parentnodeids:
|
||||
yield fixturedef
|
||||
|
||||
|
||||
def show_fixtures_per_test(config: Config) -> Union[int, ExitCode]:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, _show_fixtures_per_test)
|
||||
|
||||
|
||||
_PYTEST_DIR = Path(_pytest.__file__).parent
|
||||
|
||||
|
||||
def _pretty_fixture_path(invocation_dir: Path, func) -> str:
|
||||
loc = Path(getlocation(func, invocation_dir))
|
||||
prefix = Path("...", "_pytest")
|
||||
try:
|
||||
return str(prefix / loc.relative_to(_PYTEST_DIR))
|
||||
except ValueError:
|
||||
return bestrelpath(invocation_dir, loc)
|
||||
|
||||
|
||||
def _show_fixtures_per_test(config: Config, session: "Session") -> None:
|
||||
import _pytest.config
|
||||
|
||||
session.perform_collect()
|
||||
invocation_dir = config.invocation_params.dir
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
verbose = config.getvalue("verbose")
|
||||
|
||||
def get_best_relpath(func) -> str:
|
||||
loc = getlocation(func, invocation_dir)
|
||||
return bestrelpath(invocation_dir, Path(loc))
|
||||
|
||||
def write_fixture(fixture_def: FixtureDef[object]) -> None:
|
||||
argname = fixture_def.argname
|
||||
if verbose <= 0 and argname.startswith("_"):
|
||||
return
|
||||
prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
|
||||
tw.write(f"{argname}", green=True)
|
||||
tw.write(f" -- {prettypath}", yellow=True)
|
||||
tw.write("\n")
|
||||
fixture_doc = inspect.getdoc(fixture_def.func)
|
||||
if fixture_doc:
|
||||
write_docstring(
|
||||
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
|
||||
)
|
||||
else:
|
||||
tw.line(" no docstring available", red=True)
|
||||
|
||||
def write_item(item: nodes.Item) -> None:
|
||||
# Not all items have _fixtureinfo attribute.
|
||||
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
|
||||
if info is None or not info.name2fixturedefs:
|
||||
# This test item does not use any fixtures.
|
||||
return
|
||||
tw.line()
|
||||
tw.sep("-", f"fixtures used by {item.name}")
|
||||
# TODO: Fix this type ignore.
|
||||
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
|
||||
# dict key not used in loop but needed for sorting.
|
||||
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
|
||||
assert fixturedefs is not None
|
||||
if not fixturedefs:
|
||||
continue
|
||||
# Last item is expected to be the one used by the test item.
|
||||
write_fixture(fixturedefs[-1])
|
||||
|
||||
for session_item in session.items:
|
||||
write_item(session_item)
|
||||
|
||||
|
||||
def showfixtures(config: Config) -> Union[int, ExitCode]:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, _showfixtures_main)
|
||||
|
||||
|
||||
def _showfixtures_main(config: Config, session: "Session") -> None:
|
||||
import _pytest.config
|
||||
|
||||
session.perform_collect()
|
||||
invocation_dir = config.invocation_params.dir
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
verbose = config.getvalue("verbose")
|
||||
|
||||
fm = session._fixturemanager
|
||||
|
||||
available = []
|
||||
seen: Set[Tuple[str, str]] = set()
|
||||
|
||||
for argname, fixturedefs in fm._arg2fixturedefs.items():
|
||||
assert fixturedefs is not None
|
||||
if not fixturedefs:
|
||||
continue
|
||||
for fixturedef in fixturedefs:
|
||||
loc = getlocation(fixturedef.func, invocation_dir)
|
||||
if (fixturedef.argname, loc) in seen:
|
||||
continue
|
||||
seen.add((fixturedef.argname, loc))
|
||||
available.append(
|
||||
(
|
||||
len(fixturedef.baseid),
|
||||
fixturedef.func.__module__,
|
||||
_pretty_fixture_path(invocation_dir, fixturedef.func),
|
||||
fixturedef.argname,
|
||||
fixturedef,
|
||||
)
|
||||
)
|
||||
|
||||
available.sort()
|
||||
currentmodule = None
|
||||
for baseid, module, prettypath, argname, fixturedef in available:
|
||||
if currentmodule != module:
|
||||
if not module.startswith("_pytest."):
|
||||
tw.line()
|
||||
tw.sep("-", f"fixtures defined from {module}")
|
||||
currentmodule = module
|
||||
if verbose <= 0 and argname.startswith("_"):
|
||||
continue
|
||||
tw.write(f"{argname}", green=True)
|
||||
if fixturedef.scope != "function":
|
||||
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
|
||||
tw.write(f" -- {prettypath}", yellow=True)
|
||||
tw.write("\n")
|
||||
doc = inspect.getdoc(fixturedef.func)
|
||||
if doc:
|
||||
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
|
||||
else:
|
||||
tw.line(" no docstring available", red=True)
|
||||
tw.line()
|
||||
|
||||
|
||||
def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
|
||||
for line in doc.split("\n"):
|
||||
tw.line(indent + line)
|
||||
|
||||
@@ -35,7 +35,7 @@ def _iter_all_modules(
|
||||
else:
|
||||
# Type ignored because typeshed doesn't define ModuleType.__path__
|
||||
# (only defined on packages).
|
||||
package_path = package.__path__ # type: ignore[attr-defined]
|
||||
package_path = package.__path__
|
||||
path, prefix = package_path[0], package.__name__ + "."
|
||||
for _, name, is_package in pkgutil.iter_modules([path]):
|
||||
if is_package:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Version info, help messages, tracing configuration."""
|
||||
|
||||
from argparse import Action
|
||||
import os
|
||||
import sys
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Hook specifications for pytest plugins which are invoked by pytest itself
|
||||
and by builtin plugins."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
@@ -14,6 +15,8 @@ from typing import Union
|
||||
|
||||
from pluggy import HookspecMarker
|
||||
|
||||
from .deprecated import HOOK_LEGACY_PATH_ARG
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pdb
|
||||
@@ -22,6 +25,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import ExceptionRepr
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.config import _PluggyPlugin
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
@@ -295,8 +299,17 @@ def pytest_collection_finish(session: "Session") -> None:
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[bool]:
|
||||
@hookspec(
|
||||
firstresult=True,
|
||||
warn_on_impl_args={
|
||||
"path": HOOK_LEGACY_PATH_ARG.format(
|
||||
pylib_path_arg="path", pathlib_path_arg="collection_path"
|
||||
),
|
||||
},
|
||||
)
|
||||
def pytest_ignore_collect(
|
||||
collection_path: Path, path: "LEGACY_PATH", config: "Config"
|
||||
) -> Optional[bool]:
|
||||
"""Return True to prevent considering this path for collection.
|
||||
|
||||
This hook is consulted for all files and directories prior to calling
|
||||
@@ -310,10 +323,8 @@ def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[b
|
||||
|
||||
.. versionchanged:: 7.0.0
|
||||
The ``collection_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``path`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``path`` parameter has been removed.
|
||||
equivalent of the ``path`` parameter. The ``path`` parameter
|
||||
has been deprecated.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
@@ -354,7 +365,16 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
|
||||
"""
|
||||
|
||||
|
||||
def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Collector]":
|
||||
@hookspec(
|
||||
warn_on_impl_args={
|
||||
"path": HOOK_LEGACY_PATH_ARG.format(
|
||||
pylib_path_arg="path", pathlib_path_arg="file_path"
|
||||
),
|
||||
},
|
||||
)
|
||||
def pytest_collect_file(
|
||||
file_path: Path, path: "LEGACY_PATH", parent: "Collector"
|
||||
) -> "Optional[Collector]":
|
||||
"""Create a :class:`~pytest.Collector` for the given path, or None if not relevant.
|
||||
|
||||
For best results, the returned collector should be a subclass of
|
||||
@@ -367,10 +387,8 @@ def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Colle
|
||||
|
||||
.. versionchanged:: 7.0.0
|
||||
The ``file_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``path`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``path`` parameter was removed.
|
||||
equivalent of the ``path`` parameter. The ``path`` parameter
|
||||
has been deprecated.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
@@ -466,8 +484,17 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]:
|
||||
@hookspec(
|
||||
firstresult=True,
|
||||
warn_on_impl_args={
|
||||
"path": HOOK_LEGACY_PATH_ARG.format(
|
||||
pylib_path_arg="path", pathlib_path_arg="module_path"
|
||||
),
|
||||
},
|
||||
)
|
||||
def pytest_pycollect_makemodule(
|
||||
module_path: Path, path: "LEGACY_PATH", parent
|
||||
) -> Optional["Module"]:
|
||||
"""Return a :class:`pytest.Module` collector or None for the given path.
|
||||
|
||||
This hook will be called for each matching test module path.
|
||||
@@ -483,8 +510,7 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]
|
||||
The ``module_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``path`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``path`` parameter has been removed in favor of ``module_path``.
|
||||
The ``path`` parameter has been deprecated in favor of ``fspath``.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
@@ -991,8 +1017,15 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@hookspec(
|
||||
warn_on_impl_args={
|
||||
"startdir": HOOK_LEGACY_PATH_ARG.format(
|
||||
pylib_path_arg="startdir", pathlib_path_arg="start_path"
|
||||
),
|
||||
},
|
||||
)
|
||||
def pytest_report_header( # type:ignore[empty-body]
|
||||
config: "Config", start_path: Path
|
||||
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
|
||||
) -> Union[str, List[str]]:
|
||||
"""Return a string or list of strings to be displayed as header info for terminal reporting.
|
||||
|
||||
@@ -1009,10 +1042,8 @@ def pytest_report_header( # type:ignore[empty-body]
|
||||
|
||||
.. versionchanged:: 7.0.0
|
||||
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``startdir`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``startdir`` parameter has been removed.
|
||||
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
|
||||
has been deprecated.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
@@ -1021,9 +1052,17 @@ def pytest_report_header( # type:ignore[empty-body]
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(
|
||||
warn_on_impl_args={
|
||||
"startdir": HOOK_LEGACY_PATH_ARG.format(
|
||||
pylib_path_arg="startdir", pathlib_path_arg="start_path"
|
||||
),
|
||||
},
|
||||
)
|
||||
def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||
config: "Config",
|
||||
start_path: Path,
|
||||
startdir: "LEGACY_PATH",
|
||||
items: Sequence["Item"],
|
||||
) -> Union[str, List[str]]:
|
||||
"""Return a string or list of strings to be displayed after collection
|
||||
@@ -1047,10 +1086,8 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||
|
||||
.. versionchanged:: 7.0.0
|
||||
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``startdir`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``startdir`` parameter has been removed.
|
||||
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
|
||||
has been deprecated.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
@@ -7,6 +7,7 @@ Based on initial code from Ross Lawley.
|
||||
Output conforms to
|
||||
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import os
|
||||
@@ -60,7 +61,7 @@ def bin_xml_escape(arg: object) -> str:
|
||||
# Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
||||
# For an unknown(?) reason, we disallow #x7F (DEL) as well.
|
||||
illegal_xml_re = (
|
||||
"[^\u0009\u000A\u000D\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]"
|
||||
"[^\u0009\u000a\u000d\u0020-\u007e\u0080-\ud7ff\ue000-\ufffd\u10000-\u10ffff]"
|
||||
)
|
||||
return re.sub(illegal_xml_re, repl, str(arg))
|
||||
|
||||
@@ -142,7 +143,7 @@ class _NodeReporter:
|
||||
# Filter out attributes not permitted by this test family.
|
||||
# Including custom attributes because they are not valid here.
|
||||
temp_attrs = {}
|
||||
for key in self.attrs.keys():
|
||||
for key in self.attrs:
|
||||
if key in families[self.family]["testcase"]:
|
||||
temp_attrs[key] = self.attrs[key]
|
||||
self.attrs = temp_attrs
|
||||
@@ -261,7 +262,7 @@ class _NodeReporter:
|
||||
self.__dict__.clear()
|
||||
# Type ignored because mypy doesn't like overriding a method.
|
||||
# Also the return value doesn't match...
|
||||
self.to_xml = lambda: data # type: ignore[assignment]
|
||||
self.to_xml = lambda: data # type: ignore[method-assign]
|
||||
|
||||
|
||||
def _warn_incompatibility_with_xunit2(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Add backward compatibility support for the legacy py path type."""
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
@@ -14,9 +14,9 @@ from typing import Union
|
||||
|
||||
from iniconfig import SectionWrapper
|
||||
|
||||
import py
|
||||
|
||||
from _pytest.cacheprovider import Cache
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.compat import legacy_path
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config import PytestPluginManager
|
||||
@@ -39,20 +39,6 @@ if TYPE_CHECKING:
|
||||
import pexpect
|
||||
|
||||
|
||||
#: constant to prepare valuing pylib path replacements/lazy proxies later on
|
||||
# intended for removal in pytest 8.0 or 9.0
|
||||
|
||||
# fmt: off
|
||||
# intentional space to create a fake difference for the verification
|
||||
LEGACY_PATH = py.path. local
|
||||
# fmt: on
|
||||
|
||||
|
||||
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
|
||||
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
|
||||
return LEGACY_PATH(path)
|
||||
|
||||
|
||||
@final
|
||||
class Testdir:
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Access and control log capturing."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from contextlib import nullcontext
|
||||
from datetime import datetime
|
||||
@@ -209,7 +210,7 @@ class PercentStyleMultiline(logging.PercentStyle):
|
||||
if "\n" in record.message:
|
||||
if hasattr(record, "auto_indent"):
|
||||
# Passed in from the "extra={}" kwarg on the call to logging.log().
|
||||
auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined]
|
||||
auto_indent = self._get_auto_indent(record.auto_indent)
|
||||
else:
|
||||
auto_indent = self._auto_indent
|
||||
|
||||
@@ -512,7 +513,7 @@ class LogCaptureFixture:
|
||||
|
||||
:return: The original disabled logging level.
|
||||
"""
|
||||
original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
|
||||
original_disable_level: int = logger_obj.manager.disable
|
||||
|
||||
if isinstance(level, str):
|
||||
# Try to translate the level string to an int for `logging.disable()`
|
||||
|
||||
@@ -37,6 +37,7 @@ from _pytest.config import hookimpl
|
||||
from _pytest.config import PytestPluginManager
|
||||
from _pytest.config import UsageError
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.config.compat import PathAwareHookProxy
|
||||
from _pytest.fixtures import FixtureManager
|
||||
from _pytest.outcomes import exit
|
||||
from _pytest.pathlib import absolutepath
|
||||
@@ -557,6 +558,7 @@ class Session(nodes.Collector):
|
||||
super().__init__(
|
||||
name="",
|
||||
path=config.rootpath,
|
||||
fspath=None,
|
||||
parent=None,
|
||||
config=config,
|
||||
session=self,
|
||||
@@ -694,7 +696,7 @@ class Session(nodes.Collector):
|
||||
proxy: pluggy.HookRelay
|
||||
if remove_mods:
|
||||
# One or more conftests are not in use at this path.
|
||||
proxy = FSHookProxy(pm, remove_mods) # type: ignore[arg-type,assignment]
|
||||
proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment]
|
||||
else:
|
||||
# All plugins are active for this fspath.
|
||||
proxy = self.config.hook
|
||||
@@ -734,14 +736,12 @@ class Session(nodes.Collector):
|
||||
@overload
|
||||
def perform_collect(
|
||||
self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
|
||||
) -> Sequence[nodes.Item]:
|
||||
...
|
||||
) -> Sequence[nodes.Item]: ...
|
||||
|
||||
@overload
|
||||
def perform_collect(
|
||||
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
|
||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
||||
...
|
||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]: ...
|
||||
|
||||
def perform_collect(
|
||||
self, args: Optional[Sequence[str]] = None, genitems: bool = True
|
||||
|
||||
@@ -342,7 +342,7 @@ class MarkDecorator:
|
||||
# return type. Not much we can do about that. Thankfully mypy picks
|
||||
# the first match so it works out even if we break the rules.
|
||||
@overload
|
||||
def __call__(self, arg: Markable) -> Markable: # type: ignore[misc]
|
||||
def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap]
|
||||
pass
|
||||
|
||||
@overload
|
||||
@@ -433,13 +433,11 @@ if TYPE_CHECKING:
|
||||
from _pytest.scope import _ScopeName
|
||||
|
||||
class _SkipMarkDecorator(MarkDecorator):
|
||||
@overload # type: ignore[override,misc,no-overload-impl]
|
||||
def __call__(self, arg: Markable) -> Markable:
|
||||
...
|
||||
@overload # type: ignore[override,no-overload-impl]
|
||||
def __call__(self, arg: Markable) -> Markable: ...
|
||||
|
||||
@overload
|
||||
def __call__(self, reason: str = ...) -> "MarkDecorator":
|
||||
...
|
||||
def __call__(self, reason: str = ...) -> "MarkDecorator": ...
|
||||
|
||||
class _SkipifMarkDecorator(MarkDecorator):
|
||||
def __call__( # type: ignore[override]
|
||||
@@ -447,13 +445,11 @@ if TYPE_CHECKING:
|
||||
condition: Union[str, bool] = ...,
|
||||
*conditions: Union[str, bool],
|
||||
reason: str = ...,
|
||||
) -> MarkDecorator:
|
||||
...
|
||||
) -> MarkDecorator: ...
|
||||
|
||||
class _XfailMarkDecorator(MarkDecorator):
|
||||
@overload # type: ignore[override,misc,no-overload-impl]
|
||||
def __call__(self, arg: Markable) -> Markable:
|
||||
...
|
||||
@overload # type: ignore[override,no-overload-impl]
|
||||
def __call__(self, arg: Markable) -> Markable: ...
|
||||
|
||||
@overload
|
||||
def __call__(
|
||||
@@ -466,8 +462,7 @@ if TYPE_CHECKING:
|
||||
None, Type[BaseException], Tuple[Type[BaseException], ...]
|
||||
] = ...,
|
||||
strict: bool = ...,
|
||||
) -> MarkDecorator:
|
||||
...
|
||||
) -> MarkDecorator: ...
|
||||
|
||||
class _ParametrizeMarkDecorator(MarkDecorator):
|
||||
def __call__( # type: ignore[override]
|
||||
@@ -483,8 +478,7 @@ if TYPE_CHECKING:
|
||||
]
|
||||
] = ...,
|
||||
scope: Optional[_ScopeName] = ...,
|
||||
) -> MarkDecorator:
|
||||
...
|
||||
) -> MarkDecorator: ...
|
||||
|
||||
class _UsefixturesMarkDecorator(MarkDecorator):
|
||||
def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Monkeypatching and mocking functionality."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import re
|
||||
@@ -167,8 +168,7 @@ class MonkeyPatch:
|
||||
name: object,
|
||||
value: Notset = ...,
|
||||
raising: bool = ...,
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
@overload
|
||||
def setattr(
|
||||
@@ -177,8 +177,7 @@ class MonkeyPatch:
|
||||
name: str,
|
||||
value: object,
|
||||
raising: bool = ...,
|
||||
) -> None:
|
||||
...
|
||||
) -> None: ...
|
||||
|
||||
def setattr(
|
||||
self,
|
||||
|
||||
@@ -3,6 +3,7 @@ import abc
|
||||
from functools import cached_property
|
||||
from inspect import signature
|
||||
import os
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
@@ -29,8 +30,11 @@ from _pytest._code import getfslineno
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._code.code import Traceback
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ConftestImportFailure
|
||||
from _pytest.config.compat import _check_path
|
||||
from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
|
||||
from _pytest.mark.structures import Mark
|
||||
from _pytest.mark.structures import MarkDecorator
|
||||
from _pytest.mark.structures import NodeKeywords
|
||||
@@ -55,6 +59,29 @@ tracebackcutdir = Path(_pytest.__file__).parent
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
def _imply_path(
|
||||
node_type: Type["Node"],
|
||||
path: Optional[Path],
|
||||
fspath: Optional[LEGACY_PATH],
|
||||
) -> Path:
|
||||
if fspath is not None:
|
||||
warnings.warn(
|
||||
NODE_CTOR_FSPATH_ARG.format(
|
||||
node_type_name=node_type.__name__,
|
||||
),
|
||||
stacklevel=6,
|
||||
)
|
||||
if path is not None:
|
||||
if fspath is not None:
|
||||
_check_path(path, fspath)
|
||||
return path
|
||||
else:
|
||||
assert fspath is not None
|
||||
return Path(fspath)
|
||||
|
||||
|
||||
_NodeType = TypeVar("_NodeType", bound="Node")
|
||||
|
||||
|
||||
@@ -110,6 +137,13 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
leaf nodes.
|
||||
"""
|
||||
|
||||
# Implemented in the legacypath plugin.
|
||||
#: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
|
||||
#: for methods not migrated to ``pathlib.Path`` yet, such as
|
||||
#: :meth:`Item.reportinfo <pytest.Item.reportinfo>`. Will be deprecated in
|
||||
#: a future release, prefer using :attr:`path` instead.
|
||||
fspath: LEGACY_PATH
|
||||
|
||||
# Use __slots__ to make attribute access faster.
|
||||
# Note that __dict__ is still available.
|
||||
__slots__ = (
|
||||
@@ -129,6 +163,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
parent: "Optional[Node]" = None,
|
||||
config: Optional[Config] = None,
|
||||
session: "Optional[Session]" = None,
|
||||
fspath: Optional[LEGACY_PATH] = None,
|
||||
path: Optional[Path] = None,
|
||||
nodeid: Optional[str] = None,
|
||||
) -> None:
|
||||
@@ -154,11 +189,10 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
raise TypeError("session or parent must be provided")
|
||||
self.session = parent.session
|
||||
|
||||
if path is None:
|
||||
if path is None and fspath is None:
|
||||
path = getattr(parent, "path", None)
|
||||
assert path is not None
|
||||
#: Filesystem path where this node was collected from (can be None).
|
||||
self.path = path
|
||||
self.path: pathlib.Path = _imply_path(type(self), path, fspath=fspath)
|
||||
|
||||
# The explicit annotation is to avoid publicly exposing NodeKeywords.
|
||||
#: Keywords/markers collected from all scopes.
|
||||
@@ -329,12 +363,10 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
yield node, mark
|
||||
|
||||
@overload
|
||||
def get_closest_marker(self, name: str) -> Optional[Mark]:
|
||||
...
|
||||
def get_closest_marker(self, name: str) -> Optional[Mark]: ...
|
||||
|
||||
@overload
|
||||
def get_closest_marker(self, name: str, default: Mark) -> Mark:
|
||||
...
|
||||
def get_closest_marker(self, name: str, default: Mark) -> Mark: ...
|
||||
|
||||
def get_closest_marker(
|
||||
self, name: str, default: Optional[Mark] = None
|
||||
@@ -529,6 +561,7 @@ class FSCollector(Collector, abc.ABC):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fspath: Optional[LEGACY_PATH] = None,
|
||||
path_or_parent: Optional[Union[Path, Node]] = None,
|
||||
path: Optional[Path] = None,
|
||||
name: Optional[str] = None,
|
||||
@@ -544,8 +577,8 @@ class FSCollector(Collector, abc.ABC):
|
||||
elif isinstance(path_or_parent, Path):
|
||||
assert path is None
|
||||
path = path_or_parent
|
||||
assert path is not None
|
||||
|
||||
path = _imply_path(type(self), path, fspath=fspath)
|
||||
if name is None:
|
||||
name = path.name
|
||||
if parent is not None and parent.path != path:
|
||||
@@ -585,11 +618,12 @@ class FSCollector(Collector, abc.ABC):
|
||||
cls,
|
||||
parent,
|
||||
*,
|
||||
fspath: Optional[LEGACY_PATH] = None,
|
||||
path: Optional[Path] = None,
|
||||
**kw,
|
||||
) -> "Self":
|
||||
"""The public constructor."""
|
||||
return super().from_parent(parent=parent, path=path, **kw)
|
||||
return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
|
||||
|
||||
|
||||
class File(FSCollector, abc.ABC):
|
||||
@@ -731,7 +765,7 @@ class Item(Node, abc.ABC):
|
||||
and lineno is a 0-based line number.
|
||||
"""
|
||||
location = self.reportinfo()
|
||||
path = absolutepath(os.fspath(location[0]))
|
||||
path = absolutepath(location[0])
|
||||
relfspath = self.session._node_location_to_relpath(path)
|
||||
assert type(location[2]) is str
|
||||
return (relfspath, location[1], location[2])
|
||||
|
||||
@@ -11,6 +11,8 @@ from typing import Protocol
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
|
||||
from .warning_types import PytestDeprecationWarning
|
||||
|
||||
|
||||
class OutcomeException(BaseException):
|
||||
"""OutcomeException and its subclass instances indicate and contain info
|
||||
@@ -192,7 +194,11 @@ def xfail(reason: str = "") -> NoReturn:
|
||||
|
||||
|
||||
def importorskip(
|
||||
modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
|
||||
modname: str,
|
||||
minversion: Optional[str] = None,
|
||||
reason: Optional[str] = None,
|
||||
*,
|
||||
exc_type: Optional[Type[ImportError]] = None,
|
||||
) -> Any:
|
||||
"""Import and return the requested module ``modname``, or skip the
|
||||
current test if the module cannot be imported.
|
||||
@@ -205,6 +211,18 @@ def importorskip(
|
||||
:param reason:
|
||||
If given, this reason is shown as the message when the module cannot
|
||||
be imported.
|
||||
:param exc_type:
|
||||
The exception that should be captured in order to skip modules.
|
||||
Must be :py:class:`ImportError` or a subclass.
|
||||
|
||||
If the module can be imported but raises :class:`ImportError`, pytest will
|
||||
issue a warning to the user, as often users expect the module not to be
|
||||
found (which would raise :class:`ModuleNotFoundError` instead).
|
||||
|
||||
This warning can be suppressed by passing ``exc_type=ImportError`` explicitly.
|
||||
|
||||
See :ref:`import-or-skip-import-error` for details.
|
||||
|
||||
|
||||
:returns:
|
||||
The imported module. This should be assigned to its canonical name.
|
||||
@@ -212,23 +230,62 @@ def importorskip(
|
||||
Example::
|
||||
|
||||
docutils = pytest.importorskip("docutils")
|
||||
|
||||
.. versionadded:: 8.2
|
||||
|
||||
The ``exc_type`` parameter.
|
||||
"""
|
||||
import warnings
|
||||
|
||||
__tracebackhide__ = True
|
||||
compile(modname, "", "eval") # to catch syntaxerrors
|
||||
|
||||
# Until pytest 9.1, we will warn the user if we catch ImportError (instead of ModuleNotFoundError),
|
||||
# as this might be hiding an installation/environment problem, which is not usually what is intended
|
||||
# when using importorskip() (#11523).
|
||||
# In 9.1, to keep the function signature compatible, we just change the code below to:
|
||||
# 1. Use `exc_type = ModuleNotFoundError` if `exc_type` is not given.
|
||||
# 2. Remove `warn_on_import` and the warning handling.
|
||||
if exc_type is None:
|
||||
exc_type = ImportError
|
||||
warn_on_import_error = True
|
||||
else:
|
||||
warn_on_import_error = False
|
||||
|
||||
skipped: Optional[Skipped] = None
|
||||
warning: Optional[Warning] = None
|
||||
|
||||
with warnings.catch_warnings():
|
||||
# Make sure to ignore ImportWarnings that might happen because
|
||||
# of existing directories with the same name we're trying to
|
||||
# import but without a __init__.py file.
|
||||
warnings.simplefilter("ignore")
|
||||
|
||||
try:
|
||||
__import__(modname)
|
||||
except ImportError as exc:
|
||||
except exc_type as exc:
|
||||
# Do not raise or issue warnings inside the catch_warnings() block.
|
||||
if reason is None:
|
||||
reason = f"could not import {modname!r}: {exc}"
|
||||
raise Skipped(reason, allow_module_level=True) from None
|
||||
skipped = Skipped(reason, allow_module_level=True)
|
||||
|
||||
if warn_on_import_error and not isinstance(exc, ModuleNotFoundError):
|
||||
lines = [
|
||||
"",
|
||||
f"Module '{modname}' was found, but when imported by pytest it raised:",
|
||||
f" {exc!r}",
|
||||
"In pytest 9.1 this warning will become an error by default.",
|
||||
"You can fix the underlying problem, or alternatively overwrite this behavior and silence this "
|
||||
"warning by passing exc_type=ImportError explicitly.",
|
||||
"See https://docs.pytest.org/en/stable/deprecations.html#pytest-importorskip-default-behavior-regarding-importerror",
|
||||
]
|
||||
warning = PytestDeprecationWarning("\n".join(lines))
|
||||
|
||||
if warning:
|
||||
warnings.warn(warning, stacklevel=2)
|
||||
if skipped:
|
||||
raise skipped
|
||||
|
||||
mod = sys.modules[modname]
|
||||
if minversion is None:
|
||||
return mod
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Submit failure or test session information to a pastebin service."""
|
||||
|
||||
from io import StringIO
|
||||
import tempfile
|
||||
from typing import IO
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import atexit
|
||||
import contextlib
|
||||
from enum import Enum
|
||||
@@ -8,6 +7,7 @@ from errno import ENOENT
|
||||
from errno import ENOTDIR
|
||||
import fnmatch
|
||||
from functools import partial
|
||||
from importlib.machinery import ModuleSpec
|
||||
import importlib.util
|
||||
import itertools
|
||||
import os
|
||||
@@ -22,6 +22,7 @@ import shutil
|
||||
import sys
|
||||
import types
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
@@ -58,7 +59,7 @@ _IGNORED_WINERRORS = (
|
||||
)
|
||||
|
||||
|
||||
def _ignore_error(exception):
|
||||
def _ignore_error(exception: Exception) -> bool:
|
||||
return (
|
||||
getattr(exception, "errno", None) in _IGNORED_ERRORS
|
||||
or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
|
||||
@@ -70,7 +71,7 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||
|
||||
|
||||
def on_rm_rf_error(
|
||||
func,
|
||||
func: Optional[Callable[..., Any]],
|
||||
path: str,
|
||||
excinfo: Union[
|
||||
BaseException,
|
||||
@@ -195,7 +196,7 @@ def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
|
||||
return extract_suffixes(find_prefixed(root, prefix), prefix)
|
||||
|
||||
|
||||
def parse_num(maybe_num) -> int:
|
||||
def parse_num(maybe_num: str) -> int:
|
||||
"""Parse number path suffixes, returns -1 on error."""
|
||||
try:
|
||||
return int(maybe_num)
|
||||
@@ -263,7 +264,9 @@ def create_cleanup_lock(p: Path) -> Path:
|
||||
return lock_path
|
||||
|
||||
|
||||
def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
|
||||
def register_cleanup_lock_removal(
|
||||
lock_path: Path, register: Any = atexit.register
|
||||
) -> Any:
|
||||
"""Register a cleanup function for removing a lock, by default on atexit."""
|
||||
pid = os.getpid()
|
||||
|
||||
@@ -354,7 +357,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||
yield Path(entry)
|
||||
|
||||
|
||||
def cleanup_dead_symlinks(root: Path):
|
||||
def cleanup_dead_symlinks(root: Path) -> None:
|
||||
for left_dir in root.iterdir():
|
||||
if left_dir.is_symlink():
|
||||
if not left_dir.resolve().exists():
|
||||
@@ -458,10 +461,14 @@ def parts(s: str) -> Set[str]:
|
||||
return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))}
|
||||
|
||||
|
||||
def symlink_or_skip(src, dst, **kwargs):
|
||||
def symlink_or_skip(
|
||||
src: Union["os.PathLike[str]", str],
|
||||
dst: Union["os.PathLike[str]", str],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Make a symlink, or skip the test in case symlinks are not supported."""
|
||||
try:
|
||||
os.symlink(str(src), str(dst), **kwargs)
|
||||
os.symlink(src, dst, **kwargs)
|
||||
except OSError as e:
|
||||
skip(f"symlinks not supported: {e}")
|
||||
|
||||
@@ -539,6 +546,10 @@ def import_path(
|
||||
except CouldNotResolvePathError:
|
||||
pass
|
||||
else:
|
||||
# If the given module name is already in sys.modules, do not import it again.
|
||||
with contextlib.suppress(KeyError):
|
||||
return sys.modules[module_name]
|
||||
|
||||
mod = _import_module_using_spec(
|
||||
module_name, path, pkg_root, insert_modules=False
|
||||
)
|
||||
@@ -615,23 +626,53 @@ def _import_module_using_spec(
|
||||
:param insert_modules:
|
||||
If True, will call insert_missing_modules to create empty intermediate modules
|
||||
for made-up module names (when importing test files not reachable from sys.path).
|
||||
Note: we can probably drop insert_missing_modules altogether: instead of
|
||||
generating module names such as "src.tests.test_foo", which require intermediate
|
||||
empty modules, we might just as well generate unique module names like
|
||||
"src_tests_test_foo".
|
||||
"""
|
||||
# Checking with sys.meta_path first in case one of its hooks can import this module,
|
||||
# such as our own assertion-rewrite hook.
|
||||
for meta_importer in sys.meta_path:
|
||||
spec = meta_importer.find_spec(module_name, [str(module_location)])
|
||||
if spec is not None:
|
||||
if spec_matches_module_path(spec, module_path):
|
||||
break
|
||||
else:
|
||||
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
|
||||
if spec is not None:
|
||||
|
||||
if spec_matches_module_path(spec, module_path):
|
||||
assert spec is not None
|
||||
# Attempt to import the parent module, seems is our responsibility:
|
||||
# https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
|
||||
parent_module_name, _, name = module_name.rpartition(".")
|
||||
parent_module: Optional[ModuleType] = None
|
||||
if parent_module_name:
|
||||
parent_module = sys.modules.get(parent_module_name)
|
||||
if parent_module is None:
|
||||
# Find the directory of this module's parent.
|
||||
parent_dir = (
|
||||
module_path.parent.parent
|
||||
if module_path.name == "__init__.py"
|
||||
else module_path.parent
|
||||
)
|
||||
# Consider the parent module path as its __init__.py file, if it has one.
|
||||
parent_module_path = (
|
||||
parent_dir / "__init__.py"
|
||||
if (parent_dir / "__init__.py").is_file()
|
||||
else parent_dir
|
||||
)
|
||||
parent_module = _import_module_using_spec(
|
||||
parent_module_name,
|
||||
parent_module_path,
|
||||
parent_dir,
|
||||
insert_modules=insert_modules,
|
||||
)
|
||||
|
||||
# Find spec and import this module.
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
sys.modules[module_name] = mod
|
||||
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
||||
|
||||
# Set this module as an attribute of the parent module (#12194).
|
||||
if parent_module is not None:
|
||||
setattr(parent_module, name, mod)
|
||||
|
||||
if insert_modules:
|
||||
insert_missing_modules(sys.modules, module_name)
|
||||
return mod
|
||||
@@ -639,6 +680,16 @@ def _import_module_using_spec(
|
||||
return None
|
||||
|
||||
|
||||
def spec_matches_module_path(
|
||||
module_spec: Optional[ModuleSpec], module_path: Path
|
||||
) -> bool:
|
||||
"""Return true if the given ModuleSpec can be used to import the given module path."""
|
||||
if module_spec is None or module_spec.origin is None:
|
||||
return False
|
||||
|
||||
return Path(module_spec.origin) == module_path
|
||||
|
||||
|
||||
# Implement a special _is_same function on Windows which returns True if the two filenames
|
||||
# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
|
||||
if sys.platform.startswith("win"):
|
||||
@@ -692,34 +743,31 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
|
||||
otherwise "src.tests.test_foo" is not importable by ``__import__``.
|
||||
"""
|
||||
module_parts = module_name.split(".")
|
||||
child_module: Union[ModuleType, None] = None
|
||||
module: Union[ModuleType, None] = None
|
||||
child_name: str = ""
|
||||
while module_name:
|
||||
if module_name not in modules:
|
||||
try:
|
||||
# If sys.meta_path is empty, calling import_module will issue
|
||||
# a warning and raise ModuleNotFoundError. To avoid the
|
||||
# warning, we check sys.meta_path explicitly and raise the error
|
||||
# ourselves to fall back to creating a dummy module.
|
||||
if not sys.meta_path:
|
||||
raise ModuleNotFoundError
|
||||
module = importlib.import_module(module_name)
|
||||
except ModuleNotFoundError:
|
||||
module = ModuleType(
|
||||
module_name,
|
||||
doc="Empty module created by pytest's importmode=importlib.",
|
||||
)
|
||||
else:
|
||||
module = modules[module_name]
|
||||
if child_module:
|
||||
parent_module_name, _, child_name = module_name.rpartition(".")
|
||||
if parent_module_name:
|
||||
parent_module = modules.get(parent_module_name)
|
||||
if parent_module is None:
|
||||
try:
|
||||
# If sys.meta_path is empty, calling import_module will issue
|
||||
# a warning and raise ModuleNotFoundError. To avoid the
|
||||
# warning, we check sys.meta_path explicitly and raise the error
|
||||
# ourselves to fall back to creating a dummy module.
|
||||
if not sys.meta_path:
|
||||
raise ModuleNotFoundError
|
||||
parent_module = importlib.import_module(parent_module_name)
|
||||
except ModuleNotFoundError:
|
||||
parent_module = ModuleType(
|
||||
module_name,
|
||||
doc="Empty module created by pytest's importmode=importlib.",
|
||||
)
|
||||
modules[parent_module_name] = parent_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
|
||||
# Keep track of the child module while moving up the tree.
|
||||
child_module, child_name = module, module_name.rpartition(".")[-1]
|
||||
if not hasattr(parent_module, child_name):
|
||||
setattr(parent_module, child_name, modules[module_name])
|
||||
|
||||
module_parts.pop(-1)
|
||||
module_name = ".".join(module_parts)
|
||||
|
||||
@@ -758,39 +806,79 @@ def resolve_pkg_root_and_module_name(
|
||||
Passing the full path to `models.py` will yield Path("src") and "app.core.models".
|
||||
|
||||
If consider_namespace_packages is True, then we additionally check upwards in the hierarchy
|
||||
until we find a directory that is reachable from sys.path, which marks it as a namespace package:
|
||||
for namespace packages:
|
||||
|
||||
https://packaging.python.org/en/latest/guides/packaging-namespace-packages
|
||||
|
||||
Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files).
|
||||
"""
|
||||
pkg_root: Optional[Path] = None
|
||||
pkg_path = resolve_package_path(path)
|
||||
if pkg_path is not None:
|
||||
pkg_root = pkg_path.parent
|
||||
# https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
|
||||
if consider_namespace_packages:
|
||||
# Go upwards in the hierarchy, if we find a parent path included
|
||||
# in sys.path, it means the package found by resolve_package_path()
|
||||
# actually belongs to a namespace package.
|
||||
for parent in pkg_root.parents:
|
||||
# If any of the parent paths has a __init__.py, it means it is not
|
||||
# a namespace package (see the docs linked above).
|
||||
if (parent / "__init__.py").is_file():
|
||||
break
|
||||
if str(parent) in sys.path:
|
||||
# Point the pkg_root to the root of the namespace package.
|
||||
pkg_root = parent
|
||||
break
|
||||
if consider_namespace_packages:
|
||||
start = pkg_root if pkg_root is not None else path.parent
|
||||
for candidate in (start, *start.parents):
|
||||
module_name = compute_module_name(candidate, path)
|
||||
if module_name and is_importable(module_name, path):
|
||||
# Point the pkg_root to the root of the namespace package.
|
||||
pkg_root = candidate
|
||||
break
|
||||
|
||||
names = list(path.with_suffix("").relative_to(pkg_root).parts)
|
||||
if names[-1] == "__init__":
|
||||
names.pop()
|
||||
module_name = ".".join(names)
|
||||
return pkg_root, module_name
|
||||
if pkg_root is not None:
|
||||
module_name = compute_module_name(pkg_root, path)
|
||||
if module_name:
|
||||
return pkg_root, module_name
|
||||
|
||||
raise CouldNotResolvePathError(f"Could not resolve for {path}")
|
||||
|
||||
|
||||
def is_importable(module_name: str, module_path: Path) -> bool:
|
||||
"""
|
||||
Return if the given module path could be imported normally by Python, akin to the user
|
||||
entering the REPL and importing the corresponding module name directly, and corresponds
|
||||
to the module_path specified.
|
||||
|
||||
:param module_name:
|
||||
Full module name that we want to check if is importable.
|
||||
For example, "app.models".
|
||||
|
||||
:param module_path:
|
||||
Full path to the python module/package we want to check if is importable.
|
||||
For example, "/projects/src/app/models.py".
|
||||
"""
|
||||
try:
|
||||
# Note this is different from what we do in ``_import_module_using_spec``, where we explicitly search through
|
||||
# sys.meta_path to be able to pass the path of the module that we want to import (``meta_importer.find_spec``).
|
||||
# Using importlib.util.find_spec() is different, it gives the same results as trying to import
|
||||
# the module normally in the REPL.
|
||||
spec = importlib.util.find_spec(module_name)
|
||||
except (ImportError, ValueError, ImportWarning):
|
||||
return False
|
||||
else:
|
||||
return spec_matches_module_path(spec, module_path)
|
||||
|
||||
|
||||
def compute_module_name(root: Path, module_path: Path) -> Optional[str]:
|
||||
"""Compute a module name based on a path and a root anchor."""
|
||||
try:
|
||||
path_without_suffix = module_path.with_suffix("")
|
||||
except ValueError:
|
||||
# Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter).
|
||||
return None
|
||||
|
||||
try:
|
||||
relative = path_without_suffix.relative_to(root)
|
||||
except ValueError: # pragma: no cover
|
||||
return None
|
||||
names = list(relative.parts)
|
||||
if not names:
|
||||
return None
|
||||
if names[-1] == "__init__":
|
||||
names.pop()
|
||||
return ".".join(names)
|
||||
|
||||
|
||||
class CouldNotResolvePathError(Exception):
|
||||
"""Custom exception raised by resolve_pkg_root_and_module_name."""
|
||||
|
||||
@@ -836,13 +924,13 @@ def visit(
|
||||
yield from visit(entry.path, recurse)
|
||||
|
||||
|
||||
def absolutepath(path: Union[Path, str]) -> Path:
|
||||
def absolutepath(path: "Union[str, os.PathLike[str]]") -> Path:
|
||||
"""Convert a path to an absolute path using os.path.abspath.
|
||||
|
||||
Prefer this over Path.resolve() (see #6523).
|
||||
Prefer this over Path.absolute() (not public, doesn't normalize).
|
||||
"""
|
||||
return Path(os.path.abspath(str(path)))
|
||||
return Path(os.path.abspath(path))
|
||||
|
||||
|
||||
def commonpath(path1: Path, path2: Path) -> Optional[Path]:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
PYTEST_DONT_REWRITE
|
||||
"""
|
||||
|
||||
import collections.abc
|
||||
import contextlib
|
||||
from fnmatch import fnmatch
|
||||
@@ -245,8 +246,7 @@ class RecordedHookCall:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# The class has undetermined attributes, this tells mypy about it.
|
||||
def __getattr__(self, key: str):
|
||||
...
|
||||
def __getattr__(self, key: str): ...
|
||||
|
||||
|
||||
@final
|
||||
@@ -327,15 +327,13 @@ class HookRecorder:
|
||||
def getreports(
|
||||
self,
|
||||
names: "Literal['pytest_collectreport']",
|
||||
) -> Sequence[CollectReport]:
|
||||
...
|
||||
) -> Sequence[CollectReport]: ...
|
||||
|
||||
@overload
|
||||
def getreports(
|
||||
self,
|
||||
names: "Literal['pytest_runtest_logreport']",
|
||||
) -> Sequence[TestReport]:
|
||||
...
|
||||
) -> Sequence[TestReport]: ...
|
||||
|
||||
@overload
|
||||
def getreports(
|
||||
@@ -344,8 +342,7 @@ class HookRecorder:
|
||||
"pytest_collectreport",
|
||||
"pytest_runtest_logreport",
|
||||
),
|
||||
) -> Sequence[Union[CollectReport, TestReport]]:
|
||||
...
|
||||
) -> Sequence[Union[CollectReport, TestReport]]: ...
|
||||
|
||||
def getreports(
|
||||
self,
|
||||
@@ -390,15 +387,13 @@ class HookRecorder:
|
||||
def getfailures(
|
||||
self,
|
||||
names: "Literal['pytest_collectreport']",
|
||||
) -> Sequence[CollectReport]:
|
||||
...
|
||||
) -> Sequence[CollectReport]: ...
|
||||
|
||||
@overload
|
||||
def getfailures(
|
||||
self,
|
||||
names: "Literal['pytest_runtest_logreport']",
|
||||
) -> Sequence[TestReport]:
|
||||
...
|
||||
) -> Sequence[TestReport]: ...
|
||||
|
||||
@overload
|
||||
def getfailures(
|
||||
@@ -407,8 +402,7 @@ class HookRecorder:
|
||||
"pytest_collectreport",
|
||||
"pytest_runtest_logreport",
|
||||
),
|
||||
) -> Sequence[Union[CollectReport, TestReport]]:
|
||||
...
|
||||
) -> Sequence[Union[CollectReport, TestReport]]: ...
|
||||
|
||||
def getfailures(
|
||||
self,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Python test discovery, setup and run of test functions."""
|
||||
|
||||
import abc
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
@@ -39,20 +40,18 @@ from _pytest._code import getfslineno
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._code.code import Traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ascii_escaped
|
||||
from _pytest.compat import get_default_arg_names
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.compat import getimfunc
|
||||
from _pytest.compat import getlocation
|
||||
from _pytest.compat import is_async_function
|
||||
from _pytest.compat import is_generator
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import safe_isclass
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
@@ -69,7 +68,6 @@ from _pytest.mark.structures import MarkDecorator
|
||||
from _pytest.mark.structures import normalize_mark_list
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportPathMismatchError
|
||||
@@ -86,27 +84,7 @@ if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
|
||||
_PYTEST_DIR = Path(_pytest.__file__).parent
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
group = parser.getgroup("general")
|
||||
group.addoption(
|
||||
"--fixtures",
|
||||
"--funcargs",
|
||||
action="store_true",
|
||||
dest="showfixtures",
|
||||
default=False,
|
||||
help="Show available fixtures, sorted by plugin appearance "
|
||||
"(fixtures with leading '_' are only shown with '-v')",
|
||||
)
|
||||
group.addoption(
|
||||
"--fixtures-per-test",
|
||||
action="store_true",
|
||||
dest="show_fixtures_per_test",
|
||||
default=False,
|
||||
help="Show fixtures per test",
|
||||
)
|
||||
parser.addini(
|
||||
"python_files",
|
||||
type="args",
|
||||
@@ -135,16 +113,6 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
)
|
||||
|
||||
|
||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
if config.option.showfixtures:
|
||||
showfixtures(config)
|
||||
return 0
|
||||
if config.option.show_fixtures_per_test:
|
||||
show_fixtures_per_test(config)
|
||||
return 0
|
||||
return None
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc: "Metafunc") -> None:
|
||||
for marker in metafunc.definition.iter_markers(name="parametrize"):
|
||||
metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker)
|
||||
@@ -301,10 +269,10 @@ class PyobjMixin(nodes.Node):
|
||||
"""Python instance object the function is bound to.
|
||||
|
||||
Returns None if not a test method, e.g. for a standalone test function,
|
||||
a staticmethod, a class or a module.
|
||||
a class or a module.
|
||||
"""
|
||||
node = self.getparent(Function)
|
||||
return getattr(node.obj, "__self__", None) if node is not None else None
|
||||
# Overridden by Function.
|
||||
return None
|
||||
|
||||
@property
|
||||
def obj(self):
|
||||
@@ -665,6 +633,7 @@ class Package(nodes.Directory):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fspath: Optional[LEGACY_PATH],
|
||||
parent: nodes.Collector,
|
||||
# NOTE: following args are unused:
|
||||
config=None,
|
||||
@@ -676,6 +645,7 @@ class Package(nodes.Directory):
|
||||
# super().__init__(self, fspath, parent=parent)
|
||||
session = parent.session
|
||||
super().__init__(
|
||||
fspath=fspath,
|
||||
path=path,
|
||||
parent=parent,
|
||||
config=config,
|
||||
@@ -1314,7 +1284,6 @@ class Metafunc:
|
||||
func=get_direct_param_fixture_func,
|
||||
scope=scope_,
|
||||
params=None,
|
||||
unittest=False,
|
||||
ids=None,
|
||||
_ispytest=True,
|
||||
)
|
||||
@@ -1522,137 +1491,6 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -
|
||||
return val if escape_option else ascii_escaped(val) # type: ignore
|
||||
|
||||
|
||||
def _pretty_fixture_path(invocation_dir: Path, func) -> str:
|
||||
loc = Path(getlocation(func, invocation_dir))
|
||||
prefix = Path("...", "_pytest")
|
||||
try:
|
||||
return str(prefix / loc.relative_to(_PYTEST_DIR))
|
||||
except ValueError:
|
||||
return bestrelpath(invocation_dir, loc)
|
||||
|
||||
|
||||
def show_fixtures_per_test(config):
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, _show_fixtures_per_test)
|
||||
|
||||
|
||||
def _show_fixtures_per_test(config: Config, session: Session) -> None:
|
||||
import _pytest.config
|
||||
|
||||
session.perform_collect()
|
||||
invocation_dir = config.invocation_params.dir
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
verbose = config.getvalue("verbose")
|
||||
|
||||
def get_best_relpath(func) -> str:
|
||||
loc = getlocation(func, invocation_dir)
|
||||
return bestrelpath(invocation_dir, Path(loc))
|
||||
|
||||
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
|
||||
argname = fixture_def.argname
|
||||
if verbose <= 0 and argname.startswith("_"):
|
||||
return
|
||||
prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
|
||||
tw.write(f"{argname}", green=True)
|
||||
tw.write(f" -- {prettypath}", yellow=True)
|
||||
tw.write("\n")
|
||||
fixture_doc = inspect.getdoc(fixture_def.func)
|
||||
if fixture_doc:
|
||||
write_docstring(
|
||||
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
|
||||
)
|
||||
else:
|
||||
tw.line(" no docstring available", red=True)
|
||||
|
||||
def write_item(item: nodes.Item) -> None:
|
||||
# Not all items have _fixtureinfo attribute.
|
||||
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
|
||||
if info is None or not info.name2fixturedefs:
|
||||
# This test item does not use any fixtures.
|
||||
return
|
||||
tw.line()
|
||||
tw.sep("-", f"fixtures used by {item.name}")
|
||||
# TODO: Fix this type ignore.
|
||||
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
|
||||
# dict key not used in loop but needed for sorting.
|
||||
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
|
||||
assert fixturedefs is not None
|
||||
if not fixturedefs:
|
||||
continue
|
||||
# Last item is expected to be the one used by the test item.
|
||||
write_fixture(fixturedefs[-1])
|
||||
|
||||
for session_item in session.items:
|
||||
write_item(session_item)
|
||||
|
||||
|
||||
def showfixtures(config: Config) -> Union[int, ExitCode]:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, _showfixtures_main)
|
||||
|
||||
|
||||
def _showfixtures_main(config: Config, session: Session) -> None:
|
||||
import _pytest.config
|
||||
|
||||
session.perform_collect()
|
||||
invocation_dir = config.invocation_params.dir
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
verbose = config.getvalue("verbose")
|
||||
|
||||
fm = session._fixturemanager
|
||||
|
||||
available = []
|
||||
seen: Set[Tuple[str, str]] = set()
|
||||
|
||||
for argname, fixturedefs in fm._arg2fixturedefs.items():
|
||||
assert fixturedefs is not None
|
||||
if not fixturedefs:
|
||||
continue
|
||||
for fixturedef in fixturedefs:
|
||||
loc = getlocation(fixturedef.func, invocation_dir)
|
||||
if (fixturedef.argname, loc) in seen:
|
||||
continue
|
||||
seen.add((fixturedef.argname, loc))
|
||||
available.append(
|
||||
(
|
||||
len(fixturedef.baseid),
|
||||
fixturedef.func.__module__,
|
||||
_pretty_fixture_path(invocation_dir, fixturedef.func),
|
||||
fixturedef.argname,
|
||||
fixturedef,
|
||||
)
|
||||
)
|
||||
|
||||
available.sort()
|
||||
currentmodule = None
|
||||
for baseid, module, prettypath, argname, fixturedef in available:
|
||||
if currentmodule != module:
|
||||
if not module.startswith("_pytest."):
|
||||
tw.line()
|
||||
tw.sep("-", f"fixtures defined from {module}")
|
||||
currentmodule = module
|
||||
if verbose <= 0 and argname.startswith("_"):
|
||||
continue
|
||||
tw.write(f"{argname}", green=True)
|
||||
if fixturedef.scope != "function":
|
||||
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
|
||||
tw.write(f" -- {prettypath}", yellow=True)
|
||||
tw.write("\n")
|
||||
doc = inspect.getdoc(fixturedef.func)
|
||||
if doc:
|
||||
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
|
||||
else:
|
||||
tw.line(" no docstring available", red=True)
|
||||
tw.line()
|
||||
|
||||
|
||||
def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
|
||||
for line in doc.split("\n"):
|
||||
tw.line(indent + line)
|
||||
|
||||
|
||||
class Function(PyobjMixin, nodes.Item):
|
||||
"""Item responsible for setting up and executing a Python test function.
|
||||
|
||||
@@ -1700,7 +1538,8 @@ class Function(PyobjMixin, nodes.Item):
|
||||
super().__init__(name, parent, config=config, session=session)
|
||||
|
||||
if callobj is not NOTSET:
|
||||
self.obj = callobj
|
||||
self._obj = callobj
|
||||
self._instance = getattr(callobj, "__self__", None)
|
||||
|
||||
#: Original function name, without any decorations (for example
|
||||
#: parametrization adds a ``"[...]"`` suffix to function names), used to access
|
||||
@@ -1750,12 +1589,31 @@ class Function(PyobjMixin, nodes.Item):
|
||||
"""Underlying python 'function' object."""
|
||||
return getimfunc(self.obj)
|
||||
|
||||
def _getobj(self):
|
||||
assert self.parent is not None
|
||||
@property
|
||||
def instance(self):
|
||||
try:
|
||||
return self._instance
|
||||
except AttributeError:
|
||||
if isinstance(self.parent, Class):
|
||||
# Each Function gets a fresh class instance.
|
||||
self._instance = self._getinstance()
|
||||
else:
|
||||
self._instance = None
|
||||
return self._instance
|
||||
|
||||
def _getinstance(self):
|
||||
if isinstance(self.parent, Class):
|
||||
# Each Function gets a fresh class instance.
|
||||
parent_obj = self.parent.newinstance()
|
||||
return self.parent.newinstance()
|
||||
else:
|
||||
return None
|
||||
|
||||
def _getobj(self):
|
||||
instance = self.instance
|
||||
if instance is not None:
|
||||
parent_obj = instance
|
||||
else:
|
||||
assert self.parent is not None
|
||||
parent_obj = self.parent.obj # type: ignore[attr-defined]
|
||||
return getattr(parent_obj, self.originalname)
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ class ApproxNumpy(ApproxBase):
|
||||
)
|
||||
return f"approx({list_scalars!r})"
|
||||
|
||||
def _repr_compare(self, other_side: "ndarray") -> List[str]:
|
||||
def _repr_compare(self, other_side: Union["ndarray", List[Any]]) -> List[str]:
|
||||
import itertools
|
||||
import math
|
||||
|
||||
@@ -163,10 +163,14 @@ class ApproxNumpy(ApproxBase):
|
||||
self._approx_scalar, self.expected.tolist()
|
||||
)
|
||||
|
||||
if np_array_shape != other_side.shape:
|
||||
# convert other_side to numpy array to ensure shape attribute is available
|
||||
other_side_as_array = _as_numpy_array(other_side)
|
||||
assert other_side_as_array is not None
|
||||
|
||||
if np_array_shape != other_side_as_array.shape:
|
||||
return [
|
||||
"Impossible to compare arrays with different shapes.",
|
||||
f"Shapes: {np_array_shape} and {other_side.shape}",
|
||||
f"Shapes: {np_array_shape} and {other_side_as_array.shape}",
|
||||
]
|
||||
|
||||
number_of_elements = self.expected.size
|
||||
@@ -175,7 +179,7 @@ class ApproxNumpy(ApproxBase):
|
||||
different_ids = []
|
||||
for index in itertools.product(*(range(i) for i in np_array_shape)):
|
||||
approx_value = get_value_from_nested_list(approx_side_as_seq, index)
|
||||
other_value = get_value_from_nested_list(other_side, index)
|
||||
other_value = get_value_from_nested_list(other_side_as_array, index)
|
||||
if approx_value != other_value:
|
||||
abs_diff = abs(approx_value.expected - other_value)
|
||||
max_abs_diff = max(max_abs_diff, abs_diff)
|
||||
@@ -188,7 +192,7 @@ class ApproxNumpy(ApproxBase):
|
||||
message_data = [
|
||||
(
|
||||
str(index),
|
||||
str(get_value_from_nested_list(other_side, index)),
|
||||
str(get_value_from_nested_list(other_side_as_array, index)),
|
||||
str(get_value_from_nested_list(approx_side_as_seq, index)),
|
||||
)
|
||||
for index in different_ids
|
||||
@@ -393,7 +397,7 @@ class ApproxScalar(ApproxBase):
|
||||
# tolerances, i.e. non-numerics and infinities. Need to call abs to
|
||||
# handle complex numbers, e.g. (inf + 1j).
|
||||
if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf(
|
||||
abs(self.expected) # type: ignore[arg-type]
|
||||
abs(self.expected)
|
||||
):
|
||||
return str(self.expected)
|
||||
|
||||
@@ -437,8 +441,8 @@ class ApproxScalar(ApproxBase):
|
||||
# Allow the user to control whether NaNs are considered equal to each
|
||||
# other or not. The abs() calls are for compatibility with complex
|
||||
# numbers.
|
||||
if math.isnan(abs(self.expected)): # type: ignore[arg-type]
|
||||
return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type]
|
||||
if math.isnan(abs(self.expected)):
|
||||
return self.nan_ok and math.isnan(abs(actual))
|
||||
|
||||
# Infinity shouldn't be approximately equal to anything but itself, but
|
||||
# if there's a relative tolerance, it will be infinite and infinity
|
||||
@@ -446,11 +450,11 @@ class ApproxScalar(ApproxBase):
|
||||
# case would have been short circuited above, so here we can just
|
||||
# return false if the expected value is infinite. The abs() call is
|
||||
# for compatibility with complex numbers.
|
||||
if math.isinf(abs(self.expected)): # type: ignore[arg-type]
|
||||
if math.isinf(abs(self.expected)):
|
||||
return False
|
||||
|
||||
# Return true if the two numbers are within the tolerance.
|
||||
result: bool = abs(self.expected - actual) <= self.tolerance
|
||||
result: bool = abs(self.expected - actual) <= self.tolerance # type: ignore[arg-type]
|
||||
return result
|
||||
|
||||
# Ignore type because of https://github.com/python/mypy/issues/4266.
|
||||
@@ -769,8 +773,7 @@ def raises(
|
||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
||||
*,
|
||||
match: Optional[Union[str, Pattern[str]]] = ...,
|
||||
) -> "RaisesContext[E]":
|
||||
...
|
||||
) -> "RaisesContext[E]": ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -779,8 +782,7 @@ def raises(
|
||||
func: Callable[..., Any],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> _pytest._code.ExceptionInfo[E]:
|
||||
...
|
||||
) -> _pytest._code.ExceptionInfo[E]: ...
|
||||
|
||||
|
||||
def raises(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Record warnings during test function execution."""
|
||||
|
||||
from pprint import pformat
|
||||
import re
|
||||
from types import TracebackType
|
||||
@@ -43,13 +44,11 @@ def recwarn() -> Generator["WarningsRecorder", None, None]:
|
||||
@overload
|
||||
def deprecated_call(
|
||||
*, match: Optional[Union[str, Pattern[str]]] = ...
|
||||
) -> "WarningsRecorder":
|
||||
...
|
||||
) -> "WarningsRecorder": ...
|
||||
|
||||
|
||||
@overload
|
||||
def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
|
||||
...
|
||||
def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ...
|
||||
|
||||
|
||||
def deprecated_call(
|
||||
@@ -91,8 +90,7 @@ def warns(
|
||||
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ...,
|
||||
*,
|
||||
match: Optional[Union[str, Pattern[str]]] = ...,
|
||||
) -> "WarningsChecker":
|
||||
...
|
||||
) -> "WarningsChecker": ...
|
||||
|
||||
|
||||
@overload
|
||||
@@ -101,8 +99,7 @@ def warns(
|
||||
func: Callable[..., T],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> T:
|
||||
...
|
||||
) -> T: ...
|
||||
|
||||
|
||||
def warns(
|
||||
@@ -184,8 +181,7 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
|
||||
|
||||
def __init__(self, *, _ispytest: bool = False) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
# Type ignored due to the way typeshed handles warnings.catch_warnings.
|
||||
super().__init__(record=True) # type: ignore[call-arg]
|
||||
super().__init__(record=True)
|
||||
self._entered = False
|
||||
self._list: List[warnings.WarningMessage] = []
|
||||
|
||||
|
||||
@@ -72,8 +72,7 @@ class BaseReport:
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Can have arbitrary fields given to __init__().
|
||||
def __getattr__(self, key: str) -> Any:
|
||||
...
|
||||
def __getattr__(self, key: str) -> Any: ...
|
||||
|
||||
def toterminal(self, out: TerminalWriter) -> None:
|
||||
if hasattr(self, "node"):
|
||||
@@ -606,9 +605,9 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
description,
|
||||
)
|
||||
)
|
||||
exception_info: Union[
|
||||
ExceptionChainRepr, ReprExceptionInfo
|
||||
] = ExceptionChainRepr(chain)
|
||||
exception_info: Union[ExceptionChainRepr, ReprExceptionInfo] = (
|
||||
ExceptionChainRepr(chain)
|
||||
)
|
||||
else:
|
||||
exception_info = ReprExceptionInfo(
|
||||
reprtraceback=reprtraceback,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Basic collect and runtest protocol implementations."""
|
||||
|
||||
import bdb
|
||||
import dataclasses
|
||||
import os
|
||||
@@ -38,7 +39,7 @@ from _pytest.outcomes import Skipped
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -84,7 +85,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
|
||||
dlist.append(rep)
|
||||
if not dlist:
|
||||
return
|
||||
dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return]
|
||||
dlist.sort(key=lambda x: x.duration, reverse=True)
|
||||
if not durations:
|
||||
tr.write_sep("=", "slowest durations")
|
||||
else:
|
||||
@@ -395,8 +396,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
||||
skip_exceptions = [Skipped]
|
||||
unittest = sys.modules.get("unittest")
|
||||
if unittest is not None:
|
||||
# Type ignored because unittest is loaded dynamically.
|
||||
skip_exceptions.append(unittest.SkipTest) # type: ignore
|
||||
skip_exceptions.append(unittest.SkipTest)
|
||||
if isinstance(call.excinfo.value, tuple(skip_exceptions)):
|
||||
outcome = "skipped"
|
||||
r_ = collector._repr_failure_py(call.excinfo, "line")
|
||||
|
||||
@@ -58,7 +58,7 @@ def pytest_fixture_post_finalizer(
|
||||
if config.option.setupshow:
|
||||
_show_fixture_action(fixturedef, request.config, "TEARDOWN")
|
||||
if hasattr(fixturedef, "cached_param"):
|
||||
del fixturedef.cached_param # type: ignore[attr-defined]
|
||||
del fixturedef.cached_param
|
||||
|
||||
|
||||
def _show_fixture_action(
|
||||
@@ -87,7 +87,7 @@ def _show_fixture_action(
|
||||
tw.write(" (fixtures used: {})".format(", ".join(deps)))
|
||||
|
||||
if hasattr(fixturedef, "cached_param"):
|
||||
tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") # type: ignore[attr-defined]
|
||||
tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]")
|
||||
|
||||
tw.flush()
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Support for skip/xfail functions and markers."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import dataclasses
|
||||
import os
|
||||
@@ -109,7 +110,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
|
||||
)
|
||||
globals_.update(dictionary)
|
||||
if hasattr(item, "obj"):
|
||||
globals_.update(item.obj.__globals__) # type: ignore[attr-defined]
|
||||
globals_.update(item.obj.__globals__)
|
||||
try:
|
||||
filename = f"<{mark.name} condition>"
|
||||
condition_code = compile(condition, filename, "eval")
|
||||
|
||||
@@ -19,6 +19,8 @@ class StashKey(Generic[T]):
|
||||
A ``StashKey`` is associated with the type ``T`` of the value of the key.
|
||||
|
||||
A ``StashKey`` is unique and cannot conflict with another key.
|
||||
|
||||
.. versionadded:: 7.0
|
||||
"""
|
||||
|
||||
__slots__ = ()
|
||||
@@ -61,6 +63,8 @@ class Stash:
|
||||
some_str = stash[some_str_key]
|
||||
# The static type of some_bool is bool.
|
||||
some_bool = stash[some_bool_key]
|
||||
|
||||
.. versionadded:: 7.0
|
||||
"""
|
||||
|
||||
__slots__ = ("_storage",)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
This is a good source for looking at the various reporting hooks.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
import dataclasses
|
||||
@@ -1266,7 +1267,7 @@ class TerminalReporter:
|
||||
|
||||
def _set_main_color(self) -> None:
|
||||
unknown_types: List[str] = []
|
||||
for found_type in self.stats.keys():
|
||||
for found_type in self.stats:
|
||||
if found_type: # setup/teardown reports have an empty key, ignore them
|
||||
if found_type not in KNOWN_TYPES and found_type not in unknown_types:
|
||||
unknown_types.append(found_type)
|
||||
@@ -1407,11 +1408,11 @@ def _get_line_with_reprcrash_message(
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
if not running_on_ci():
|
||||
if running_on_ci() or config.option.verbose >= 2:
|
||||
msg = f" - {msg}"
|
||||
else:
|
||||
available_width = tw.fullwidth - line_width
|
||||
msg = _format_trimmed(" - {}", msg, available_width)
|
||||
else:
|
||||
msg = f" - {msg}"
|
||||
if msg is not None:
|
||||
line += msg
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Support for providing temporary directories to test functions."""
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Discover and run std-library "unittest" style tests."""
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
@@ -15,7 +16,6 @@ from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import _pytest._code
|
||||
from _pytest.compat import getimfunc
|
||||
from _pytest.compat import is_async_function
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
@@ -32,6 +32,9 @@ from _pytest.runner import CallInfo
|
||||
import pytest
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import unittest
|
||||
|
||||
@@ -63,6 +66,14 @@ class UnitTestCase(Class):
|
||||
# to declare that our children do not support funcargs.
|
||||
nofuncargs = True
|
||||
|
||||
def newinstance(self):
|
||||
# TestCase __init__ takes the method (test) name. The TestCase
|
||||
# constructor treats the name "runTest" as a special no-op, so it can be
|
||||
# used when a dummy instance is needed. While unittest.TestCase has a
|
||||
# default, some subclasses omit the default (#9610), so always supply
|
||||
# it.
|
||||
return self.obj("runTest")
|
||||
|
||||
def collect(self) -> Iterable[Union[Item, Collector]]:
|
||||
from unittest import TestLoader
|
||||
|
||||
@@ -76,23 +87,22 @@ class UnitTestCase(Class):
|
||||
self._register_unittest_setup_class_fixture(cls)
|
||||
self._register_setup_class_fixture()
|
||||
|
||||
self.session._fixturemanager.parsefactories(self, unittest=True)
|
||||
self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid)
|
||||
|
||||
loader = TestLoader()
|
||||
foundsomething = False
|
||||
for name in loader.getTestCaseNames(self.obj):
|
||||
x = getattr(self.obj, name)
|
||||
if not getattr(x, "__test__", True):
|
||||
continue
|
||||
funcobj = getimfunc(x)
|
||||
yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj)
|
||||
yield TestCaseFunction.from_parent(self, name=name)
|
||||
foundsomething = True
|
||||
|
||||
if not foundsomething:
|
||||
runtest = getattr(self.obj, "runTest", None)
|
||||
if runtest is not None:
|
||||
ut = sys.modules.get("twisted.trial.unittest", None)
|
||||
# Type ignored because `ut` is an opaque module.
|
||||
if ut is None or runtest != ut.TestCase.runTest: # type: ignore
|
||||
if ut is None or runtest != ut.TestCase.runTest:
|
||||
yield TestCaseFunction.from_parent(self, name="runTest")
|
||||
|
||||
def _register_unittest_setup_class_fixture(self, cls: type) -> None:
|
||||
@@ -104,6 +114,20 @@ class UnitTestCase(Class):
|
||||
return None
|
||||
cleanup = getattr(cls, "doClassCleanups", lambda: None)
|
||||
|
||||
def process_teardown_exceptions() -> None:
|
||||
# tearDown_exceptions is a list set in the class containing exc_infos for errors during
|
||||
# teardown for the class.
|
||||
exc_infos = getattr(cls, "tearDown_exceptions", None)
|
||||
if not exc_infos:
|
||||
return
|
||||
exceptions = [exc for (_, exc, _) in exc_infos]
|
||||
# If a single exception, raise it directly as this provides a more readable
|
||||
# error (hopefully this will improve in #12255).
|
||||
if len(exceptions) == 1:
|
||||
raise exceptions[0]
|
||||
else:
|
||||
raise ExceptionGroup("Unittest class cleanup errors", exceptions)
|
||||
|
||||
def unittest_setup_class_fixture(
|
||||
request: FixtureRequest,
|
||||
) -> Generator[None, None, None]:
|
||||
@@ -118,6 +142,7 @@ class UnitTestCase(Class):
|
||||
# follow this here.
|
||||
except Exception:
|
||||
cleanup()
|
||||
process_teardown_exceptions()
|
||||
raise
|
||||
yield
|
||||
try:
|
||||
@@ -125,6 +150,7 @@ class UnitTestCase(Class):
|
||||
teardown()
|
||||
finally:
|
||||
cleanup()
|
||||
process_teardown_exceptions()
|
||||
|
||||
self.session._fixturemanager._register_fixture(
|
||||
# Use a unique name to speed up lookup.
|
||||
@@ -169,23 +195,20 @@ class UnitTestCase(Class):
|
||||
class TestCaseFunction(Function):
|
||||
nofuncargs = True
|
||||
_excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
|
||||
_testcase: Optional["unittest.TestCase"] = None
|
||||
|
||||
def _getobj(self):
|
||||
assert self.parent is not None
|
||||
# Unlike a regular Function in a Class, where `item.obj` returns
|
||||
# a *bound* method (attached to an instance), TestCaseFunction's
|
||||
# `obj` returns an *unbound* method (not attached to an instance).
|
||||
# This inconsistency is probably not desirable, but needs some
|
||||
# consideration before changing.
|
||||
return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined]
|
||||
def _getinstance(self):
|
||||
assert isinstance(self.parent, UnitTestCase)
|
||||
return self.parent.obj(self.name)
|
||||
|
||||
# Backward compat for pytest-django; can be removed after pytest-django
|
||||
# updates + some slack.
|
||||
@property
|
||||
def _testcase(self):
|
||||
return self.instance
|
||||
|
||||
def setup(self) -> None:
|
||||
# A bound method to be called during teardown() if set (see 'runtest()').
|
||||
self._explicit_tearDown: Optional[Callable[[], None]] = None
|
||||
assert self.parent is not None
|
||||
self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined]
|
||||
self._obj = getattr(self._testcase, self.name)
|
||||
super().setup()
|
||||
|
||||
def teardown(self) -> None:
|
||||
@@ -193,7 +216,6 @@ class TestCaseFunction(Function):
|
||||
if self._explicit_tearDown is not None:
|
||||
self._explicit_tearDown()
|
||||
self._explicit_tearDown = None
|
||||
self._testcase = None
|
||||
self._obj = None
|
||||
|
||||
def startTest(self, testcase: "unittest.TestCase") -> None:
|
||||
@@ -292,14 +314,14 @@ class TestCaseFunction(Function):
|
||||
def runtest(self) -> None:
|
||||
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
||||
|
||||
assert self._testcase is not None
|
||||
testcase = self.instance
|
||||
assert testcase is not None
|
||||
|
||||
maybe_wrap_pytest_function_for_tracing(self)
|
||||
|
||||
# Let the unittest framework handle async functions.
|
||||
if is_async_function(self.obj):
|
||||
# Type ignored because self acts as the TestResult, but is not actually one.
|
||||
self._testcase(result=self) # type: ignore[arg-type]
|
||||
testcase(result=self)
|
||||
else:
|
||||
# When --pdb is given, we want to postpone calling tearDown() otherwise
|
||||
# when entering the pdb prompt, tearDown() would have probably cleaned up
|
||||
@@ -311,16 +333,16 @@ class TestCaseFunction(Function):
|
||||
assert isinstance(self.parent, UnitTestCase)
|
||||
skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj)
|
||||
if self.config.getoption("usepdb") and not skipped:
|
||||
self._explicit_tearDown = self._testcase.tearDown
|
||||
setattr(self._testcase, "tearDown", lambda *args: None)
|
||||
self._explicit_tearDown = testcase.tearDown
|
||||
setattr(testcase, "tearDown", lambda *args: None)
|
||||
|
||||
# We need to update the actual bound method with self.obj, because
|
||||
# wrap_pytest_function_for_tracing replaces self.obj by a wrapper.
|
||||
setattr(self._testcase, self.name, self.obj)
|
||||
setattr(testcase, self.name, self.obj)
|
||||
try:
|
||||
self._testcase(result=self) # type: ignore[arg-type]
|
||||
testcase(result=self)
|
||||
finally:
|
||||
delattr(self._testcase, self.name)
|
||||
delattr(testcase, self.name)
|
||||
|
||||
def _traceback_filter(
|
||||
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
|
||||
@@ -349,9 +371,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
|
||||
# its own nose.SkipTest. For unittest TestCases, SkipTest is already
|
||||
# handled internally, and doesn't reach here.
|
||||
unittest = sys.modules.get("unittest")
|
||||
if (
|
||||
unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined]
|
||||
):
|
||||
if unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest):
|
||||
excinfo = call.excinfo
|
||||
call2 = CallInfo[None].from_call(
|
||||
lambda: pytest.skip(str(excinfo.value)), call.when
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
"""pytest: unit and functional testing with Python."""
|
||||
|
||||
from _pytest import __version__
|
||||
from _pytest import version_tuple
|
||||
from _pytest._code import ExceptionInfo
|
||||
|
||||
@@ -16,8 +16,8 @@ import pytest
|
||||
@contextlib.contextmanager
|
||||
def ignore_encoding_warning():
|
||||
with warnings.catch_warnings():
|
||||
with contextlib.suppress(NameError): # new in 3.10
|
||||
warnings.simplefilter("ignore", EncodingWarning) # type: ignore [name-defined]
|
||||
if sys.version_info > (3, 10):
|
||||
warnings.simplefilter("ignore", EncodingWarning)
|
||||
yield
|
||||
|
||||
|
||||
@@ -822,7 +822,7 @@ class TestLocalPath(CommonFSTests):
|
||||
# depending on how the paths are used), but > 4096 (which is the
|
||||
# Linux' limitation) - the behaviour of paths with names > 4096 chars
|
||||
# is undetermined
|
||||
newfilename = "/test" * 60 # type:ignore[unreachable]
|
||||
newfilename = "/test" * 60 # type:ignore[unreachable,unused-ignore]
|
||||
l1 = tmpdir.join(newfilename)
|
||||
l1.ensure(file=True)
|
||||
l1.write_text("foo", encoding="utf-8")
|
||||
@@ -1368,8 +1368,8 @@ class TestPOSIXLocalPath:
|
||||
assert realpath.basename == "file"
|
||||
|
||||
def test_owner(self, path1, tmpdir):
|
||||
from grp import getgrgid # type:ignore[attr-defined]
|
||||
from pwd import getpwuid # type:ignore[attr-defined]
|
||||
from grp import getgrgid # type:ignore[attr-defined,unused-ignore]
|
||||
from pwd import getpwuid # type:ignore[attr-defined,unused-ignore]
|
||||
|
||||
stat = path1.stat()
|
||||
assert stat.path == path1
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import dataclasses
|
||||
import importlib.metadata
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import types
|
||||
@@ -541,6 +542,32 @@ class TestGeneralUsage:
|
||||
res = pytester.runpytest(p)
|
||||
res.assert_outcomes(passed=3)
|
||||
|
||||
# Warning ignore because of:
|
||||
# https://github.com/python/cpython/issues/85308
|
||||
# Can be removed once Python<3.12 support is dropped.
|
||||
@pytest.mark.filterwarnings("ignore:'encoding' argument not specified")
|
||||
def test_command_line_args_from_file(
|
||||
self, pytester: Pytester, tmp_path: Path
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
test_file="""
|
||||
import pytest
|
||||
|
||||
class TestClass:
|
||||
@pytest.mark.parametrize("a", ["x","y"])
|
||||
def test_func(self, a):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
tests = [
|
||||
"test_file.py::TestClass::test_func[x]",
|
||||
"test_file.py::TestClass::test_func[y]",
|
||||
"-q",
|
||||
]
|
||||
args_file = pytester.maketxtfile(tests="\n".join(tests))
|
||||
result = pytester.runpytest(f"@{args_file}")
|
||||
result.assert_outcomes(failed=0, passed=2)
|
||||
|
||||
|
||||
class TestInvocationVariants:
|
||||
def test_earlyinit(self, pytester: Pytester) -> None:
|
||||
|
||||
@@ -28,7 +28,7 @@ import pytest
|
||||
if TYPE_CHECKING:
|
||||
from _pytest._code.code import _TracebackStyle
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
if sys.version_info < (3, 11):
|
||||
from exceptiongroup import ExceptionGroup
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ class TestTraceback_f_g_h:
|
||||
def test_traceback_cut_excludepath(self, pytester: Pytester) -> None:
|
||||
p = pytester.makepyfile("def f(): raise ValueError")
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
import_path(p, root=pytester.path, consider_namespace_packages=False).f() # type: ignore[attr-defined]
|
||||
import_path(p, root=pytester.path, consider_namespace_packages=False).f()
|
||||
basedir = Path(pytest.__file__).parent
|
||||
newtraceback = excinfo.traceback.cut(excludepath=basedir)
|
||||
for x in newtraceback:
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
from _pytest import deprecated
|
||||
from _pytest.compat import legacy_path
|
||||
from _pytest.pytester import Pytester
|
||||
import pytest
|
||||
from pytest import PytestDeprecationWarning
|
||||
@@ -85,6 +90,82 @@ def test_private_is_deprecated() -> None:
|
||||
PrivateInit(10, _ispytest=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hooktype", ["hook", "ihook"])
|
||||
def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request):
|
||||
path = legacy_path(tmp_path)
|
||||
|
||||
PATH_WARN_MATCH = r".*path: py\.path\.local\) argument is deprecated, please use \(collection_path: pathlib\.Path.*"
|
||||
if hooktype == "ihook":
|
||||
hooks = request.node.ihook
|
||||
else:
|
||||
hooks = request.config.hook
|
||||
|
||||
with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
|
||||
l1 = sys._getframe().f_lineno
|
||||
hooks.pytest_ignore_collect(
|
||||
config=request.config, path=path, collection_path=tmp_path
|
||||
)
|
||||
l2 = sys._getframe().f_lineno
|
||||
|
||||
(record,) = r
|
||||
assert record.filename == __file__
|
||||
assert l1 < record.lineno < l2
|
||||
|
||||
hooks.pytest_ignore_collect(config=request.config, collection_path=tmp_path)
|
||||
|
||||
# Passing entirely *different* paths is an outright error.
|
||||
with pytest.raises(ValueError, match=r"path.*fspath.*need to be equal"):
|
||||
with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
|
||||
hooks.pytest_ignore_collect(
|
||||
config=request.config, path=path, collection_path=Path("/bla/bla")
|
||||
)
|
||||
|
||||
|
||||
def test_hookimpl_warnings_for_pathlib() -> None:
|
||||
class Plugin:
|
||||
def pytest_ignore_collect(self, path: object) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def pytest_collect_file(self, path: object) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def pytest_pycollect_makemodule(self, path: object) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def pytest_report_header(self, startdir: object) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
def pytest_report_collectionfinish(self, startdir: object) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
pm = pytest.PytestPluginManager()
|
||||
with pytest.warns(
|
||||
pytest.PytestRemovedIn9Warning,
|
||||
match=r"py\.path\.local.* argument is deprecated",
|
||||
) as wc:
|
||||
pm.register(Plugin())
|
||||
assert len(wc.list) == 5
|
||||
|
||||
|
||||
def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None:
|
||||
mod = pytester.getmodulecol("")
|
||||
|
||||
class MyFile(pytest.File):
|
||||
def collect(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
with pytest.warns(
|
||||
pytest.PytestDeprecationWarning,
|
||||
match=re.escape(
|
||||
"The (fspath: py.path.local) argument to MyFile is deprecated."
|
||||
),
|
||||
):
|
||||
MyFile.from_parent(
|
||||
parent=mod.parent,
|
||||
fspath=legacy_path("bla"),
|
||||
)
|
||||
|
||||
|
||||
def test_fixture_disallow_on_marked_functions():
|
||||
"""Test that applying @pytest.fixture to a marked function warns (#3364)."""
|
||||
with pytest.warns(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user