somewhat messy merge + minor updates to docstring/comments after review
This commit is contained in:
commit
fa853dfd97
|
@ -47,7 +47,7 @@ jobs:
|
||||||
path: dist
|
path: dist
|
||||||
|
|
||||||
- name: Publish package to PyPI
|
- 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
|
- name: Push tag
|
||||||
run: |
|
run: |
|
||||||
|
@ -94,7 +94,7 @@ jobs:
|
||||||
tox -e generate-gh-release-notes -- ${{ github.event.inputs.version }} scripts/latest-release-notes.md
|
tox -e generate-gh-release-notes -- ${{ github.event.inputs.version }} scripts/latest-release-notes.md
|
||||||
|
|
||||||
- name: Publish GitHub Release
|
- name: Publish GitHub Release
|
||||||
uses: softprops/action-gh-release@v1
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
body_path: scripts/latest-release-notes.md
|
body_path: scripts/latest-release-notes.md
|
||||||
files: dist/*
|
files: dist/*
|
||||||
|
|
|
@ -51,6 +51,7 @@ coverage.xml
|
||||||
.settings
|
.settings
|
||||||
.vscode
|
.vscode
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
.python-version
|
||||||
|
|
||||||
# generated by pip
|
# generated by pip
|
||||||
pip-wheel-metadata/
|
pip-wheel-metadata/
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: "v0.2.2"
|
rev: "v0.3.2"
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
args: ["--fix"]
|
args: ["--fix"]
|
||||||
|
@ -26,7 +26,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-use-type-annotations
|
- id: python-use-type-annotations
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.8.0
|
rev: v1.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/|scripts/)
|
files: ^(src/|testing/|scripts/)
|
||||||
|
|
2
AUTHORS
2
AUTHORS
|
@ -235,6 +235,7 @@ Kyle Altendorf
|
||||||
Lawrence Mitchell
|
Lawrence Mitchell
|
||||||
Lee Kamentsky
|
Lee Kamentsky
|
||||||
Lev Maximov
|
Lev Maximov
|
||||||
|
Levon Saldamli
|
||||||
Lewis Cowles
|
Lewis Cowles
|
||||||
Llandy Riveron Del Risco
|
Llandy Riveron Del Risco
|
||||||
Loic Esteve
|
Loic Esteve
|
||||||
|
@ -283,6 +284,7 @@ Mike Hoyle (hoylemd)
|
||||||
Mike Lundy
|
Mike Lundy
|
||||||
Milan Lesnek
|
Milan Lesnek
|
||||||
Miro Hrončok
|
Miro Hrončok
|
||||||
|
mrbean-bremen
|
||||||
Nathaniel Compton
|
Nathaniel Compton
|
||||||
Nathaniel Waisbrot
|
Nathaniel Waisbrot
|
||||||
Ned Batchelder
|
Ned Batchelder
|
||||||
|
|
|
@ -297,12 +297,12 @@ Here is a simple overview, with pytest-specific bits:
|
||||||
When committing, ``pre-commit`` will re-format the files if necessary.
|
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
|
#. 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
|
$ python3 -m venv .venv
|
||||||
$ source .venv/bin/activate # Linux
|
$ source .venv/bin/activate # Linux
|
||||||
$ .venv/Scripts/activate.bat # Windows
|
$ .venv/Scripts/activate.bat # Windows
|
||||||
$ pip install -e ".[testing]"
|
$ pip install -e ".[dev]"
|
||||||
|
|
||||||
Afterwards, you can edit the files and run pytest normally::
|
Afterwards, you can edit the files and run pytest normally::
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ The current list of contributors receiving funding are:
|
||||||
|
|
||||||
* `@nicoddemus`_
|
* `@nicoddemus`_
|
||||||
* `@The-Compiler`_
|
* `@The-Compiler`_
|
||||||
|
* `@RonnyPfannschmidt`_
|
||||||
|
|
||||||
Contributors interested in receiving a part of the funds just need to submit a PR adding their
|
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
|
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
|
.. _`@nicoddemus`: https://github.com/nicoddemus
|
||||||
.. _`@The-Compiler`: https://github.com/The-Compiler
|
.. _`@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,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.
|
|
|
@ -0,0 +1 @@
|
||||||
|
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.
|
|
@ -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.
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
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``.
|
|
@ -6,6 +6,8 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-8.1.1
|
||||||
|
release-8.1.0
|
||||||
release-8.0.2
|
release-8.0.2
|
||||||
release-8.0.1
|
release-8.0.1
|
||||||
release-8.0.0
|
release-8.0.0
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
|
|
||||||
Values can be any object handled by the json stdlib module.
|
Values can be any object handled by the json stdlib module.
|
||||||
|
|
||||||
capsysbinary -- .../_pytest/capture.py:1007
|
capsysbinary -- .../_pytest/capture.py:1008
|
||||||
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||||
|
|
||||||
The captured output is made available via ``capsysbinary.readouterr()``
|
The captured output is made available via ``capsysbinary.readouterr()``
|
||||||
|
@ -50,7 +50,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
captured = capsysbinary.readouterr()
|
captured = capsysbinary.readouterr()
|
||||||
assert captured.out == b"hello\n"
|
assert captured.out == b"hello\n"
|
||||||
|
|
||||||
capfd -- .../_pytest/capture.py:1034
|
capfd -- .../_pytest/capture.py:1035
|
||||||
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||||
|
|
||||||
The captured output is made available via ``capfd.readouterr()`` method
|
The captured output is made available via ``capfd.readouterr()`` method
|
||||||
|
@ -67,7 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
captured = capfd.readouterr()
|
captured = capfd.readouterr()
|
||||||
assert captured.out == "hello\n"
|
assert captured.out == "hello\n"
|
||||||
|
|
||||||
capfdbinary -- .../_pytest/capture.py:1061
|
capfdbinary -- .../_pytest/capture.py:1062
|
||||||
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||||
|
|
||||||
The captured output is made available via ``capfd.readouterr()`` method
|
The captured output is made available via ``capfd.readouterr()`` method
|
||||||
|
@ -84,7 +84,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
captured = capfdbinary.readouterr()
|
captured = capfdbinary.readouterr()
|
||||||
assert captured.out == b"hello\n"
|
assert captured.out == b"hello\n"
|
||||||
|
|
||||||
capsys -- .../_pytest/capture.py:980
|
capsys -- .../_pytest/capture.py:981
|
||||||
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||||
|
|
||||||
The captured output is made available via ``capsys.readouterr()`` method
|
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()
|
captured = capsys.readouterr()
|
||||||
assert captured.out == "hello\n"
|
assert captured.out == "hello\n"
|
||||||
|
|
||||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:745
|
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
|
||||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||||
namespace of doctests.
|
namespace of doctests.
|
||||||
|
|
||||||
|
@ -115,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
|
|
||||||
For more details: :ref:`doctest_namespace`.
|
For more details: :ref:`doctest_namespace`.
|
||||||
|
|
||||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1354
|
pytestconfig [session scope] -- .../_pytest/fixtures.py:1346
|
||||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||||
object.
|
object.
|
||||||
|
|
||||||
|
@ -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
|
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
||||||
:issue:`7767` for details.
|
:issue:`7767` for details.
|
||||||
|
|
||||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
|
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:303
|
||||||
Return a :class:`pytest.TempdirFactory` instance for the test session.
|
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
|
Return a temporary directory path object which is unique to each test
|
||||||
function invocation, created as a sub directory of the base temporary
|
function invocation, created as a sub directory of the base temporary
|
||||||
directory.
|
directory.
|
||||||
|
|
||||||
By default, a new base temporary directory is created each test session,
|
By default, a new base temporary directory is created each test session,
|
||||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
``--basetemp`` is used then it is cleared each session. See
|
||||||
temporary directory`.
|
:ref:`temporary directory location and retention`.
|
||||||
|
|
||||||
The returned object is a `legacy_path`_ object.
|
The returned object is a `legacy_path`_ object.
|
||||||
|
|
||||||
|
@ -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
|
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||||
|
|
||||||
caplog -- .../_pytest/logging.py:594
|
caplog -- .../_pytest/logging.py:601
|
||||||
Access and control log capturing.
|
Access and control log capturing.
|
||||||
|
|
||||||
Captured logs are available through the following properties/methods::
|
Captured logs are available through the following properties/methods::
|
||||||
|
@ -227,7 +227,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
To undo modifications done by the fixture in a contained scope,
|
To undo modifications done by the fixture in a contained scope,
|
||||||
use :meth:`context() <pytest.MonkeyPatch.context>`.
|
use :meth:`context() <pytest.MonkeyPatch.context>`.
|
||||||
|
|
||||||
recwarn -- .../_pytest/recwarn.py:32
|
recwarn -- .../_pytest/recwarn.py:31
|
||||||
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
|
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
|
||||||
|
|
||||||
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
||||||
|
@ -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.
|
and old bases are removed after 3 sessions, to aid in debugging.
|
||||||
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||||
:confval:`tmp_path_retention_policy`.
|
:confval:`tmp_path_retention_policy`.
|
||||||
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
|
If ``--basetemp`` is used then it is cleared each session. See
|
||||||
temporary directory`.
|
:ref:`temporary directory location and retention`.
|
||||||
|
|
||||||
The returned object is a :class:`pathlib.Path` object.
|
The returned object is a :class:`pathlib.Path` object.
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,127 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
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)
|
pytest 8.0.2 (2024-02-24)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
|
|
@ -200,6 +200,7 @@ nitpick_ignore = [
|
||||||
("py:class", "_tracing.TagTracerSub"),
|
("py:class", "_tracing.TagTracerSub"),
|
||||||
("py:class", "warnings.WarningMessage"),
|
("py:class", "warnings.WarningMessage"),
|
||||||
# Undocumented type aliases
|
# Undocumented type aliases
|
||||||
|
("py:class", "LEGACY_PATH"),
|
||||||
("py:class", "_PluggyPlugin"),
|
("py:class", "_PluggyPlugin"),
|
||||||
# TypeVars
|
# TypeVars
|
||||||
("py:class", "_pytest._code.code.E"),
|
("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.
|
# The format is a list of tuples containing the path and title.
|
||||||
# epub_pre_files = []
|
# 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.
|
# The format is a list of tuples containing the path and title.
|
||||||
# epub_post_files = []
|
# epub_post_files = []
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,45 @@ 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>`.
|
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
|
||||||
|
|
||||||
|
|
||||||
.. _legacy-path-hooks-deprecated:
|
.. _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
|
Configuring hook specs/impls using markers
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -62,6 +100,33 @@ Changed ``hookwrapper`` attributes:
|
||||||
* ``historic``
|
* ``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
|
Directly constructing internal classes
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -208,73 +273,6 @@ an appropriate period of deprecation has passed.
|
||||||
|
|
||||||
Some breaking changes which could not be deprecated are also listed.
|
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:
|
.. _nose-deprecation:
|
||||||
|
|
||||||
Support for tests written for nose
|
Support for tests written for nose
|
||||||
|
|
|
@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 8 items
|
collected 8 items
|
||||||
|
|
||||||
<Dir parametrize.rst-194>
|
<Dir parametrize.rst-196>
|
||||||
<Module test_time.py>
|
<Module test_time.py>
|
||||||
<Function test_timedistance_v0[a0-b0-expected0]>
|
<Function test_timedistance_v0[a0-b0-expected0]>
|
||||||
<Function test_timedistance_v0[a1-b1-expected1]>
|
<Function test_timedistance_v0[a1-b1-expected1]>
|
||||||
|
@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 4 items
|
collected 4 items
|
||||||
|
|
||||||
<Dir parametrize.rst-194>
|
<Dir parametrize.rst-196>
|
||||||
<Module test_scenarios.py>
|
<Module test_scenarios.py>
|
||||||
<Class TestSampleWithScenarios>
|
<Class TestSampleWithScenarios>
|
||||||
<Function test_demo1[basic]>
|
<Function test_demo1[basic]>
|
||||||
|
@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
<Dir parametrize.rst-194>
|
<Dir parametrize.rst-196>
|
||||||
<Module test_backends.py>
|
<Module test_backends.py>
|
||||||
<Function test_db_initialized[d1]>
|
<Function test_db_initialized[d1]>
|
||||||
<Function test_db_initialized[d2]>
|
<Function test_db_initialized[d2]>
|
||||||
|
|
|
@ -152,7 +152,7 @@ The test collection would look like this:
|
||||||
configfile: pytest.ini
|
configfile: pytest.ini
|
||||||
collected 2 items
|
collected 2 items
|
||||||
|
|
||||||
<Dir pythoncollection.rst-195>
|
<Dir pythoncollection.rst-197>
|
||||||
<Module check_myapp.py>
|
<Module check_myapp.py>
|
||||||
<Class CheckMyApp>
|
<Class CheckMyApp>
|
||||||
<Function simple_check>
|
<Function simple_check>
|
||||||
|
@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
|
||||||
configfile: pytest.ini
|
configfile: pytest.ini
|
||||||
collected 3 items
|
collected 3 items
|
||||||
|
|
||||||
<Dir pythoncollection.rst-195>
|
<Dir pythoncollection.rst-197>
|
||||||
<Dir CWD>
|
<Dir CWD>
|
||||||
<Module pythoncollection.py>
|
<Module pythoncollection.py>
|
||||||
<Function test_function>
|
<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>
|
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
||||||
|
|
||||||
def test_tupleerror(self):
|
def test_tupleerror(self):
|
||||||
> a, b = [1] # NOQA
|
> a, b = [1] # noqa: F841
|
||||||
E ValueError: not enough values to unpack (expected 2, got 1)
|
E ValueError: not enough values to unpack (expected 2, got 1)
|
||||||
|
|
||||||
failure_demo.py:175: ValueError
|
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>
|
self = <failure_demo.TestRaises object at 0xdeadbeef0022>
|
||||||
|
|
||||||
def test_some_error(self):
|
def test_some_error(self):
|
||||||
> if namenotexi: # NOQA
|
> if namenotexi: # noqa: F821
|
||||||
E NameError: name 'namenotexi' is not defined
|
E NameError: name 'namenotexi' is not defined
|
||||||
|
|
||||||
failure_demo.py:183: NameError
|
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:
|
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-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-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:
|
Plugins to deliberately randomize tests can help expose tests with state problems:
|
||||||
|
|
||||||
|
|
|
@ -60,8 +60,10 @@ Within Python modules, ``pytest`` also discovers tests using the standard
|
||||||
:ref:`unittest.TestCase <unittest.TestCase>` subclassing technique.
|
:ref:`unittest.TestCase <unittest.TestCase>` subclassing technique.
|
||||||
|
|
||||||
|
|
||||||
Choosing a test layout / import rules
|
.. _`test layout`:
|
||||||
-------------------------------------
|
|
||||||
|
Choosing a test layout
|
||||||
|
----------------------
|
||||||
|
|
||||||
``pytest`` supports two common test layouts:
|
``pytest`` supports two common test layouts:
|
||||||
|
|
||||||
|
|
|
@ -10,19 +10,27 @@ Import modes
|
||||||
|
|
||||||
pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution.
|
pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution.
|
||||||
|
|
||||||
Importing files in Python (at least until recently) is a non-trivial processes, often requiring
|
Importing files in Python is a non-trivial processes, so aspects of the
|
||||||
changing :data:`sys.path`. Some aspects of the
|
|
||||||
import process can be controlled through the ``--import-mode`` command-line flag, which can assume
|
import process can be controlled through the ``--import-mode`` command-line flag, which can assume
|
||||||
these values:
|
these values:
|
||||||
|
|
||||||
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
.. _`import-mode-prepend`:
|
||||||
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
|
|
||||||
|
|
||||||
This requires test module names to be unique when the test directory tree is not arranged in
|
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
||||||
packages, because the modules will put in :py:data:`sys.modules` after importing.
|
of :py:data:`sys.path` if not already there, and then imported with
|
||||||
|
the :func:`importlib.import_module <importlib.import_module>` function.
|
||||||
|
|
||||||
|
It is highly recommended to arrange your test modules as packages by adding ``__init__.py`` files to your directories
|
||||||
|
containing tests. This will make the tests part of a proper Python package, allowing pytest to resolve their full
|
||||||
|
name (for example ``tests.core.test_core`` for ``test_core.py`` inside the ``tests.core`` package).
|
||||||
|
|
||||||
|
If the test directory tree is not arranged as packages, then each test file needs to have a unique name
|
||||||
|
compared to the other test files, otherwise pytest will raise an error if it finds two tests with the same name.
|
||||||
|
|
||||||
This is the classic mechanism, dating back from the time Python 2 was still supported.
|
This is the classic mechanism, dating back from the time Python 2 was still supported.
|
||||||
|
|
||||||
|
.. _`import-mode-append`:
|
||||||
|
|
||||||
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
||||||
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
||||||
|
|
||||||
|
@ -38,32 +46,78 @@ these values:
|
||||||
the tests will run against the installed version
|
the tests will run against the installed version
|
||||||
of ``pkg_under_test`` when ``--import-mode=append`` is used whereas
|
of ``pkg_under_test`` when ``--import-mode=append`` is used whereas
|
||||||
with ``prepend`` they would pick up the local version. This kind of confusion is why
|
with ``prepend`` they would pick up the local version. This kind of confusion is why
|
||||||
we advocate for using :ref:`src <src-layout>` layouts.
|
we advocate for using :ref:`src-layouts <src-layout>`.
|
||||||
|
|
||||||
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
||||||
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
|
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
|
||||||
|
|
||||||
* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
|
.. _`import-mode-importlib`:
|
||||||
|
|
||||||
For this reason this doesn't require test module names to be unique.
|
* ``importlib``: this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules, without changing :py:data:`sys.path`.
|
||||||
|
|
||||||
One drawback however is that test modules are non-importable by each other. Also, utility
|
Advantages of this mode:
|
||||||
modules in the tests directories are not automatically importable because the tests directory is no longer
|
|
||||||
added to :py:data:`sys.path`.
|
|
||||||
|
|
||||||
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
|
* pytest will not change :py:data:`sys.path` at all.
|
||||||
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
|
* Test module names do not need to be unique -- pytest will generate a unique name automatically based on the ``rootdir``.
|
||||||
|
|
||||||
|
Disadvantages:
|
||||||
|
|
||||||
|
* Test modules can't import each other.
|
||||||
|
* Testing utility modules in the tests directories (for example a ``tests.helpers`` module containing test-related functions/classes)
|
||||||
|
are not importable. The recommendation in this case it to place testing utility modules together with the application/library
|
||||||
|
code, for example ``app.testing.helpers``.
|
||||||
|
|
||||||
|
Important: by "test utility modules" we mean functions/classes which are imported by
|
||||||
|
other tests directly; this does not include fixtures, which should be placed in ``conftest.py`` files, along
|
||||||
|
with the test modules, and are discovered automatically by pytest.
|
||||||
|
|
||||||
|
It works like this:
|
||||||
|
|
||||||
|
1. Given a certain module path, for example ``tests/core/test_models.py``, derives a canonical name
|
||||||
|
like ``tests.core.test_models`` and tries to import it.
|
||||||
|
|
||||||
|
For non-test modules this will work if they are accessible via :py:data:`sys.path`, so
|
||||||
|
for example ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``.
|
||||||
|
This is happens when plugins import non-test modules (for example doctesting).
|
||||||
|
|
||||||
|
If this step succeeds, the module is returned.
|
||||||
|
|
||||||
|
For test modules, unless they are reachable from :py:data:`sys.path`, this step will fail.
|
||||||
|
|
||||||
|
2. If the previous step fails, we import the module directly using ``importlib`` facilities, which lets us import it without
|
||||||
|
changing :py:data:`sys.path`.
|
||||||
|
|
||||||
|
Because Python requires the module to also be available in :py:data:`sys.modules`, pytest derives a unique name for it based
|
||||||
|
on its relative location from the ``rootdir``, and adds the module to :py:data:`sys.modules`.
|
||||||
|
|
||||||
|
For example, ``tests/core/test_models.py`` will end up being imported as the module ``tests.core.test_models``.
|
||||||
|
|
||||||
|
.. versionadded:: 6.0
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
|
||||||
|
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
By default, pytest will not attempt to resolve namespace packages automatically, but that can
|
||||||
|
be changed via the :confval:`consider_namespace_packages` configuration variable.
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
|
|
||||||
The :confval:`pythonpath` configuration variable.
|
The :confval:`pythonpath` configuration variable.
|
||||||
|
|
||||||
|
The :confval:`consider_namespace_packages` configuration variable.
|
||||||
|
|
||||||
|
:ref:`test layout`.
|
||||||
|
|
||||||
|
|
||||||
``prepend`` and ``append`` import modes scenarios
|
``prepend`` and ``append`` import modes scenarios
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
|
|
||||||
Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to
|
Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to
|
||||||
change ``sys.path`` in order to import test modules or ``conftest.py`` files, and the issues users
|
change :py:data:`sys.path` in order to import test modules or ``conftest.py`` files, and the issues users
|
||||||
might encounter because of that.
|
might encounter because of that.
|
||||||
|
|
||||||
Test modules / ``conftest.py`` files inside packages
|
Test modules / ``conftest.py`` files inside packages
|
||||||
|
@ -92,7 +146,7 @@ pytest will find ``foo/bar/tests/test_foo.py`` and realize it is part of a packa
|
||||||
there's an ``__init__.py`` file in the same folder. It will then search upwards until it can find the
|
there's an ``__init__.py`` file in the same folder. It will then search upwards until it can find the
|
||||||
last folder which still contains an ``__init__.py`` file in order to find the package *root* (in
|
last folder which still contains an ``__init__.py`` file in order to find the package *root* (in
|
||||||
this case ``foo/``). To load the module, it will insert ``root/`` to the front of
|
this case ``foo/``). To load the module, it will insert ``root/`` to the front of
|
||||||
``sys.path`` (if not there already) in order to load
|
:py:data:`sys.path` (if not there already) in order to load
|
||||||
``test_foo.py`` as the *module* ``foo.bar.tests.test_foo``.
|
``test_foo.py`` as the *module* ``foo.bar.tests.test_foo``.
|
||||||
|
|
||||||
The same logic applies to the ``conftest.py`` file: it will be imported as ``foo.conftest`` module.
|
The same logic applies to the ``conftest.py`` file: it will be imported as ``foo.conftest`` module.
|
||||||
|
@ -122,8 +176,8 @@ When executing:
|
||||||
|
|
||||||
pytest will find ``foo/bar/tests/test_foo.py`` and realize it is NOT part of a package given that
|
pytest will find ``foo/bar/tests/test_foo.py`` and realize it is NOT part of a package given that
|
||||||
there's no ``__init__.py`` file in the same folder. It will then add ``root/foo/bar/tests`` to
|
there's no ``__init__.py`` file in the same folder. It will then add ``root/foo/bar/tests`` to
|
||||||
``sys.path`` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done
|
:py:data:`sys.path` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done
|
||||||
with the ``conftest.py`` file by adding ``root/foo`` to ``sys.path`` to import it as ``conftest``.
|
with the ``conftest.py`` file by adding ``root/foo`` to :py:data:`sys.path` to import it as ``conftest``.
|
||||||
|
|
||||||
For this reason this layout cannot have test modules with the same name, as they all will be
|
For this reason this layout cannot have test modules with the same name, as they all will be
|
||||||
imported in the global import namespace.
|
imported in the global import namespace.
|
||||||
|
@ -136,7 +190,7 @@ Invoking ``pytest`` versus ``python -m pytest``
|
||||||
-----------------------------------------------
|
-----------------------------------------------
|
||||||
|
|
||||||
Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly
|
Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly
|
||||||
equivalent behaviour, except that the latter will add the current directory to ``sys.path``, which
|
equivalent behaviour, except that the latter will add the current directory to :py:data:`sys.path`, which
|
||||||
is standard ``python`` behavior.
|
is standard ``python`` behavior.
|
||||||
|
|
||||||
See also :ref:`invoke-python`.
|
See also :ref:`invoke-python`.
|
||||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 8.0.2
|
pytest 8.1.1
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|
|
@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
|
||||||
rootdir: /home/sweet/project
|
rootdir: /home/sweet/project
|
||||||
collected 12 items
|
collected 12 items
|
||||||
|
|
||||||
<Dir fixtures.rst-213>
|
<Dir fixtures.rst-215>
|
||||||
<Module test_anothersmtp.py>
|
<Module test_anothersmtp.py>
|
||||||
<Function test_showhelo[smtp.gmail.com]>
|
<Function test_showhelo[smtp.gmail.com]>
|
||||||
<Function test_showhelo[mail.python.org]>
|
<Function test_showhelo[mail.python.org]>
|
||||||
|
|
|
@ -17,7 +17,8 @@ in the current directory and its subdirectories. More generally, pytest follows
|
||||||
Specifying which tests to run
|
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**
|
**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.
|
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
|
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
|
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,
|
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
|
For more information, consult the
|
||||||
:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.
|
:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.
|
||||||
|
|
|
@ -87,7 +87,7 @@ Features
|
||||||
Documentation
|
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:`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:`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
|
* :ref:`Explanation <explanation>` - background, discussion of key topics, answers to higher-level questions
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1274,6 +1274,19 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
variables, that will be expanded. For more information about cache plugin
|
variables, that will be expanded. For more information about cache plugin
|
||||||
please refer to :ref:`cache_provider`.
|
please refer to :ref:`cache_provider`.
|
||||||
|
|
||||||
|
.. confval:: consider_namespace_packages
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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>`__.
|
||||||
|
|
||||||
|
.. versionadded:: 8.1
|
||||||
|
|
||||||
.. confval:: console_output_style
|
.. confval:: console_output_style
|
||||||
|
|
||||||
Sets the console output style while running tests:
|
Sets the console output style while running tests:
|
||||||
|
@ -2090,6 +2103,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
--log-cli-date-format=LOG_CLI_DATE_FORMAT
|
--log-cli-date-format=LOG_CLI_DATE_FORMAT
|
||||||
Log date format used by the logging module
|
Log date format used by the logging module
|
||||||
--log-file=LOG_FILE Path to a file when logging will be written to
|
--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-level=LOG_FILE_LEVEL
|
||||||
Log file logging level
|
Log file logging level
|
||||||
--log-file-format=LOG_FILE_FORMAT
|
--log-file-format=LOG_FILE_FORMAT
|
||||||
|
@ -2115,6 +2130,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
Each line specifies a pattern for
|
Each line specifies a pattern for
|
||||||
warnings.filterwarnings. Processed after
|
warnings.filterwarnings. Processed after
|
||||||
-W/--pythonwarnings.
|
-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
|
usefixtures (args): List of default fixtures to be used with this
|
||||||
project
|
project
|
||||||
python_files (args): Glob-style file patterns for Python test module
|
python_files (args): Glob-style file patterns for Python test module
|
||||||
|
@ -2133,6 +2151,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
progress information ("progress" (percentage) |
|
progress information ("progress" (percentage) |
|
||||||
"count" | "progress-even-when-capture-no" (forces
|
"count" | "progress-even-when-capture-no" (forces
|
||||||
progress even when capture=no)
|
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
|
xfail_strict (bool): Default for the strict parameter of xfail markers
|
||||||
when not given explicitly (default: False)
|
when not given explicitly (default: False)
|
||||||
tmp_path_retention_count (string):
|
tmp_path_retention_count (string):
|
||||||
|
@ -2180,6 +2203,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
log_cli_date_format (string):
|
log_cli_date_format (string):
|
||||||
Default value for --log-cli-date-format
|
Default value for --log-cli-date-format
|
||||||
log_file (string): Default value for --log-file
|
log_file (string): Default value for --log-file
|
||||||
|
log_file_mode (string):
|
||||||
|
Default value for --log-file-mode
|
||||||
log_file_level (string):
|
log_file_level (string):
|
||||||
Default value for --log-file-level
|
Default value for --log-file-level
|
||||||
log_file_format (string):
|
log_file_format (string):
|
||||||
|
|
|
@ -47,7 +47,7 @@ dependencies = [
|
||||||
'tomli>=1; python_version < "3.11"',
|
'tomli>=1; python_version < "3.11"',
|
||||||
]
|
]
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
testing = [
|
dev = [
|
||||||
"argcomplete",
|
"argcomplete",
|
||||||
"attrs>=19.2",
|
"attrs>=19.2",
|
||||||
"hypothesis>=3.56",
|
"hypothesis>=3.56",
|
||||||
|
@ -281,6 +281,7 @@ template = "changelog/_template.rst"
|
||||||
showcontent = true
|
showcontent = true
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
|
files = ["src", "testing", "scripts"]
|
||||||
mypy_path = ["src"]
|
mypy_path = ["src"]
|
||||||
check_untyped_defs = true
|
check_untyped_defs = true
|
||||||
disallow_any_generics = true
|
disallow_any_generics = true
|
||||||
|
@ -293,3 +294,4 @@ warn_return_any = true
|
||||||
warn_unreachable = true
|
warn_unreachable = true
|
||||||
warn_unused_configs = true
|
warn_unused_configs = true
|
||||||
no_implicit_reexport = 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+.
|
Requires Python3.6+.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
import sys
|
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
|
**Token**: currently the token from the GitHub Actions is used, pushed with
|
||||||
`pytest bot <pytestbot@gmail.com>` commit author.
|
`pytest bot <pytestbot@gmail.com>` commit author.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: disallow-untyped-defs
|
# mypy: disallow-untyped-defs
|
||||||
"""Invoke development tasks."""
|
"""Invoke development tasks."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
|
@ -7,4 +7,4 @@ except ImportError: # pragma: no cover
|
||||||
# broken installation, we don't even try
|
# broken installation, we don't even try
|
||||||
# unknown only works because we do poor mans version compare
|
# unknown only works because we do poor mans version compare
|
||||||
__version__ = "unknown"
|
__version__ = "unknown"
|
||||||
version_tuple = (0, 0, "unknown") # type:ignore[assignment]
|
version_tuple = (0, 0, "unknown")
|
||||||
|
|
|
@ -279,9 +279,9 @@ class TracebackEntry:
|
||||||
|
|
||||||
Mostly for internal use.
|
Mostly for internal use.
|
||||||
"""
|
"""
|
||||||
tbh: Union[
|
tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = (
|
||||||
bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]
|
False
|
||||||
] = False
|
)
|
||||||
for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
|
for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals):
|
||||||
# in normal cases, f_locals and f_globals are dictionaries
|
# in normal cases, f_locals and f_globals are dictionaries
|
||||||
# however via `exec(...)` / `eval(...)` they can be other types
|
# however via `exec(...)` / `eval(...)` they can be other types
|
||||||
|
@ -378,12 +378,10 @@ class Traceback(List[TracebackEntry]):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __getitem__(self, key: "SupportsIndex") -> TracebackEntry:
|
def __getitem__(self, key: "SupportsIndex") -> TracebackEntry: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __getitem__(self, key: slice) -> "Traceback":
|
def __getitem__(self, key: slice) -> "Traceback": ...
|
||||||
...
|
|
||||||
|
|
||||||
def __getitem__(
|
def __getitem__(
|
||||||
self, key: Union["SupportsIndex", slice]
|
self, key: Union["SupportsIndex", slice]
|
||||||
|
@ -1051,13 +1049,13 @@ class FormattedExcinfo:
|
||||||
# full support for exception groups added to ExceptionInfo.
|
# full support for exception groups added to ExceptionInfo.
|
||||||
# See https://github.com/pytest-dev/pytest/issues/9159
|
# See https://github.com/pytest-dev/pytest/issues/9159
|
||||||
if isinstance(e, BaseExceptionGroup):
|
if isinstance(e, BaseExceptionGroup):
|
||||||
reprtraceback: Union[
|
reprtraceback: Union[ReprTracebackNative, ReprTraceback] = (
|
||||||
ReprTracebackNative, ReprTraceback
|
ReprTracebackNative(
|
||||||
] = ReprTracebackNative(
|
traceback.format_exception(
|
||||||
traceback.format_exception(
|
type(excinfo_.value),
|
||||||
type(excinfo_.value),
|
excinfo_.value,
|
||||||
excinfo_.value,
|
excinfo_.traceback[0]._rawentry,
|
||||||
excinfo_.traceback[0]._rawentry,
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
@ -1348,7 +1346,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
|
||||||
# in 6ec13a2b9. It ("place_as") appears to be something very custom.
|
# in 6ec13a2b9. It ("place_as") appears to be something very custom.
|
||||||
obj = get_real_func(obj)
|
obj = get_real_func(obj)
|
||||||
if hasattr(obj, "place_as"):
|
if hasattr(obj, "place_as"):
|
||||||
obj = obj.place_as # type: ignore[attr-defined]
|
obj = obj.place_as
|
||||||
|
|
||||||
try:
|
try:
|
||||||
code = Code.from_function(obj)
|
code = Code.from_function(obj)
|
||||||
|
|
|
@ -47,12 +47,10 @@ class Source:
|
||||||
__hash__ = None # type: ignore
|
__hash__ = None # type: ignore
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __getitem__(self, key: int) -> str:
|
def __getitem__(self, key: int) -> str: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __getitem__(self, key: slice) -> "Source":
|
def __getitem__(self, key: slice) -> "Source": ...
|
||||||
...
|
|
||||||
|
|
||||||
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:
|
def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]:
|
||||||
if isinstance(key, int):
|
if isinstance(key, int):
|
||||||
|
|
|
@ -9,6 +9,7 @@ from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
|
|
||||||
|
from ..compat import assert_never
|
||||||
from .wcwidth import wcswidth
|
from .wcwidth import wcswidth
|
||||||
|
|
||||||
|
|
||||||
|
@ -209,6 +210,8 @@ class TerminalWriter:
|
||||||
from pygments.lexers.python import PythonLexer as Lexer
|
from pygments.lexers.python import PythonLexer as Lexer
|
||||||
elif lexer == "diff":
|
elif lexer == "diff":
|
||||||
from pygments.lexers.diff import DiffLexer as Lexer
|
from pygments.lexers.diff import DiffLexer as Lexer
|
||||||
|
else:
|
||||||
|
assert_never(lexer)
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
import pygments.util
|
import pygments.util
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""local path implementation."""
|
"""local path implementation."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import atexit
|
import atexit
|
||||||
|
@ -205,12 +206,10 @@ class Stat:
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self) -> int:
|
def size(self) -> int: ...
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mtime(self) -> float:
|
def mtime(self) -> float: ...
|
||||||
...
|
|
||||||
|
|
||||||
def __getattr__(self, name: str) -> Any:
|
def __getattr__(self, name: str) -> Any:
|
||||||
return getattr(self._osstatresult, "st_" + name)
|
return getattr(self._osstatresult, "st_" + name)
|
||||||
|
@ -225,7 +224,7 @@ class Stat:
|
||||||
raise NotImplementedError("XXX win32")
|
raise NotImplementedError("XXX win32")
|
||||||
import pwd
|
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]
|
return entry[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -235,7 +234,7 @@ class Stat:
|
||||||
raise NotImplementedError("XXX win32")
|
raise NotImplementedError("XXX win32")
|
||||||
import grp
|
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]
|
return entry[0]
|
||||||
|
|
||||||
def isdir(self):
|
def isdir(self):
|
||||||
|
@ -253,7 +252,7 @@ def getuserid(user):
|
||||||
import pwd
|
import pwd
|
||||||
|
|
||||||
if not isinstance(user, int):
|
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
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
@ -261,7 +260,7 @@ def getgroupid(group):
|
||||||
import grp
|
import grp
|
||||||
|
|
||||||
if not isinstance(group, int):
|
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
|
return group
|
||||||
|
|
||||||
|
|
||||||
|
@ -318,7 +317,7 @@ class LocalPath:
|
||||||
def readlink(self) -> str:
|
def readlink(self) -> str:
|
||||||
"""Return value of a symbolic link."""
|
"""Return value of a symbolic link."""
|
||||||
# https://github.com/python/mypy/issues/12278
|
# 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):
|
def mklinkto(self, oldname):
|
||||||
"""Posix style hard link to another name."""
|
"""Posix style hard link to another name."""
|
||||||
|
@ -757,15 +756,11 @@ class LocalPath:
|
||||||
if ensure:
|
if ensure:
|
||||||
self.dirpath().ensure(dir=1)
|
self.dirpath().ensure(dir=1)
|
||||||
if encoding:
|
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(
|
return error.checked_call(
|
||||||
io.open,
|
io.open,
|
||||||
self.strpath,
|
self.strpath,
|
||||||
mode,
|
mode,
|
||||||
encoding=encoding, # type:ignore[arg-type]
|
encoding=encoding,
|
||||||
)
|
)
|
||||||
return error.checked_call(open, self.strpath, mode)
|
return error.checked_call(open, self.strpath, mode)
|
||||||
|
|
||||||
|
@ -966,12 +961,10 @@ class LocalPath:
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def stat(self, raising: Literal[True] = ...) -> Stat:
|
def stat(self, raising: Literal[True] = ...) -> Stat: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@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:
|
def stat(self, raising: bool = True) -> Stat | None:
|
||||||
"""Return an os.stat() tuple."""
|
"""Return an os.stat() tuple."""
|
||||||
|
@ -1277,13 +1270,7 @@ class LocalPath:
|
||||||
|
|
||||||
if rootdir is None:
|
if rootdir is None:
|
||||||
rootdir = cls.get_temproot()
|
rootdir = cls.get_temproot()
|
||||||
# Using type ignore here because of this error:
|
path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir))
|
||||||
# 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]
|
|
||||||
)
|
|
||||||
return cls(path)
|
return cls(path)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Support for presenting detailed information in failing assertions."""
|
"""Support for presenting detailed information in failing assertions."""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
|
@ -289,15 +289,13 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||||
else:
|
else:
|
||||||
from importlib.abc import TraversableResources
|
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):
|
if sys.version_info < (3, 11):
|
||||||
from importlib.readers import FileReader
|
from importlib.readers import FileReader
|
||||||
else:
|
else:
|
||||||
from importlib.resources.readers import FileReader
|
from importlib.resources.readers import FileReader
|
||||||
|
|
||||||
return FileReader( # type:ignore[no-any-return]
|
return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
|
||||||
types.SimpleNamespace(path=self._rewritten_names[name])
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _write_pyc_fp(
|
def _write_pyc_fp(
|
||||||
|
@ -672,9 +670,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
self.enable_assertion_pass_hook = False
|
self.enable_assertion_pass_hook = False
|
||||||
self.source = source
|
self.source = source
|
||||||
self.scope: tuple[ast.AST, ...] = ()
|
self.scope: tuple[ast.AST, ...] = ()
|
||||||
self.variables_overwrite: defaultdict[
|
self.variables_overwrite: defaultdict[tuple[ast.AST, ...], Dict[str, str]] = (
|
||||||
tuple[ast.AST, ...], Dict[str, str]
|
defaultdict(dict)
|
||||||
] = defaultdict(dict)
|
)
|
||||||
|
|
||||||
def run(self, mod: ast.Module) -> None:
|
def run(self, mod: ast.Module) -> None:
|
||||||
"""Find all assert statements in *mod* and rewrite them."""
|
"""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()
|
# name if it's a local variable or _should_repr_global_name()
|
||||||
# thinks it's acceptable.
|
# thinks it's acceptable.
|
||||||
locs = ast.Call(self.builtin("locals"), [], [])
|
locs = ast.Call(self.builtin("locals"), [], [])
|
||||||
target_id = name.target.id # type: ignore[attr-defined]
|
target_id = name.target.id
|
||||||
inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
|
inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
|
||||||
dorepr = self.helper("_should_repr_global_name", name)
|
dorepr = self.helper("_should_repr_global_name", name)
|
||||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Utilities for assertion debugging."""
|
"""Utilities for assertion debugging."""
|
||||||
|
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import os
|
import os
|
||||||
import pprint
|
import pprint
|
||||||
|
@ -222,10 +223,9 @@ def assertrepr_compare(
|
||||||
except outcomes.Exit:
|
except outcomes.Exit:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
|
repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
|
||||||
explanation = [
|
explanation = [
|
||||||
"(pytest_assertion plugin: representation of details failed: {}.".format(
|
f"(pytest_assertion plugin: representation of details failed: {repr_crash}.",
|
||||||
_pytest._code.ExceptionInfo.from_current()._getreprcrash()
|
|
||||||
),
|
|
||||||
" Probably an object has a faulty __repr__.)",
|
" Probably an object has a faulty __repr__.)",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Implementation of the cache provider."""
|
"""Implementation of the cache provider."""
|
||||||
|
|
||||||
# This plugin was not named "cache" to avoid conflicts with the external
|
# This plugin was not named "cache" to avoid conflicts with the external
|
||||||
# pytest-cache version.
|
# pytest-cache version.
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
@ -432,7 +433,7 @@ class NFPlugin:
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
|
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
|
||||||
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]
|
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True)
|
||||||
|
|
||||||
def pytest_sessionfinish(self) -> None:
|
def pytest_sessionfinish(self) -> None:
|
||||||
config = self.config
|
config = self.config
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Per-test stdout/stderr capturing mechanism."""
|
"""Per-test stdout/stderr capturing mechanism."""
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
|
@ -105,17 +106,16 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
|
# 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
|
return
|
||||||
|
|
||||||
buffered = hasattr(stream.buffer, "raw")
|
raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer
|
||||||
raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]
|
|
||||||
|
|
||||||
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
|
return
|
||||||
|
|
||||||
def _reopen_stdio(f, mode):
|
def _reopen_stdio(f, mode):
|
||||||
if not buffered and mode[0] == "w":
|
if not hasattr(stream.buffer, "raw") and mode[0] == "w":
|
||||||
buffering = 0
|
buffering = 0
|
||||||
else:
|
else:
|
||||||
buffering = -1
|
buffering = -1
|
||||||
|
@ -482,12 +482,9 @@ class FDCaptureBase(CaptureBase[AnyStr]):
|
||||||
self._state = "initialized"
|
self._state = "initialized"
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format(
|
return (
|
||||||
self.__class__.__name__,
|
f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} "
|
||||||
self.targetfd,
|
f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
|
||||||
self.targetfd_save,
|
|
||||||
self._state,
|
|
||||||
self.tmpfile,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
|
def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
|
||||||
|
@ -621,12 +618,9 @@ class MultiCapture(Generic[AnyStr]):
|
||||||
self.err: Optional[CaptureBase[AnyStr]] = err
|
self.err: Optional[CaptureBase[AnyStr]] = err
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
|
return (
|
||||||
self.out,
|
f"<MultiCapture out={self.out!r} err={self.err!r} in_={self.in_!r} "
|
||||||
self.err,
|
f"_state={self._state!r} _in_suspended={self._in_suspended!r}>"
|
||||||
self.in_,
|
|
||||||
self._state,
|
|
||||||
self._in_suspended,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def start_capturing(self) -> None:
|
def start_capturing(self) -> None:
|
||||||
|
@ -735,8 +729,9 @@ class CaptureManager:
|
||||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
|
return (
|
||||||
self._method, self._global_capturing, self._capture_fixture
|
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]:
|
def is_capturing(self) -> Union[str, bool]:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Python version compatibility code."""
|
"""Python version compatibility code."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
@ -16,6 +17,22 @@ from typing import Callable
|
||||||
from typing import Final
|
from typing import Final
|
||||||
from typing import NoReturn
|
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
|
# fmt: off
|
||||||
# Singleton type for NOTSET, as described in:
|
# Singleton type for NOTSET, as described in:
|
||||||
|
@ -86,7 +103,6 @@ def getfuncargnames(
|
||||||
function: Callable[..., object],
|
function: Callable[..., object],
|
||||||
*,
|
*,
|
||||||
name: str = "",
|
name: str = "",
|
||||||
is_method: bool = False,
|
|
||||||
cls: type | None = None,
|
cls: type | None = None,
|
||||||
) -> tuple[str, ...]:
|
) -> tuple[str, ...]:
|
||||||
"""Return the names of a function's mandatory arguments.
|
"""Return the names of a function's mandatory arguments.
|
||||||
|
@ -97,9 +113,8 @@ def getfuncargnames(
|
||||||
* Aren't bound with functools.partial.
|
* Aren't bound with functools.partial.
|
||||||
* Aren't replaced with mocks.
|
* Aren't replaced with mocks.
|
||||||
|
|
||||||
The is_method and cls arguments indicate that the function should
|
The cls arguments indicate that the function should be treated as a bound
|
||||||
be treated as a bound method even though it's not unless, only in
|
method even though it's not unless the function is a static method.
|
||||||
the case of cls, the function is a static method.
|
|
||||||
|
|
||||||
The name parameter should be the original name in which the function was collected.
|
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
|
# If this function should be treated as a bound method even though
|
||||||
# it's passed as an unbound method or function, remove the first
|
# it's passed as an unbound method or function, remove the first
|
||||||
# parameter name.
|
# parameter name.
|
||||||
if is_method or (
|
if (
|
||||||
# Not using `getattr` because we don't want to resolve the staticmethod.
|
# Not using `getattr` because we don't want to resolve the staticmethod.
|
||||||
# Not using `cls.__dict__` because we want to check the entire MRO.
|
# Not using `cls.__dict__` because we want to check the entire MRO.
|
||||||
cls
|
cls
|
||||||
|
@ -289,7 +304,7 @@ def get_user_id() -> int | None:
|
||||||
# mypy follows the version and platform checking expectation of PEP 484:
|
# mypy follows the version and platform checking expectation of PEP 484:
|
||||||
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
||||||
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
||||||
if sys.platform == "win32" or sys.platform == "emscripten": # noqa: PLR1714
|
if sys.platform == "win32" or sys.platform == "emscripten":
|
||||||
# win32 does not have a getuid() function.
|
# win32 does not have a getuid() function.
|
||||||
# Emscripten has a return 0 stub.
|
# Emscripten has a return 0 stub.
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Command line options, ini-file and conftest.py processing."""
|
"""Command line options, ini-file and conftest.py processing."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import copy
|
import copy
|
||||||
|
@ -38,12 +39,14 @@ from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
import pluggy
|
||||||
from pluggy import HookimplMarker
|
from pluggy import HookimplMarker
|
||||||
from pluggy import HookimplOpts
|
from pluggy import HookimplOpts
|
||||||
from pluggy import HookspecMarker
|
from pluggy import HookspecMarker
|
||||||
from pluggy import HookspecOpts
|
from pluggy import HookspecOpts
|
||||||
from pluggy import PluginManager
|
from pluggy import PluginManager
|
||||||
|
|
||||||
|
from .compat import PathAwareHookProxy
|
||||||
from .exceptions import PrintHelp as PrintHelp
|
from .exceptions import PrintHelp as PrintHelp
|
||||||
from .exceptions import UsageError as UsageError
|
from .exceptions import UsageError as UsageError
|
||||||
from .findpaths import determine_setup
|
from .findpaths import determine_setup
|
||||||
|
@ -547,6 +550,8 @@ class PytestPluginManager(PluginManager):
|
||||||
confcutdir: Optional[Path],
|
confcutdir: Optional[Path],
|
||||||
invocation_dir: Path,
|
invocation_dir: Path,
|
||||||
importmode: Union[ImportMode, str],
|
importmode: Union[ImportMode, str],
|
||||||
|
*,
|
||||||
|
consider_namespace_packages: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Load initial conftest files given a preparsed "namespace".
|
"""Load initial conftest files given a preparsed "namespace".
|
||||||
|
|
||||||
|
@ -572,10 +577,20 @@ class PytestPluginManager(PluginManager):
|
||||||
# Ensure we do not break if what appears to be an anchor
|
# Ensure we do not break if what appears to be an anchor
|
||||||
# is in fact a very long option (#10169, #11394).
|
# is in fact a very long option (#10169, #11394).
|
||||||
if safe_exists(anchor):
|
if safe_exists(anchor):
|
||||||
self._try_load_conftest(anchor, importmode, rootpath)
|
self._try_load_conftest(
|
||||||
|
anchor,
|
||||||
|
importmode,
|
||||||
|
rootpath,
|
||||||
|
consider_namespace_packages=consider_namespace_packages,
|
||||||
|
)
|
||||||
foundanchor = True
|
foundanchor = True
|
||||||
if not foundanchor:
|
if not foundanchor:
|
||||||
self._try_load_conftest(invocation_dir, importmode, rootpath)
|
self._try_load_conftest(
|
||||||
|
invocation_dir,
|
||||||
|
importmode,
|
||||||
|
rootpath,
|
||||||
|
consider_namespace_packages=consider_namespace_packages,
|
||||||
|
)
|
||||||
|
|
||||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||||
"""Whether to consider the given path to load conftests from."""
|
"""Whether to consider the given path to load conftests from."""
|
||||||
|
@ -593,17 +608,37 @@ class PytestPluginManager(PluginManager):
|
||||||
return path not in self._confcutdir.parents
|
return path not in self._confcutdir.parents
|
||||||
|
|
||||||
def _try_load_conftest(
|
def _try_load_conftest(
|
||||||
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
self,
|
||||||
|
anchor: Path,
|
||||||
|
importmode: Union[str, ImportMode],
|
||||||
|
rootpath: Path,
|
||||||
|
*,
|
||||||
|
consider_namespace_packages: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._loadconftestmodules(anchor, importmode, rootpath)
|
self._loadconftestmodules(
|
||||||
|
anchor,
|
||||||
|
importmode,
|
||||||
|
rootpath,
|
||||||
|
consider_namespace_packages=consider_namespace_packages,
|
||||||
|
)
|
||||||
# let's also consider test* subdirs
|
# let's also consider test* subdirs
|
||||||
if anchor.is_dir():
|
if anchor.is_dir():
|
||||||
for x in anchor.glob("test*"):
|
for x in anchor.glob("test*"):
|
||||||
if x.is_dir():
|
if x.is_dir():
|
||||||
self._loadconftestmodules(x, importmode, rootpath)
|
self._loadconftestmodules(
|
||||||
|
x,
|
||||||
|
importmode,
|
||||||
|
rootpath,
|
||||||
|
consider_namespace_packages=consider_namespace_packages,
|
||||||
|
)
|
||||||
|
|
||||||
def _loadconftestmodules(
|
def _loadconftestmodules(
|
||||||
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
|
self,
|
||||||
|
path: Path,
|
||||||
|
importmode: Union[str, ImportMode],
|
||||||
|
rootpath: Path,
|
||||||
|
*,
|
||||||
|
consider_namespace_packages: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
if self._noconftest:
|
if self._noconftest:
|
||||||
return
|
return
|
||||||
|
@ -620,7 +655,12 @@ class PytestPluginManager(PluginManager):
|
||||||
if self._is_in_confcutdir(parent):
|
if self._is_in_confcutdir(parent):
|
||||||
conftestpath = parent / "conftest.py"
|
conftestpath = parent / "conftest.py"
|
||||||
if conftestpath.is_file():
|
if conftestpath.is_file():
|
||||||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
mod = self._importconftest(
|
||||||
|
conftestpath,
|
||||||
|
importmode,
|
||||||
|
rootpath,
|
||||||
|
consider_namespace_packages=consider_namespace_packages,
|
||||||
|
)
|
||||||
clist.append(mod)
|
clist.append(mod)
|
||||||
self._dirpath2confmods[directory] = clist
|
self._dirpath2confmods[directory] = clist
|
||||||
|
|
||||||
|
@ -642,7 +682,12 @@ class PytestPluginManager(PluginManager):
|
||||||
raise KeyError(name)
|
raise KeyError(name)
|
||||||
|
|
||||||
def _importconftest(
|
def _importconftest(
|
||||||
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
self,
|
||||||
|
conftestpath: Path,
|
||||||
|
importmode: Union[str, ImportMode],
|
||||||
|
rootpath: Path,
|
||||||
|
*,
|
||||||
|
consider_namespace_packages: bool,
|
||||||
) -> types.ModuleType:
|
) -> types.ModuleType:
|
||||||
conftestpath_plugin_name = str(conftestpath)
|
conftestpath_plugin_name = str(conftestpath)
|
||||||
existing = self.get_plugin(conftestpath_plugin_name)
|
existing = self.get_plugin(conftestpath_plugin_name)
|
||||||
|
@ -661,7 +706,12 @@ class PytestPluginManager(PluginManager):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
mod = import_path(conftestpath, mode=importmode, root=rootpath)
|
mod = import_path(
|
||||||
|
conftestpath,
|
||||||
|
mode=importmode,
|
||||||
|
root=rootpath,
|
||||||
|
consider_namespace_packages=consider_namespace_packages,
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
assert e.__traceback__ is not None
|
assert e.__traceback__ is not None
|
||||||
raise ConftestImportFailure(conftestpath, cause=e) from e
|
raise ConftestImportFailure(conftestpath, cause=e) from e
|
||||||
|
@ -1021,7 +1071,7 @@ class Config:
|
||||||
self._store = self.stash
|
self._store = self.stash
|
||||||
|
|
||||||
self.trace = self.pluginmanager.trace.root.get("config")
|
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._inicache: Dict[str, Any] = {}
|
||||||
self._override_ini: Sequence[str] = ()
|
self._override_ini: Sequence[str] = ()
|
||||||
self._opt2dest: Dict[str, str] = {}
|
self._opt2dest: Dict[str, str] = {}
|
||||||
|
@ -1177,6 +1227,9 @@ class Config:
|
||||||
confcutdir=early_config.known_args_namespace.confcutdir,
|
confcutdir=early_config.known_args_namespace.confcutdir,
|
||||||
invocation_dir=early_config.invocation_params.dir,
|
invocation_dir=early_config.invocation_params.dir,
|
||||||
importmode=early_config.known_args_namespace.importmode,
|
importmode=early_config.known_args_namespace.importmode,
|
||||||
|
consider_namespace_packages=early_config.getini(
|
||||||
|
"consider_namespace_packages"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _initini(self, args: Sequence[str]) -> None:
|
def _initini(self, args: Sequence[str]) -> None:
|
||||||
|
|
|
@ -415,6 +415,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||||
add_help=False,
|
add_help=False,
|
||||||
formatter_class=DropShorterLongHelpFormatter,
|
formatter_class=DropShorterLongHelpFormatter,
|
||||||
allow_abbrev=False,
|
allow_abbrev=False,
|
||||||
|
fromfile_prefix_chars="@",
|
||||||
)
|
)
|
||||||
# extra_info is a dict of (param -> value) to display if there's
|
# extra_info is a dict of (param -> value) to display if there's
|
||||||
# an usage error to provide more contextual information to the user.
|
# 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}"
|
msg = f"{self.prog}: error: {message}"
|
||||||
|
|
||||||
if hasattr(self._parser, "_config_source_hint"):
|
if hasattr(self._parser, "_config_source_hint"):
|
||||||
# Type ignored because the attribute is set dynamically.
|
msg = f"{msg} ({self._parser._config_source_hint})"
|
||||||
msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore
|
|
||||||
|
|
||||||
raise UsageError(self.format_usage() + msg)
|
raise UsageError(self.format_usage() + msg)
|
||||||
|
|
||||||
|
|
|
@ -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
|
# mypy: allow-untyped-defs
|
||||||
"""Interactive debugging with PDB, the Python Debugger."""
|
"""Interactive debugging with PDB, the Python Debugger."""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import functools
|
import functools
|
||||||
import sys
|
import sys
|
||||||
|
@ -154,9 +155,7 @@ class pytestPDB:
|
||||||
def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
|
def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
|
||||||
import _pytest.config
|
import _pytest.config
|
||||||
|
|
||||||
# Type ignored because mypy doesn't support "dynamic"
|
class PytestPdbWrapper(pdb_cls):
|
||||||
# inheritance like this.
|
|
||||||
class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc]
|
|
||||||
_pytest_capman = capman
|
_pytest_capman = capman
|
||||||
_continued = False
|
_continued = False
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,21 @@ YIELD_FIXTURE = PytestDeprecationWarning(
|
||||||
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
|
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(
|
HOOK_LEGACY_MARKING = UnformattedWarning(
|
||||||
PytestDeprecationWarning,
|
PytestDeprecationWarning,
|
||||||
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
|
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Discover and run doctests in modules and test files."""
|
"""Discover and run doctests in modules and test files."""
|
||||||
|
|
||||||
import bdb
|
import bdb
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import functools
|
import functools
|
||||||
|
|
|
@ -7,6 +7,7 @@ import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
from typing import AbstractSet
|
from typing import AbstractSet
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
@ -67,6 +68,10 @@ from _pytest.scope import HIGH_SCOPES
|
||||||
from _pytest.scope import Scope
|
from _pytest.scope import Scope
|
||||||
|
|
||||||
|
|
||||||
|
if sys.version_info[:2] < (3, 11):
|
||||||
|
from exceptiongroup import BaseExceptionGroup
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from typing import Deque
|
from typing import Deque
|
||||||
|
|
||||||
|
@ -343,7 +348,6 @@ class FixtureRequest(abc.ABC):
|
||||||
pyfuncitem: "Function",
|
pyfuncitem: "Function",
|
||||||
fixturename: Optional[str],
|
fixturename: Optional[str],
|
||||||
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
|
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
|
||||||
arg2index: Dict[str, int],
|
|
||||||
fixture_defs: Dict[str, "FixtureDef[Any]"],
|
fixture_defs: Dict[str, "FixtureDef[Any]"],
|
||||||
*,
|
*,
|
||||||
_ispytest: bool = False,
|
_ispytest: bool = False,
|
||||||
|
@ -357,16 +361,6 @@ class FixtureRequest(abc.ABC):
|
||||||
# collection. Dynamically requested fixtures (using
|
# collection. Dynamically requested fixtures (using
|
||||||
# `request.getfixturevalue("foo")`) are added dynamically.
|
# `request.getfixturevalue("foo")`) are added dynamically.
|
||||||
self._arg2fixturedefs: Final = arg2fixturedefs
|
self._arg2fixturedefs: Final = arg2fixturedefs
|
||||||
# A fixture may override another fixture with the same name, e.g. a fixture
|
|
||||||
# in a module can override a fixture in a conftest, a fixture in a class can
|
|
||||||
# override a fixture in the module, and so on.
|
|
||||||
# An overriding fixture can request its own name; in this case it gets
|
|
||||||
# the value of the fixture it overrides, one level up.
|
|
||||||
# The _arg2index state keeps the current depth in the overriding chain.
|
|
||||||
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
|
|
||||||
# furthest to closest, so we use negative indexing -1, -2, ... to go from
|
|
||||||
# last to first.
|
|
||||||
self._arg2index: Final = arg2index
|
|
||||||
# The evaluated argnames so far, mapping to the FixtureDef they resolved
|
# The evaluated argnames so far, mapping to the FixtureDef they resolved
|
||||||
# to.
|
# to.
|
||||||
self._fixture_defs: Final = fixture_defs
|
self._fixture_defs: Final = fixture_defs
|
||||||
|
@ -394,6 +388,14 @@ class FixtureRequest(abc.ABC):
|
||||||
"""Scope string, one of "function", "class", "module", "package", "session"."""
|
"""Scope string, one of "function", "class", "module", "package", "session"."""
|
||||||
return self._scope.value
|
return self._scope.value
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def _check_scope(
|
||||||
|
self,
|
||||||
|
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
|
||||||
|
requested_scope: Scope,
|
||||||
|
) -> None:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fixturenames(self) -> List[str]:
|
def fixturenames(self) -> List[str]:
|
||||||
"""Names of all active fixtures in this request."""
|
"""Names of all active fixtures in this request."""
|
||||||
|
@ -422,17 +424,30 @@ class FixtureRequest(abc.ABC):
|
||||||
# The are no fixtures with this name applicable for the function.
|
# The are no fixtures with this name applicable for the function.
|
||||||
if not fixturedefs:
|
if not fixturedefs:
|
||||||
raise FixtureLookupError(argname, self)
|
raise FixtureLookupError(argname, self)
|
||||||
index = self._arg2index.get(argname, 0) - 1
|
|
||||||
# The fixture requested its own name, but no remaining to override.
|
# 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):
|
if -index > len(fixturedefs):
|
||||||
raise FixtureLookupError(argname, self)
|
raise FixtureLookupError(argname, self)
|
||||||
self._arg2index[argname] = index
|
|
||||||
return fixturedefs[index]
|
return fixturedefs[index]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self) -> Config:
|
def config(self) -> Config:
|
||||||
"""The pytest config object associated with this request."""
|
"""The pytest config object associated with this request."""
|
||||||
return self._pyfuncitem.config # type: ignore[no-any-return]
|
return self._pyfuncitem.config
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def function(self):
|
def function(self):
|
||||||
|
@ -455,12 +470,9 @@ class FixtureRequest(abc.ABC):
|
||||||
@property
|
@property
|
||||||
def instance(self):
|
def instance(self):
|
||||||
"""Instance (can be None) on which test function was collected."""
|
"""Instance (can be None) on which test function was collected."""
|
||||||
# unittest support hack, see _pytest.unittest.TestCaseFunction.
|
if self.scope != "function":
|
||||||
try:
|
return None
|
||||||
return self._pyfuncitem._testcase # type: ignore[attr-defined]
|
return getattr(self._pyfuncitem, "instance", None)
|
||||||
except AttributeError:
|
|
||||||
function = getattr(self, "function", None)
|
|
||||||
return getattr(function, "__self__", None)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def module(self):
|
def module(self):
|
||||||
|
@ -487,7 +499,7 @@ class FixtureRequest(abc.ABC):
|
||||||
@property
|
@property
|
||||||
def session(self) -> "Session":
|
def session(self) -> "Session":
|
||||||
"""Pytest session object."""
|
"""Pytest session object."""
|
||||||
return self._pyfuncitem.session # type: ignore[no-any-return]
|
return self._pyfuncitem.session
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||||
|
@ -535,6 +547,7 @@ class FixtureRequest(abc.ABC):
|
||||||
# getfixturevalue() is also called by pytest itself during item setup to
|
# getfixturevalue() is also called by pytest itself during item setup to
|
||||||
# evaluate the fixtures that are requested statically
|
# evaluate the fixtures that are requested statically
|
||||||
# (using function parameters, autouse, etc).
|
# (using function parameters, autouse, etc).
|
||||||
|
# As well as called by `pytest_fixture_setup()`
|
||||||
|
|
||||||
fixturedef = self._get_active_fixturedef(argname)
|
fixturedef = self._get_active_fixturedef(argname)
|
||||||
assert fixturedef.cached_result is not None, (
|
assert fixturedef.cached_result is not None, (
|
||||||
|
@ -543,6 +556,16 @@ class FixtureRequest(abc.ABC):
|
||||||
)
|
)
|
||||||
return fixturedef.cached_result[0]
|
return fixturedef.cached_result[0]
|
||||||
|
|
||||||
|
def _iter_chain(self) -> Iterator["SubRequest"]:
|
||||||
|
"""Yield all SubRequests in the chain, from self up.
|
||||||
|
|
||||||
|
Note: does *not* yield the TopRequest.
|
||||||
|
"""
|
||||||
|
current = self
|
||||||
|
while isinstance(current, SubRequest):
|
||||||
|
yield current
|
||||||
|
current = current._parent_request
|
||||||
|
|
||||||
def _get_active_fixturedef(
|
def _get_active_fixturedef(
|
||||||
self, argname: str
|
self, argname: str
|
||||||
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
|
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
|
||||||
|
@ -557,14 +580,12 @@ class FixtureRequest(abc.ABC):
|
||||||
raise
|
raise
|
||||||
self._compute_fixture_value(fixturedef)
|
self._compute_fixture_value(fixturedef)
|
||||||
self._fixture_defs[argname] = fixturedef
|
self._fixture_defs[argname] = fixturedef
|
||||||
|
else:
|
||||||
|
self._check_scope(fixturedef, fixturedef._scope)
|
||||||
return fixturedef
|
return fixturedef
|
||||||
|
|
||||||
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
|
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
|
||||||
current = self
|
values = [request._fixturedef for request in self._iter_chain()]
|
||||||
values: List[FixtureDef[Any]] = []
|
|
||||||
while isinstance(current, SubRequest):
|
|
||||||
values.append(current._fixturedef) # type: ignore[has-type]
|
|
||||||
current = current._parent_request
|
|
||||||
values.reverse()
|
values.reverse()
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
@ -613,26 +634,24 @@ class FixtureRequest(abc.ABC):
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
source_path_str = str(source_path)
|
source_path_str = str(source_path)
|
||||||
|
location = getlocation(fixturedef.func, funcitem.config.rootpath)
|
||||||
msg = (
|
msg = (
|
||||||
"The requested fixture has no parameter defined for test:\n"
|
"The requested fixture has no parameter defined for test:\n"
|
||||||
" {}\n\n"
|
f" {funcitem.nodeid}\n\n"
|
||||||
"Requested fixture '{}' defined in:\n{}"
|
f"Requested fixture '{fixturedef.argname}' defined in:\n"
|
||||||
"\n\nRequested here:\n{}:{}".format(
|
f"{location}\n\n"
|
||||||
funcitem.nodeid,
|
f"Requested here:\n"
|
||||||
fixturedef.argname,
|
f"{source_path_str}:{source_lineno}"
|
||||||
getlocation(fixturedef.func, funcitem.config.rootpath),
|
|
||||||
source_path_str,
|
|
||||||
source_lineno,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
fail(msg, pytrace=False)
|
fail(msg, pytrace=False)
|
||||||
|
|
||||||
|
# Check if a higher-level scoped fixture accesses a lower level one.
|
||||||
|
self._check_scope(fixturedef, scope)
|
||||||
|
|
||||||
subrequest = SubRequest(
|
subrequest = SubRequest(
|
||||||
self, scope, param, param_index, fixturedef, _ispytest=True
|
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)
|
|
||||||
# Make sure the fixture value is cached, running it if it isn't
|
# Make sure the fixture value is cached, running it if it isn't
|
||||||
fixturedef.execute(request=subrequest)
|
fixturedef.execute(request=subrequest)
|
||||||
|
|
||||||
|
@ -646,7 +665,6 @@ class TopRequest(FixtureRequest):
|
||||||
fixturename=None,
|
fixturename=None,
|
||||||
pyfuncitem=pyfuncitem,
|
pyfuncitem=pyfuncitem,
|
||||||
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
|
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
|
||||||
arg2index={},
|
|
||||||
fixture_defs={},
|
fixture_defs={},
|
||||||
_ispytest=_ispytest,
|
_ispytest=_ispytest,
|
||||||
)
|
)
|
||||||
|
@ -655,6 +673,14 @@ class TopRequest(FixtureRequest):
|
||||||
def _scope(self) -> Scope:
|
def _scope(self) -> Scope:
|
||||||
return Scope.Function
|
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
|
@property
|
||||||
def node(self):
|
def node(self):
|
||||||
return self._pyfuncitem
|
return self._pyfuncitem
|
||||||
|
@ -692,12 +718,11 @@ class SubRequest(FixtureRequest):
|
||||||
fixturename=fixturedef.argname,
|
fixturename=fixturedef.argname,
|
||||||
fixture_defs=request._fixture_defs,
|
fixture_defs=request._fixture_defs,
|
||||||
arg2fixturedefs=request._arg2fixturedefs,
|
arg2fixturedefs=request._arg2fixturedefs,
|
||||||
arg2index=request._arg2index,
|
|
||||||
_ispytest=_ispytest,
|
_ispytest=_ispytest,
|
||||||
)
|
)
|
||||||
self._parent_request: Final[FixtureRequest] = request
|
self._parent_request: Final[FixtureRequest] = request
|
||||||
self._scope_field: Final = scope
|
self._scope_field: Final = scope
|
||||||
self._fixturedef: Final = fixturedef
|
self._fixturedef: Final[FixtureDef[object]] = fixturedef
|
||||||
if param is not NOTSET:
|
if param is not NOTSET:
|
||||||
self.param = param
|
self.param = param
|
||||||
self.param_index: Final = param_index
|
self.param_index: Final = param_index
|
||||||
|
@ -727,37 +752,34 @@ class SubRequest(FixtureRequest):
|
||||||
|
|
||||||
def _check_scope(
|
def _check_scope(
|
||||||
self,
|
self,
|
||||||
argname: str,
|
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
|
||||||
invoking_scope: Scope,
|
|
||||||
requested_scope: Scope,
|
requested_scope: Scope,
|
||||||
) -> None:
|
) -> None:
|
||||||
if argname == "request":
|
if isinstance(requested_fixturedef, PseudoFixtureDef):
|
||||||
return
|
return
|
||||||
if invoking_scope > requested_scope:
|
if self._scope > requested_scope:
|
||||||
# Try to report something helpful.
|
# 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(
|
fail(
|
||||||
f"ScopeMismatch: You tried to access the {requested_scope.value} scoped "
|
f"ScopeMismatch: You tried to access the {requested_scope.value} scoped "
|
||||||
f"fixture {argname} with a {invoking_scope.value} scoped request object, "
|
f"fixture {argname} with a {self._scope.value} scoped request object. "
|
||||||
f"involved factories:\n{text}",
|
f"Requesting fixture stack:\n{fixture_stack}\n"
|
||||||
|
f"Requested fixture:\n{requested_fixture}",
|
||||||
pytrace=False,
|
pytrace=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _factorytraceback(self) -> List[str]:
|
def _format_fixturedef_line(self, fixturedef: "FixtureDef[object]") -> str:
|
||||||
lines = []
|
factory = fixturedef.func
|
||||||
for fixturedef in self._get_fixturestack():
|
path, lineno = getfslineno(factory)
|
||||||
factory = fixturedef.func
|
if isinstance(path, Path):
|
||||||
fs, lineno = getfslineno(factory)
|
path = bestrelpath(self._pyfuncitem.session.path, path)
|
||||||
if isinstance(fs, Path):
|
signature = inspect.signature(factory)
|
||||||
session: Session = self._pyfuncitem.session
|
return f"{path}:{lineno + 1}: def {factory.__name__}{signature}"
|
||||||
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 addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
|
||||||
self._fixturedef.addfinalizer(finalizer)
|
self._fixturedef.addfinalizer(finalizer)
|
||||||
|
@ -933,7 +955,6 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
func: "_FixtureFunc[FixtureValue]",
|
func: "_FixtureFunc[FixtureValue]",
|
||||||
scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None],
|
scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None],
|
||||||
params: Optional[Sequence[object]],
|
params: Optional[Sequence[object]],
|
||||||
unittest: bool = False,
|
|
||||||
ids: Optional[
|
ids: Optional[
|
||||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||||
] = None,
|
] = None,
|
||||||
|
@ -979,9 +1000,7 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
# a parameter value.
|
# a parameter value.
|
||||||
self.ids: Final = ids
|
self.ids: Final = ids
|
||||||
# The names requested by the fixtures.
|
# The names requested by the fixtures.
|
||||||
self.argnames: Final = getfuncargnames(func, name=argname, is_method=unittest)
|
self.argnames: Final = getfuncargnames(func, name=argname)
|
||||||
# Whether the fixture was collected from a unittest TestCase class.
|
|
||||||
self.unittest: Final = unittest
|
|
||||||
# If the fixture was executed, the current value of the fixture.
|
# If the fixture was executed, the current value of the fixture.
|
||||||
# Can change if the fixture is executed with different parameters.
|
# Can change if the fixture is executed with different parameters.
|
||||||
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
|
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
|
||||||
|
@ -996,27 +1015,25 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
self._finalizers.append(finalizer)
|
self._finalizers.append(finalizer)
|
||||||
|
|
||||||
def finish(self, request: SubRequest) -> None:
|
def finish(self, request: SubRequest) -> None:
|
||||||
exc = None
|
exceptions: List[BaseException] = []
|
||||||
try:
|
while self._finalizers:
|
||||||
while self._finalizers:
|
fin = self._finalizers.pop()
|
||||||
try:
|
try:
|
||||||
func = self._finalizers.pop()
|
fin()
|
||||||
func()
|
except BaseException as e:
|
||||||
except BaseException as e:
|
exceptions.append(e)
|
||||||
# XXX Only first exception will be seen by user,
|
node = request.node
|
||||||
# ideally all should be reported.
|
node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||||
if exc is None:
|
# Even if finalization fails, we invalidate the cached fixture
|
||||||
exc = e
|
# value and remove all finalizers because they may be bound methods
|
||||||
if exc:
|
# which will keep instances alive.
|
||||||
raise exc
|
self.cached_result = None
|
||||||
finally:
|
self._finalizers.clear()
|
||||||
ihook = request.node.ihook
|
if len(exceptions) == 1:
|
||||||
ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
raise exceptions[0]
|
||||||
# Even if finalization fails, we invalidate the cached fixture
|
elif len(exceptions) > 1:
|
||||||
# value and remove all finalizers because they may be bound methods
|
msg = f'errors while tearing down fixture "{self.argname}" of {node}'
|
||||||
# which will keep instances alive.
|
raise BaseExceptionGroup(msg, exceptions[::-1])
|
||||||
self.cached_result = None
|
|
||||||
self._finalizers.clear()
|
|
||||||
|
|
||||||
def execute(self, request: SubRequest) -> FixtureValue:
|
def execute(self, request: SubRequest) -> FixtureValue:
|
||||||
finalizer = functools.partial(self.finish, request=request)
|
finalizer = functools.partial(self.finish, request=request)
|
||||||
|
@ -1024,9 +1041,7 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
# with their finalization.
|
# with their finalization.
|
||||||
for argname in self.argnames:
|
for argname in self.argnames:
|
||||||
fixturedef = request._get_active_fixturedef(argname)
|
fixturedef = request._get_active_fixturedef(argname)
|
||||||
if argname != "request":
|
if not isinstance(fixturedef, PseudoFixtureDef):
|
||||||
# PseudoFixtureDef is only for "request".
|
|
||||||
assert isinstance(fixturedef, FixtureDef)
|
|
||||||
fixturedef.addfinalizer(finalizer)
|
fixturedef.addfinalizer(finalizer)
|
||||||
|
|
||||||
my_cache_key = self.cache_key(request)
|
my_cache_key = self.cache_key(request)
|
||||||
|
@ -1037,7 +1052,6 @@ class FixtureDef(Generic[FixtureValue]):
|
||||||
if my_cache_key is cache_key:
|
if my_cache_key is cache_key:
|
||||||
if self.cached_result[2] is not None:
|
if self.cached_result[2] is not None:
|
||||||
exc = self.cached_result[2]
|
exc = self.cached_result[2]
|
||||||
# this would previously trigger adding a finalizer. Should it?
|
|
||||||
raise exc
|
raise exc
|
||||||
else:
|
else:
|
||||||
result = self.cached_result[0]
|
result = self.cached_result[0]
|
||||||
|
@ -1069,27 +1083,23 @@ def resolve_fixture_function(
|
||||||
fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
|
fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
|
||||||
) -> "_FixtureFunc[FixtureValue]":
|
) -> "_FixtureFunc[FixtureValue]":
|
||||||
"""Get the actual callable that can be called to obtain the fixture
|
"""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
|
fixturefunc = fixturedef.func
|
||||||
if fixturedef.unittest:
|
# The fixture function needs to be bound to the actual
|
||||||
if request.instance is not None:
|
# request.instance so that code working with "fixturedef" behaves
|
||||||
# Bind the unbound method to the TestCase instance.
|
# as expected.
|
||||||
fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr]
|
instance = request.instance
|
||||||
else:
|
if instance is not None:
|
||||||
# The fixture function needs to be bound to the actual
|
# Handle the case where fixture is defined not in a test class, but some other class
|
||||||
# request.instance so that code working with "fixturedef" behaves
|
# (for example a plugin class with a fixture), see #2270.
|
||||||
# as expected.
|
if hasattr(fixturefunc, "__self__") and not isinstance(
|
||||||
if request.instance is not None:
|
instance,
|
||||||
# Handle the case where fixture is defined not in a test class, but some other class
|
fixturefunc.__self__.__class__,
|
||||||
# (for example a plugin class with a fixture), see #2270.
|
):
|
||||||
if hasattr(fixturefunc, "__self__") and not isinstance(
|
return fixturefunc
|
||||||
request.instance,
|
fixturefunc = getimfunc(fixturedef.func)
|
||||||
fixturefunc.__self__.__class__, # type: ignore[union-attr]
|
if fixturefunc != fixturedef.func:
|
||||||
):
|
fixturefunc = fixturefunc.__get__(instance)
|
||||||
return fixturefunc
|
|
||||||
fixturefunc = getimfunc(fixturedef.func)
|
|
||||||
if fixturefunc != fixturedef.func:
|
|
||||||
fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr]
|
|
||||||
return fixturefunc
|
return fixturefunc
|
||||||
|
|
||||||
|
|
||||||
|
@ -1099,11 +1109,7 @@ def pytest_fixture_setup(
|
||||||
"""Execution of fixture setup."""
|
"""Execution of fixture setup."""
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
for argname in fixturedef.argnames:
|
for argname in fixturedef.argnames:
|
||||||
fixdef = request._get_active_fixturedef(argname)
|
kwargs[argname] = request.getfixturevalue(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
|
|
||||||
|
|
||||||
fixturefunc = resolve_fixture_function(fixturedef, request)
|
fixturefunc = resolve_fixture_function(fixturedef, request)
|
||||||
my_cache_key = fixturedef.cache_key(request)
|
my_cache_key = fixturedef.cache_key(request)
|
||||||
|
@ -1127,12 +1133,13 @@ def wrap_function_to_error_out_if_called_directly(
|
||||||
) -> FixtureFunction:
|
) -> FixtureFunction:
|
||||||
"""Wrap the given fixture function so we can raise an error about it being called directly,
|
"""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."""
|
instead of used as an argument in a test function."""
|
||||||
|
name = fixture_marker.name or function.__name__
|
||||||
message = (
|
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"
|
"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"
|
"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."
|
"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)
|
@functools.wraps(function)
|
||||||
def result(*args, **kwargs):
|
def result(*args, **kwargs):
|
||||||
|
@ -1199,8 +1206,7 @@ def fixture(
|
||||||
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
||||||
] = ...,
|
] = ...,
|
||||||
name: Optional[str] = ...,
|
name: Optional[str] = ...,
|
||||||
) -> FixtureFunction:
|
) -> FixtureFunction: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -1214,8 +1220,7 @@ def fixture(
|
||||||
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
|
||||||
] = ...,
|
] = ...,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
) -> FixtureFunctionMarker:
|
) -> FixtureFunctionMarker: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def fixture(
|
def fixture(
|
||||||
|
@ -1593,7 +1598,6 @@ class FixtureManager:
|
||||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||||
] = None,
|
] = None,
|
||||||
autouse: bool = False,
|
autouse: bool = False,
|
||||||
unittest: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register a fixture
|
"""Register a fixture
|
||||||
|
|
||||||
|
@ -1614,8 +1618,6 @@ class FixtureManager:
|
||||||
The fixture's IDs.
|
The fixture's IDs.
|
||||||
:param autouse:
|
:param autouse:
|
||||||
Whether this is an autouse fixture.
|
Whether this is an autouse fixture.
|
||||||
:param unittest:
|
|
||||||
Set this if this is a unittest fixture.
|
|
||||||
"""
|
"""
|
||||||
fixture_def = FixtureDef(
|
fixture_def = FixtureDef(
|
||||||
config=self.config,
|
config=self.config,
|
||||||
|
@ -1624,7 +1626,6 @@ class FixtureManager:
|
||||||
func=func,
|
func=func,
|
||||||
scope=scope,
|
scope=scope,
|
||||||
params=params,
|
params=params,
|
||||||
unittest=unittest,
|
|
||||||
ids=ids,
|
ids=ids,
|
||||||
_ispytest=True,
|
_ispytest=True,
|
||||||
)
|
)
|
||||||
|
@ -1646,8 +1647,6 @@ class FixtureManager:
|
||||||
def parsefactories(
|
def parsefactories(
|
||||||
self,
|
self,
|
||||||
node_or_obj: nodes.Node,
|
node_or_obj: nodes.Node,
|
||||||
*,
|
|
||||||
unittest: bool = ...,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@ -1656,8 +1655,6 @@ class FixtureManager:
|
||||||
self,
|
self,
|
||||||
node_or_obj: object,
|
node_or_obj: object,
|
||||||
nodeid: Optional[str],
|
nodeid: Optional[str],
|
||||||
*,
|
|
||||||
unittest: bool = ...,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@ -1665,8 +1662,6 @@ class FixtureManager:
|
||||||
self,
|
self,
|
||||||
node_or_obj: Union[nodes.Node, object],
|
node_or_obj: Union[nodes.Node, object],
|
||||||
nodeid: Union[str, NotSetType, None] = NOTSET,
|
nodeid: Union[str, NotSetType, None] = NOTSET,
|
||||||
*,
|
|
||||||
unittest: bool = False,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Collect fixtures from a collection node or object.
|
"""Collect fixtures from a collection node or object.
|
||||||
|
|
||||||
|
@ -1718,7 +1713,6 @@ class FixtureManager:
|
||||||
func=func,
|
func=func,
|
||||||
scope=marker.scope,
|
scope=marker.scope,
|
||||||
params=marker.params,
|
params=marker.params,
|
||||||
unittest=unittest,
|
|
||||||
ids=marker.ids,
|
ids=marker.ids,
|
||||||
autouse=marker.autouse,
|
autouse=marker.autouse,
|
||||||
)
|
)
|
||||||
|
|
|
@ -35,7 +35,7 @@ def _iter_all_modules(
|
||||||
else:
|
else:
|
||||||
# Type ignored because typeshed doesn't define ModuleType.__path__
|
# Type ignored because typeshed doesn't define ModuleType.__path__
|
||||||
# (only defined on packages).
|
# (only defined on packages).
|
||||||
package_path = package.__path__ # type: ignore[attr-defined]
|
package_path = package.__path__
|
||||||
path, prefix = package_path[0], package.__name__ + "."
|
path, prefix = package_path[0], package.__name__ + "."
|
||||||
for _, name, is_package in pkgutil.iter_modules([path]):
|
for _, name, is_package in pkgutil.iter_modules([path]):
|
||||||
if is_package:
|
if is_package:
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Version info, help messages, tracing configuration."""
|
"""Version info, help messages, tracing configuration."""
|
||||||
|
|
||||||
from argparse import Action
|
from argparse import Action
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Hook specifications for pytest plugins which are invoked by pytest itself
|
"""Hook specifications for pytest plugins which are invoked by pytest itself
|
||||||
and by builtin plugins."""
|
and by builtin plugins."""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
|
@ -22,6 +23,7 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import ExceptionRepr
|
from _pytest._code.code import ExceptionRepr
|
||||||
|
from _pytest.compat import LEGACY_PATH
|
||||||
from _pytest.config import _PluggyPlugin
|
from _pytest.config import _PluggyPlugin
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
@ -296,7 +298,9 @@ def pytest_collection_finish(session: "Session") -> None:
|
||||||
|
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[bool]:
|
def pytest_ignore_collect(
|
||||||
|
collection_path: Path, path: "LEGACY_PATH", config: "Config"
|
||||||
|
) -> Optional[bool]:
|
||||||
"""Return True to prevent considering this path for collection.
|
"""Return True to prevent considering this path for collection.
|
||||||
|
|
||||||
This hook is consulted for all files and directories prior to calling
|
This hook is consulted for all files and directories prior to calling
|
||||||
|
@ -310,10 +314,8 @@ def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[b
|
||||||
|
|
||||||
.. versionchanged:: 7.0.0
|
.. versionchanged:: 7.0.0
|
||||||
The ``collection_path`` parameter was added as a :class:`pathlib.Path`
|
The ``collection_path`` parameter was added as a :class:`pathlib.Path`
|
||||||
equivalent of the ``path`` parameter.
|
equivalent of the ``path`` parameter. The ``path`` parameter
|
||||||
|
has been deprecated.
|
||||||
.. versionchanged:: 8.0.0
|
|
||||||
The ``path`` parameter has been removed.
|
|
||||||
|
|
||||||
Use in conftest plugins
|
Use in conftest plugins
|
||||||
=======================
|
=======================
|
||||||
|
@ -354,7 +356,9 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Collector]":
|
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.
|
"""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
|
For best results, the returned collector should be a subclass of
|
||||||
|
@ -367,10 +371,8 @@ def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Colle
|
||||||
|
|
||||||
.. versionchanged:: 7.0.0
|
.. versionchanged:: 7.0.0
|
||||||
The ``file_path`` parameter was added as a :class:`pathlib.Path`
|
The ``file_path`` parameter was added as a :class:`pathlib.Path`
|
||||||
equivalent of the ``path`` parameter.
|
equivalent of the ``path`` parameter. The ``path`` parameter
|
||||||
|
has been deprecated.
|
||||||
.. versionchanged:: 8.0.0
|
|
||||||
The ``path`` parameter was removed.
|
|
||||||
|
|
||||||
Use in conftest plugins
|
Use in conftest plugins
|
||||||
=======================
|
=======================
|
||||||
|
@ -467,7 +469,9 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
|
||||||
|
|
||||||
|
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]:
|
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.
|
"""Return a :class:`pytest.Module` collector or None for the given path.
|
||||||
|
|
||||||
This hook will be called for each matching test module path.
|
This hook will be called for each matching test module path.
|
||||||
|
@ -483,8 +487,7 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]
|
||||||
The ``module_path`` parameter was added as a :class:`pathlib.Path`
|
The ``module_path`` parameter was added as a :class:`pathlib.Path`
|
||||||
equivalent of the ``path`` parameter.
|
equivalent of the ``path`` parameter.
|
||||||
|
|
||||||
.. versionchanged:: 8.0.0
|
The ``path`` parameter has been deprecated in favor of ``fspath``.
|
||||||
The ``path`` parameter has been removed in favor of ``module_path``.
|
|
||||||
|
|
||||||
Use in conftest plugins
|
Use in conftest plugins
|
||||||
=======================
|
=======================
|
||||||
|
@ -992,7 +995,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
|
||||||
|
|
||||||
|
|
||||||
def pytest_report_header( # type:ignore[empty-body]
|
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]]:
|
) -> Union[str, List[str]]:
|
||||||
"""Return a string or list of strings to be displayed as header info for terminal reporting.
|
"""Return a string or list of strings to be displayed as header info for terminal reporting.
|
||||||
|
|
||||||
|
@ -1009,10 +1012,8 @@ def pytest_report_header( # type:ignore[empty-body]
|
||||||
|
|
||||||
.. versionchanged:: 7.0.0
|
.. versionchanged:: 7.0.0
|
||||||
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
||||||
equivalent of the ``startdir`` parameter.
|
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
|
||||||
|
has been deprecated.
|
||||||
.. versionchanged:: 8.0.0
|
|
||||||
The ``startdir`` parameter has been removed.
|
|
||||||
|
|
||||||
Use in conftest plugins
|
Use in conftest plugins
|
||||||
=======================
|
=======================
|
||||||
|
@ -1024,6 +1025,7 @@ def pytest_report_header( # type:ignore[empty-body]
|
||||||
def pytest_report_collectionfinish( # type:ignore[empty-body]
|
def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||||
config: "Config",
|
config: "Config",
|
||||||
start_path: Path,
|
start_path: Path,
|
||||||
|
startdir: "LEGACY_PATH",
|
||||||
items: Sequence["Item"],
|
items: Sequence["Item"],
|
||||||
) -> Union[str, List[str]]:
|
) -> Union[str, List[str]]:
|
||||||
"""Return a string or list of strings to be displayed after collection
|
"""Return a string or list of strings to be displayed after collection
|
||||||
|
@ -1047,10 +1049,8 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||||
|
|
||||||
.. versionchanged:: 7.0.0
|
.. versionchanged:: 7.0.0
|
||||||
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
||||||
equivalent of the ``startdir`` parameter.
|
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
|
||||||
|
has been deprecated.
|
||||||
.. versionchanged:: 8.0.0
|
|
||||||
The ``startdir`` parameter has been removed.
|
|
||||||
|
|
||||||
Use in conftest plugins
|
Use in conftest plugins
|
||||||
=======================
|
=======================
|
||||||
|
|
|
@ -7,6 +7,7 @@ Based on initial code from Ross Lawley.
|
||||||
Output conforms to
|
Output conforms to
|
||||||
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import functools
|
import functools
|
||||||
import os
|
import os
|
||||||
|
@ -60,7 +61,7 @@ def bin_xml_escape(arg: object) -> str:
|
||||||
# Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
# Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
|
||||||
# For an unknown(?) reason, we disallow #x7F (DEL) as well.
|
# For an unknown(?) reason, we disallow #x7F (DEL) as well.
|
||||||
illegal_xml_re = (
|
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))
|
return re.sub(illegal_xml_re, repl, str(arg))
|
||||||
|
|
||||||
|
@ -261,7 +262,7 @@ class _NodeReporter:
|
||||||
self.__dict__.clear()
|
self.__dict__.clear()
|
||||||
# Type ignored because mypy doesn't like overriding a method.
|
# Type ignored because mypy doesn't like overriding a method.
|
||||||
# Also the return value doesn't match...
|
# 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(
|
def _warn_incompatibility_with_xunit2(
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Add backward compatibility support for the legacy py path type."""
|
"""Add backward compatibility support for the legacy py path type."""
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -14,9 +14,9 @@ from typing import Union
|
||||||
|
|
||||||
from iniconfig import SectionWrapper
|
from iniconfig import SectionWrapper
|
||||||
|
|
||||||
import py
|
|
||||||
|
|
||||||
from _pytest.cacheprovider import Cache
|
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 Config
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.config import PytestPluginManager
|
from _pytest.config import PytestPluginManager
|
||||||
|
@ -39,20 +39,6 @@ if TYPE_CHECKING:
|
||||||
import pexpect
|
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
|
@final
|
||||||
class Testdir:
|
class Testdir:
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Access and control log capturing."""
|
"""Access and control log capturing."""
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from contextlib import nullcontext
|
from contextlib import nullcontext
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -209,7 +210,7 @@ class PercentStyleMultiline(logging.PercentStyle):
|
||||||
if "\n" in record.message:
|
if "\n" in record.message:
|
||||||
if hasattr(record, "auto_indent"):
|
if hasattr(record, "auto_indent"):
|
||||||
# Passed in from the "extra={}" kwarg on the call to logging.log().
|
# 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:
|
else:
|
||||||
auto_indent = self._auto_indent
|
auto_indent = self._auto_indent
|
||||||
|
|
||||||
|
@ -512,7 +513,7 @@ class LogCaptureFixture:
|
||||||
|
|
||||||
:return: The original disabled logging level.
|
: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):
|
if isinstance(level, str):
|
||||||
# Try to translate the level string to an int for `logging.disable()`
|
# 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 PytestPluginManager
|
||||||
from _pytest.config import UsageError
|
from _pytest.config import UsageError
|
||||||
from _pytest.config.argparsing import Parser
|
from _pytest.config.argparsing import Parser
|
||||||
|
from _pytest.config.compat import PathAwareHookProxy
|
||||||
from _pytest.fixtures import FixtureManager
|
from _pytest.fixtures import FixtureManager
|
||||||
from _pytest.outcomes import exit
|
from _pytest.outcomes import exit
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
|
@ -222,6 +223,12 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
help="Prepend/append to sys.path when importing test modules and conftest "
|
help="Prepend/append to sys.path when importing test modules and conftest "
|
||||||
"files. Default: prepend.",
|
"files. Default: prepend.",
|
||||||
)
|
)
|
||||||
|
parser.addini(
|
||||||
|
"consider_namespace_packages",
|
||||||
|
type="bool",
|
||||||
|
default=False,
|
||||||
|
help="Consider namespace packages when resolving module names during import",
|
||||||
|
)
|
||||||
|
|
||||||
group = parser.getgroup("debugconfig", "test session debugging and configuration")
|
group = parser.getgroup("debugconfig", "test session debugging and configuration")
|
||||||
group.addoption(
|
group.addoption(
|
||||||
|
@ -551,6 +558,7 @@ class Session(nodes.Collector):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
name="",
|
name="",
|
||||||
path=config.rootpath,
|
path=config.rootpath,
|
||||||
|
fspath=None,
|
||||||
parent=None,
|
parent=None,
|
||||||
config=config,
|
config=config,
|
||||||
session=self,
|
session=self,
|
||||||
|
@ -688,7 +696,7 @@ class Session(nodes.Collector):
|
||||||
proxy: pluggy.HookRelay
|
proxy: pluggy.HookRelay
|
||||||
if remove_mods:
|
if remove_mods:
|
||||||
# One or more conftests are not in use at this path.
|
# 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:
|
else:
|
||||||
# All plugins are active for this fspath.
|
# All plugins are active for this fspath.
|
||||||
proxy = self.config.hook
|
proxy = self.config.hook
|
||||||
|
@ -728,14 +736,12 @@ class Session(nodes.Collector):
|
||||||
@overload
|
@overload
|
||||||
def perform_collect(
|
def perform_collect(
|
||||||
self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
|
self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
|
||||||
) -> Sequence[nodes.Item]:
|
) -> Sequence[nodes.Item]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def perform_collect(
|
def perform_collect(
|
||||||
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
|
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
|
||||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
) -> Sequence[Union[nodes.Item, nodes.Collector]]: ...
|
||||||
...
|
|
||||||
|
|
||||||
def perform_collect(
|
def perform_collect(
|
||||||
self, args: Optional[Sequence[str]] = None, genitems: bool = True
|
self, args: Optional[Sequence[str]] = None, genitems: bool = True
|
||||||
|
@ -924,7 +930,14 @@ class Session(nodes.Collector):
|
||||||
if sys.platform == "win32" and not is_match:
|
if sys.platform == "win32" and not is_match:
|
||||||
# In case the file paths do not match, fallback to samefile() to
|
# In case the file paths do not match, fallback to samefile() to
|
||||||
# account for short-paths on Windows (#11895).
|
# account for short-paths on Windows (#11895).
|
||||||
is_match = os.path.samefile(node.path, matchparts[0])
|
same_file = os.path.samefile(node.path, matchparts[0])
|
||||||
|
# We don't want to match links to the current node,
|
||||||
|
# otherwise we would match the same file more than once (#12039).
|
||||||
|
is_match = same_file and (
|
||||||
|
os.path.islink(node.path)
|
||||||
|
== os.path.islink(matchparts[0])
|
||||||
|
)
|
||||||
|
|
||||||
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
|
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
|
||||||
else:
|
else:
|
||||||
# TODO: Remove parametrized workaround once collection structure contains
|
# TODO: Remove parametrized workaround once collection structure contains
|
||||||
|
|
|
@ -342,7 +342,7 @@ class MarkDecorator:
|
||||||
# return type. Not much we can do about that. Thankfully mypy picks
|
# 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.
|
# the first match so it works out even if we break the rules.
|
||||||
@overload
|
@overload
|
||||||
def __call__(self, arg: Markable) -> Markable: # type: ignore[misc]
|
def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap]
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -433,13 +433,11 @@ if TYPE_CHECKING:
|
||||||
from _pytest.scope import _ScopeName
|
from _pytest.scope import _ScopeName
|
||||||
|
|
||||||
class _SkipMarkDecorator(MarkDecorator):
|
class _SkipMarkDecorator(MarkDecorator):
|
||||||
@overload # type: ignore[override,misc,no-overload-impl]
|
@overload # type: ignore[override,no-overload-impl]
|
||||||
def __call__(self, arg: Markable) -> Markable:
|
def __call__(self, arg: Markable) -> Markable: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __call__(self, reason: str = ...) -> "MarkDecorator":
|
def __call__(self, reason: str = ...) -> "MarkDecorator": ...
|
||||||
...
|
|
||||||
|
|
||||||
class _SkipifMarkDecorator(MarkDecorator):
|
class _SkipifMarkDecorator(MarkDecorator):
|
||||||
def __call__( # type: ignore[override]
|
def __call__( # type: ignore[override]
|
||||||
|
@ -447,13 +445,11 @@ if TYPE_CHECKING:
|
||||||
condition: Union[str, bool] = ...,
|
condition: Union[str, bool] = ...,
|
||||||
*conditions: Union[str, bool],
|
*conditions: Union[str, bool],
|
||||||
reason: str = ...,
|
reason: str = ...,
|
||||||
) -> MarkDecorator:
|
) -> MarkDecorator: ...
|
||||||
...
|
|
||||||
|
|
||||||
class _XfailMarkDecorator(MarkDecorator):
|
class _XfailMarkDecorator(MarkDecorator):
|
||||||
@overload # type: ignore[override,misc,no-overload-impl]
|
@overload # type: ignore[override,no-overload-impl]
|
||||||
def __call__(self, arg: Markable) -> Markable:
|
def __call__(self, arg: Markable) -> Markable: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def __call__(
|
def __call__(
|
||||||
|
@ -466,8 +462,7 @@ if TYPE_CHECKING:
|
||||||
None, Type[BaseException], Tuple[Type[BaseException], ...]
|
None, Type[BaseException], Tuple[Type[BaseException], ...]
|
||||||
] = ...,
|
] = ...,
|
||||||
strict: bool = ...,
|
strict: bool = ...,
|
||||||
) -> MarkDecorator:
|
) -> MarkDecorator: ...
|
||||||
...
|
|
||||||
|
|
||||||
class _ParametrizeMarkDecorator(MarkDecorator):
|
class _ParametrizeMarkDecorator(MarkDecorator):
|
||||||
def __call__( # type: ignore[override]
|
def __call__( # type: ignore[override]
|
||||||
|
@ -483,8 +478,7 @@ if TYPE_CHECKING:
|
||||||
]
|
]
|
||||||
] = ...,
|
] = ...,
|
||||||
scope: Optional[_ScopeName] = ...,
|
scope: Optional[_ScopeName] = ...,
|
||||||
) -> MarkDecorator:
|
) -> MarkDecorator: ...
|
||||||
...
|
|
||||||
|
|
||||||
class _UsefixturesMarkDecorator(MarkDecorator):
|
class _UsefixturesMarkDecorator(MarkDecorator):
|
||||||
def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
|
def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Monkeypatching and mocking functionality."""
|
"""Monkeypatching and mocking functionality."""
|
||||||
|
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -167,8 +168,7 @@ class MonkeyPatch:
|
||||||
name: object,
|
name: object,
|
||||||
value: Notset = ...,
|
value: Notset = ...,
|
||||||
raising: bool = ...,
|
raising: bool = ...,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def setattr(
|
def setattr(
|
||||||
|
@ -177,8 +177,7 @@ class MonkeyPatch:
|
||||||
name: str,
|
name: str,
|
||||||
value: object,
|
value: object,
|
||||||
raising: bool = ...,
|
raising: bool = ...,
|
||||||
) -> None:
|
) -> None: ...
|
||||||
...
|
|
||||||
|
|
||||||
def setattr(
|
def setattr(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import abc
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from inspect import signature
|
from inspect import signature
|
||||||
import os
|
import os
|
||||||
|
import pathlib
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
|
@ -29,8 +30,11 @@ from _pytest._code import getfslineno
|
||||||
from _pytest._code.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
from _pytest._code.code import Traceback
|
from _pytest._code.code import Traceback
|
||||||
|
from _pytest.compat import LEGACY_PATH
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ConftestImportFailure
|
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 Mark
|
||||||
from _pytest.mark.structures import MarkDecorator
|
from _pytest.mark.structures import MarkDecorator
|
||||||
from _pytest.mark.structures import NodeKeywords
|
from _pytest.mark.structures import NodeKeywords
|
||||||
|
@ -55,6 +59,29 @@ tracebackcutdir = Path(_pytest.__file__).parent
|
||||||
|
|
||||||
|
|
||||||
_T = TypeVar("_T")
|
_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")
|
_NodeType = TypeVar("_NodeType", bound="Node")
|
||||||
|
|
||||||
|
|
||||||
|
@ -110,6 +137,13 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
leaf nodes.
|
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.
|
# Use __slots__ to make attribute access faster.
|
||||||
# Note that __dict__ is still available.
|
# Note that __dict__ is still available.
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
|
@ -129,6 +163,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
parent: "Optional[Node]" = None,
|
parent: "Optional[Node]" = None,
|
||||||
config: Optional[Config] = None,
|
config: Optional[Config] = None,
|
||||||
session: "Optional[Session]" = None,
|
session: "Optional[Session]" = None,
|
||||||
|
fspath: Optional[LEGACY_PATH] = None,
|
||||||
path: Optional[Path] = None,
|
path: Optional[Path] = None,
|
||||||
nodeid: Optional[str] = None,
|
nodeid: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -154,11 +189,10 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
raise TypeError("session or parent must be provided")
|
raise TypeError("session or parent must be provided")
|
||||||
self.session = parent.session
|
self.session = parent.session
|
||||||
|
|
||||||
if path is None:
|
if path is None and fspath is None:
|
||||||
path = getattr(parent, "path", None)
|
path = getattr(parent, "path", None)
|
||||||
assert path is not None
|
|
||||||
#: Filesystem path where this node was collected from (can be 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.
|
# The explicit annotation is to avoid publicly exposing NodeKeywords.
|
||||||
#: Keywords/markers collected from all scopes.
|
#: Keywords/markers collected from all scopes.
|
||||||
|
@ -329,12 +363,10 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||||
yield node, mark
|
yield node, mark
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def get_closest_marker(self, name: str) -> Optional[Mark]:
|
def get_closest_marker(self, name: str) -> Optional[Mark]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@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(
|
def get_closest_marker(
|
||||||
self, name: str, default: Optional[Mark] = None
|
self, name: str, default: Optional[Mark] = None
|
||||||
|
@ -529,6 +561,7 @@ class FSCollector(Collector, abc.ABC):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
fspath: Optional[LEGACY_PATH] = None,
|
||||||
path_or_parent: Optional[Union[Path, Node]] = None,
|
path_or_parent: Optional[Union[Path, Node]] = None,
|
||||||
path: Optional[Path] = None,
|
path: Optional[Path] = None,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
|
@ -544,8 +577,8 @@ class FSCollector(Collector, abc.ABC):
|
||||||
elif isinstance(path_or_parent, Path):
|
elif isinstance(path_or_parent, Path):
|
||||||
assert path is None
|
assert path is None
|
||||||
path = path_or_parent
|
path = path_or_parent
|
||||||
assert path is not None
|
|
||||||
|
|
||||||
|
path = _imply_path(type(self), path, fspath=fspath)
|
||||||
if name is None:
|
if name is None:
|
||||||
name = path.name
|
name = path.name
|
||||||
if parent is not None and parent.path != path:
|
if parent is not None and parent.path != path:
|
||||||
|
@ -585,11 +618,12 @@ class FSCollector(Collector, abc.ABC):
|
||||||
cls,
|
cls,
|
||||||
parent,
|
parent,
|
||||||
*,
|
*,
|
||||||
|
fspath: Optional[LEGACY_PATH] = None,
|
||||||
path: Optional[Path] = None,
|
path: Optional[Path] = None,
|
||||||
**kw,
|
**kw,
|
||||||
) -> "Self":
|
) -> "Self":
|
||||||
"""The public constructor."""
|
"""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):
|
class File(FSCollector, abc.ABC):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Submit failure or test session information to a pastebin service."""
|
"""Submit failure or test session information to a pastebin service."""
|
||||||
|
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
import tempfile
|
import tempfile
|
||||||
from typing import IO
|
from typing import IO
|
||||||
|
|
|
@ -484,73 +484,90 @@ class ImportPathMismatchError(ImportError):
|
||||||
|
|
||||||
|
|
||||||
def import_path(
|
def import_path(
|
||||||
p: Union[str, "os.PathLike[str]"],
|
path: Union[str, "os.PathLike[str]"],
|
||||||
*,
|
*,
|
||||||
mode: Union[str, ImportMode] = ImportMode.prepend,
|
mode: Union[str, ImportMode] = ImportMode.prepend,
|
||||||
root: Path,
|
root: Path,
|
||||||
|
consider_namespace_packages: bool,
|
||||||
) -> ModuleType:
|
) -> ModuleType:
|
||||||
"""Import and return a module from the given path, which can be a file (a module) or
|
"""
|
||||||
|
Import and return a module from the given path, which can be a file (a module) or
|
||||||
a directory (a package).
|
a directory (a package).
|
||||||
|
|
||||||
The import mechanism used is controlled by the `mode` parameter:
|
:param path:
|
||||||
|
Path to the file to import.
|
||||||
|
|
||||||
* `mode == ImportMode.prepend`: the directory containing the module (or package, taking
|
:param mode:
|
||||||
`__init__.py` files into account) will be put at the *start* of `sys.path` before
|
Controls the underlying import mechanism that will be used:
|
||||||
being imported with `importlib.import_module`.
|
|
||||||
|
|
||||||
* `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
|
* ImportMode.prepend: the directory containing the module (or package, taking
|
||||||
to the end of `sys.path`, if not already in `sys.path`.
|
`__init__.py` files into account) will be put at the *start* of `sys.path` before
|
||||||
|
being imported with `importlib.import_module`.
|
||||||
|
|
||||||
* `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib`
|
* ImportMode.append: same as `prepend`, but the directory will be appended
|
||||||
to import the module, which avoids having to muck with `sys.path` at all. It effectively
|
to the end of `sys.path`, if not already in `sys.path`.
|
||||||
allows having same-named test modules in different places.
|
|
||||||
|
* ImportMode.importlib: uses more fine control mechanisms provided by `importlib`
|
||||||
|
to import the module, which avoids having to muck with `sys.path` at all. It effectively
|
||||||
|
allows having same-named test modules in different places.
|
||||||
|
|
||||||
:param root:
|
:param root:
|
||||||
Used as an anchor when mode == ImportMode.importlib to obtain
|
Used as an anchor when mode == ImportMode.importlib to obtain
|
||||||
a unique name for the module being imported so it can safely be stored
|
a unique name for the module being imported so it can safely be stored
|
||||||
into ``sys.modules``.
|
into ``sys.modules``.
|
||||||
|
|
||||||
|
:param consider_namespace_packages:
|
||||||
|
If True, consider namespace packages when resolving module names.
|
||||||
|
|
||||||
:raises ImportPathMismatchError:
|
:raises ImportPathMismatchError:
|
||||||
If after importing the given `path` and the module `__file__`
|
If after importing the given `path` and the module `__file__`
|
||||||
are different. Only raised in `prepend` and `append` modes.
|
are different. Only raised in `prepend` and `append` modes.
|
||||||
"""
|
"""
|
||||||
|
path = Path(path)
|
||||||
mode = ImportMode(mode)
|
mode = ImportMode(mode)
|
||||||
|
|
||||||
path = Path(p)
|
|
||||||
|
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
raise ImportError(path)
|
raise ImportError(path)
|
||||||
|
|
||||||
if mode is ImportMode.importlib:
|
if mode is ImportMode.importlib:
|
||||||
|
# Try to import this module using the standard import mechanisms, but
|
||||||
|
# without touching sys.path.
|
||||||
|
try:
|
||||||
|
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||||
|
path, consider_namespace_packages=consider_namespace_packages
|
||||||
|
)
|
||||||
|
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
|
||||||
|
)
|
||||||
|
if mod is not None:
|
||||||
|
return mod
|
||||||
|
|
||||||
|
# Could not import the module with the current sys.path, so we fall back
|
||||||
|
# to importing the file as a single module, not being a part of a package.
|
||||||
module_name = module_name_from_path(path, root)
|
module_name = module_name_from_path(path, root)
|
||||||
with contextlib.suppress(KeyError):
|
with contextlib.suppress(KeyError):
|
||||||
return sys.modules[module_name]
|
return sys.modules[module_name]
|
||||||
|
|
||||||
for meta_importer in sys.meta_path:
|
mod = _import_module_using_spec(
|
||||||
spec = meta_importer.find_spec(module_name, [str(path.parent)])
|
module_name, path, path.parent, insert_modules=True
|
||||||
if spec is not None:
|
)
|
||||||
break
|
if mod is None:
|
||||||
else:
|
|
||||||
spec = importlib.util.spec_from_file_location(module_name, str(path))
|
|
||||||
|
|
||||||
if spec is None:
|
|
||||||
raise ImportError(f"Can't find module {module_name} at location {path}")
|
raise ImportError(f"Can't find module {module_name} at location {path}")
|
||||||
mod = importlib.util.module_from_spec(spec)
|
|
||||||
sys.modules[module_name] = mod
|
|
||||||
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
|
||||||
insert_missing_modules(sys.modules, module_name)
|
|
||||||
return mod
|
return mod
|
||||||
|
|
||||||
pkg_path = resolve_package_path(path)
|
try:
|
||||||
if pkg_path is not None:
|
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||||
pkg_root = pkg_path.parent
|
path, consider_namespace_packages=consider_namespace_packages
|
||||||
names = list(path.with_suffix("").relative_to(pkg_root).parts)
|
)
|
||||||
if names[-1] == "__init__":
|
except CouldNotResolvePathError:
|
||||||
names.pop()
|
pkg_root, module_name = path.parent, path.stem
|
||||||
module_name = ".".join(names)
|
|
||||||
else:
|
|
||||||
pkg_root = path.parent
|
|
||||||
module_name = path.stem
|
|
||||||
|
|
||||||
# Change sys.path permanently: restoring it at the end of this function would cause surprising
|
# Change sys.path permanently: restoring it at the end of this function would cause surprising
|
||||||
# problems because of delayed imports: for example, a conftest.py file imported by this function
|
# problems because of delayed imports: for example, a conftest.py file imported by this function
|
||||||
|
@ -592,6 +609,40 @@ def import_path(
|
||||||
return mod
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def _import_module_using_spec(
|
||||||
|
module_name: str, module_path: Path, module_location: Path, *, insert_modules: bool
|
||||||
|
) -> Optional[ModuleType]:
|
||||||
|
"""
|
||||||
|
Tries to import a module by its canonical name, path to the .py file, and its
|
||||||
|
parent location.
|
||||||
|
|
||||||
|
: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:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
|
||||||
|
if spec is not None:
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[module_name] = mod
|
||||||
|
spec.loader.exec_module(mod) # type: ignore[union-attr]
|
||||||
|
if insert_modules:
|
||||||
|
insert_missing_modules(sys.modules, module_name)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# Implement a special _is_same function on Windows which returns True if the two filenames
|
# 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).
|
# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
|
||||||
if sys.platform.startswith("win"):
|
if sys.platform.startswith("win"):
|
||||||
|
@ -628,6 +679,11 @@ def module_name_from_path(path: Path, root: Path) -> str:
|
||||||
if len(path_parts) >= 2 and path_parts[-1] == "__init__":
|
if len(path_parts) >= 2 and path_parts[-1] == "__init__":
|
||||||
path_parts = path_parts[:-1]
|
path_parts = path_parts[:-1]
|
||||||
|
|
||||||
|
# Module names cannot contain ".", normalize them to "_". This prevents
|
||||||
|
# a directory having a "." in the name (".env.310" for example) causing extra intermediate modules.
|
||||||
|
# Also, important to replace "." at the start of paths, as those are considered relative imports.
|
||||||
|
path_parts = tuple(x.replace(".", "_") for x in path_parts)
|
||||||
|
|
||||||
return ".".join(path_parts)
|
return ".".join(path_parts)
|
||||||
|
|
||||||
|
|
||||||
|
@ -689,6 +745,60 @@ def resolve_package_path(path: Path) -> Optional[Path]:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_pkg_root_and_module_name(
|
||||||
|
path: Path, *, consider_namespace_packages: bool = False
|
||||||
|
) -> Tuple[Path, str]:
|
||||||
|
"""
|
||||||
|
Return the path to the directory of the root package that contains the
|
||||||
|
given Python file, and its module name:
|
||||||
|
|
||||||
|
src/
|
||||||
|
app/
|
||||||
|
__init__.py
|
||||||
|
core/
|
||||||
|
__init__.py
|
||||||
|
models.py
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
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_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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
raise CouldNotResolvePathError(f"Could not resolve for {path}")
|
||||||
|
|
||||||
|
|
||||||
|
class CouldNotResolvePathError(Exception):
|
||||||
|
"""Custom exception raised by resolve_pkg_root_and_module_name."""
|
||||||
|
|
||||||
|
|
||||||
def scandir(
|
def scandir(
|
||||||
path: Union[str, "os.PathLike[str]"],
|
path: Union[str, "os.PathLike[str]"],
|
||||||
sort_key: Callable[["os.DirEntry[str]"], object] = lambda entry: entry.name,
|
sort_key: Callable[["os.DirEntry[str]"], object] = lambda entry: entry.name,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
PYTEST_DONT_REWRITE
|
PYTEST_DONT_REWRITE
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import collections.abc
|
import collections.abc
|
||||||
import contextlib
|
import contextlib
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
|
@ -245,8 +246,7 @@ class RecordedHookCall:
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# The class has undetermined attributes, this tells mypy about it.
|
# The class has undetermined attributes, this tells mypy about it.
|
||||||
def __getattr__(self, key: str):
|
def __getattr__(self, key: str): ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
@ -327,15 +327,13 @@ class HookRecorder:
|
||||||
def getreports(
|
def getreports(
|
||||||
self,
|
self,
|
||||||
names: "Literal['pytest_collectreport']",
|
names: "Literal['pytest_collectreport']",
|
||||||
) -> Sequence[CollectReport]:
|
) -> Sequence[CollectReport]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def getreports(
|
def getreports(
|
||||||
self,
|
self,
|
||||||
names: "Literal['pytest_runtest_logreport']",
|
names: "Literal['pytest_runtest_logreport']",
|
||||||
) -> Sequence[TestReport]:
|
) -> Sequence[TestReport]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def getreports(
|
def getreports(
|
||||||
|
@ -344,8 +342,7 @@ class HookRecorder:
|
||||||
"pytest_collectreport",
|
"pytest_collectreport",
|
||||||
"pytest_runtest_logreport",
|
"pytest_runtest_logreport",
|
||||||
),
|
),
|
||||||
) -> Sequence[Union[CollectReport, TestReport]]:
|
) -> Sequence[Union[CollectReport, TestReport]]: ...
|
||||||
...
|
|
||||||
|
|
||||||
def getreports(
|
def getreports(
|
||||||
self,
|
self,
|
||||||
|
@ -390,15 +387,13 @@ class HookRecorder:
|
||||||
def getfailures(
|
def getfailures(
|
||||||
self,
|
self,
|
||||||
names: "Literal['pytest_collectreport']",
|
names: "Literal['pytest_collectreport']",
|
||||||
) -> Sequence[CollectReport]:
|
) -> Sequence[CollectReport]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def getfailures(
|
def getfailures(
|
||||||
self,
|
self,
|
||||||
names: "Literal['pytest_runtest_logreport']",
|
names: "Literal['pytest_runtest_logreport']",
|
||||||
) -> Sequence[TestReport]:
|
) -> Sequence[TestReport]: ...
|
||||||
...
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
def getfailures(
|
def getfailures(
|
||||||
|
@ -407,8 +402,7 @@ class HookRecorder:
|
||||||
"pytest_collectreport",
|
"pytest_collectreport",
|
||||||
"pytest_runtest_logreport",
|
"pytest_runtest_logreport",
|
||||||
),
|
),
|
||||||
) -> Sequence[Union[CollectReport, TestReport]]:
|
) -> Sequence[Union[CollectReport, TestReport]]: ...
|
||||||
...
|
|
||||||
|
|
||||||
def getfailures(
|
def getfailures(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Python test discovery, setup and run of test functions."""
|
"""Python test discovery, setup and run of test functions."""
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
@ -48,6 +49,7 @@ from _pytest.compat import getimfunc
|
||||||
from _pytest.compat import getlocation
|
from _pytest.compat import getlocation
|
||||||
from _pytest.compat import is_async_function
|
from _pytest.compat import is_async_function
|
||||||
from _pytest.compat import is_generator
|
from _pytest.compat import is_generator
|
||||||
|
from _pytest.compat import LEGACY_PATH
|
||||||
from _pytest.compat import NOTSET
|
from _pytest.compat import NOTSET
|
||||||
from _pytest.compat import safe_getattr
|
from _pytest.compat import safe_getattr
|
||||||
from _pytest.compat import safe_isclass
|
from _pytest.compat import safe_isclass
|
||||||
|
@ -301,10 +303,10 @@ class PyobjMixin(nodes.Node):
|
||||||
"""Python instance object the function is bound to.
|
"""Python instance object the function is bound to.
|
||||||
|
|
||||||
Returns None if not a test method, e.g. for a standalone test function,
|
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)
|
# Overridden by Function.
|
||||||
return getattr(node.obj, "__self__", None) if node is not None else None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def obj(self):
|
def obj(self):
|
||||||
|
@ -516,7 +518,12 @@ def importtestmodule(
|
||||||
# We assume we are only called once per module.
|
# We assume we are only called once per module.
|
||||||
importmode = config.getoption("--import-mode")
|
importmode = config.getoption("--import-mode")
|
||||||
try:
|
try:
|
||||||
mod = import_path(path, mode=importmode, root=config.rootpath)
|
mod = import_path(
|
||||||
|
path,
|
||||||
|
mode=importmode,
|
||||||
|
root=config.rootpath,
|
||||||
|
consider_namespace_packages=config.getini("consider_namespace_packages"),
|
||||||
|
)
|
||||||
except SyntaxError as e:
|
except SyntaxError as e:
|
||||||
raise nodes.Collector.CollectError(
|
raise nodes.Collector.CollectError(
|
||||||
ExceptionInfo.from_current().getrepr(style="short")
|
ExceptionInfo.from_current().getrepr(style="short")
|
||||||
|
@ -660,6 +667,7 @@ class Package(nodes.Directory):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
fspath: Optional[LEGACY_PATH],
|
||||||
parent: nodes.Collector,
|
parent: nodes.Collector,
|
||||||
# NOTE: following args are unused:
|
# NOTE: following args are unused:
|
||||||
config=None,
|
config=None,
|
||||||
|
@ -671,6 +679,7 @@ class Package(nodes.Directory):
|
||||||
# super().__init__(self, fspath, parent=parent)
|
# super().__init__(self, fspath, parent=parent)
|
||||||
session = parent.session
|
session = parent.session
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
fspath=fspath,
|
||||||
path=path,
|
path=path,
|
||||||
parent=parent,
|
parent=parent,
|
||||||
config=config,
|
config=config,
|
||||||
|
@ -1309,7 +1318,6 @@ class Metafunc:
|
||||||
func=get_direct_param_fixture_func,
|
func=get_direct_param_fixture_func,
|
||||||
scope=scope_,
|
scope=scope_,
|
||||||
params=None,
|
params=None,
|
||||||
unittest=False,
|
|
||||||
ids=None,
|
ids=None,
|
||||||
_ispytest=True,
|
_ispytest=True,
|
||||||
)
|
)
|
||||||
|
@ -1695,7 +1703,8 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
super().__init__(name, parent, config=config, session=session)
|
super().__init__(name, parent, config=config, session=session)
|
||||||
|
|
||||||
if callobj is not NOTSET:
|
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
|
#: Original function name, without any decorations (for example
|
||||||
#: parametrization adds a ``"[...]"`` suffix to function names), used to access
|
#: parametrization adds a ``"[...]"`` suffix to function names), used to access
|
||||||
|
@ -1745,12 +1754,31 @@ class Function(PyobjMixin, nodes.Item):
|
||||||
"""Underlying python 'function' object."""
|
"""Underlying python 'function' object."""
|
||||||
return getimfunc(self.obj)
|
return getimfunc(self.obj)
|
||||||
|
|
||||||
def _getobj(self):
|
@property
|
||||||
assert self.parent is not None
|
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):
|
if isinstance(self.parent, Class):
|
||||||
# Each Function gets a fresh class instance.
|
# Each Function gets a fresh class instance.
|
||||||
parent_obj = self.parent.newinstance()
|
return self.parent.newinstance()
|
||||||
else:
|
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]
|
parent_obj = self.parent.obj # type: ignore[attr-defined]
|
||||||
return getattr(parent_obj, self.originalname)
|
return getattr(parent_obj, self.originalname)
|
||||||
|
|
||||||
|
|
|
@ -393,7 +393,7 @@ class ApproxScalar(ApproxBase):
|
||||||
# tolerances, i.e. non-numerics and infinities. Need to call abs to
|
# tolerances, i.e. non-numerics and infinities. Need to call abs to
|
||||||
# handle complex numbers, e.g. (inf + 1j).
|
# handle complex numbers, e.g. (inf + 1j).
|
||||||
if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf(
|
if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf(
|
||||||
abs(self.expected) # type: ignore[arg-type]
|
abs(self.expected)
|
||||||
):
|
):
|
||||||
return str(self.expected)
|
return str(self.expected)
|
||||||
|
|
||||||
|
@ -437,8 +437,8 @@ class ApproxScalar(ApproxBase):
|
||||||
# Allow the user to control whether NaNs are considered equal to each
|
# Allow the user to control whether NaNs are considered equal to each
|
||||||
# other or not. The abs() calls are for compatibility with complex
|
# other or not. The abs() calls are for compatibility with complex
|
||||||
# numbers.
|
# numbers.
|
||||||
if math.isnan(abs(self.expected)): # type: ignore[arg-type]
|
if math.isnan(abs(self.expected)):
|
||||||
return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type]
|
return self.nan_ok and math.isnan(abs(actual))
|
||||||
|
|
||||||
# Infinity shouldn't be approximately equal to anything but itself, but
|
# Infinity shouldn't be approximately equal to anything but itself, but
|
||||||
# if there's a relative tolerance, it will be infinite and infinity
|
# if there's a relative tolerance, it will be infinite and infinity
|
||||||
|
@ -446,11 +446,11 @@ class ApproxScalar(ApproxBase):
|
||||||
# case would have been short circuited above, so here we can just
|
# case would have been short circuited above, so here we can just
|
||||||
# return false if the expected value is infinite. The abs() call is
|
# return false if the expected value is infinite. The abs() call is
|
||||||
# for compatibility with complex numbers.
|
# for compatibility with complex numbers.
|
||||||
if math.isinf(abs(self.expected)): # type: ignore[arg-type]
|
if math.isinf(abs(self.expected)):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Return true if the two numbers are within the tolerance.
|
# 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
|
return result
|
||||||
|
|
||||||
# Ignore type because of https://github.com/python/mypy/issues/4266.
|
# Ignore type because of https://github.com/python/mypy/issues/4266.
|
||||||
|
@ -769,8 +769,7 @@ def raises(
|
||||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
||||||
*,
|
*,
|
||||||
match: Optional[Union[str, Pattern[str]]] = ...,
|
match: Optional[Union[str, Pattern[str]]] = ...,
|
||||||
) -> "RaisesContext[E]":
|
) -> "RaisesContext[E]": ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -779,8 +778,7 @@ def raises(
|
||||||
func: Callable[..., Any],
|
func: Callable[..., Any],
|
||||||
*args: Any,
|
*args: Any,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> _pytest._code.ExceptionInfo[E]:
|
) -> _pytest._code.ExceptionInfo[E]: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def raises(
|
def raises(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Record warnings during test function execution."""
|
"""Record warnings during test function execution."""
|
||||||
|
|
||||||
from pprint import pformat
|
from pprint import pformat
|
||||||
import re
|
import re
|
||||||
from types import TracebackType
|
from types import TracebackType
|
||||||
|
@ -43,13 +44,11 @@ def recwarn() -> Generator["WarningsRecorder", None, None]:
|
||||||
@overload
|
@overload
|
||||||
def deprecated_call(
|
def deprecated_call(
|
||||||
*, match: Optional[Union[str, Pattern[str]]] = ...
|
*, match: Optional[Union[str, Pattern[str]]] = ...
|
||||||
) -> "WarningsRecorder":
|
) -> "WarningsRecorder": ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@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(
|
def deprecated_call(
|
||||||
|
@ -91,8 +90,7 @@ def warns(
|
||||||
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ...,
|
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ...,
|
||||||
*,
|
*,
|
||||||
match: Optional[Union[str, Pattern[str]]] = ...,
|
match: Optional[Union[str, Pattern[str]]] = ...,
|
||||||
) -> "WarningsChecker":
|
) -> "WarningsChecker": ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
@ -101,8 +99,7 @@ def warns(
|
||||||
func: Callable[..., T],
|
func: Callable[..., T],
|
||||||
*args: Any,
|
*args: Any,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> T:
|
) -> T: ...
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def warns(
|
def warns(
|
||||||
|
@ -184,8 +181,7 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
|
||||||
|
|
||||||
def __init__(self, *, _ispytest: bool = False) -> None:
|
def __init__(self, *, _ispytest: bool = False) -> None:
|
||||||
check_ispytest(_ispytest)
|
check_ispytest(_ispytest)
|
||||||
# Type ignored due to the way typeshed handles warnings.catch_warnings.
|
super().__init__(record=True)
|
||||||
super().__init__(record=True) # type: ignore[call-arg]
|
|
||||||
self._entered = False
|
self._entered = False
|
||||||
self._list: List[warnings.WarningMessage] = []
|
self._list: List[warnings.WarningMessage] = []
|
||||||
|
|
||||||
|
|
|
@ -72,8 +72,7 @@ class BaseReport:
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# Can have arbitrary fields given to __init__().
|
# 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:
|
def toterminal(self, out: TerminalWriter) -> None:
|
||||||
if hasattr(self, "node"):
|
if hasattr(self, "node"):
|
||||||
|
@ -606,9 +605,9 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
description,
|
description,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
exception_info: Union[
|
exception_info: Union[ExceptionChainRepr, ReprExceptionInfo] = (
|
||||||
ExceptionChainRepr, ReprExceptionInfo
|
ExceptionChainRepr(chain)
|
||||||
] = ExceptionChainRepr(chain)
|
)
|
||||||
else:
|
else:
|
||||||
exception_info = ReprExceptionInfo(
|
exception_info = ReprExceptionInfo(
|
||||||
reprtraceback=reprtraceback,
|
reprtraceback=reprtraceback,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Basic collect and runtest protocol implementations."""
|
"""Basic collect and runtest protocol implementations."""
|
||||||
|
|
||||||
import bdb
|
import bdb
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
|
@ -84,7 +85,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
|
||||||
dlist.append(rep)
|
dlist.append(rep)
|
||||||
if not dlist:
|
if not dlist:
|
||||||
return
|
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:
|
if not durations:
|
||||||
tr.write_sep("=", "slowest durations")
|
tr.write_sep("=", "slowest durations")
|
||||||
else:
|
else:
|
||||||
|
@ -380,6 +381,9 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
||||||
collector.path,
|
collector.path,
|
||||||
collector.config.getoption("importmode"),
|
collector.config.getoption("importmode"),
|
||||||
rootpath=collector.config.rootpath,
|
rootpath=collector.config.rootpath,
|
||||||
|
consider_namespace_packages=collector.config.getini(
|
||||||
|
"consider_namespace_packages"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return list(collector.collect())
|
return list(collector.collect())
|
||||||
|
@ -392,8 +396,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
||||||
skip_exceptions = [Skipped]
|
skip_exceptions = [Skipped]
|
||||||
unittest = sys.modules.get("unittest")
|
unittest = sys.modules.get("unittest")
|
||||||
if unittest is not None:
|
if unittest is not None:
|
||||||
# Type ignored because unittest is loaded dynamically.
|
skip_exceptions.append(unittest.SkipTest)
|
||||||
skip_exceptions.append(unittest.SkipTest) # type: ignore
|
|
||||||
if isinstance(call.excinfo.value, tuple(skip_exceptions)):
|
if isinstance(call.excinfo.value, tuple(skip_exceptions)):
|
||||||
outcome = "skipped"
|
outcome = "skipped"
|
||||||
r_ = collector._repr_failure_py(call.excinfo, "line")
|
r_ = collector._repr_failure_py(call.excinfo, "line")
|
||||||
|
|
|
@ -58,7 +58,7 @@ def pytest_fixture_post_finalizer(
|
||||||
if config.option.setupshow:
|
if config.option.setupshow:
|
||||||
_show_fixture_action(fixturedef, request.config, "TEARDOWN")
|
_show_fixture_action(fixturedef, request.config, "TEARDOWN")
|
||||||
if hasattr(fixturedef, "cached_param"):
|
if hasattr(fixturedef, "cached_param"):
|
||||||
del fixturedef.cached_param # type: ignore[attr-defined]
|
del fixturedef.cached_param
|
||||||
|
|
||||||
|
|
||||||
def _show_fixture_action(
|
def _show_fixture_action(
|
||||||
|
@ -87,7 +87,7 @@ def _show_fixture_action(
|
||||||
tw.write(" (fixtures used: {})".format(", ".join(deps)))
|
tw.write(" (fixtures used: {})".format(", ".join(deps)))
|
||||||
|
|
||||||
if hasattr(fixturedef, "cached_param"):
|
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()
|
tw.flush()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Support for skip/xfail functions and markers."""
|
"""Support for skip/xfail functions and markers."""
|
||||||
|
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
|
@ -109,7 +110,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
|
||||||
)
|
)
|
||||||
globals_.update(dictionary)
|
globals_.update(dictionary)
|
||||||
if hasattr(item, "obj"):
|
if hasattr(item, "obj"):
|
||||||
globals_.update(item.obj.__globals__) # type: ignore[attr-defined]
|
globals_.update(item.obj.__globals__)
|
||||||
try:
|
try:
|
||||||
filename = f"<{mark.name} condition>"
|
filename = f"<{mark.name} condition>"
|
||||||
condition_code = compile(condition, filename, "eval")
|
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 associated with the type ``T`` of the value of the key.
|
||||||
|
|
||||||
A ``StashKey`` is unique and cannot conflict with another key.
|
A ``StashKey`` is unique and cannot conflict with another key.
|
||||||
|
|
||||||
|
.. versionadded:: 7.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
@ -61,6 +63,8 @@ class Stash:
|
||||||
some_str = stash[some_str_key]
|
some_str = stash[some_str_key]
|
||||||
# The static type of some_bool is bool.
|
# The static type of some_bool is bool.
|
||||||
some_bool = stash[some_bool_key]
|
some_bool = stash[some_bool_key]
|
||||||
|
|
||||||
|
.. versionadded:: 7.0
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ("_storage",)
|
__slots__ = ("_storage",)
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
This is a good source for looking at the various reporting hooks.
|
This is a good source for looking at the various reporting hooks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Support for providing temporary directories to test functions."""
|
"""Support for providing temporary directories to test functions."""
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Discover and run std-library "unittest" style tests."""
|
"""Discover and run std-library "unittest" style tests."""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
|
@ -15,7 +16,6 @@ from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
from _pytest.compat import getimfunc
|
|
||||||
from _pytest.compat import is_async_function
|
from _pytest.compat import is_async_function
|
||||||
from _pytest.config import hookimpl
|
from _pytest.config import hookimpl
|
||||||
from _pytest.fixtures import FixtureRequest
|
from _pytest.fixtures import FixtureRequest
|
||||||
|
@ -63,6 +63,14 @@ class UnitTestCase(Class):
|
||||||
# to declare that our children do not support funcargs.
|
# to declare that our children do not support funcargs.
|
||||||
nofuncargs = True
|
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]]:
|
def collect(self) -> Iterable[Union[Item, Collector]]:
|
||||||
from unittest import TestLoader
|
from unittest import TestLoader
|
||||||
|
|
||||||
|
@ -76,23 +84,22 @@ class UnitTestCase(Class):
|
||||||
self._register_unittest_setup_class_fixture(cls)
|
self._register_unittest_setup_class_fixture(cls)
|
||||||
self._register_setup_class_fixture()
|
self._register_setup_class_fixture()
|
||||||
|
|
||||||
self.session._fixturemanager.parsefactories(self, unittest=True)
|
self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid)
|
||||||
|
|
||||||
loader = TestLoader()
|
loader = TestLoader()
|
||||||
foundsomething = False
|
foundsomething = False
|
||||||
for name in loader.getTestCaseNames(self.obj):
|
for name in loader.getTestCaseNames(self.obj):
|
||||||
x = getattr(self.obj, name)
|
x = getattr(self.obj, name)
|
||||||
if not getattr(x, "__test__", True):
|
if not getattr(x, "__test__", True):
|
||||||
continue
|
continue
|
||||||
funcobj = getimfunc(x)
|
yield TestCaseFunction.from_parent(self, name=name)
|
||||||
yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj)
|
|
||||||
foundsomething = True
|
foundsomething = True
|
||||||
|
|
||||||
if not foundsomething:
|
if not foundsomething:
|
||||||
runtest = getattr(self.obj, "runTest", None)
|
runtest = getattr(self.obj, "runTest", None)
|
||||||
if runtest is not None:
|
if runtest is not None:
|
||||||
ut = sys.modules.get("twisted.trial.unittest", 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:
|
||||||
if ut is None or runtest != ut.TestCase.runTest: # type: ignore
|
|
||||||
yield TestCaseFunction.from_parent(self, name="runTest")
|
yield TestCaseFunction.from_parent(self, name="runTest")
|
||||||
|
|
||||||
def _register_unittest_setup_class_fixture(self, cls: type) -> None:
|
def _register_unittest_setup_class_fixture(self, cls: type) -> None:
|
||||||
|
@ -169,23 +176,20 @@ class UnitTestCase(Class):
|
||||||
class TestCaseFunction(Function):
|
class TestCaseFunction(Function):
|
||||||
nofuncargs = True
|
nofuncargs = True
|
||||||
_excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
|
_excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
|
||||||
_testcase: Optional["unittest.TestCase"] = None
|
|
||||||
|
|
||||||
def _getobj(self):
|
def _getinstance(self):
|
||||||
assert self.parent is not None
|
assert isinstance(self.parent, UnitTestCase)
|
||||||
# Unlike a regular Function in a Class, where `item.obj` returns
|
return self.parent.obj(self.name)
|
||||||
# a *bound* method (attached to an instance), TestCaseFunction's
|
|
||||||
# `obj` returns an *unbound* method (not attached to an instance).
|
# Backward compat for pytest-django; can be removed after pytest-django
|
||||||
# This inconsistency is probably not desirable, but needs some
|
# updates + some slack.
|
||||||
# consideration before changing.
|
@property
|
||||||
return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined]
|
def _testcase(self):
|
||||||
|
return self.instance
|
||||||
|
|
||||||
def setup(self) -> None:
|
def setup(self) -> None:
|
||||||
# A bound method to be called during teardown() if set (see 'runtest()').
|
# A bound method to be called during teardown() if set (see 'runtest()').
|
||||||
self._explicit_tearDown: Optional[Callable[[], None]] = None
|
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()
|
super().setup()
|
||||||
|
|
||||||
def teardown(self) -> None:
|
def teardown(self) -> None:
|
||||||
|
@ -193,7 +197,6 @@ class TestCaseFunction(Function):
|
||||||
if self._explicit_tearDown is not None:
|
if self._explicit_tearDown is not None:
|
||||||
self._explicit_tearDown()
|
self._explicit_tearDown()
|
||||||
self._explicit_tearDown = None
|
self._explicit_tearDown = None
|
||||||
self._testcase = None
|
|
||||||
self._obj = None
|
self._obj = None
|
||||||
|
|
||||||
def startTest(self, testcase: "unittest.TestCase") -> None:
|
def startTest(self, testcase: "unittest.TestCase") -> None:
|
||||||
|
@ -292,14 +295,14 @@ class TestCaseFunction(Function):
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
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)
|
maybe_wrap_pytest_function_for_tracing(self)
|
||||||
|
|
||||||
# Let the unittest framework handle async functions.
|
# Let the unittest framework handle async functions.
|
||||||
if is_async_function(self.obj):
|
if is_async_function(self.obj):
|
||||||
# Type ignored because self acts as the TestResult, but is not actually one.
|
testcase(result=self)
|
||||||
self._testcase(result=self) # type: ignore[arg-type]
|
|
||||||
else:
|
else:
|
||||||
# When --pdb is given, we want to postpone calling tearDown() otherwise
|
# When --pdb is given, we want to postpone calling tearDown() otherwise
|
||||||
# when entering the pdb prompt, tearDown() would have probably cleaned up
|
# when entering the pdb prompt, tearDown() would have probably cleaned up
|
||||||
|
@ -311,16 +314,16 @@ class TestCaseFunction(Function):
|
||||||
assert isinstance(self.parent, UnitTestCase)
|
assert isinstance(self.parent, UnitTestCase)
|
||||||
skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj)
|
skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj)
|
||||||
if self.config.getoption("usepdb") and not skipped:
|
if self.config.getoption("usepdb") and not skipped:
|
||||||
self._explicit_tearDown = self._testcase.tearDown
|
self._explicit_tearDown = testcase.tearDown
|
||||||
setattr(self._testcase, "tearDown", lambda *args: None)
|
setattr(testcase, "tearDown", lambda *args: None)
|
||||||
|
|
||||||
# We need to update the actual bound method with self.obj, because
|
# We need to update the actual bound method with self.obj, because
|
||||||
# wrap_pytest_function_for_tracing replaces self.obj by a wrapper.
|
# 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:
|
try:
|
||||||
self._testcase(result=self) # type: ignore[arg-type]
|
testcase(result=self)
|
||||||
finally:
|
finally:
|
||||||
delattr(self._testcase, self.name)
|
delattr(testcase, self.name)
|
||||||
|
|
||||||
def _traceback_filter(
|
def _traceback_filter(
|
||||||
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
|
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
|
||||||
|
@ -349,9 +352,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
|
||||||
# its own nose.SkipTest. For unittest TestCases, SkipTest is already
|
# its own nose.SkipTest. For unittest TestCases, SkipTest is already
|
||||||
# handled internally, and doesn't reach here.
|
# handled internally, and doesn't reach here.
|
||||||
unittest = sys.modules.get("unittest")
|
unittest = sys.modules.get("unittest")
|
||||||
if (
|
if unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest):
|
||||||
unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined]
|
|
||||||
):
|
|
||||||
excinfo = call.excinfo
|
excinfo = call.excinfo
|
||||||
call2 = CallInfo[None].from_call(
|
call2 = CallInfo[None].from_call(
|
||||||
lambda: pytest.skip(str(excinfo.value)), call.when
|
lambda: pytest.skip(str(excinfo.value)), call.when
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# PYTHON_ARGCOMPLETE_OK
|
# PYTHON_ARGCOMPLETE_OK
|
||||||
"""pytest: unit and functional testing with Python."""
|
"""pytest: unit and functional testing with Python."""
|
||||||
|
|
||||||
from _pytest import __version__
|
from _pytest import __version__
|
||||||
from _pytest import version_tuple
|
from _pytest import version_tuple
|
||||||
from _pytest._code import ExceptionInfo
|
from _pytest._code import ExceptionInfo
|
||||||
|
|
|
@ -16,8 +16,8 @@ import pytest
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def ignore_encoding_warning():
|
def ignore_encoding_warning():
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
with contextlib.suppress(NameError): # new in 3.10
|
if sys.version_info > (3, 10):
|
||||||
warnings.simplefilter("ignore", EncodingWarning) # type: ignore [name-defined]
|
warnings.simplefilter("ignore", EncodingWarning)
|
||||||
yield
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@ -822,7 +822,7 @@ class TestLocalPath(CommonFSTests):
|
||||||
# depending on how the paths are used), but > 4096 (which is the
|
# depending on how the paths are used), but > 4096 (which is the
|
||||||
# Linux' limitation) - the behaviour of paths with names > 4096 chars
|
# Linux' limitation) - the behaviour of paths with names > 4096 chars
|
||||||
# is undetermined
|
# is undetermined
|
||||||
newfilename = "/test" * 60 # type:ignore[unreachable]
|
newfilename = "/test" * 60 # type:ignore[unreachable,unused-ignore]
|
||||||
l1 = tmpdir.join(newfilename)
|
l1 = tmpdir.join(newfilename)
|
||||||
l1.ensure(file=True)
|
l1.ensure(file=True)
|
||||||
l1.write_text("foo", encoding="utf-8")
|
l1.write_text("foo", encoding="utf-8")
|
||||||
|
@ -1368,8 +1368,8 @@ class TestPOSIXLocalPath:
|
||||||
assert realpath.basename == "file"
|
assert realpath.basename == "file"
|
||||||
|
|
||||||
def test_owner(self, path1, tmpdir):
|
def test_owner(self, path1, tmpdir):
|
||||||
from grp import getgrgid # type:ignore[attr-defined]
|
from grp import getgrgid # type:ignore[attr-defined,unused-ignore]
|
||||||
from pwd import getpwuid # type:ignore[attr-defined]
|
from pwd import getpwuid # type:ignore[attr-defined,unused-ignore]
|
||||||
|
|
||||||
stat = path1.stat()
|
stat = path1.stat()
|
||||||
assert stat.path == path1
|
assert stat.path == path1
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import importlib.metadata
|
import importlib.metadata
|
||||||
import os
|
import os
|
||||||
|
from pathlib import Path
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import types
|
import types
|
||||||
|
@ -541,6 +542,32 @@ class TestGeneralUsage:
|
||||||
res = pytester.runpytest(p)
|
res = pytester.runpytest(p)
|
||||||
res.assert_outcomes(passed=3)
|
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:
|
class TestInvocationVariants:
|
||||||
def test_earlyinit(self, pytester: Pytester) -> None:
|
def test_earlyinit(self, pytester: Pytester) -> None:
|
||||||
|
|
|
@ -180,7 +180,7 @@ class TestTraceback_f_g_h:
|
||||||
def test_traceback_cut_excludepath(self, pytester: Pytester) -> None:
|
def test_traceback_cut_excludepath(self, pytester: Pytester) -> None:
|
||||||
p = pytester.makepyfile("def f(): raise ValueError")
|
p = pytester.makepyfile("def f(): raise ValueError")
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError) as excinfo:
|
||||||
import_path(p, root=pytester.path).f() # type: ignore[attr-defined]
|
import_path(p, root=pytester.path, consider_namespace_packages=False).f()
|
||||||
basedir = Path(pytest.__file__).parent
|
basedir = Path(pytest.__file__).parent
|
||||||
newtraceback = excinfo.traceback.cut(excludepath=basedir)
|
newtraceback = excinfo.traceback.cut(excludepath=basedir)
|
||||||
for x in newtraceback:
|
for x in newtraceback:
|
||||||
|
@ -543,7 +543,9 @@ class TestFormattedExcinfo:
|
||||||
tmp_path.joinpath("__init__.py").touch()
|
tmp_path.joinpath("__init__.py").touch()
|
||||||
modpath.write_text(source, encoding="utf-8")
|
modpath.write_text(source, encoding="utf-8")
|
||||||
importlib.invalidate_caches()
|
importlib.invalidate_caches()
|
||||||
return import_path(modpath, root=tmp_path)
|
return import_path(
|
||||||
|
modpath, root=tmp_path, consider_namespace_packages=False
|
||||||
|
)
|
||||||
|
|
||||||
return importasmod
|
return importasmod
|
||||||
|
|
||||||
|
|
|
@ -296,7 +296,7 @@ def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) -
|
||||||
)
|
)
|
||||||
path = tmp_path.joinpath("a.py")
|
path = tmp_path.joinpath("a.py")
|
||||||
path.write_text(str(source), encoding="utf-8")
|
path.write_text(str(source), encoding="utf-8")
|
||||||
mod: Any = import_path(path, root=tmp_path)
|
mod: Any = import_path(path, root=tmp_path, consider_namespace_packages=False)
|
||||||
s2 = Source(mod.A)
|
s2 = Source(mod.A)
|
||||||
assert str(source).strip() == str(s2).strip()
|
assert str(source).strip() == str(s2).strip()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
from _pytest import deprecated
|
from _pytest import deprecated
|
||||||
|
from _pytest.compat import legacy_path
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
import pytest
|
import pytest
|
||||||
from pytest import PytestDeprecationWarning
|
from pytest import PytestDeprecationWarning
|
||||||
|
@ -85,6 +90,56 @@ def test_private_is_deprecated() -> None:
|
||||||
PrivateInit(10, _ispytest=True)
|
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_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():
|
def test_fixture_disallow_on_marked_functions():
|
||||||
"""Test that applying @pytest.fixture to a marked function warns (#3364)."""
|
"""Test that applying @pytest.fixture to a marked function warns (#3364)."""
|
||||||
with pytest.warns(
|
with pytest.warns(
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Reproduces issue #3774"""
|
"""Reproduces issue #3774"""
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Skipping an entire subclass with unittest.skip() should *not* call setUp from a base class."""
|
"""Skipping an entire subclass with unittest.skip() should *not* call setUp from a base class."""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Skipping an entire subclass with unittest.skip() should *not* call setUpClass from a base class."""
|
"""Skipping an entire subclass with unittest.skip() should *not* call setUpClass from a base class."""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""setUpModule is always called, even if all tests in the module are skipped"""
|
"""setUpModule is always called, even if all tests in the module are skipped"""
|
||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# mypy: allow-untyped-defs
|
# mypy: allow-untyped-defs
|
||||||
"""Issue #7110"""
|
"""Issue #7110"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ import pytest
|
||||||
("a", 1),
|
("a", 1),
|
||||||
("1", 1),
|
("1", 1),
|
||||||
("א", 1),
|
("א", 1),
|
||||||
("\u200B", 0),
|
("\u200b", 0),
|
||||||
("\u1ABE", 0),
|
("\u1abe", 0),
|
||||||
("\u0591", 0),
|
("\u0591", 0),
|
||||||
("🉐", 2),
|
("🉐", 2),
|
||||||
("$", 2), # noqa: RUF001
|
("$", 2), # noqa: RUF001
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
anyio[curio,trio]==4.3.0
|
anyio[curio,trio]==4.3.0
|
||||||
django==5.0.2
|
django==5.0.3
|
||||||
pytest-asyncio==0.23.5
|
pytest-asyncio==0.23.5
|
||||||
# Temporarily not installed until pytest-bdd is fixed:
|
# Temporarily not installed until pytest-bdd is fixed:
|
||||||
# https://github.com/pytest-dev/pytest/pull/11785
|
# https://github.com/pytest-dev/pytest/pull/11785
|
||||||
|
@ -13,5 +13,5 @@ pytest-rerunfailures==13.0
|
||||||
pytest-sugar==1.0.0
|
pytest-sugar==1.0.0
|
||||||
pytest-trio==0.7.0
|
pytest-trio==0.7.0
|
||||||
pytest-twisted==1.14.0
|
pytest-twisted==1.14.0
|
||||||
twisted==23.10.0
|
twisted==24.3.0
|
||||||
pytest-xvfb==3.0.0
|
pytest-xvfb==3.0.0
|
||||||
|
|
|
@ -932,8 +932,9 @@ class TestRequestBasic:
|
||||||
self, pytester: Pytester
|
self, pytester: Pytester
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Ensure exceptions raised during teardown by a finalizer are suppressed
|
Ensure exceptions raised during teardown by finalizers are suppressed
|
||||||
until all finalizers are called, re-raising the first exception (#2440)
|
until all finalizers are called, then re-reaised together in an
|
||||||
|
exception group (#2440)
|
||||||
"""
|
"""
|
||||||
pytester.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
@ -960,8 +961,16 @@ class TestRequestBasic:
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
|
result.assert_outcomes(passed=2, errors=1)
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["*Exception: Error in excepts fixture", "* 2 passed, 1 error in *"]
|
[
|
||||||
|
' | *ExceptionGroup: errors while tearing down fixture "subrequest" of <Function test_first> (2 sub-exceptions)', # noqa: E501
|
||||||
|
" +-+---------------- 1 ----------------",
|
||||||
|
" | Exception: Error in something fixture",
|
||||||
|
" +---------------- 2 ----------------",
|
||||||
|
" | Exception: Error in excepts fixture",
|
||||||
|
" +------------------------------------",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_request_getmodulepath(self, pytester: Pytester) -> None:
|
def test_request_getmodulepath(self, pytester: Pytester) -> None:
|
||||||
|
@ -1238,8 +1247,9 @@ class TestFixtureUsages:
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*ScopeMismatch*involved factories*",
|
"*ScopeMismatch*Requesting fixture stack*",
|
||||||
"test_receives_funcargs_scope_mismatch.py:6: def arg2(arg1)",
|
"test_receives_funcargs_scope_mismatch.py:6: def arg2(arg1)",
|
||||||
|
"Requested fixture:",
|
||||||
"test_receives_funcargs_scope_mismatch.py:2: def arg1()",
|
"test_receives_funcargs_scope_mismatch.py:2: def arg1()",
|
||||||
"*1 error*",
|
"*1 error*",
|
||||||
]
|
]
|
||||||
|
@ -1265,7 +1275,13 @@ class TestFixtureUsages:
|
||||||
)
|
)
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
["*ScopeMismatch*involved factories*", "* def arg2*", "*1 error*"]
|
[
|
||||||
|
"*ScopeMismatch*Requesting fixture stack*",
|
||||||
|
"* def arg2(arg1)",
|
||||||
|
"Requested fixture:",
|
||||||
|
"* def arg1()",
|
||||||
|
"*1 error*",
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_invalid_scope(self, pytester: Pytester) -> None:
|
def test_invalid_scope(self, pytester: Pytester) -> None:
|
||||||
|
@ -2479,8 +2495,10 @@ class TestFixtureMarker:
|
||||||
assert result.ret == ExitCode.TESTS_FAILED
|
assert result.ret == ExitCode.TESTS_FAILED
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
[
|
[
|
||||||
"*ScopeMismatch*involved factories*",
|
"*ScopeMismatch*Requesting fixture stack*",
|
||||||
"test_it.py:6: def fixmod(fixfunc)",
|
"test_it.py:6: def fixmod(fixfunc)",
|
||||||
|
"Requested fixture:",
|
||||||
|
"test_it.py:3: def fixfunc()",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4561,6 +4579,51 @@ def test_deduplicate_names() -> None:
|
||||||
assert items == ("a", "b", "c", "d", "g", "f", "e")
|
assert items == ("a", "b", "c", "d", "g", "f", "e")
|
||||||
|
|
||||||
|
|
||||||
|
def test_staticmethod_classmethod_fixture_instance(pytester: Pytester) -> None:
|
||||||
|
"""Ensure that static and class methods get and have access to a fresh
|
||||||
|
instance.
|
||||||
|
|
||||||
|
This also ensures `setup_method` works well with static and class methods.
|
||||||
|
|
||||||
|
Regression test for #12065.
|
||||||
|
"""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
ran_setup_method = False
|
||||||
|
ran_fixture = False
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
assert not self.ran_setup_method
|
||||||
|
self.ran_setup_method = True
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def fixture(self):
|
||||||
|
assert not self.ran_fixture
|
||||||
|
self.ran_fixture = True
|
||||||
|
|
||||||
|
def test_method(self):
|
||||||
|
assert self.ran_setup_method
|
||||||
|
assert self.ran_fixture
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def test_1(request):
|
||||||
|
assert request.instance.ran_setup_method
|
||||||
|
assert request.instance.ran_fixture
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def test_2(cls, request):
|
||||||
|
assert request.instance.ran_setup_method
|
||||||
|
assert request.instance.ran_fixture
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == ExitCode.OK
|
||||||
|
result.assert_outcomes(passed=3)
|
||||||
|
|
||||||
|
|
||||||
def test_scoped_fixture_teardown_order(pytester: Pytester) -> None:
|
def test_scoped_fixture_teardown_order(pytester: Pytester) -> None:
|
||||||
"""
|
"""
|
||||||
Make sure teardowns happen in reverse order of setup with scoped fixtures, when
|
Make sure teardowns happen in reverse order of setup with scoped fixtures, when
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue