Compare commits
55 Commits
8.2.0.dev0
...
8.0.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31afeeb0df | ||
|
|
1b00a2f4fb | ||
|
|
ff2f66d84a | ||
|
|
8a8eed609c | ||
|
|
74346f027c | ||
|
|
b7657b4d6b | ||
|
|
feb7c5e12e | ||
|
|
090965574e | ||
|
|
68524d4858 | ||
|
|
d7d320a15a | ||
|
|
93699166dc | ||
|
|
a232abd56c | ||
|
|
92203d2b78 | ||
|
|
f1aa9226ac | ||
|
|
d86d081563 | ||
|
|
c554c3d200 | ||
|
|
a6851e3459 | ||
|
|
e6f6be3bc9 | ||
|
|
23b91d12de | ||
|
|
b188f4d10d | ||
|
|
d60b6b0e28 | ||
|
|
c11cdfabd1 | ||
|
|
368cc6225e | ||
|
|
06e592370e | ||
|
|
a76aa6ff80 | ||
|
|
73371a03da | ||
|
|
eb698a64a0 | ||
|
|
3e48ef64cd | ||
|
|
8b70bb64d3 | ||
|
|
169d775eec | ||
|
|
42f709ed44 | ||
|
|
24c681d4ee | ||
|
|
478f8233bc | ||
|
|
608590097a | ||
|
|
3b41c65c81 | ||
|
|
747072ad26 | ||
|
|
011a475baf | ||
|
|
97960bdd14 | ||
|
|
6be0a3cbf7 | ||
|
|
44ffe07165 | ||
|
|
14ecb04973 | ||
|
|
41c8dabee3 | ||
|
|
6f4cbd7cd4 | ||
|
|
b0c7f923aa | ||
|
|
f15aff06dc | ||
|
|
10c8898845 | ||
|
|
5b7ddedbf9 | ||
|
|
3ae38baca6 | ||
|
|
6123b247d4 | ||
|
|
bb6f5d1b10 | ||
|
|
72eb1b7ad1 | ||
|
|
620a454dba | ||
|
|
838151638e | ||
|
|
665e4e58d3 | ||
|
|
e17d5ec871 |
@@ -23,11 +23,9 @@ afc607cfd81458d4e4f3b1f3cf8cc931b933907e
|
||||
5f95dce95602921a70bfbc7d8de2f7712c5e4505
|
||||
# ran pyupgrade-docs again
|
||||
75d0b899bbb56d6849e9d69d83a9426ed3f43f8b
|
||||
|
||||
# move argument parser to own file
|
||||
c9df77cbd6a365dcb73c39618e4842711817e871
|
||||
|
||||
# Replace reorder-python-imports by isort due to black incompatibility (#11896)
|
||||
8b54596639f41dfac070030ef20394b9001fe63c
|
||||
# Run blacken-docs with black's 2024's style
|
||||
4546d5445aaefe6a03957db028c263521dfb5c4b
|
||||
# Migration to ruff / ruff format
|
||||
4588653b2497ed25976b7aaff225b889fb476756
|
||||
|
||||
2
.github/workflows/deploy.yml
vendored
2
.github/workflows/deploy.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.1
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.0
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.1
|
||||
uses: hynek/build-and-inspect-python-package@v2.0.0
|
||||
|
||||
build:
|
||||
needs: [package]
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: "matrix.use_coverage"
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
|
||||
4
.github/workflows/update-plugin-list.yml
vendored
4
.github/workflows/update-plugin-list.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
- name: requests-cache
|
||||
uses: actions/cache@v4
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pytest-plugin-list/
|
||||
key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50
|
||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: "v0.2.2"
|
||||
rev: "v0.1.15"
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: ["--fix"]
|
||||
@@ -21,6 +21,11 @@ repos:
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==24.1.1]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
@@ -34,7 +39,7 @@ repos:
|
||||
additional_dependencies:
|
||||
- iniconfig>=1.1.0
|
||||
- attrs>=19.2.0
|
||||
- pluggy>=1.4.0
|
||||
- pluggy
|
||||
- packaging
|
||||
- tomli
|
||||
- types-pkg_resources
|
||||
@@ -42,12 +47,6 @@ repos:
|
||||
# for mypy running on python>=3.11 since exceptiongroup is only a dependency
|
||||
# on <3.11
|
||||
- exceptiongroup>=1.0.0rc8
|
||||
- repo: https://github.com/tox-dev/pyproject-fmt
|
||||
rev: "1.7.0"
|
||||
hooks:
|
||||
- id: pyproject-fmt
|
||||
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
|
||||
additional_dependencies: ["tox>=4.9"]
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: rst
|
||||
|
||||
7
AUTHORS
7
AUTHORS
@@ -56,7 +56,6 @@ Babak Keyvani
|
||||
Barney Gale
|
||||
Ben Brown
|
||||
Ben Gartner
|
||||
Ben Leith
|
||||
Ben Webb
|
||||
Benjamin Peterson
|
||||
Benjamin Schubert
|
||||
@@ -128,8 +127,6 @@ Edison Gustavo Muenz
|
||||
Edoardo Batini
|
||||
Edson Tadeu M. Manoel
|
||||
Eduardo Schettino
|
||||
Edward Haigh
|
||||
Eero Vaher
|
||||
Eli Boyarski
|
||||
Elizaveta Shashkova
|
||||
Éloi Rivard
|
||||
@@ -145,7 +142,6 @@ Evgeny Seliverstov
|
||||
Fabian Sturm
|
||||
Fabien Zarifian
|
||||
Fabio Zadrozny
|
||||
faph
|
||||
Felix Hofstätter
|
||||
Felix Nieuwenhuizen
|
||||
Feng Ma
|
||||
@@ -249,7 +245,6 @@ Marc Mueller
|
||||
Marc Schlaich
|
||||
Marcelo Duarte Trevisani
|
||||
Marcin Bachry
|
||||
Marc Bresson
|
||||
Marco Gorelli
|
||||
Mark Abramowitz
|
||||
Mark Dickinson
|
||||
@@ -283,7 +278,6 @@ Mike Hoyle (hoylemd)
|
||||
Mike Lundy
|
||||
Milan Lesnek
|
||||
Miro Hrončok
|
||||
mrbean-bremen
|
||||
Nathaniel Compton
|
||||
Nathaniel Waisbrot
|
||||
Ned Batchelder
|
||||
@@ -420,7 +414,6 @@ Vivaan Verma
|
||||
Vlad Dragos
|
||||
Vlad Radziuk
|
||||
Vladyslav Rachek
|
||||
Volodymyr Kochetkov
|
||||
Volodymyr Piskun
|
||||
Wei Lin
|
||||
Wil Cooley
|
||||
|
||||
@@ -94,12 +94,12 @@ Features
|
||||
- `Modular fixtures <https://docs.pytest.org/en/stable/explanation/fixtures.html>`_ for
|
||||
managing small or parametrized long-lived test resources
|
||||
|
||||
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial)
|
||||
test suites out of the box
|
||||
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
|
||||
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
|
||||
|
||||
- Python 3.8+ or PyPy3
|
||||
|
||||
- Rich plugin architecture, with over 1300+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
|
||||
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
|
||||
|
||||
|
||||
Documentation
|
||||
|
||||
@@ -23,6 +23,7 @@ members of the `contributors team`_ interested in receiving funding.
|
||||
|
||||
The current list of contributors receiving funding are:
|
||||
|
||||
* `@asottile`_
|
||||
* `@nicoddemus`_
|
||||
* `@The-Compiler`_
|
||||
|
||||
@@ -54,5 +55,6 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the
|
||||
.. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members
|
||||
.. _`agreement`: https://tidelift.com/docs/lifting/agreement
|
||||
|
||||
.. _`@asottile`: https://github.com/asottile
|
||||
.. _`@nicoddemus`: https://github.com/nicoddemus
|
||||
.. _`@The-Compiler`: https://github.com/The-Compiler
|
||||
|
||||
@@ -5,7 +5,7 @@ if __name__ == "__main__":
|
||||
import cProfile
|
||||
import pstats
|
||||
|
||||
import pytest # noqa: F401
|
||||
import pytest # NOQA
|
||||
|
||||
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
|
||||
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
|
||||
|
||||
@@ -44,7 +44,7 @@ Partner projects, sign up here! (by 22 March)
|
||||
What does it mean to "adopt pytest"?
|
||||
-----------------------------------------
|
||||
|
||||
There can be many different definitions of "success". Pytest can run many unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
|
||||
There can be many different definitions of "success". Pytest can run many nose_ and unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
|
||||
|
||||
Progressive success might look like:
|
||||
|
||||
@@ -62,6 +62,7 @@ Progressive success might look like:
|
||||
|
||||
It may be after the month is up, the partner project decides that pytest is not right for it. That's okay - hopefully the pytest team will also learn something about its weaknesses or deficiencies.
|
||||
|
||||
.. _nose: nose.html
|
||||
.. _unittest: unittest.html
|
||||
.. _assert: assert.html
|
||||
.. _pycmd: https://bitbucket.org/hpk42/pycmd/overview
|
||||
|
||||
@@ -6,7 +6,6 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-8.1.0
|
||||
release-8.0.2
|
||||
release-8.0.1
|
||||
release-8.0.0
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
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
|
||||
@@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
Values can be any object handled by the json stdlib module.
|
||||
|
||||
capsysbinary -- .../_pytest/capture.py:1008
|
||||
capsysbinary -- .../_pytest/capture.py:1007
|
||||
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsysbinary.readouterr()``
|
||||
@@ -50,7 +50,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsysbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capfd -- .../_pytest/capture.py:1035
|
||||
capfd -- .../_pytest/capture.py:1034
|
||||
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
@@ -67,7 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capfd.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
capfdbinary -- .../_pytest/capture.py:1062
|
||||
capfdbinary -- .../_pytest/capture.py:1061
|
||||
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
@@ -84,7 +84,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capfdbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capsys -- .../_pytest/capture.py:981
|
||||
capsys -- .../_pytest/capture.py:980
|
||||
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsys.readouterr()`` method
|
||||
@@ -101,7 +101,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:745
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
@@ -115,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1346
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1354
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
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
|
||||
:issue:`7767` for details.
|
||||
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:317
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
|
||||
Return a :class:`pytest.TempdirFactory` instance for the test session.
|
||||
|
||||
tmpdir -- .../_pytest/legacypath.py:324
|
||||
tmpdir -- .../_pytest/legacypath.py:309
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See
|
||||
:ref:`temporary directory location and retention`.
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a `legacy_path`_ object.
|
||||
|
||||
@@ -192,7 +192,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
caplog -- .../_pytest/logging.py:601
|
||||
caplog -- .../_pytest/logging.py:594
|
||||
Access and control log capturing.
|
||||
|
||||
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,
|
||||
use :meth:`context() <pytest.MonkeyPatch.context>`.
|
||||
|
||||
recwarn -- .../_pytest/recwarn.py:31
|
||||
recwarn -- .../_pytest/recwarn.py:32
|
||||
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
|
||||
@@ -245,8 +245,8 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
and old bases are removed after 3 sessions, to aid in debugging.
|
||||
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See
|
||||
:ref:`temporary directory location and retention`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a :class:`pathlib.Path` object.
|
||||
|
||||
|
||||
@@ -28,98 +28,6 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 8.1.0 (2024-03-03)
|
||||
=========================
|
||||
|
||||
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
|
||||
---------
|
||||
|
||||
- `#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.
|
||||
|
||||
|
||||
pytest 8.0.2 (2024-02-24)
|
||||
=========================
|
||||
|
||||
@@ -407,7 +315,7 @@ These are breaking changes where deprecation was not possible.
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#10465 <https://github.com/pytest-dev/pytest/issues/10465>`_: Test functions returning a value other than ``None`` will now issue a :class:`pytest.PytestWarning` instead of ``pytest.PytestRemovedIn8Warning``, meaning this will stay a warning instead of becoming an error in the future.
|
||||
- `#10465 <https://github.com/pytest-dev/pytest/issues/10465>`_: Test functions returning a value other than ``None`` will now issue a :class:`pytest.PytestWarning` instead of :class:`pytest.PytestRemovedIn8Warning`, meaning this will stay a warning instead of becoming an error in the future.
|
||||
|
||||
|
||||
- `#3664 <https://github.com/pytest-dev/pytest/issues/3664>`_: Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work.
|
||||
@@ -1437,7 +1345,7 @@ Deprecations
|
||||
See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details.
|
||||
|
||||
|
||||
- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: ``pytest_cmdline_preparse`` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead.
|
||||
- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: :hook:`pytest_cmdline_preparse` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead.
|
||||
|
||||
See :ref:`the deprecation note <cmdline-preparse-deprecated>` for full details.
|
||||
|
||||
|
||||
@@ -200,6 +200,7 @@ nitpick_ignore = [
|
||||
("py:class", "_tracing.TagTracerSub"),
|
||||
("py:class", "warnings.WarningMessage"),
|
||||
# Undocumented type aliases
|
||||
("py:class", "LEGACY_PATH"),
|
||||
("py:class", "_PluggyPlugin"),
|
||||
# TypeVars
|
||||
("py:class", "_pytest._code.code.E"),
|
||||
|
||||
@@ -44,6 +44,7 @@ How-to guides
|
||||
|
||||
how-to/existingtestsuite
|
||||
how-to/unittest
|
||||
how-to/nose
|
||||
how-to/xunit_setup
|
||||
|
||||
how-to/bash-completion
|
||||
|
||||
@@ -19,269 +19,12 @@ Below is a complete list of all pytest features which are considered deprecated.
|
||||
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
|
||||
|
||||
|
||||
.. _legacy-path-hooks-deprecated:
|
||||
|
||||
Configuring hook specs/impls using markers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Before pluggy, pytest's plugin library, was its own package and had a clear API,
|
||||
pytest just used ``pytest.mark`` to configure hooks.
|
||||
|
||||
The :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec` decorators
|
||||
have been available since years and should be used instead.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
|
||||
# or
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
|
||||
pytest_runtest_call.tryfirst = True
|
||||
|
||||
should be changed to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
Changed ``hookimpl`` attributes:
|
||||
|
||||
* ``tryfirst``
|
||||
* ``trylast``
|
||||
* ``optionalhook``
|
||||
* ``hookwrapper``
|
||||
|
||||
Changed ``hookwrapper`` attributes:
|
||||
|
||||
* ``firstresult``
|
||||
* ``historic``
|
||||
|
||||
|
||||
Directly constructing internal classes
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Directly constructing the following classes is now deprecated:
|
||||
|
||||
- ``_pytest.mark.structures.Mark``
|
||||
- ``_pytest.mark.structures.MarkDecorator``
|
||||
- ``_pytest.mark.structures.MarkGenerator``
|
||||
- ``_pytest.python.Metafunc``
|
||||
- ``_pytest.runner.CallInfo``
|
||||
- ``_pytest._code.ExceptionInfo``
|
||||
- ``_pytest.config.argparsing.Parser``
|
||||
- ``_pytest.config.argparsing.OptionGroup``
|
||||
- ``_pytest.pytester.HookRecorder``
|
||||
|
||||
These constructors have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.
|
||||
|
||||
.. _diamond-inheritance-deprecated:
|
||||
|
||||
Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning.
|
||||
It was never sanely supported and triggers hard to debug errors.
|
||||
|
||||
Some plugins providing linting/code analysis have been using this as a hack.
|
||||
Instead, a separate collector node should be used, which collects the item. See
|
||||
:ref:`non-python tests` for an example, as well as an `example pr fixing inheritance`_.
|
||||
|
||||
.. _example pr fixing inheritance: https://github.com/asmeurer/pytest-flakes/pull/40/files
|
||||
|
||||
|
||||
.. _uncooperative-constructors-deprecated:
|
||||
|
||||
Constructors of custom :class:`~_pytest.nodes.Node` subclasses should take ``**kwargs``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
If custom subclasses of nodes like :class:`pytest.Item` override the
|
||||
``__init__`` method, they should take ``**kwargs``. Thus,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CustomItem(pytest.Item):
|
||||
def __init__(self, name, parent, additional_arg):
|
||||
super().__init__(name, parent)
|
||||
self.additional_arg = additional_arg
|
||||
|
||||
should be turned into:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CustomItem(pytest.Item):
|
||||
def __init__(self, *, additional_arg, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.additional_arg = additional_arg
|
||||
|
||||
to avoid hard-coding the arguments pytest can pass to the superclass.
|
||||
See :ref:`non-python tests` for a full example.
|
||||
|
||||
For cases without conflicts, no deprecation warning is emitted. For cases with
|
||||
conflicts (such as :class:`pytest.File` now taking ``path`` instead of
|
||||
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
|
||||
deprecation warning is now raised.
|
||||
|
||||
Applying a mark to a fixture function
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.4
|
||||
|
||||
Applying a mark to a fixture function never had any effect, but it is a common user error.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.usefixtures("clean_database")
|
||||
@pytest.fixture
|
||||
def user() -> User: ...
|
||||
|
||||
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
|
||||
|
||||
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
|
||||
|
||||
|
||||
Returning non-None value in test functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.2
|
||||
|
||||
A :class:`pytest.PytestReturnNotNoneWarning` is now emitted if a test function returns something other than `None`.
|
||||
|
||||
This prevents a common mistake among beginners that expect that returning a `bool` would cause a test to pass or fail, for example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["a", "b", "result"],
|
||||
[
|
||||
[1, 2, 5],
|
||||
[2, 3, 8],
|
||||
[5, 3, 18],
|
||||
],
|
||||
)
|
||||
def test_foo(a, b, result):
|
||||
return foo(a, b) == result
|
||||
|
||||
Given that pytest ignores the return value, this might be surprising that it will never fail.
|
||||
|
||||
The proper fix is to change the `return` to an `assert`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["a", "b", "result"],
|
||||
[
|
||||
[1, 2, 5],
|
||||
[2, 3, 8],
|
||||
[5, 3, 18],
|
||||
],
|
||||
)
|
||||
def test_foo(a, b, result):
|
||||
assert foo(a, b) == result
|
||||
|
||||
|
||||
The ``yield_fixture`` function/decorator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.2
|
||||
|
||||
``pytest.yield_fixture`` is a deprecated alias for :func:`pytest.fixture`.
|
||||
|
||||
It has been so for a very long time, so can be search/replaced safely.
|
||||
|
||||
|
||||
Removed Features and Breaking Changes
|
||||
-------------------------------------
|
||||
|
||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||
an appropriate period of deprecation has passed.
|
||||
|
||||
Some breaking changes which could not be deprecated are also listed.
|
||||
|
||||
.. _node-ctor-fspath-deprecation:
|
||||
|
||||
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
|
||||
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
|
||||
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
|
||||
is now deprecated.
|
||||
|
||||
Plugins which construct nodes should pass the ``path`` argument, of type
|
||||
:class:`pathlib.Path`, instead of the ``fspath`` argument.
|
||||
|
||||
Plugins which implement custom items and collectors are encouraged to replace
|
||||
``fspath`` parameters (``py.path.local``) with ``path`` parameters
|
||||
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
|
||||
|
||||
If possible, plugins with custom items should use :ref:`cooperative
|
||||
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
|
||||
arguments they only pass on to the superclass.
|
||||
|
||||
.. note::
|
||||
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
|
||||
new attribute being ``path``) is **the opposite** of the situation for
|
||||
hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
|
||||
argument being ``path``).
|
||||
|
||||
This is an unfortunate artifact due to historical reasons, which should be
|
||||
resolved in future versions as we slowly get rid of the :pypi:`py`
|
||||
dependency (see :issue:`9283` for a longer discussion).
|
||||
|
||||
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
|
||||
which still is expected to return a ``py.path.local`` object, nodes still have
|
||||
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
|
||||
no matter what argument was used in the constructor. We expect to deprecate the
|
||||
``fspath`` attribute in a future release.
|
||||
|
||||
|
||||
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
|
||||
|
||||
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
|
||||
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
|
||||
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
|
||||
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
|
||||
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
|
||||
|
||||
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
|
||||
|
||||
.. note::
|
||||
The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
|
||||
:ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
|
||||
being ``path``) is **the opposite** of the situation for hooks (the old
|
||||
argument being ``path``).
|
||||
|
||||
This is an unfortunate artifact due to historical reasons, which should be
|
||||
resolved in future versions as we slowly get rid of the :pypi:`py`
|
||||
dependency (see :issue:`9283` for a longer discussion).
|
||||
|
||||
|
||||
.. _nose-deprecation:
|
||||
|
||||
Support for tests written for nose
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.2
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated.
|
||||
|
||||
@@ -372,20 +115,157 @@ Will also need to be ported to a supported pytest style. One way to do it is usi
|
||||
|
||||
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
||||
|
||||
.. _instance-collector-deprecation:
|
||||
|
||||
The ``compat_co_firstlineno`` attribute
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
The ``pytest.Instance`` collector
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Nose inspects this attribute on function objects to allow overriding the function's inferred line number.
|
||||
Pytest no longer respects this attribute.
|
||||
.. versionremoved:: 7.0
|
||||
|
||||
The ``pytest.Instance`` collector type has been removed.
|
||||
|
||||
Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`.
|
||||
Now :class:`~pytest.Class` collects the test methods directly.
|
||||
|
||||
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
|
||||
using a check such as ``if isinstance(node, Instance): return``.
|
||||
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
|
||||
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
|
||||
and importing it emits a deprecation warning. This will be removed in pytest 8.
|
||||
|
||||
|
||||
.. _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.
|
||||
|
||||
.. _legacy-path-hooks-deprecated:
|
||||
|
||||
Configuring hook specs/impls using markers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Before pluggy, pytest's plugin library, was its own package and had a clear API,
|
||||
pytest just used ``pytest.mark`` to configure hooks.
|
||||
|
||||
The :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec` decorators
|
||||
have been available since years and should be used instead.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
|
||||
# or
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
|
||||
pytest_runtest_call.tryfirst = True
|
||||
|
||||
should be changed to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_runtest_call(): ...
|
||||
|
||||
Changed ``hookimpl`` attributes:
|
||||
|
||||
* ``tryfirst``
|
||||
* ``trylast``
|
||||
* ``optionalhook``
|
||||
* ``hookwrapper``
|
||||
|
||||
Changed ``hookwrapper`` attributes:
|
||||
|
||||
* ``firstresult``
|
||||
* ``historic``
|
||||
|
||||
|
||||
``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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Directly constructing the following classes is now deprecated:
|
||||
|
||||
- ``_pytest.mark.structures.Mark``
|
||||
- ``_pytest.mark.structures.MarkDecorator``
|
||||
- ``_pytest.mark.structures.MarkGenerator``
|
||||
- ``_pytest.python.Metafunc``
|
||||
- ``_pytest.runner.CallInfo``
|
||||
- ``_pytest._code.ExceptionInfo``
|
||||
- ``_pytest.config.argparsing.Parser``
|
||||
- ``_pytest.config.argparsing.OptionGroup``
|
||||
- ``_pytest.pytester.HookRecorder``
|
||||
|
||||
These constructors have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.
|
||||
|
||||
.. _cmdline-preparse-deprecated:
|
||||
|
||||
Passing ``msg=`` to ``pytest.skip``, ``pytest.fail`` or ``pytest.exit``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
Passing the keyword argument ``msg`` to :func:`pytest.skip`, :func:`pytest.fail` or :func:`pytest.exit`
|
||||
is now deprecated and ``reason`` should be used instead. This change is to bring consistency between these
|
||||
@@ -414,74 +294,12 @@ functions and the ``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which
|
||||
pytest.exit(reason="bar")
|
||||
|
||||
|
||||
.. _instance-collector-deprecation:
|
||||
|
||||
The ``pytest.Instance`` collector
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionremoved:: 7.0
|
||||
|
||||
The ``pytest.Instance`` collector type has been removed.
|
||||
|
||||
Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`.
|
||||
Now :class:`~pytest.Class` collects the test methods directly.
|
||||
|
||||
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
|
||||
using a check such as ``if isinstance(node, Instance): return``.
|
||||
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
|
||||
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
|
||||
and importing it emits a deprecation warning. This was removed in pytest 8.
|
||||
|
||||
|
||||
Using ``pytest.warns(None)``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
:func:`pytest.warns(None) <pytest.warns>` is now deprecated because it was frequently misused.
|
||||
Its correct usage was checking that the code emits at least one warning of any type - like ``pytest.warns()``
|
||||
or ``pytest.warns(Warning)``.
|
||||
|
||||
See :ref:`warns use cases` for examples.
|
||||
|
||||
|
||||
Backward compatibilities in ``Parser.addoption``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 2.4
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
|
||||
removed in pytest 8 (deprecated since pytest 2.4.0):
|
||||
|
||||
- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead.
|
||||
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
|
||||
|
||||
|
||||
The ``--strict`` command-line option
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.2
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
|
||||
better conveys what the option does.
|
||||
|
||||
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
|
||||
flag for all strictness related options (``--strict-markers`` and ``--strict-config``
|
||||
at the moment, more might be introduced in the future).
|
||||
|
||||
|
||||
.. _cmdline-preparse-deprecated:
|
||||
|
||||
Implementing the ``pytest_cmdline_preparse`` hook
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
Implementing the ``pytest_cmdline_preparse`` hook has been officially deprecated.
|
||||
Implementing the :hook:`pytest_cmdline_preparse` hook has been officially deprecated.
|
||||
Implement the :hook:`pytest_load_initial_conftests` hook instead.
|
||||
|
||||
.. code-block:: python
|
||||
@@ -496,6 +314,170 @@ Implement the :hook:`pytest_load_initial_conftests` hook instead.
|
||||
early_config: Config, parser: Parser, args: List[str]
|
||||
) -> None: ...
|
||||
|
||||
.. _diamond-inheritance-deprecated:
|
||||
|
||||
Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning.
|
||||
It was never sanely supported and triggers hard to debug errors.
|
||||
|
||||
Some plugins providing linting/code analysis have been using this as a hack.
|
||||
Instead, a separate collector node should be used, which collects the item. See
|
||||
:ref:`non-python tests` for an example, as well as an `example pr fixing inheritance`_.
|
||||
|
||||
.. _example pr fixing inheritance: https://github.com/asmeurer/pytest-flakes/pull/40/files
|
||||
|
||||
|
||||
.. _uncooperative-constructors-deprecated:
|
||||
|
||||
Constructors of custom :class:`~_pytest.nodes.Node` subclasses should take ``**kwargs``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
If custom subclasses of nodes like :class:`pytest.Item` override the
|
||||
``__init__`` method, they should take ``**kwargs``. Thus,
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CustomItem(pytest.Item):
|
||||
def __init__(self, name, parent, additional_arg):
|
||||
super().__init__(name, parent)
|
||||
self.additional_arg = additional_arg
|
||||
|
||||
should be turned into:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class CustomItem(pytest.Item):
|
||||
def __init__(self, *, additional_arg, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.additional_arg = additional_arg
|
||||
|
||||
to avoid hard-coding the arguments pytest can pass to the superclass.
|
||||
See :ref:`non-python tests` for a full example.
|
||||
|
||||
For cases without conflicts, no deprecation warning is emitted. For cases with
|
||||
conflicts (such as :class:`pytest.File` now taking ``path`` instead of
|
||||
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
|
||||
deprecation warning is now raised.
|
||||
|
||||
Applying a mark to a fixture function
|
||||
-------------------------------------
|
||||
|
||||
.. deprecated:: 7.4
|
||||
|
||||
Applying a mark to a fixture function never had any effect, but it is a common user error.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.usefixtures("clean_database")
|
||||
@pytest.fixture
|
||||
def user() -> User: ...
|
||||
|
||||
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
|
||||
|
||||
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
|
||||
|
||||
|
||||
Backward compatibilities in ``Parser.addoption``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 2.4
|
||||
|
||||
Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
|
||||
scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):
|
||||
|
||||
- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead.
|
||||
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
|
||||
|
||||
|
||||
Using ``pytest.warns(None)``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.0
|
||||
|
||||
:func:`pytest.warns(None) <pytest.warns>` is now deprecated because it was frequently misused.
|
||||
Its correct usage was checking that the code emits at least one warning of any type - like ``pytest.warns()``
|
||||
or ``pytest.warns(Warning)``.
|
||||
|
||||
See :ref:`warns use cases` for examples.
|
||||
|
||||
|
||||
Returning non-None value in test functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.2
|
||||
|
||||
A :class:`pytest.PytestReturnNotNoneWarning` is now emitted if a test function returns something other than `None`.
|
||||
|
||||
This prevents a common mistake among beginners that expect that returning a `bool` would cause a test to pass or fail, for example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["a", "b", "result"],
|
||||
[
|
||||
[1, 2, 5],
|
||||
[2, 3, 8],
|
||||
[5, 3, 18],
|
||||
],
|
||||
)
|
||||
def test_foo(a, b, result):
|
||||
return foo(a, b) == result
|
||||
|
||||
Given that pytest ignores the return value, this might be surprising that it will never fail.
|
||||
|
||||
The proper fix is to change the `return` to an `assert`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["a", "b", "result"],
|
||||
[
|
||||
[1, 2, 5],
|
||||
[2, 3, 8],
|
||||
[5, 3, 18],
|
||||
],
|
||||
)
|
||||
def test_foo(a, b, result):
|
||||
assert foo(a, b) == result
|
||||
|
||||
|
||||
The ``--strict`` command-line option
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.2
|
||||
|
||||
The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
|
||||
better conveys what the option does.
|
||||
|
||||
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
|
||||
flag for all strictness related options (``--strict-markers`` and ``--strict-config``
|
||||
at the moment, more might be introduced in the future).
|
||||
|
||||
|
||||
The ``yield_fixture`` function/decorator
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 6.2
|
||||
|
||||
``pytest.yield_fixture`` is a deprecated alias for :func:`pytest.fixture`.
|
||||
|
||||
It has been so for a very long time, so can be search/replaced safely.
|
||||
|
||||
|
||||
Removed Features and Breaking Changes
|
||||
-------------------------------------
|
||||
|
||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||
an appropriate period of deprecation has passed.
|
||||
|
||||
Some breaking changes which could not be deprecated are also listed.
|
||||
|
||||
|
||||
Collection changes in pytest 8
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -172,7 +172,7 @@ class TestRaises:
|
||||
raise ValueError("demo error")
|
||||
|
||||
def test_tupleerror(self):
|
||||
a, b = [1] # noqa: F841
|
||||
a, b = [1] # NOQA
|
||||
|
||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||
items = [1, 2, 3]
|
||||
@@ -180,7 +180,7 @@ class TestRaises:
|
||||
a, b = items.pop()
|
||||
|
||||
def test_some_error(self):
|
||||
if namenotexi: # noqa: F821
|
||||
if namenotexi: # NOQA
|
||||
pass
|
||||
|
||||
def func1(self):
|
||||
|
||||
@@ -18,6 +18,7 @@ For basic examples, see
|
||||
- :ref:`Fixtures <fixtures>` for basic fixture/setup examples
|
||||
- :ref:`parametrize` for basic test function parametrization
|
||||
- :ref:`unittest` for basic unittest integration
|
||||
- :ref:`noseintegration` for basic nosetests integration
|
||||
|
||||
The following examples aim at various use cases you might encounter.
|
||||
|
||||
|
||||
@@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
|
||||
rootdir: /home/sweet/project
|
||||
collected 8 items
|
||||
|
||||
<Dir parametrize.rst-195>
|
||||
<Dir parametrize.rst-194>
|
||||
<Module test_time.py>
|
||||
<Function test_timedistance_v0[a0-b0-expected0]>
|
||||
<Function test_timedistance_v0[a1-b1-expected1]>
|
||||
@@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
|
||||
rootdir: /home/sweet/project
|
||||
collected 4 items
|
||||
|
||||
<Dir parametrize.rst-195>
|
||||
<Dir parametrize.rst-194>
|
||||
<Module test_scenarios.py>
|
||||
<Class TestSampleWithScenarios>
|
||||
<Function test_demo1[basic]>
|
||||
@@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
|
||||
rootdir: /home/sweet/project
|
||||
collected 2 items
|
||||
|
||||
<Dir parametrize.rst-195>
|
||||
<Dir parametrize.rst-194>
|
||||
<Module test_backends.py>
|
||||
<Function test_db_initialized[d1]>
|
||||
<Function test_db_initialized[d2]>
|
||||
|
||||
@@ -152,7 +152,7 @@ The test collection would look like this:
|
||||
configfile: pytest.ini
|
||||
collected 2 items
|
||||
|
||||
<Dir pythoncollection.rst-196>
|
||||
<Dir pythoncollection.rst-195>
|
||||
<Module check_myapp.py>
|
||||
<Class CheckMyApp>
|
||||
<Function simple_check>
|
||||
@@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
|
||||
configfile: pytest.ini
|
||||
collected 3 items
|
||||
|
||||
<Dir pythoncollection.rst-196>
|
||||
<Dir pythoncollection.rst-195>
|
||||
<Dir CWD>
|
||||
<Module pythoncollection.py>
|
||||
<Function test_function>
|
||||
|
||||
@@ -445,7 +445,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
||||
|
||||
def test_tupleerror(self):
|
||||
> a, b = [1] # noqa: F841
|
||||
> a, b = [1] # NOQA
|
||||
E ValueError: not enough values to unpack (expected 2, got 1)
|
||||
|
||||
failure_demo.py:175: ValueError
|
||||
@@ -467,7 +467,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0022>
|
||||
|
||||
def test_some_error(self):
|
||||
> if namenotexi: # noqa: F821
|
||||
> if namenotexi: # NOQA
|
||||
E NameError: name 'namenotexi' is not defined
|
||||
|
||||
failure_demo.py:183: NameError
|
||||
|
||||
@@ -85,7 +85,7 @@ style of setup/teardown functions:
|
||||
In addition, pytest continues to support :ref:`xunitsetup`. You can mix
|
||||
both styles, moving incrementally from classic to new style, as you
|
||||
prefer. You can also start out from existing :ref:`unittest.TestCase
|
||||
style <unittest.TestCase>`.
|
||||
style <unittest.TestCase>` or :ref:`nose based <nosestyle>` projects.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -60,10 +60,8 @@ Within Python modules, ``pytest`` also discovers tests using the standard
|
||||
:ref:`unittest.TestCase <unittest.TestCase>` subclassing technique.
|
||||
|
||||
|
||||
.. _`test layout`:
|
||||
|
||||
Choosing a test layout
|
||||
----------------------
|
||||
Choosing a test layout / import rules
|
||||
-------------------------------------
|
||||
|
||||
``pytest`` supports two common test layouts:
|
||||
|
||||
|
||||
@@ -10,27 +10,19 @@ Import modes
|
||||
|
||||
pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution.
|
||||
|
||||
Importing files in Python is a non-trivial processes, so aspects of the
|
||||
Importing files in Python (at least until recently) is a non-trivial processes, often requiring
|
||||
changing :data:`sys.path`. Some aspects of the
|
||||
import process can be controlled through the ``--import-mode`` command-line flag, which can assume
|
||||
these values:
|
||||
|
||||
.. _`import-mode-prepend`:
|
||||
|
||||
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
||||
of :py:data:`sys.path` if not already there, and then imported with
|
||||
the :func:`importlib.import_module <importlib.import_module>` function.
|
||||
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 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.
|
||||
|
||||
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
|
||||
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
||||
|
||||
@@ -46,78 +38,32 @@ these values:
|
||||
the tests will run against the installed version
|
||||
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
|
||||
we advocate for using :ref:`src-layouts <src-layout>`.
|
||||
we advocate for using :ref:`src <src-layout>` layouts.
|
||||
|
||||
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.
|
||||
|
||||
.. _`import-mode-importlib`:
|
||||
* ``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`.
|
||||
|
||||
* ``importlib``: this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules, without changing :py:data:`sys.path`.
|
||||
For this reason this doesn't require test module names to be unique.
|
||||
|
||||
Advantages of this mode:
|
||||
One drawback however is that test modules are non-importable by each other. Also, utility
|
||||
modules in the tests directories are not automatically importable because the tests directory is no longer
|
||||
added to :py:data:`sys.path`.
|
||||
|
||||
* pytest will not change :py:data:`sys.path` at all.
|
||||
* 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.
|
||||
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.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The :confval:`pythonpath` configuration variable.
|
||||
|
||||
The :confval:`consider_namespace_packages` configuration variable.
|
||||
|
||||
:ref:`test layout`.
|
||||
|
||||
|
||||
``prepend`` and ``append`` import modes scenarios
|
||||
-------------------------------------------------
|
||||
|
||||
Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to
|
||||
change :py:data:`sys.path` in order to import test modules or ``conftest.py`` files, and the issues users
|
||||
change ``sys.path`` in order to import test modules or ``conftest.py`` files, and the issues users
|
||||
might encounter because of that.
|
||||
|
||||
Test modules / ``conftest.py`` files inside packages
|
||||
@@ -146,7 +92,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
|
||||
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
|
||||
:py:data:`sys.path` (if not there already) in order to load
|
||||
``sys.path`` (if not there already) in order to load
|
||||
``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.
|
||||
@@ -176,8 +122,8 @@ When executing:
|
||||
|
||||
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
|
||||
: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 :py:data:`sys.path` to import it as ``conftest``.
|
||||
``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``.
|
||||
|
||||
For this reason this layout cannot have test modules with the same name, as they all will be
|
||||
imported in the global import namespace.
|
||||
@@ -190,7 +136,7 @@ Invoking ``pytest`` versus ``python -m pytest``
|
||||
-----------------------------------------------
|
||||
|
||||
Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly
|
||||
equivalent behaviour, except that the latter will add the current directory to :py:data:`sys.path`, which
|
||||
equivalent behaviour, except that the latter will add the current directory to ``sys.path``, which
|
||||
is standard ``python`` behavior.
|
||||
|
||||
See also :ref:`invoke-python`.
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 8.1.0
|
||||
pytest 8.0.2
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ How to use pytest with an existing test suite
|
||||
==============================================
|
||||
|
||||
Pytest can be used with most existing test suites, but its
|
||||
behavior differs from other test runners such as Python's
|
||||
default unittest framework.
|
||||
behavior differs from other test runners such as :ref:`nose <noseintegration>` or
|
||||
Python's default unittest framework.
|
||||
|
||||
Before using this section you will want to :ref:`install pytest <getstarted>`.
|
||||
|
||||
|
||||
@@ -494,7 +494,7 @@ Fixtures are created when first requested by a test, and are destroyed based on
|
||||
* ``function``: the default scope, the fixture is destroyed at the end of the test.
|
||||
* ``class``: the fixture is destroyed during teardown of the last test in the class.
|
||||
* ``module``: the fixture is destroyed during teardown of the last test in the module.
|
||||
* ``package``: the fixture is destroyed during teardown of the last test in the package where the fixture is defined, including sub-packages and sub-directories within it.
|
||||
* ``package``: the fixture is destroyed during teardown of the last test in the package.
|
||||
* ``session``: the fixture is destroyed at the end of the test session.
|
||||
|
||||
.. note::
|
||||
@@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
|
||||
rootdir: /home/sweet/project
|
||||
collected 12 items
|
||||
|
||||
<Dir fixtures.rst-214>
|
||||
<Dir fixtures.rst-213>
|
||||
<Module test_anothersmtp.py>
|
||||
<Function test_showhelo[smtp.gmail.com]>
|
||||
<Function test_showhelo[mail.python.org]>
|
||||
|
||||
@@ -52,6 +52,7 @@ pytest and other test systems
|
||||
|
||||
existingtestsuite
|
||||
unittest
|
||||
nose
|
||||
xunit_setup
|
||||
|
||||
pytest development environment
|
||||
|
||||
@@ -206,9 +206,8 @@ option names are:
|
||||
* ``log_cli_date_format``
|
||||
|
||||
If you need to record the whole test suite logging calls to a file, you can pass
|
||||
``--log-file=/path/to/log/file``. This log file is opened in write mode by default which
|
||||
``--log-file=/path/to/log/file``. This log file is opened in write mode which
|
||||
means that it will be overwritten at each run tests session.
|
||||
If you'd like the file opened in append mode instead, then you can pass ``--log-file-mode=a``.
|
||||
Note that relative paths for the log-file location, whether passed on the CLI or declared in a
|
||||
config file, are always resolved relative to the current working directory.
|
||||
|
||||
@@ -224,13 +223,12 @@ All of the log file options can also be set in the configuration INI file. The
|
||||
option names are:
|
||||
|
||||
* ``log_file``
|
||||
* ``log_file_mode``
|
||||
* ``log_file_level``
|
||||
* ``log_file_format``
|
||||
* ``log_file_date_format``
|
||||
|
||||
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
|
||||
is considered **experimental**. Note that ``set_log_path()`` respects the ``log_file_mode`` option.
|
||||
is considered **experimental**.
|
||||
|
||||
.. _log_colors:
|
||||
|
||||
|
||||
99
doc/en/how-to/nose.rst
Normal file
99
doc/en/how-to/nose.rst
Normal file
@@ -0,0 +1,99 @@
|
||||
.. _`noseintegration`:
|
||||
|
||||
How to run tests written for nose
|
||||
=======================================
|
||||
|
||||
``pytest`` has basic support for running tests written for nose_.
|
||||
|
||||
.. warning::
|
||||
This functionality has been deprecated and is likely to be removed in ``pytest 8.x``.
|
||||
|
||||
.. _nosestyle:
|
||||
|
||||
Usage
|
||||
-------------
|
||||
|
||||
After :ref:`installation` type:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python setup.py develop # make sure tests can import our package
|
||||
pytest # instead of 'nosetests'
|
||||
|
||||
and you should be able to run your nose style tests and
|
||||
make use of pytest's capabilities.
|
||||
|
||||
Supported nose Idioms
|
||||
----------------------
|
||||
|
||||
* ``setup()`` and ``teardown()`` at module/class/method level: any function or method called ``setup`` will be called during the setup phase for each test, same for ``teardown``.
|
||||
* ``SkipTest`` exceptions and markers
|
||||
* setup/teardown decorators
|
||||
* ``__test__`` attribute on modules/classes/functions
|
||||
* general usage of nose utilities
|
||||
|
||||
Unsupported idioms / known issues
|
||||
----------------------------------
|
||||
|
||||
- unittest-style ``setUp, tearDown, setUpClass, tearDownClass``
|
||||
are recognized only on ``unittest.TestCase`` classes but not
|
||||
on plain classes. ``nose`` supports these methods also on plain
|
||||
classes but pytest deliberately does not. As nose and pytest already
|
||||
both support ``setup_class, teardown_class, setup_method, teardown_method``
|
||||
it doesn't seem useful to duplicate the unittest-API like nose does.
|
||||
If you however rather think pytest should support the unittest-spelling on
|
||||
plain classes please post to :issue:`377`.
|
||||
|
||||
- nose imports test modules with the same import path (e.g.
|
||||
``tests.test_mode``) but different file system paths
|
||||
(e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``)
|
||||
by extending sys.path/import semantics. pytest does not do that. Note that
|
||||
`nose2 choose to avoid this sys.path/import hackery <https://nose2.readthedocs.io/en/latest/differences.html#test-discovery-and-loading>`_.
|
||||
|
||||
If you place a conftest.py file in the root directory of your project
|
||||
(as determined by pytest) pytest will run tests "nose style" against
|
||||
the code below that directory by adding it to your ``sys.path`` instead of
|
||||
running against your installed code.
|
||||
|
||||
You may find yourself wanting to do this if you ran ``python setup.py install``
|
||||
to set up your project, as opposed to ``python setup.py develop`` or any of
|
||||
the package manager equivalents. Installing with develop in a
|
||||
virtual environment like tox is recommended over this pattern.
|
||||
|
||||
- nose-style doctests are not collected and executed correctly,
|
||||
also doctest fixtures don't work.
|
||||
|
||||
- no nose-configuration is recognized.
|
||||
|
||||
- ``yield``-based methods are
|
||||
fundamentally incompatible with pytest because they don't support fixtures
|
||||
properly since collection and test execution are separated.
|
||||
|
||||
Here is a table comparing the default supported naming conventions for both
|
||||
nose and pytest.
|
||||
|
||||
========= ========================== ======= =====
|
||||
what default naming convention pytest nose
|
||||
========= ========================== ======= =====
|
||||
module ``test*.py`` ✅
|
||||
module ``test_*.py`` ✅ ✅
|
||||
module ``*_test.py`` ✅
|
||||
module ``*_tests.py``
|
||||
class ``*(unittest.TestCase)`` ✅ ✅
|
||||
method ``test_*`` ✅ ✅
|
||||
class ``Test*`` ✅
|
||||
method ``test_*`` ✅
|
||||
function ``test_*`` ✅
|
||||
========= ========================== ======= =====
|
||||
|
||||
|
||||
Migrating from nose to pytest
|
||||
------------------------------
|
||||
|
||||
`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script
|
||||
and pytest plugin to help convert Nose-based tests into pytest-based tests.
|
||||
Specifically, the script transforms ``nose.tools.assert_*`` function calls into
|
||||
raw assert statements, while preserving format of original arguments
|
||||
as much as possible.
|
||||
|
||||
.. _nose: https://nose.readthedocs.io/en/latest/
|
||||
@@ -325,9 +325,7 @@ This is done by setting a verbosity level in the configuration file for the spec
|
||||
``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside
|
||||
the file is shown by a single character in the output.
|
||||
|
||||
:confval:`verbosity_test_cases`: Controls how verbose the test execution output should be when pytest is executed.
|
||||
Running ``pytest --no-header`` with a value of ``2`` would have the same output as the first verbosity example, but each
|
||||
test inside the file gets its own line in the output.
|
||||
(Note: currently this is the only option available, but more might be added in the future).
|
||||
|
||||
.. _`pytest.detailed_failed_tests_usage`:
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ How to use temporary directories and files in tests
|
||||
The ``tmp_path`` fixture
|
||||
------------------------
|
||||
|
||||
You can use the ``tmp_path`` fixture which will provide a temporary directory
|
||||
unique to each test function.
|
||||
You can use the ``tmp_path`` fixture which will
|
||||
provide a temporary directory unique to the test invocation,
|
||||
created in the `base temporary directory`_.
|
||||
|
||||
``tmp_path`` is a :class:`pathlib.Path` object. Here is an example test usage:
|
||||
|
||||
@@ -61,11 +62,6 @@ Running this would result in a passed test except for the last
|
||||
FAILED test_tmp_path.py::test_create_file - assert 0
|
||||
============================ 1 failed in 0.12s =============================
|
||||
|
||||
By default, ``pytest`` retains the temporary directory for the last 3 ``pytest``
|
||||
invocations. Concurrent invocations of the same test function are supported by
|
||||
configuring the base temporary directory to be unique for each concurrent
|
||||
run. See `temporary directory location and retention`_ for details.
|
||||
|
||||
.. _`tmp_path_factory example`:
|
||||
|
||||
The ``tmp_path_factory`` fixture
|
||||
@@ -104,7 +100,7 @@ See :ref:`tmp_path_factory API <tmp_path_factory factory api>` for details.
|
||||
.. _tmpdir:
|
||||
|
||||
The ``tmpdir`` and ``tmpdir_factory`` fixtures
|
||||
----------------------------------------------
|
||||
---------------------------------------------------
|
||||
|
||||
The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path``
|
||||
and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects
|
||||
@@ -128,10 +124,10 @@ See :fixture:`tmpdir <tmpdir>` :fixture:`tmpdir_factory <tmpdir_factory>`
|
||||
API for details.
|
||||
|
||||
|
||||
.. _`temporary directory location and retention`:
|
||||
.. _`base temporary directory`:
|
||||
|
||||
Temporary directory location and retention
|
||||
------------------------------------------
|
||||
The default base temporary directory
|
||||
-----------------------------------------------
|
||||
|
||||
Temporary directories are by default created as sub-directories of
|
||||
the system temporary directory. The base name will be ``pytest-NUM`` where
|
||||
@@ -156,7 +152,7 @@ You can override the default temporary directory setting like this:
|
||||
for that purpose only.
|
||||
|
||||
When distributing tests on the local machine using ``pytest-xdist``, care is taken to
|
||||
automatically configure a `basetemp` directory for the sub processes such that all temporary
|
||||
data lands below a single per-test run temporary directory.
|
||||
automatically configure a basetemp directory for the sub processes such that all temporary
|
||||
data lands below a single per-test run basetemp directory.
|
||||
|
||||
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
@@ -46,18 +46,24 @@ Plugin discovery order at tool startup
|
||||
|
||||
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
|
||||
|
||||
6. by loading all "initial ":file:`conftest.py` files:
|
||||
6. by loading all :file:`conftest.py` files as inferred by the command line
|
||||
invocation:
|
||||
|
||||
- determine the test paths: specified on the command line, otherwise in
|
||||
:confval:`testpaths` if defined and running from the rootdir, otherwise the
|
||||
current dir
|
||||
- for each test path, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||
to the directory part of the test path, if exist. Before a ``conftest.py``
|
||||
file is loaded, load ``conftest.py`` files in all of its parent directories.
|
||||
After a ``conftest.py`` file is loaded, recursively load all plugins specified
|
||||
in its :globalvar:`pytest_plugins` variable if present.
|
||||
- if no test paths are specified, use the current dir as a test path
|
||||
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||
to the directory part of the first test path. After the ``conftest.py``
|
||||
file is loaded, load all plugins specified in its
|
||||
:globalvar:`pytest_plugins` variable if present.
|
||||
|
||||
Note that pytest does not find ``conftest.py`` files in deeper nested
|
||||
sub directories at tool startup. It is usually a good idea to keep
|
||||
your ``conftest.py`` file in the top level test or project root directory.
|
||||
|
||||
7. by recursively loading all plugins specified by the
|
||||
:globalvar:`pytest_plugins` variable in ``conftest.py`` files.
|
||||
|
||||
|
||||
.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/
|
||||
.. _`conftest.py plugins`:
|
||||
.. _`localplugin`:
|
||||
.. _`local conftest plugins`:
|
||||
@@ -102,9 +108,9 @@ Here is how you might run it::
|
||||
See also: :ref:`pythonpath`.
|
||||
|
||||
.. note::
|
||||
Some hooks cannot be implemented in conftest.py files which are not
|
||||
:ref:`initial <pluginorder>` due to how pytest discovers plugins during
|
||||
startup. See the documentation of each hook for details.
|
||||
Some hooks should be implemented only in plugins or conftest.py files situated at the
|
||||
tests root directory due to how pytest discovers plugins during startup,
|
||||
see the documentation of each hook for details.
|
||||
|
||||
Writing your own plugin
|
||||
-----------------------
|
||||
|
||||
@@ -77,11 +77,11 @@ Features
|
||||
|
||||
- :ref:`Modular fixtures <fixture>` for managing small or parametrized long-lived test resources
|
||||
|
||||
- Can run :ref:`unittest <unittest>` (including trial) test suites out of the box
|
||||
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
|
||||
|
||||
- Python 3.8+ or PyPy 3
|
||||
|
||||
- Rich plugin architecture, with over 1300+ :ref:`external plugins <plugin-list>` and thriving community
|
||||
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
|
||||
|
||||
|
||||
Documentation
|
||||
|
||||
@@ -177,20 +177,13 @@ Files will only be matched for configuration if:
|
||||
* ``tox.ini``: contains a ``[pytest]`` section.
|
||||
* ``setup.cfg``: contains a ``[tool:pytest]`` section.
|
||||
|
||||
Finally, a ``pyproject.toml`` file will be considered the ``configfile`` if no other match was found, in this case
|
||||
even if it does not contain a ``[tool.pytest.ini_options]`` table (this was added in ``8.1``).
|
||||
|
||||
The files are considered in the order above. Options from multiple ``configfiles`` candidates
|
||||
are never merged - the first match wins.
|
||||
|
||||
The configuration file also determines the value of the ``rootpath``.
|
||||
|
||||
The :class:`Config <pytest.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
|
||||
will subsequently carry these attributes:
|
||||
|
||||
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist. It is used as
|
||||
a reference directory for constructing test addresses ("nodeids") and can be used also by plugins for storing
|
||||
per-testrun information.
|
||||
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist.
|
||||
|
||||
- :attr:`config.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None``
|
||||
(it is named ``inipath`` for historical reasons).
|
||||
@@ -200,7 +193,9 @@ will subsequently carry these attributes:
|
||||
versions of the older ``config.rootdir`` and ``config.inifile``, which have type
|
||||
``py.path.local``, and still exist for backward compatibility.
|
||||
|
||||
|
||||
The ``rootdir`` is used as a reference directory for constructing test
|
||||
addresses ("nodeids") and can be used also by plugins for storing
|
||||
per-testrun information.
|
||||
|
||||
Example:
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -640,6 +640,8 @@ Bootstrapping hooks called for plugins registered early enough (internal and set
|
||||
|
||||
.. hook:: pytest_load_initial_conftests
|
||||
.. autofunction:: pytest_load_initial_conftests
|
||||
.. hook:: pytest_cmdline_preparse
|
||||
.. autofunction:: pytest_cmdline_preparse
|
||||
.. hook:: pytest_cmdline_parse
|
||||
.. autofunction:: pytest_cmdline_parse
|
||||
.. hook:: pytest_cmdline_main
|
||||
@@ -1204,6 +1206,9 @@ Custom warnings generated in some situations such as improper usage or deprecate
|
||||
.. autoclass:: pytest.PytestReturnNotNoneWarning
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: pytest.PytestRemovedIn8Warning
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: pytest.PytestRemovedIn9Warning
|
||||
:show-inheritance:
|
||||
|
||||
@@ -1274,19 +1279,6 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
variables, that will be expanded. For more information about cache plugin
|
||||
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
|
||||
|
||||
Sets the console output style while running tests:
|
||||
@@ -1878,19 +1870,6 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
"auto" can be used to explicitly use the global verbosity level.
|
||||
|
||||
|
||||
.. confval:: verbosity_test_cases
|
||||
|
||||
Set a verbosity level specifically for test case execution related output, overriding the application wide level.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
verbosity_test_cases = 2
|
||||
|
||||
Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of
|
||||
"auto" can be used to explicitly use the global verbosity level.
|
||||
|
||||
|
||||
.. confval:: xfail_strict
|
||||
|
||||
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
|
||||
@@ -2103,8 +2082,6 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
--log-cli-date-format=LOG_CLI_DATE_FORMAT
|
||||
Log date format used by the logging module
|
||||
--log-file=LOG_FILE Path to a file when logging will be written to
|
||||
--log-file-mode={w,a}
|
||||
Log file open mode
|
||||
--log-file-level=LOG_FILE_LEVEL
|
||||
Log file logging level
|
||||
--log-file-format=LOG_FILE_FORMAT
|
||||
@@ -2120,7 +2097,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
|
||||
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
||||
|
||||
markers (linelist): Register new markers for test functions
|
||||
markers (linelist): Markers for test functions
|
||||
empty_parameter_set_mark (string):
|
||||
Default marker for empty parametersets
|
||||
norecursedirs (args): Directory patterns to avoid for recursion
|
||||
@@ -2130,9 +2107,6 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
Each line specifies a pattern for
|
||||
warnings.filterwarnings. Processed after
|
||||
-W/--pythonwarnings.
|
||||
consider_namespace_packages (bool):
|
||||
Consider namespace packages when resolving module
|
||||
names during import
|
||||
usefixtures (args): List of default fixtures to be used with this
|
||||
project
|
||||
python_files (args): Glob-style file patterns for Python test module
|
||||
@@ -2151,11 +2125,6 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
progress information ("progress" (percentage) |
|
||||
"count" | "progress-even-when-capture-no" (forces
|
||||
progress even when capture=no)
|
||||
verbosity_test_cases (string):
|
||||
Specify a verbosity level for test case execution,
|
||||
overriding the main level. Higher levels will
|
||||
provide more detailed information about each test
|
||||
case executed.
|
||||
xfail_strict (bool): Default for the strict parameter of xfail markers
|
||||
when not given explicitly (default: False)
|
||||
tmp_path_retention_count (string):
|
||||
@@ -2203,8 +2172,6 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
log_cli_date_format (string):
|
||||
Default value for --log-cli-date-format
|
||||
log_file (string): Default value for --log-file
|
||||
log_file_mode (string):
|
||||
Default value for --log-file-mode
|
||||
log_file_level (string):
|
||||
Default value for --log-file-level
|
||||
log_file_format (string):
|
||||
|
||||
247
pyproject.toml
247
pyproject.toml
@@ -1,178 +1,13 @@
|
||||
[project]
|
||||
name = "pytest"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
readme = "README.rst"
|
||||
keywords = [
|
||||
"test",
|
||||
"unittest",
|
||||
]
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "Holger Krekel"},
|
||||
{name = "Bruno Oliveira"},
|
||||
{name = "Ronny Pfannschmidt"},
|
||||
{name = "Floris Bruynooghe"},
|
||||
{name = "Brianna Laugher"},
|
||||
{name = "Florian Bruhin"},
|
||||
{name = "Others (See AUTHORS)"},
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
classifiers = [
|
||||
"Development Status :: 6 - Mature",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: MacOS",
|
||||
"Operating System :: Microsoft :: Windows",
|
||||
"Operating System :: POSIX",
|
||||
"Operating System :: Unix",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
"Topic :: Software Development :: Testing",
|
||||
"Topic :: Utilities",
|
||||
]
|
||||
dynamic = [
|
||||
"version",
|
||||
]
|
||||
dependencies = [
|
||||
'colorama; sys_platform == "win32"',
|
||||
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
|
||||
"iniconfig",
|
||||
"packaging",
|
||||
"pluggy<2.0,>=1.4",
|
||||
'tomli>=1; python_version < "3.11"',
|
||||
]
|
||||
[project.optional-dependencies]
|
||||
testing = [
|
||||
"argcomplete",
|
||||
"attrs>=19.2",
|
||||
"hypothesis>=3.56",
|
||||
"mock",
|
||||
"pygments>=2.7.2",
|
||||
"requests",
|
||||
"setuptools",
|
||||
"xmlschema",
|
||||
]
|
||||
[project.urls]
|
||||
Changelog = "https://docs.pytest.org/en/stable/changelog.html"
|
||||
Homepage = "https://docs.pytest.org/en/latest/"
|
||||
Source = "https://github.com/pytest-dev/pytest"
|
||||
Tracker = "https://github.com/pytest-dev/pytest/issues"
|
||||
Twitter = "https://twitter.com/pytestdotorg"
|
||||
[project.scripts]
|
||||
"py.test" = "pytest:console_main"
|
||||
pytest = "pytest:console_main"
|
||||
|
||||
[build-system]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = [
|
||||
"setuptools>=61",
|
||||
"setuptools-scm[toml]>=6.2.3",
|
||||
"setuptools>=45.0",
|
||||
"setuptools-scm[toml]>=6.2.3",
|
||||
]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"_pytest" = ["py.typed"]
|
||||
"pytest" = ["py.typed"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
write_to = "src/_pytest/_version.py"
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py38']
|
||||
|
||||
[tool.ruff]
|
||||
src = ["src"]
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff.format]
|
||||
docstring-code-format = true
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"B", # bugbear
|
||||
"D", # pydocstyle
|
||||
"E", # pycodestyle
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"PYI", # flake8-pyi
|
||||
"UP", # pyupgrade
|
||||
"RUF", # ruff
|
||||
"W", # pycodestyle
|
||||
"PIE", # flake8-pie
|
||||
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
|
||||
"PLE", # pylint error
|
||||
"PLW", # pylint warning
|
||||
"PLR1714", # Consider merging multiple comparisons
|
||||
]
|
||||
ignore = [
|
||||
# bugbear ignore
|
||||
"B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
|
||||
"B007", # Loop control variable `i` not used within loop body
|
||||
"B009", # Do not call `getattr` with a constant attribute value
|
||||
"B010", # [*] Do not call `setattr` with a constant attribute value.
|
||||
"B011", # Do not `assert False` (`python -O` removes these calls)
|
||||
"B028", # No explicit `stacklevel` keyword argument found
|
||||
# pycodestyle ignore
|
||||
# pytest can do weird low-level things, and we usually know
|
||||
# what we're doing when we use type(..) is ...
|
||||
"E721", # Do not compare types, use `isinstance()`
|
||||
# pydocstyle ignore
|
||||
"D100", # Missing docstring in public module
|
||||
"D101", # Missing docstring in public class
|
||||
"D102", # Missing docstring in public method
|
||||
"D103", # Missing docstring in public function
|
||||
"D104", # Missing docstring in public package
|
||||
"D105", # Missing docstring in magic method
|
||||
"D106", # Missing docstring in public nested class
|
||||
"D107", # Missing docstring in `__init__`
|
||||
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
|
||||
"D205", # 1 blank line required between summary line and description
|
||||
"D400", # First line should end with a period
|
||||
"D401", # First line of docstring should be in imperative mood
|
||||
"D402", # First line should not be the function's signature
|
||||
"D404", # First word of the docstring should not be "This"
|
||||
"D415", # First line should end with a period, question mark, or exclamation point
|
||||
# ruff ignore
|
||||
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
# pylint ignore
|
||||
"PLW0603", # Using the global statement
|
||||
"PLW0120", # remove the else and dedent its contents
|
||||
"PLW2901", # for loop variable overwritten by assignment target
|
||||
"PLR5501", # Use `elif` instead of `else` then `if`
|
||||
]
|
||||
|
||||
[tool.ruff.lint.pycodestyle]
|
||||
# In order to be able to format for 88 char in ruff format
|
||||
max-line-length = 120
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "pep257"
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
force-single-line = true
|
||||
combine-as-imports = true
|
||||
force-sort-within-sections = true
|
||||
order-by-type = false
|
||||
known-local-folder = ["pytest", "_pytest"]
|
||||
lines-after-imports = 2
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"src/_pytest/_py/**/*.py" = ["B", "PYI"]
|
||||
"src/_pytest/_version.py" = ["I001"]
|
||||
"testing/python/approx.py" = ["B015"]
|
||||
|
||||
[tool.check-wheel-contents]
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
# W009: Wheel contains multiple toplevel library entries
|
||||
ignore = "W009"
|
||||
|
||||
[tool.pyproject-fmt]
|
||||
indent = 4
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
minversion = "2.0"
|
||||
addopts = "-rfEX -p pytester --strict-markers"
|
||||
@@ -232,6 +67,7 @@ markers = [
|
||||
"uses_pexpect",
|
||||
]
|
||||
|
||||
|
||||
[tool.towncrier]
|
||||
package = "pytest"
|
||||
package_dir = "src"
|
||||
@@ -280,16 +116,65 @@ template = "changelog/_template.rst"
|
||||
name = "Trivial/Internal Changes"
|
||||
showcontent = true
|
||||
|
||||
[tool.mypy]
|
||||
mypy_path = ["src"]
|
||||
check_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
disallow_untyped_defs = true
|
||||
ignore_missing_imports = true
|
||||
show_error_codes = true
|
||||
strict_equality = true
|
||||
warn_redundant_casts = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
warn_unused_configs = true
|
||||
no_implicit_reexport = true
|
||||
[tool.black]
|
||||
target-version = ['py38']
|
||||
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
[tool.check-wheel-contents]
|
||||
# W009: Wheel contains multiple toplevel library entries
|
||||
ignore = "W009"
|
||||
|
||||
[tool.ruff]
|
||||
src = ["src"]
|
||||
line-length = 88
|
||||
select = [
|
||||
"D", # pydocstyle
|
||||
"E", # pycodestyle
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"UP", # pyupgrade
|
||||
"W", # pycodestyle
|
||||
]
|
||||
ignore = [
|
||||
# pycodestyle ignore
|
||||
# pytest can do weird low-level things, and we usually know
|
||||
# what we're doing when we use type(..) is ...
|
||||
"E721", # Do not compare types, use `isinstance()`
|
||||
# pydocstyle ignore
|
||||
"D100", # Missing docstring in public module
|
||||
"D101", # Missing docstring in public class
|
||||
"D102", # Missing docstring in public method
|
||||
"D103", # Missing docstring in public function
|
||||
"D104", # Missing docstring in public package
|
||||
"D105", # Missing docstring in magic method
|
||||
"D106", # Missing docstring in public nested class
|
||||
"D107", # Missing docstring in `__init__`
|
||||
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
|
||||
"D205", # 1 blank line required between summary line and description
|
||||
"D400", # First line should end with a period
|
||||
"D401", # First line of docstring should be in imperative mood
|
||||
"D402", # First line should not be the function's signature
|
||||
"D404", # First word of the docstring should not be "This"
|
||||
"D415", # First line should end with a period, question mark, or exclamation point
|
||||
# Temp for backport 8.0.x
|
||||
"E501",
|
||||
"UP031",
|
||||
]
|
||||
|
||||
[tool.ruff.format]
|
||||
docstring-code-format = true
|
||||
|
||||
[tool.ruff.lint.pycodestyle]
|
||||
# In order to be able to format for 88 char in ruff format
|
||||
max-line-length = 120
|
||||
|
||||
[tool.ruff.lint.pydocstyle]
|
||||
convention = "pep257"
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
force-single-line = true
|
||||
combine-as-imports = true
|
||||
force-sort-within-sections = true
|
||||
order-by-type = false
|
||||
known-local-folder = ["pytest", "_pytest"]
|
||||
lines-after-imports = 2
|
||||
|
||||
@@ -79,7 +79,7 @@ def prepare_release_pr(
|
||||
)
|
||||
except InvalidFeatureRelease as e:
|
||||
print(f"{Fore.RED}{e}")
|
||||
raise SystemExit(1) from None
|
||||
raise SystemExit(1)
|
||||
|
||||
print(f"Version: {Fore.CYAN}{version}")
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ def pre_release(
|
||||
|
||||
def changelog(version: str, write_out: bool = False) -> None:
|
||||
addopts = [] if write_out else ["--draft"]
|
||||
check_call(["towncrier", "--yes", "--version", version, *addopts])
|
||||
check_call(["towncrier", "--yes", "--version", version] + addopts)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
|
||||
@@ -29,7 +29,7 @@ Pytest Plugin List
|
||||
==================
|
||||
|
||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||
It includes PyPI projects whose names begin with ``pytest-`` or ``pytest_`` and a handful of manually selected projects.
|
||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||
Packages classified as inactive are excluded.
|
||||
|
||||
For detailed insights into how this list is generated,
|
||||
@@ -110,10 +110,7 @@ def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
|
||||
return {
|
||||
name: p["_last-serial"]
|
||||
for p in response.json()["projects"]
|
||||
if (
|
||||
(name := p["name"]).startswith(("pytest-", "pytest_"))
|
||||
or name in ADDITIONAL_PROJECTS
|
||||
)
|
||||
if (name := p["name"]).startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +209,7 @@ def main() -> None:
|
||||
f.write(f"This list contains {len(plugins)} plugins.\n\n")
|
||||
f.write(".. only:: not latex\n\n")
|
||||
|
||||
_ = wcwidth # reference library that must exist for tabulate to work
|
||||
wcwidth # reference library that must exist for tabulate to work
|
||||
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
|
||||
f.write(indent(plugin_table, " "))
|
||||
f.write("\n\n")
|
||||
|
||||
105
setup.cfg
Normal file
105
setup.cfg
Normal file
@@ -0,0 +1,105 @@
|
||||
[metadata]
|
||||
name = pytest
|
||||
description = pytest: simple powerful testing with Python
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
url = https://docs.pytest.org/en/latest/
|
||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||
license = MIT
|
||||
license_files = LICENSE
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
classifiers =
|
||||
Development Status :: 6 - Mature
|
||||
Intended Audience :: Developers
|
||||
License :: OSI Approved :: MIT License
|
||||
Operating System :: MacOS :: MacOS X
|
||||
Operating System :: Microsoft :: Windows
|
||||
Operating System :: POSIX
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Software Development :: Testing
|
||||
Topic :: Utilities
|
||||
keywords = test, unittest
|
||||
project_urls =
|
||||
Changelog=https://docs.pytest.org/en/stable/changelog.html
|
||||
Twitter=https://twitter.com/pytestdotorg
|
||||
Source=https://github.com/pytest-dev/pytest
|
||||
Tracker=https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
[options]
|
||||
packages =
|
||||
_pytest
|
||||
_pytest._code
|
||||
_pytest._io
|
||||
_pytest._py
|
||||
_pytest.assertion
|
||||
_pytest.config
|
||||
_pytest.mark
|
||||
pytest
|
||||
py_modules = py
|
||||
install_requires =
|
||||
iniconfig
|
||||
packaging
|
||||
pluggy>=1.3.0,<2.0
|
||||
colorama;sys_platform=="win32"
|
||||
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
||||
tomli>=1.0.0;python_version<"3.11"
|
||||
python_requires = >=3.8
|
||||
package_dir =
|
||||
=src
|
||||
setup_requires =
|
||||
setuptools
|
||||
setuptools-scm>=6.0
|
||||
zip_safe = no
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
pytest=pytest:console_main
|
||||
py.test=pytest:console_main
|
||||
|
||||
[options.extras_require]
|
||||
testing =
|
||||
argcomplete
|
||||
attrs>=19.2.0
|
||||
hypothesis>=3.56
|
||||
mock
|
||||
nose
|
||||
pygments>=2.7.2
|
||||
requests
|
||||
setuptools
|
||||
xmlschema
|
||||
|
||||
[options.package_data]
|
||||
_pytest = py.typed
|
||||
pytest = py.typed
|
||||
|
||||
[build_sphinx]
|
||||
source_dir = doc/en/
|
||||
build_dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[check-manifest]
|
||||
ignore =
|
||||
src/_pytest/_version.py
|
||||
|
||||
[devpi:upload]
|
||||
formats = sdist.tgz,bdist_wheel
|
||||
|
||||
[mypy]
|
||||
mypy_path = src
|
||||
check_untyped_defs = True
|
||||
disallow_any_generics = True
|
||||
ignore_missing_imports = True
|
||||
show_error_codes = True
|
||||
strict_equality = True
|
||||
warn_redundant_casts = True
|
||||
warn_return_any = True
|
||||
warn_unreachable = True
|
||||
warn_unused_configs = True
|
||||
no_implicit_reexport = True
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import ast
|
||||
import dataclasses
|
||||
import inspect
|
||||
@@ -1018,7 +1017,7 @@ class FormattedExcinfo:
|
||||
extraline: Optional[str] = (
|
||||
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
|
||||
" The following exception happened when comparing locals in the stack frame:\n"
|
||||
f" {type(e).__name__}: {e!s}\n"
|
||||
f" {type(e).__name__}: {str(e)}\n"
|
||||
f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
|
||||
)
|
||||
# Type ignored because adding two instances of a List subtype
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import ast
|
||||
from bisect import bisect_right
|
||||
import inspect
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# This module was imported from the cpython standard library
|
||||
# (https://github.com/python/cpython/) at commit
|
||||
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
|
||||
|
||||
@@ -232,17 +232,17 @@ class TerminalWriter:
|
||||
# which may lead to the previous color being propagated to the
|
||||
# start of the expression, so reset first.
|
||||
return "\x1b[0m" + highlighted
|
||||
except pygments.util.ClassNotFound as e:
|
||||
except pygments.util.ClassNotFound:
|
||||
raise UsageError(
|
||||
"PYTEST_THEME environment variable had an invalid value: '{}'. "
|
||||
"Only valid pygment styles are allowed.".format(
|
||||
os.getenv("PYTEST_THEME")
|
||||
)
|
||||
) from e
|
||||
except pygments.util.OptionError as e:
|
||||
)
|
||||
except pygments.util.OptionError:
|
||||
raise UsageError(
|
||||
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
|
||||
"The only allowed values are 'dark' and 'light'.".format(
|
||||
os.getenv("PYTEST_THEME_MODE")
|
||||
)
|
||||
) from e
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""local path implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
@@ -452,7 +452,7 @@ class LocalPath:
|
||||
|
||||
def ensure_dir(self, *args):
|
||||
"""Ensure the path joined with args is a directory."""
|
||||
return self.ensure(*args, dir=True)
|
||||
return self.ensure(*args, **{"dir": True})
|
||||
|
||||
def bestrelpath(self, dest):
|
||||
"""Return a string which is a relative path from self
|
||||
@@ -1105,7 +1105,9 @@ class LocalPath:
|
||||
modname = self.purebasename
|
||||
spec = importlib.util.spec_from_file_location(modname, str(self))
|
||||
if spec is None or spec.loader is None:
|
||||
raise ImportError(f"Can't find module {modname} at location {self!s}")
|
||||
raise ImportError(
|
||||
f"Can't find module {modname} at location {str(self)}"
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Support for presenting detailed information in failing assertions."""
|
||||
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
|
||||
@@ -925,7 +925,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# If any hooks implement assert_pass hook
|
||||
hook_impl_test = ast.If(
|
||||
self.helper("_check_if_assertion_pass_impl"),
|
||||
[*self.expl_stmts, hook_call_pass],
|
||||
self.expl_stmts + [hook_call_pass],
|
||||
[],
|
||||
)
|
||||
statements_pass = [hook_impl_test]
|
||||
@@ -1006,7 +1006,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if i:
|
||||
fail_inner: List[ast.stmt] = []
|
||||
# cond is set in a prior loop iteration below
|
||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
|
||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
|
||||
self.expl_stmts = fail_inner
|
||||
# Check if the left operand is a ast.NamedExpr and the value has already been visited
|
||||
if (
|
||||
|
||||
@@ -92,8 +92,7 @@ def _truncate_explanation(
|
||||
else:
|
||||
# Add proper ellipsis when we were able to fit a full line exactly
|
||||
truncated_explanation[-1] = "..."
|
||||
return [
|
||||
*truncated_explanation,
|
||||
return truncated_explanation + [
|
||||
"",
|
||||
f"...Full output truncated ({truncated_line_count} line"
|
||||
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Utilities for assertion debugging."""
|
||||
|
||||
import collections.abc
|
||||
import os
|
||||
import pprint
|
||||
@@ -233,8 +233,8 @@ def assertrepr_compare(
|
||||
return None
|
||||
|
||||
if explanation[0] != "":
|
||||
explanation = ["", *explanation]
|
||||
return [summary, *explanation]
|
||||
explanation = [""] + explanation
|
||||
return [summary] + explanation
|
||||
|
||||
|
||||
def _compare_eq_any(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Implementation of the cache provider."""
|
||||
|
||||
# This plugin was not named "cache" to avoid conflicts with the external
|
||||
# pytest-cache version.
|
||||
import dataclasses
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Per-test stdout/stderr capturing mechanism."""
|
||||
|
||||
import abc
|
||||
import collections
|
||||
import contextlib
|
||||
@@ -598,8 +598,7 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
||||
else:
|
||||
|
||||
class CaptureResult(
|
||||
collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
|
||||
Generic[AnyStr],
|
||||
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
||||
):
|
||||
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Python version compatibility code."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
@@ -15,6 +15,26 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Final
|
||||
from typing import NoReturn
|
||||
from typing import TypeVar
|
||||
|
||||
import py
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_S = TypeVar("_S")
|
||||
|
||||
#: 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
|
||||
@@ -22,7 +42,7 @@ from typing import NoReturn
|
||||
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||
class NotSetType(enum.Enum):
|
||||
token = 0
|
||||
NOTSET: Final = NotSetType.token
|
||||
NOTSET: Final = NotSetType.token # noqa: E305
|
||||
# fmt: on
|
||||
|
||||
|
||||
@@ -172,13 +192,25 @@ _non_printable_ascii_translate_table.update(
|
||||
)
|
||||
|
||||
|
||||
def _translate_non_printable(s: str) -> str:
|
||||
return s.translate(_non_printable_ascii_translate_table)
|
||||
|
||||
|
||||
STRING_TYPES = bytes, str
|
||||
|
||||
|
||||
def _bytes_to_ascii(val: bytes) -> str:
|
||||
return val.decode("ascii", "backslashreplace")
|
||||
|
||||
|
||||
def ascii_escaped(val: bytes | str) -> str:
|
||||
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
||||
bytes objects into a sequence of escaped bytes:
|
||||
|
||||
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
|
||||
|
||||
and escapes strings into a sequence of escaped unicode ids, e.g.:
|
||||
and escapes unicode objects into a sequence of escaped unicode
|
||||
ids, e.g.:
|
||||
|
||||
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
|
||||
|
||||
@@ -189,10 +221,10 @@ def ascii_escaped(val: bytes | str) -> str:
|
||||
a UTF-8 string.
|
||||
"""
|
||||
if isinstance(val, bytes):
|
||||
ret = val.decode("ascii", "backslashreplace")
|
||||
ret = _bytes_to_ascii(val)
|
||||
else:
|
||||
ret = val.encode("unicode_escape").decode("ascii")
|
||||
return ret.translate(_non_printable_ascii_translate_table)
|
||||
return _translate_non_printable(ret)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
@@ -289,7 +321,7 @@ def get_user_id() -> int | None:
|
||||
# mypy follows the version and platform checking expectation of PEP 484:
|
||||
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
||||
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
||||
if sys.platform == "win32" or sys.platform == "emscripten": # noqa: PLR1714
|
||||
if sys.platform == "win32" or sys.platform == "emscripten":
|
||||
# win32 does not have a getuid() function.
|
||||
# Emscripten has a return 0 stub.
|
||||
return None
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Command line options, ini-file and conftest.py processing."""
|
||||
|
||||
import argparse
|
||||
import collections.abc
|
||||
import copy
|
||||
@@ -17,6 +17,7 @@ import sys
|
||||
from textwrap import dedent
|
||||
import types
|
||||
from types import FunctionType
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
@@ -38,12 +39,14 @@ from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
import pluggy
|
||||
from pluggy import HookimplMarker
|
||||
from pluggy import HookimplOpts
|
||||
from pluggy import HookspecMarker
|
||||
from pluggy import HookspecOpts
|
||||
from pluggy import PluginManager
|
||||
|
||||
from .compat import PathAwareHookProxy
|
||||
from .exceptions import PrintHelp as PrintHelp
|
||||
from .exceptions import UsageError as UsageError
|
||||
from .findpaths import determine_setup
|
||||
@@ -114,14 +117,14 @@ class ConftestImportFailure(Exception):
|
||||
def __init__(
|
||||
self,
|
||||
path: Path,
|
||||
*,
|
||||
cause: Exception,
|
||||
excinfo: Tuple[Type[Exception], Exception, TracebackType],
|
||||
) -> None:
|
||||
super().__init__(path, excinfo)
|
||||
self.path = path
|
||||
self.cause = cause
|
||||
self.excinfo = excinfo
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
|
||||
return f"{self.excinfo[0].__name__}: {self.excinfo[1]} (from {self.path})"
|
||||
|
||||
|
||||
def filter_traceback_for_conftest_import_failure(
|
||||
@@ -152,7 +155,7 @@ def main(
|
||||
try:
|
||||
config = _prepareconfig(args, plugins)
|
||||
except ConftestImportFailure as e:
|
||||
exc_info = ExceptionInfo.from_exception(e.cause)
|
||||
exc_info = ExceptionInfo.from_exc_info(e.excinfo)
|
||||
tw = TerminalWriter(sys.stderr)
|
||||
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
|
||||
exc_info.traceback = exc_info.traceback.filter(
|
||||
@@ -238,8 +241,7 @@ essential_plugins = (
|
||||
"helpconfig", # Provides -p.
|
||||
)
|
||||
|
||||
default_plugins = (
|
||||
*essential_plugins,
|
||||
default_plugins = essential_plugins + (
|
||||
"python",
|
||||
"terminal",
|
||||
"debugging",
|
||||
@@ -251,6 +253,7 @@ default_plugins = (
|
||||
"monkeypatch",
|
||||
"recwarn",
|
||||
"pastebin",
|
||||
"nose",
|
||||
"assertion",
|
||||
"junitxml",
|
||||
"doctest",
|
||||
@@ -545,10 +548,7 @@ class PytestPluginManager(PluginManager):
|
||||
noconftest: bool,
|
||||
rootpath: Path,
|
||||
confcutdir: Optional[Path],
|
||||
invocation_dir: Path,
|
||||
importmode: Union[ImportMode, str],
|
||||
*,
|
||||
consider_namespace_packages: bool,
|
||||
) -> None:
|
||||
"""Load initial conftest files given a preparsed "namespace".
|
||||
|
||||
@@ -557,9 +557,8 @@ class PytestPluginManager(PluginManager):
|
||||
All builtin and 3rd party plugins will have been loaded, however, so
|
||||
common options will not confuse our logic here.
|
||||
"""
|
||||
self._confcutdir = (
|
||||
absolutepath(invocation_dir / confcutdir) if confcutdir else None
|
||||
)
|
||||
current = Path.cwd()
|
||||
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
|
||||
self._noconftest = noconftest
|
||||
self._using_pyargs = pyargs
|
||||
foundanchor = False
|
||||
@@ -569,73 +568,37 @@ class PytestPluginManager(PluginManager):
|
||||
i = path.find("::")
|
||||
if i != -1:
|
||||
path = path[:i]
|
||||
anchor = absolutepath(invocation_dir / path)
|
||||
anchor = absolutepath(current / path)
|
||||
|
||||
# Ensure we do not break if what appears to be an anchor
|
||||
# is in fact a very long option (#10169, #11394).
|
||||
if safe_exists(anchor):
|
||||
self._try_load_conftest(
|
||||
anchor,
|
||||
importmode,
|
||||
rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
self._try_load_conftest(anchor, importmode, rootpath)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
self._try_load_conftest(
|
||||
invocation_dir,
|
||||
importmode,
|
||||
rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
self._try_load_conftest(current, importmode, rootpath)
|
||||
|
||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||
"""Whether to consider the given path to load conftests from."""
|
||||
"""Whether a path is within the confcutdir.
|
||||
|
||||
When false, should not load conftest.
|
||||
"""
|
||||
if self._confcutdir is None:
|
||||
return True
|
||||
# The semantics here are literally:
|
||||
# Do not load a conftest if it is found upwards from confcut dir.
|
||||
# But this is *not* the same as:
|
||||
# Load only conftests from confcutdir or below.
|
||||
# At first glance they might seem the same thing, however we do support use cases where
|
||||
# we want to load conftests that are not found in confcutdir or below, but are found
|
||||
# in completely different directory hierarchies like packages installed
|
||||
# in out-of-source trees.
|
||||
# (see #9767 for a regression where the logic was inverted).
|
||||
return path not in self._confcutdir.parents
|
||||
|
||||
def _try_load_conftest(
|
||||
self,
|
||||
anchor: Path,
|
||||
importmode: Union[str, ImportMode],
|
||||
rootpath: Path,
|
||||
*,
|
||||
consider_namespace_packages: bool,
|
||||
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> None:
|
||||
self._loadconftestmodules(
|
||||
anchor,
|
||||
importmode,
|
||||
rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
self._loadconftestmodules(anchor, importmode, rootpath)
|
||||
# let's also consider test* subdirs
|
||||
if anchor.is_dir():
|
||||
for x in anchor.glob("test*"):
|
||||
if x.is_dir():
|
||||
self._loadconftestmodules(
|
||||
x,
|
||||
importmode,
|
||||
rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
self._loadconftestmodules(x, importmode, rootpath)
|
||||
|
||||
def _loadconftestmodules(
|
||||
self,
|
||||
path: Path,
|
||||
importmode: Union[str, ImportMode],
|
||||
rootpath: Path,
|
||||
*,
|
||||
consider_namespace_packages: bool,
|
||||
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> None:
|
||||
if self._noconftest:
|
||||
return
|
||||
@@ -647,17 +610,15 @@ class PytestPluginManager(PluginManager):
|
||||
if directory in self._dirpath2confmods:
|
||||
return
|
||||
|
||||
# XXX these days we may rather want to use config.rootpath
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
# directories instead of requiring to specify confcutdir.
|
||||
clist = []
|
||||
for parent in reversed((directory, *directory.parents)):
|
||||
if self._is_in_confcutdir(parent):
|
||||
conftestpath = parent / "conftest.py"
|
||||
if conftestpath.is_file():
|
||||
mod = self._importconftest(
|
||||
conftestpath,
|
||||
importmode,
|
||||
rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
||||
clist.append(mod)
|
||||
self._dirpath2confmods[directory] = clist
|
||||
|
||||
@@ -679,39 +640,23 @@ class PytestPluginManager(PluginManager):
|
||||
raise KeyError(name)
|
||||
|
||||
def _importconftest(
|
||||
self,
|
||||
conftestpath: Path,
|
||||
importmode: Union[str, ImportMode],
|
||||
rootpath: Path,
|
||||
*,
|
||||
consider_namespace_packages: bool,
|
||||
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> types.ModuleType:
|
||||
conftestpath_plugin_name = str(conftestpath)
|
||||
existing = self.get_plugin(conftestpath_plugin_name)
|
||||
if existing is not None:
|
||||
return cast(types.ModuleType, existing)
|
||||
|
||||
# conftest.py files there are not in a Python package all have module
|
||||
# name "conftest", and thus conflict with each other. Clear the existing
|
||||
# before loading the new one, otherwise the existing one will be
|
||||
# returned from the module cache.
|
||||
pkgpath = resolve_package_path(conftestpath)
|
||||
if pkgpath is None:
|
||||
try:
|
||||
del sys.modules[conftestpath.stem]
|
||||
except KeyError:
|
||||
pass
|
||||
_ensure_removed_sysmodule(conftestpath.stem)
|
||||
|
||||
try:
|
||||
mod = import_path(
|
||||
conftestpath,
|
||||
mode=importmode,
|
||||
root=rootpath,
|
||||
consider_namespace_packages=consider_namespace_packages,
|
||||
)
|
||||
mod = import_path(conftestpath, mode=importmode, root=rootpath)
|
||||
except Exception as e:
|
||||
assert e.__traceback__ is not None
|
||||
raise ConftestImportFailure(conftestpath, cause=e) from e
|
||||
exc_info = (type(e), e, e.__traceback__)
|
||||
raise ConftestImportFailure(conftestpath, exc_info) from e
|
||||
|
||||
self._check_non_top_pytest_plugins(mod, conftestpath)
|
||||
|
||||
@@ -722,7 +667,7 @@ class PytestPluginManager(PluginManager):
|
||||
if dirpath in path.parents or path == dirpath:
|
||||
if mod in mods:
|
||||
raise AssertionError(
|
||||
f"While trying to load conftest path {conftestpath!s}, "
|
||||
f"While trying to load conftest path {str(conftestpath)}, "
|
||||
f"found that the module {mod} is already loaded with path {mod.__file__}. "
|
||||
"This is not supposed to happen. Please report this issue to pytest."
|
||||
)
|
||||
@@ -799,10 +744,13 @@ class PytestPluginManager(PluginManager):
|
||||
self.set_blocked("pytest_" + name)
|
||||
else:
|
||||
name = arg
|
||||
# Unblock the plugin.
|
||||
self.unblock(name)
|
||||
# Unblock the plugin. None indicates that it has been blocked.
|
||||
# There is no interface with pluggy for this.
|
||||
if self._name2plugin.get(name, -1) is None:
|
||||
del self._name2plugin[name]
|
||||
if not name.startswith("pytest_"):
|
||||
self.unblock("pytest_" + name)
|
||||
if self._name2plugin.get("pytest_" + name, -1) is None:
|
||||
del self._name2plugin["pytest_" + name]
|
||||
self.import_plugin(arg, consider_entry_points=True)
|
||||
|
||||
def consider_conftest(
|
||||
@@ -886,6 +834,13 @@ def _get_plugin_specs_as_list(
|
||||
)
|
||||
|
||||
|
||||
def _ensure_removed_sysmodule(modname: str) -> None:
|
||||
try:
|
||||
del sys.modules[modname]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
class Notset:
|
||||
def __repr__(self):
|
||||
return "<NOTSET>"
|
||||
@@ -1068,7 +1023,7 @@ class Config:
|
||||
self._store = self.stash
|
||||
|
||||
self.trace = self.pluginmanager.trace.root.get("config")
|
||||
self.hook = self.pluginmanager.hook # type: ignore[assignment]
|
||||
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
|
||||
self._inicache: Dict[str, Any] = {}
|
||||
self._override_ini: Sequence[str] = ()
|
||||
self._opt2dest: Dict[str, str] = {}
|
||||
@@ -1222,11 +1177,7 @@ class Config:
|
||||
noconftest=early_config.known_args_namespace.noconftest,
|
||||
rootpath=early_config.rootpath,
|
||||
confcutdir=early_config.known_args_namespace.confcutdir,
|
||||
invocation_dir=early_config.invocation_params.dir,
|
||||
importmode=early_config.known_args_namespace.importmode,
|
||||
consider_namespace_packages=early_config.getini(
|
||||
"consider_namespace_packages"
|
||||
),
|
||||
)
|
||||
|
||||
def _initini(self, args: Sequence[str]) -> None:
|
||||
@@ -1234,8 +1185,8 @@ class Config:
|
||||
args, namespace=copy.copy(self.option)
|
||||
)
|
||||
rootpath, inipath, inicfg = determine_setup(
|
||||
inifile=ns.inifilename,
|
||||
args=ns.file_or_dir + unknown_args,
|
||||
ns.inifilename,
|
||||
ns.file_or_dir + unknown_args,
|
||||
rootdir_cmd_arg=ns.rootdir or None,
|
||||
invocation_dir=self.invocation_params.dir,
|
||||
)
|
||||
@@ -1319,8 +1270,6 @@ class Config:
|
||||
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
|
||||
|
||||
:param warn: Whether can issue warnings.
|
||||
|
||||
:returns: The args and the args source. Guaranteed to be non-empty.
|
||||
"""
|
||||
if args:
|
||||
source = Config.ArgsSource.ARGS
|
||||
@@ -1384,6 +1333,11 @@ class Config:
|
||||
self._validate_plugins()
|
||||
self._warn_about_skipped_plugins()
|
||||
|
||||
if self.known_args_namespace.strict:
|
||||
self.issue_config_time_warning(
|
||||
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
||||
)
|
||||
|
||||
if self.known_args_namespace.confcutdir is None:
|
||||
if self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
@@ -1429,7 +1383,12 @@ class Config:
|
||||
|
||||
if Version(minver) > Version(pytest.__version__):
|
||||
raise pytest.UsageError(
|
||||
f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
|
||||
"%s: 'minversion' requires pytest-%s, actual pytest-%s'"
|
||||
% (
|
||||
self.inipath,
|
||||
minver,
|
||||
pytest.__version__,
|
||||
)
|
||||
)
|
||||
|
||||
def _validate_config_options(self) -> None:
|
||||
@@ -1488,6 +1447,8 @@ class Config:
|
||||
kwargs=dict(pluginmanager=self.pluginmanager)
|
||||
)
|
||||
self._preparse(args, addopts=addopts)
|
||||
# XXX deprecated hook:
|
||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||
self._parser.after_preparse = True # type: ignore
|
||||
try:
|
||||
args = self._parser.parse_setoption(
|
||||
@@ -1616,11 +1577,9 @@ class Config:
|
||||
# in this case, we already have a list ready to use.
|
||||
#
|
||||
if type == "paths":
|
||||
dp = (
|
||||
self.inipath.parent
|
||||
if self.inipath is not None
|
||||
else self.invocation_params.dir
|
||||
)
|
||||
# TODO: This assert is probably not valid in all cases.
|
||||
assert self.inipath is not None
|
||||
dp = self.inipath.parent
|
||||
input_values = shlex.split(value) if isinstance(value, str) else value
|
||||
return [dp / x for x in input_values]
|
||||
elif type == "args":
|
||||
@@ -1707,8 +1666,6 @@ class Config:
|
||||
|
||||
#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
|
||||
VERBOSITY_ASSERTIONS: Final = "assertions"
|
||||
#: Verbosity type for test case execution (see :confval:`verbosity_test_cases`).
|
||||
VERBOSITY_TEST_CASES: Final = "test_cases"
|
||||
_VERBOSITY_INI_DEFAULT: Final = "auto"
|
||||
|
||||
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
|
||||
@@ -1905,13 +1862,13 @@ def parse_warning_filter(
|
||||
try:
|
||||
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
|
||||
except warnings._OptionError as e:
|
||||
raise UsageError(error_template.format(error=str(e))) from None
|
||||
raise UsageError(error_template.format(error=str(e)))
|
||||
try:
|
||||
category: Type[Warning] = _resolve_warning_category(category_)
|
||||
except Exception:
|
||||
exc_info = ExceptionInfo.from_current()
|
||||
exception_text = exc_info.getrepr(style="native")
|
||||
raise UsageError(error_template.format(error=exception_text)) from None
|
||||
raise UsageError(error_template.format(error=exception_text))
|
||||
if message and escape:
|
||||
message = re.escape(message)
|
||||
if module and escape:
|
||||
@@ -1924,7 +1881,7 @@ def parse_warning_filter(
|
||||
except ValueError as e:
|
||||
raise UsageError(
|
||||
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
|
||||
) from None
|
||||
)
|
||||
else:
|
||||
lineno = 0
|
||||
return action, message, category, module, lineno
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import argparse
|
||||
from gettext import gettext
|
||||
import os
|
||||
@@ -16,9 +15,13 @@ from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
import _pytest._io
|
||||
from _pytest.config.exceptions import UsageError
|
||||
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
|
||||
from _pytest.deprecated import ARGUMENT_TYPE_STR
|
||||
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
|
||||
from _pytest.deprecated import check_ispytest
|
||||
|
||||
|
||||
@@ -122,7 +125,7 @@ class Parser:
|
||||
from _pytest._argcomplete import filescompleter
|
||||
|
||||
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
|
||||
groups = [*self._groups, self._anonymous]
|
||||
groups = self._groups + [self._anonymous]
|
||||
for group in groups:
|
||||
if group.options:
|
||||
desc = group.description or group.name
|
||||
@@ -198,16 +201,9 @@ class Parser:
|
||||
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
|
||||
* ``pathlist``: a list of ``py.path``, separated as in a shell
|
||||
|
||||
For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file.
|
||||
In case the execution is happening without an ini-file defined,
|
||||
they will be considered relative to the current working directory (for example with ``--override-ini``).
|
||||
|
||||
.. versionadded:: 7.0
|
||||
The ``paths`` variable type.
|
||||
|
||||
.. versionadded:: 8.1
|
||||
Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of an ini-file.
|
||||
|
||||
Defaults to ``string`` if ``None`` or not passed.
|
||||
:param default:
|
||||
Default value if no ini-file option exists but is queried.
|
||||
@@ -264,15 +260,39 @@ class Argument:
|
||||
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
|
||||
"""
|
||||
|
||||
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
|
||||
|
||||
def __init__(self, *names: str, **attrs: Any) -> None:
|
||||
"""Store params in private vars for use in add_argument."""
|
||||
self._attrs = attrs
|
||||
self._short_opts: List[str] = []
|
||||
self._long_opts: List[str] = []
|
||||
if "%default" in (attrs.get("help") or ""):
|
||||
warnings.warn(ARGUMENT_PERCENT_DEFAULT, stacklevel=3)
|
||||
try:
|
||||
self.type = attrs["type"]
|
||||
typ = attrs["type"]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
# This might raise a keyerror as well, don't want to catch that.
|
||||
if isinstance(typ, str):
|
||||
if typ == "choice":
|
||||
warnings.warn(
|
||||
ARGUMENT_TYPE_STR_CHOICE.format(typ=typ, names=names),
|
||||
stacklevel=4,
|
||||
)
|
||||
# argparse expects a type here take it from
|
||||
# the type of the first element
|
||||
attrs["type"] = type(attrs["choices"][0])
|
||||
else:
|
||||
warnings.warn(
|
||||
ARGUMENT_TYPE_STR.format(typ=typ, names=names), stacklevel=4
|
||||
)
|
||||
attrs["type"] = Argument._typ_map[typ]
|
||||
# Used in test_parseopt -> test_parse_defaultgetter.
|
||||
self.type = attrs["type"]
|
||||
else:
|
||||
self.type = typ
|
||||
try:
|
||||
# Attribute existence is tested in Config._processopt.
|
||||
self.default = attrs["default"]
|
||||
@@ -303,6 +323,11 @@ class Argument:
|
||||
self._attrs[attr] = getattr(self, attr)
|
||||
except AttributeError:
|
||||
pass
|
||||
if self._attrs.get("help"):
|
||||
a = self._attrs["help"]
|
||||
a = a.replace("%default", "%(default)s")
|
||||
# a = a.replace('%prog', '%(prog)s')
|
||||
self._attrs["help"] = a
|
||||
return self._attrs
|
||||
|
||||
def _set_opt_strings(self, opts: Sequence[str]) -> None:
|
||||
|
||||
84
src/_pytest/config/compat.py
Normal file
84
src/_pytest/config/compat.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from pathlib import Path
|
||||
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):
|
||||
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]
|
||||
@@ -86,7 +86,6 @@ def load_config_dict_from_file(
|
||||
|
||||
|
||||
def locate_config(
|
||||
invocation_dir: Path,
|
||||
args: Iterable[Path],
|
||||
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
|
||||
"""Search in the list of arguments for a valid ini-file for pytest,
|
||||
@@ -100,28 +99,20 @@ def locate_config(
|
||||
]
|
||||
args = [x for x in args if not str(x).startswith("-")]
|
||||
if not args:
|
||||
args = [invocation_dir]
|
||||
found_pyproject_toml: Optional[Path] = None
|
||||
args = [Path.cwd()]
|
||||
for arg in args:
|
||||
argpath = absolutepath(arg)
|
||||
for base in (argpath, *argpath.parents):
|
||||
for config_name in config_names:
|
||||
p = base / config_name
|
||||
if p.is_file():
|
||||
if p.name == "pyproject.toml" and found_pyproject_toml is None:
|
||||
found_pyproject_toml = p
|
||||
ini_config = load_config_dict_from_file(p)
|
||||
if ini_config is not None:
|
||||
return base, p, ini_config
|
||||
if found_pyproject_toml is not None:
|
||||
return found_pyproject_toml.parent, found_pyproject_toml, {}
|
||||
return None, None, {}
|
||||
|
||||
|
||||
def get_common_ancestor(
|
||||
invocation_dir: Path,
|
||||
paths: Iterable[Path],
|
||||
) -> Path:
|
||||
def get_common_ancestor(paths: Iterable[Path]) -> Path:
|
||||
common_ancestor: Optional[Path] = None
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
@@ -138,7 +129,7 @@ def get_common_ancestor(
|
||||
if shared is not None:
|
||||
common_ancestor = shared
|
||||
if common_ancestor is None:
|
||||
common_ancestor = invocation_dir
|
||||
common_ancestor = Path.cwd()
|
||||
elif common_ancestor.is_file():
|
||||
common_ancestor = common_ancestor.parent
|
||||
return common_ancestor
|
||||
@@ -170,11 +161,10 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte
|
||||
|
||||
|
||||
def determine_setup(
|
||||
*,
|
||||
inifile: Optional[str],
|
||||
args: Sequence[str],
|
||||
rootdir_cmd_arg: Optional[str],
|
||||
invocation_dir: Path,
|
||||
rootdir_cmd_arg: Optional[str] = None,
|
||||
invocation_dir: Optional[Path] = None,
|
||||
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
|
||||
"""Determine the rootdir, inifile and ini configuration values from the
|
||||
command line arguments.
|
||||
@@ -186,7 +176,8 @@ def determine_setup(
|
||||
:param rootdir_cmd_arg:
|
||||
The `--rootdir` command line argument, if given.
|
||||
:param invocation_dir:
|
||||
The working directory when pytest was invoked.
|
||||
The working directory when pytest was invoked, if known.
|
||||
If not known, the current working directory is used.
|
||||
"""
|
||||
rootdir = None
|
||||
dirs = get_dirs_from_args(args)
|
||||
@@ -197,8 +188,8 @@ def determine_setup(
|
||||
if rootdir_cmd_arg is None:
|
||||
rootdir = inipath_.parent
|
||||
else:
|
||||
ancestor = get_common_ancestor(invocation_dir, dirs)
|
||||
rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor])
|
||||
ancestor = get_common_ancestor(dirs)
|
||||
rootdir, inipath, inicfg = locate_config([ancestor])
|
||||
if rootdir is None and rootdir_cmd_arg is None:
|
||||
for possible_rootdir in (ancestor, *ancestor.parents):
|
||||
if (possible_rootdir / "setup.py").is_file():
|
||||
@@ -206,11 +197,13 @@ def determine_setup(
|
||||
break
|
||||
else:
|
||||
if dirs != [ancestor]:
|
||||
rootdir, inipath, inicfg = locate_config(invocation_dir, dirs)
|
||||
rootdir, inipath, inicfg = locate_config(dirs)
|
||||
if rootdir is None:
|
||||
rootdir = get_common_ancestor(
|
||||
invocation_dir, [invocation_dir, ancestor]
|
||||
)
|
||||
if invocation_dir is not None:
|
||||
cwd = invocation_dir
|
||||
else:
|
||||
cwd = Path.cwd()
|
||||
rootdir = get_common_ancestor([cwd, ancestor])
|
||||
if is_fs_root(rootdir):
|
||||
rootdir = ancestor
|
||||
if rootdir_cmd_arg:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Interactive debugging with PDB, the Python Debugger."""
|
||||
|
||||
import argparse
|
||||
import functools
|
||||
import sys
|
||||
@@ -378,8 +378,7 @@ def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.Traceb
|
||||
elif isinstance(excinfo.value, ConftestImportFailure):
|
||||
# A config.ConftestImportFailure is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
assert excinfo.value.cause.__traceback__ is not None
|
||||
return excinfo.value.cause.__traceback__
|
||||
return excinfo.value.excinfo[2]
|
||||
else:
|
||||
assert excinfo._excinfo is not None
|
||||
return excinfo._excinfo[2]
|
||||
|
||||
@@ -12,6 +12,7 @@ in case of warnings which need to format their messages.
|
||||
from warnings import warn
|
||||
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import PytestRemovedIn8Warning
|
||||
from _pytest.warning_types import PytestRemovedIn9Warning
|
||||
from _pytest.warning_types import UnformattedWarning
|
||||
|
||||
@@ -24,6 +25,21 @@ DEPRECATED_EXTERNAL_PLUGINS = {
|
||||
"pytest_faulthandler",
|
||||
}
|
||||
|
||||
NOSE_SUPPORT = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"Support for nose tests is deprecated and will be removed in a future release.\n"
|
||||
"{nodeid} is using nose method: `{method}` ({stage})\n"
|
||||
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
|
||||
)
|
||||
|
||||
NOSE_SUPPORT_METHOD = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"Support for nose tests is deprecated and will be removed in a future release.\n"
|
||||
"{nodeid} is using nose-specific method: `{method}(self)`\n"
|
||||
"To remove this warning, rename it to `{method}_method(self)`\n"
|
||||
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
|
||||
)
|
||||
|
||||
|
||||
# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
|
||||
# * If you're in the future: "could have been".
|
||||
@@ -32,10 +48,74 @@ YIELD_FIXTURE = PytestDeprecationWarning(
|
||||
"Use @pytest.fixture instead; they are the same."
|
||||
)
|
||||
|
||||
WARNING_CMDLINE_PREPARSE_HOOK = PytestRemovedIn8Warning(
|
||||
"The pytest_cmdline_preparse hook is deprecated and will be removed in a future release. \n"
|
||||
"Please use pytest_load_initial_conftests hook instead."
|
||||
)
|
||||
|
||||
FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestRemovedIn8Warning(
|
||||
"The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; "
|
||||
"use self.session.gethookproxy() and self.session.isinitpath() instead. "
|
||||
)
|
||||
|
||||
STRICT_OPTION = PytestRemovedIn8Warning(
|
||||
"The --strict option is deprecated, use --strict-markers instead."
|
||||
)
|
||||
|
||||
# This deprecation is never really meant to be removed.
|
||||
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
|
||||
|
||||
ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning(
|
||||
'pytest now uses argparse. "%default" should be changed to "%(default)s"',
|
||||
)
|
||||
|
||||
ARGUMENT_TYPE_STR_CHOICE = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"`type` argument to addoption() is the string {typ!r}."
|
||||
" For choices this is optional and can be omitted, "
|
||||
" but when supplied should be a type (for example `str` or `int`)."
|
||||
" (options: {names})",
|
||||
)
|
||||
|
||||
ARGUMENT_TYPE_STR = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"`type` argument to addoption() is the string {typ!r}, "
|
||||
" but when supplied should be a type (for example `str` or `int`)."
|
||||
" (options: {names})",
|
||||
)
|
||||
|
||||
|
||||
HOOK_LEGACY_PATH_ARG = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"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(
|
||||
PytestRemovedIn8Warning,
|
||||
"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",
|
||||
)
|
||||
|
||||
WARNS_NONE_ARG = PytestRemovedIn8Warning(
|
||||
"Passing None has been deprecated.\n"
|
||||
"See https://docs.pytest.org/en/latest/how-to/capture-warnings.html"
|
||||
"#additional-use-cases-of-warnings-in-tests"
|
||||
" for alternatives in common use cases."
|
||||
)
|
||||
|
||||
KEYWORD_MSG_ARG = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"pytest.{func}(msg=...) is now deprecated, use pytest.{func}(reason=...) instead",
|
||||
)
|
||||
|
||||
INSTANCE_COLLECTOR = PytestRemovedIn8Warning(
|
||||
"The pytest.Instance collector type is deprecated and is no longer used. "
|
||||
"See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector",
|
||||
)
|
||||
HOOK_LEGACY_MARKING = UnformattedWarning(
|
||||
PytestDeprecationWarning,
|
||||
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Discover and run doctests in modules and test files."""
|
||||
|
||||
import bdb
|
||||
from contextlib import contextmanager
|
||||
import functools
|
||||
@@ -47,7 +47,6 @@ from _pytest.warning_types import PytestWarning
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import doctest
|
||||
from typing import Self
|
||||
|
||||
DOCTEST_REPORT_CHOICE_NONE = "none"
|
||||
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
|
||||
@@ -134,9 +133,11 @@ def pytest_collect_file(
|
||||
if config.option.doctestmodules and not any(
|
||||
(_is_setup_py(file_path), _is_main_py(file_path))
|
||||
):
|
||||
return DoctestModule.from_parent(parent, path=file_path)
|
||||
mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)
|
||||
return mod
|
||||
elif _is_doctest(config, file_path, parent):
|
||||
return DoctestTextfile.from_parent(parent, path=file_path)
|
||||
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
|
||||
return txt
|
||||
return None
|
||||
|
||||
|
||||
@@ -271,14 +272,14 @@ class DoctestItem(Item):
|
||||
self._initrequest()
|
||||
|
||||
@classmethod
|
||||
def from_parent( # type: ignore[override]
|
||||
def from_parent( # type: ignore
|
||||
cls,
|
||||
parent: "Union[DoctestTextfile, DoctestModule]",
|
||||
*,
|
||||
name: str,
|
||||
runner: "doctest.DocTestRunner",
|
||||
dtest: "doctest.DocTest",
|
||||
) -> "Self":
|
||||
):
|
||||
# incompatible signature due to imposed limits on subclass
|
||||
"""The public named constructor."""
|
||||
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
|
||||
@@ -558,17 +559,24 @@ class DoctestModule(Module):
|
||||
else: # pragma: no cover
|
||||
pass
|
||||
|
||||
try:
|
||||
module = self.obj
|
||||
except Collector.CollectError:
|
||||
if self.config.getvalue("doctest_ignore_import_errors"):
|
||||
skip("unable to import module %r" % self.path)
|
||||
else:
|
||||
raise
|
||||
if self.path.name == "conftest.py":
|
||||
module = self.config.pluginmanager._importconftest(
|
||||
self.path,
|
||||
self.config.getoption("importmode"),
|
||||
rootpath=self.config.rootpath,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
module = self.obj
|
||||
except Collector.CollectError:
|
||||
if self.config.getvalue("doctest_ignore_import_errors"):
|
||||
skip("unable to import module %r" % self.path)
|
||||
else:
|
||||
raise
|
||||
|
||||
# While doctests currently don't support fixtures directly, we still
|
||||
# need to pick up autouse fixtures.
|
||||
self.session._fixturemanager.parsefactories(self)
|
||||
# While doctests currently don't support fixtures directly, we still
|
||||
# need to pick up autouse fixtures.
|
||||
self.session._fixturemanager.parsefactories(self)
|
||||
|
||||
# Uses internal doctest module parsing mechanism.
|
||||
finder = MockAwareDocTestFinder()
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import abc
|
||||
from collections import defaultdict
|
||||
from collections import deque
|
||||
from contextlib import suppress
|
||||
import dataclasses
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from typing import AbstractSet
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
@@ -68,10 +67,6 @@ from _pytest.scope import HIGH_SCOPES
|
||||
from _pytest.scope import Scope
|
||||
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Deque
|
||||
|
||||
@@ -121,16 +116,22 @@ def pytest_sessionstart(session: "Session") -> None:
|
||||
def get_scope_package(
|
||||
node: nodes.Item,
|
||||
fixturedef: "FixtureDef[object]",
|
||||
) -> Optional[nodes.Node]:
|
||||
) -> Optional[Union[nodes.Item, nodes.Collector]]:
|
||||
from _pytest.python import Package
|
||||
|
||||
for parent in node.iter_parents():
|
||||
if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid:
|
||||
return parent
|
||||
return node.session
|
||||
current: Optional[Union[nodes.Item, nodes.Collector]] = node
|
||||
while current and (
|
||||
not isinstance(current, Package) or current.nodeid != fixturedef.baseid
|
||||
):
|
||||
current = current.parent # type: ignore[assignment]
|
||||
if current is None:
|
||||
return node.session
|
||||
return current
|
||||
|
||||
|
||||
def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]:
|
||||
def get_scope_node(
|
||||
node: nodes.Node, scope: Scope
|
||||
) -> Optional[Union[nodes.Item, nodes.Collector]]:
|
||||
import _pytest.python
|
||||
|
||||
if scope is Scope.Function:
|
||||
@@ -173,28 +174,33 @@ def get_parametrized_fixture_keys(
|
||||
the specified scope."""
|
||||
assert scope is not Scope.Function
|
||||
try:
|
||||
callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
|
||||
callspec = item.callspec # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
return
|
||||
for argname in callspec.indices:
|
||||
if callspec._arg2scope[argname] != scope:
|
||||
continue
|
||||
pass
|
||||
else:
|
||||
cs: CallSpec2 = callspec
|
||||
# cs.indices is random order of argnames. Need to
|
||||
# sort this so that different calls to
|
||||
# get_parametrized_fixture_keys will be deterministic.
|
||||
for argname in sorted(cs.indices):
|
||||
if cs._arg2scope[argname] != scope:
|
||||
continue
|
||||
|
||||
item_cls = None
|
||||
if scope is Scope.Session:
|
||||
scoped_item_path = None
|
||||
elif scope is Scope.Package:
|
||||
scoped_item_path = item.path
|
||||
elif scope is Scope.Module:
|
||||
scoped_item_path = item.path
|
||||
elif scope is Scope.Class:
|
||||
scoped_item_path = item.path
|
||||
item_cls = item.cls # type: ignore[attr-defined]
|
||||
else:
|
||||
assert_never(scope)
|
||||
item_cls = None
|
||||
if scope is Scope.Session:
|
||||
scoped_item_path = None
|
||||
elif scope is Scope.Package:
|
||||
scoped_item_path = item.path
|
||||
elif scope is Scope.Module:
|
||||
scoped_item_path = item.path
|
||||
elif scope is Scope.Class:
|
||||
scoped_item_path = item.path
|
||||
item_cls = item.cls # type: ignore[attr-defined]
|
||||
else:
|
||||
assert_never(scope)
|
||||
|
||||
param_index = callspec.indices[argname]
|
||||
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
||||
param_index = cs.indices[argname]
|
||||
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
|
||||
|
||||
|
||||
# Algorithm for sorting on a per-parametrized resource setup basis.
|
||||
@@ -348,6 +354,7 @@ class FixtureRequest(abc.ABC):
|
||||
pyfuncitem: "Function",
|
||||
fixturename: Optional[str],
|
||||
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
|
||||
arg2index: Dict[str, int],
|
||||
fixture_defs: Dict[str, "FixtureDef[Any]"],
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
@@ -361,6 +368,16 @@ class FixtureRequest(abc.ABC):
|
||||
# collection. Dynamically requested fixtures (using
|
||||
# `request.getfixturevalue("foo")`) are added dynamically.
|
||||
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
|
||||
# to.
|
||||
self._fixture_defs: Final = fixture_defs
|
||||
@@ -407,7 +424,9 @@ class FixtureRequest(abc.ABC):
|
||||
# We arrive here because of a dynamic call to
|
||||
# getfixturevalue(argname) usage which was naturally
|
||||
# not known at parsing/collection time.
|
||||
fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
|
||||
assert self._pyfuncitem.parent is not None
|
||||
parentid = self._pyfuncitem.parent.nodeid
|
||||
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
|
||||
if fixturedefs is not None:
|
||||
self._arg2fixturedefs[argname] = fixturedefs
|
||||
# No fixtures defined with this name.
|
||||
@@ -416,24 +435,11 @@ class FixtureRequest(abc.ABC):
|
||||
# The are no fixtures with this name applicable for the function.
|
||||
if not fixturedefs:
|
||||
raise FixtureLookupError(argname, self)
|
||||
|
||||
# A fixture may override another fixture with the same name, e.g. a
|
||||
# fixture in a module can override a fixture in a conftest, a fixture in
|
||||
# a class can override a fixture in the module, and so on.
|
||||
# An overriding fixture can request its own name (possibly indirectly);
|
||||
# in this case it gets the value of the fixture it overrides, one level
|
||||
# up.
|
||||
# Check how many `argname`s deep we are, and take the next one.
|
||||
# `fixturedefs` is sorted from furthest to closest, so use negative
|
||||
# indexing to go in reverse.
|
||||
index = -1
|
||||
for request in self._iter_chain():
|
||||
if request.fixturename == argname:
|
||||
index -= 1
|
||||
# If already consumed all of the available levels, fail.
|
||||
index = self._arg2index.get(argname, 0) - 1
|
||||
# The fixture requested its own name, but no remaining to override.
|
||||
if -index > len(fixturedefs):
|
||||
raise FixtureLookupError(argname, self)
|
||||
|
||||
self._arg2index[argname] = index
|
||||
return fixturedefs[index]
|
||||
|
||||
@property
|
||||
@@ -519,7 +525,7 @@ class FixtureRequest(abc.ABC):
|
||||
:param msg:
|
||||
An optional custom error message.
|
||||
"""
|
||||
raise FixtureLookupError(None, self, msg)
|
||||
raise self._fixturemanager.FixtureLookupError(None, self, msg)
|
||||
|
||||
def getfixturevalue(self, argname: str) -> Any:
|
||||
"""Dynamically run a named fixture function.
|
||||
@@ -545,16 +551,6 @@ class FixtureRequest(abc.ABC):
|
||||
)
|
||||
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(
|
||||
self, argname: str
|
||||
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
|
||||
@@ -572,7 +568,11 @@ class FixtureRequest(abc.ABC):
|
||||
return fixturedef
|
||||
|
||||
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
|
||||
values = [request._fixturedef for request in self._iter_chain()]
|
||||
current = self
|
||||
values: List[FixtureDef[Any]] = []
|
||||
while isinstance(current, SubRequest):
|
||||
values.append(current._fixturedef) # type: ignore[has-type]
|
||||
current = current._parent_request
|
||||
values.reverse()
|
||||
return values
|
||||
|
||||
@@ -588,6 +588,7 @@ class FixtureRequest(abc.ABC):
|
||||
# (latter managed by fixturedef)
|
||||
argname = fixturedef.argname
|
||||
funcitem = self._pyfuncitem
|
||||
scope = fixturedef._scope
|
||||
try:
|
||||
callspec = funcitem.callspec
|
||||
except AttributeError:
|
||||
@@ -595,13 +596,13 @@ class FixtureRequest(abc.ABC):
|
||||
if callspec is not None and argname in callspec.params:
|
||||
param = callspec.params[argname]
|
||||
param_index = callspec.indices[argname]
|
||||
# The parametrize invocation scope overrides the fixture's scope.
|
||||
scope = callspec._arg2scope[argname]
|
||||
# If a parametrize invocation set a scope it will override
|
||||
# the static scope defined with the fixture function.
|
||||
with suppress(KeyError):
|
||||
scope = callspec._arg2scope[argname]
|
||||
else:
|
||||
param = NOTSET
|
||||
param_index = 0
|
||||
scope = fixturedef._scope
|
||||
|
||||
has_params = fixturedef.params is not None
|
||||
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
|
||||
if has_params and fixtures_not_supported:
|
||||
@@ -665,6 +666,7 @@ class TopRequest(FixtureRequest):
|
||||
fixturename=None,
|
||||
pyfuncitem=pyfuncitem,
|
||||
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
|
||||
arg2index={},
|
||||
fixture_defs={},
|
||||
_ispytest=_ispytest,
|
||||
)
|
||||
@@ -710,11 +712,12 @@ class SubRequest(FixtureRequest):
|
||||
fixturename=fixturedef.argname,
|
||||
fixture_defs=request._fixture_defs,
|
||||
arg2fixturedefs=request._arg2fixturedefs,
|
||||
arg2index=request._arg2index,
|
||||
_ispytest=_ispytest,
|
||||
)
|
||||
self._parent_request: Final[FixtureRequest] = request
|
||||
self._scope_field: Final = scope
|
||||
self._fixturedef: Final[FixtureDef[object]] = fixturedef
|
||||
self._fixturedef: Final = fixturedef
|
||||
if param is not NOTSET:
|
||||
self.param = param
|
||||
self.param_index: Final = param_index
|
||||
@@ -731,7 +734,7 @@ class SubRequest(FixtureRequest):
|
||||
scope = self._scope
|
||||
if scope is Scope.Function:
|
||||
# This might also be a non-function Item despite its attribute name.
|
||||
node: Optional[nodes.Node] = self._pyfuncitem
|
||||
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
||||
elif scope is Scope.Package:
|
||||
node = get_scope_package(self._pyfuncitem, self._fixturedef)
|
||||
else:
|
||||
@@ -837,8 +840,9 @@ class FixtureLookupError(LookupError):
|
||||
available = set()
|
||||
parent = self.request._pyfuncitem.parent
|
||||
assert parent is not None
|
||||
parentid = parent.nodeid
|
||||
for name, fixturedefs in fm._arg2fixturedefs.items():
|
||||
faclist = list(fm._matchfactories(fixturedefs, parent))
|
||||
faclist = list(fm._matchfactories(fixturedefs, parentid))
|
||||
if faclist:
|
||||
available.add(name)
|
||||
if self.argname in available:
|
||||
@@ -959,7 +963,7 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: Config,
|
||||
fixturemanager: "FixtureManager",
|
||||
baseid: Optional[str],
|
||||
argname: str,
|
||||
func: "_FixtureFunc[FixtureValue]",
|
||||
@@ -973,11 +977,13 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._fixturemanager = fixturemanager
|
||||
# The "base" node ID for the fixture.
|
||||
#
|
||||
# This is a node ID prefix. A fixture is only available to a node (e.g.
|
||||
# a `Function` item) if the fixture's baseid is a nodeid of a parent of
|
||||
# node.
|
||||
# a `Function` item) if the fixture's baseid is a parent of the node's
|
||||
# nodeid (see the `iterparentnodeids` function for what constitutes a
|
||||
# "parent" and a "prefix" in this context).
|
||||
#
|
||||
# For a fixture found in a Collector's object (e.g. a `Module`s module,
|
||||
# a `Class`'s class), the baseid is the Collector's nodeid.
|
||||
@@ -998,7 +1004,7 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
if scope is None:
|
||||
scope = Scope.Function
|
||||
elif callable(scope):
|
||||
scope = _eval_scope_callable(scope, argname, config)
|
||||
scope = _eval_scope_callable(scope, argname, fixturemanager.config)
|
||||
if isinstance(scope, str):
|
||||
scope = Scope.from_user(
|
||||
scope, descr=f"Fixture '{func.__name__}'", where=baseid
|
||||
@@ -1028,25 +1034,27 @@ class FixtureDef(Generic[FixtureValue]):
|
||||
self._finalizers.append(finalizer)
|
||||
|
||||
def finish(self, request: SubRequest) -> None:
|
||||
exceptions: List[BaseException] = []
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
try:
|
||||
fin()
|
||||
except BaseException as e:
|
||||
exceptions.append(e)
|
||||
node = request.node
|
||||
node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||
# Even if finalization fails, we invalidate the cached fixture
|
||||
# value and remove all finalizers because they may be bound methods
|
||||
# which will keep instances alive.
|
||||
self.cached_result = None
|
||||
self._finalizers.clear()
|
||||
if len(exceptions) == 1:
|
||||
raise exceptions[0]
|
||||
elif len(exceptions) > 1:
|
||||
msg = f'errors while tearing down fixture "{self.argname}" of {node}'
|
||||
raise BaseExceptionGroup(msg, exceptions[::-1])
|
||||
exc = None
|
||||
try:
|
||||
while self._finalizers:
|
||||
try:
|
||||
func = self._finalizers.pop()
|
||||
func()
|
||||
except BaseException as e:
|
||||
# XXX Only first exception will be seen by user,
|
||||
# ideally all should be reported.
|
||||
if exc is None:
|
||||
exc = e
|
||||
if exc:
|
||||
raise exc
|
||||
finally:
|
||||
ihook = request.node.ihook
|
||||
ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||
# Even if finalization fails, we invalidate the cached fixture
|
||||
# value and remove all finalizers because they may be bound methods
|
||||
# which will keep instances alive.
|
||||
self.cached_result = None
|
||||
self._finalizers.clear()
|
||||
|
||||
def execute(self, request: SubRequest) -> FixtureValue:
|
||||
# Get required arguments and register our own finish()
|
||||
@@ -1225,7 +1233,7 @@ def fixture(
|
||||
|
||||
|
||||
@overload
|
||||
def fixture(
|
||||
def fixture( # noqa: F811
|
||||
fixture_function: None = ...,
|
||||
*,
|
||||
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
|
||||
@@ -1239,7 +1247,7 @@ def fixture(
|
||||
...
|
||||
|
||||
|
||||
def fixture(
|
||||
def fixture( # noqa: F811
|
||||
fixture_function: Optional[FixtureFunction] = None,
|
||||
*,
|
||||
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
|
||||
@@ -1422,6 +1430,9 @@ class FixtureManager:
|
||||
by a lookup of their FuncFixtureInfo.
|
||||
"""
|
||||
|
||||
FixtureLookupError = FixtureLookupError
|
||||
FixtureLookupErrorRepr = FixtureLookupErrorRepr
|
||||
|
||||
def __init__(self, session: "Session") -> None:
|
||||
self.session = session
|
||||
self.config: Config = session.config
|
||||
@@ -1460,7 +1471,7 @@ class FixtureManager:
|
||||
else:
|
||||
argnames = ()
|
||||
usefixturesnames = self._getusefixturesnames(node)
|
||||
autousenames = self._getautousenames(node)
|
||||
autousenames = self._getautousenames(node.nodeid)
|
||||
initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
|
||||
|
||||
direct_parametrize_args = _get_direct_parametrize_args(node)
|
||||
@@ -1497,10 +1508,10 @@ class FixtureManager:
|
||||
|
||||
self.parsefactories(plugin, nodeid)
|
||||
|
||||
def _getautousenames(self, node: nodes.Node) -> Iterator[str]:
|
||||
"""Return the names of autouse fixtures applicable to node."""
|
||||
for parentnode in node.listchain():
|
||||
basenames = self._nodeid_autousenames.get(parentnode.nodeid)
|
||||
def _getautousenames(self, nodeid: str) -> Iterator[str]:
|
||||
"""Return the names of autouse fixtures applicable to nodeid."""
|
||||
for parentnodeid in nodes.iterparentnodeids(nodeid):
|
||||
basenames = self._nodeid_autousenames.get(parentnodeid)
|
||||
if basenames:
|
||||
yield from basenames
|
||||
|
||||
@@ -1522,6 +1533,7 @@ class FixtureManager:
|
||||
# to re-discover fixturedefs again for each fixturename
|
||||
# (discovering matching fixtures for a given name/node is expensive).
|
||||
|
||||
parentid = parentnode.nodeid
|
||||
fixturenames_closure = list(initialnames)
|
||||
|
||||
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
|
||||
@@ -1533,7 +1545,7 @@ class FixtureManager:
|
||||
continue
|
||||
if argname in arg2fixturedefs:
|
||||
continue
|
||||
fixturedefs = self.getfixturedefs(argname, parentnode)
|
||||
fixturedefs = self.getfixturedefs(argname, parentid)
|
||||
if fixturedefs:
|
||||
arg2fixturedefs[argname] = fixturedefs
|
||||
for arg in fixturedefs[-1].argnames:
|
||||
@@ -1600,69 +1612,6 @@ class FixtureManager:
|
||||
# Separate parametrized setups.
|
||||
items[:] = reorder_items(items)
|
||||
|
||||
def _register_fixture(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
func: "_FixtureFunc[object]",
|
||||
nodeid: Optional[str],
|
||||
scope: Union[
|
||||
Scope, _ScopeName, Callable[[str, Config], _ScopeName], None
|
||||
] = "function",
|
||||
params: Optional[Sequence[object]] = None,
|
||||
ids: Optional[
|
||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||
] = None,
|
||||
autouse: bool = False,
|
||||
unittest: bool = False,
|
||||
) -> None:
|
||||
"""Register a fixture
|
||||
|
||||
:param name:
|
||||
The fixture's name.
|
||||
:param func:
|
||||
The fixture's implementation function.
|
||||
:param nodeid:
|
||||
The visibility of the fixture. The fixture will be available to the
|
||||
node with this nodeid and its children in the collection tree.
|
||||
None means that the fixture is visible to the entire collection tree,
|
||||
e.g. a fixture defined for general use in a plugin.
|
||||
:param scope:
|
||||
The fixture's scope.
|
||||
:param params:
|
||||
The fixture's parametrization params.
|
||||
:param ids:
|
||||
The fixture's IDs.
|
||||
:param autouse:
|
||||
Whether this is an autouse fixture.
|
||||
:param unittest:
|
||||
Set this if this is a unittest fixture.
|
||||
"""
|
||||
fixture_def = FixtureDef(
|
||||
config=self.config,
|
||||
baseid=nodeid,
|
||||
argname=name,
|
||||
func=func,
|
||||
scope=scope,
|
||||
params=params,
|
||||
unittest=unittest,
|
||||
ids=ids,
|
||||
_ispytest=True,
|
||||
)
|
||||
|
||||
faclist = self._arg2fixturedefs.setdefault(name, [])
|
||||
if fixture_def.has_location:
|
||||
faclist.append(fixture_def)
|
||||
else:
|
||||
# fixturedefs with no location are at the front
|
||||
# so this inserts the current fixturedef after the
|
||||
# existing fixturedefs from external plugins but
|
||||
# before the fixturedefs provided in conftests.
|
||||
i = len([f for f in faclist if not f.has_location])
|
||||
faclist.insert(i, fixture_def)
|
||||
if autouse:
|
||||
self._nodeid_autousenames.setdefault(nodeid or "", []).append(name)
|
||||
|
||||
@overload
|
||||
def parsefactories(
|
||||
self,
|
||||
@@ -1673,7 +1622,7 @@ class FixtureManager:
|
||||
raise NotImplementedError()
|
||||
|
||||
@overload
|
||||
def parsefactories(
|
||||
def parsefactories( # noqa: F811
|
||||
self,
|
||||
node_or_obj: object,
|
||||
nodeid: Optional[str],
|
||||
@@ -1682,7 +1631,7 @@ class FixtureManager:
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def parsefactories(
|
||||
def parsefactories( # noqa: F811
|
||||
self,
|
||||
node_or_obj: Union[nodes.Node, object],
|
||||
nodeid: Union[str, NotSetType, None] = NOTSET,
|
||||
@@ -1714,7 +1663,13 @@ class FixtureManager:
|
||||
return
|
||||
|
||||
self._holderobjseen.add(holderobj)
|
||||
autousenames = []
|
||||
for name in dir(holderobj):
|
||||
# ugly workaround for one of the fspath deprecated property of node
|
||||
# todo: safely generalize
|
||||
if isinstance(holderobj, nodes.Node) and name == "fspath":
|
||||
continue
|
||||
|
||||
# The attribute can be an arbitrary descriptor, so the attribute
|
||||
# access below can raise. safe_getatt() ignores such exceptions.
|
||||
obj = safe_getattr(holderobj, name, None)
|
||||
@@ -1731,21 +1686,38 @@ class FixtureManager:
|
||||
# to issue a warning if called directly, so here we unwrap it in
|
||||
# order to not emit the warning when pytest itself calls the
|
||||
# fixture function.
|
||||
func = get_real_method(obj, holderobj)
|
||||
obj = get_real_method(obj, holderobj)
|
||||
|
||||
self._register_fixture(
|
||||
name=name,
|
||||
nodeid=nodeid,
|
||||
func=func,
|
||||
fixture_def = FixtureDef(
|
||||
fixturemanager=self,
|
||||
baseid=nodeid,
|
||||
argname=name,
|
||||
func=obj,
|
||||
scope=marker.scope,
|
||||
params=marker.params,
|
||||
unittest=unittest,
|
||||
ids=marker.ids,
|
||||
autouse=marker.autouse,
|
||||
_ispytest=True,
|
||||
)
|
||||
|
||||
faclist = self._arg2fixturedefs.setdefault(name, [])
|
||||
if fixture_def.has_location:
|
||||
faclist.append(fixture_def)
|
||||
else:
|
||||
# fixturedefs with no location are at the front
|
||||
# so this inserts the current fixturedef after the
|
||||
# existing fixturedefs from external plugins but
|
||||
# before the fixturedefs provided in conftests.
|
||||
i = len([f for f in faclist if not f.has_location])
|
||||
faclist.insert(i, fixture_def)
|
||||
if marker.autouse:
|
||||
autousenames.append(name)
|
||||
|
||||
if autousenames:
|
||||
self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames)
|
||||
|
||||
def getfixturedefs(
|
||||
self, argname: str, node: nodes.Node
|
||||
self, argname: str, nodeid: str
|
||||
) -> Optional[Sequence[FixtureDef[Any]]]:
|
||||
"""Get FixtureDefs for a fixture name which are applicable
|
||||
to a given node.
|
||||
@@ -1756,18 +1728,18 @@ class FixtureManager:
|
||||
an empty result is returned).
|
||||
|
||||
:param argname: Name of the fixture to search for.
|
||||
:param node: The requesting Node.
|
||||
:param nodeid: Full node id of the requesting test.
|
||||
"""
|
||||
try:
|
||||
fixturedefs = self._arg2fixturedefs[argname]
|
||||
except KeyError:
|
||||
return None
|
||||
return tuple(self._matchfactories(fixturedefs, node))
|
||||
return tuple(self._matchfactories(fixturedefs, nodeid))
|
||||
|
||||
def _matchfactories(
|
||||
self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
|
||||
self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
|
||||
) -> Iterator[FixtureDef[Any]]:
|
||||
parentnodeids = {n.nodeid for n in node.iter_parents()}
|
||||
parentnodeids = set(nodes.iterparentnodeids(nodeid))
|
||||
for fixturedef in fixturedefs:
|
||||
if fixturedef.baseid in parentnodeids:
|
||||
yield fixturedef
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Version info, help messages, tracing configuration."""
|
||||
|
||||
from argparse import Action
|
||||
import os
|
||||
import sys
|
||||
@@ -109,11 +109,11 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
|
||||
path = config.option.debug
|
||||
debugfile = open(path, "w", encoding="utf-8")
|
||||
debugfile.write(
|
||||
"versions pytest-{}, "
|
||||
"python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format(
|
||||
"versions pytest-%s, "
|
||||
"python-%s\ncwd=%s\nargs=%s\n\n"
|
||||
% (
|
||||
pytest.__version__,
|
||||
".".join(map(str, sys.version_info)),
|
||||
config.invocation_params.dir,
|
||||
os.getcwd(),
|
||||
config.invocation_params.args,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Hook specifications for pytest plugins which are invoked by pytest itself
|
||||
and by builtin plugins."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
@@ -14,6 +14,8 @@ from typing import Union
|
||||
|
||||
from pluggy import HookspecMarker
|
||||
|
||||
from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import pdb
|
||||
@@ -22,6 +24,7 @@ if TYPE_CHECKING:
|
||||
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import ExceptionRepr
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.config import _PluggyPlugin
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
@@ -56,16 +59,10 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
|
||||
"""Called at plugin registration time to allow adding new hooks via a call to
|
||||
:func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`.
|
||||
|
||||
:param pluginmanager: The pytest plugin manager.
|
||||
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with hook wrappers.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
If a conftest plugin implements this hook, it will be called immediately
|
||||
when the conftest is registered.
|
||||
"""
|
||||
|
||||
|
||||
@@ -83,14 +80,6 @@ def pytest_plugin_registered(
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with hook wrappers.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
If a conftest plugin implements this hook, it will be called immediately
|
||||
when the conftest is registered, once for each plugin registered thus far
|
||||
(including itself!), and for all plugins thereafter when they are
|
||||
registered.
|
||||
"""
|
||||
|
||||
|
||||
@@ -99,13 +88,19 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||
"""Register argparse-style options and ini-style config values,
|
||||
called once at the beginning of a test run.
|
||||
|
||||
:param parser:
|
||||
.. note::
|
||||
|
||||
This function should be implemented only in plugins or ``conftest.py``
|
||||
files situated at the tests root directory due to how pytest
|
||||
:ref:`discovers plugins during startup <pluginorder>`.
|
||||
|
||||
:param pytest.Parser parser:
|
||||
To add command line options, call
|
||||
:py:func:`parser.addoption(...) <pytest.Parser.addoption>`.
|
||||
To add ini-file values call :py:func:`parser.addini(...)
|
||||
<pytest.Parser.addini>`.
|
||||
|
||||
:param pluginmanager:
|
||||
:param pytest.PytestPluginManager pluginmanager:
|
||||
The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
|
||||
or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks
|
||||
to change how command line options are added.
|
||||
@@ -124,14 +119,6 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with hook wrappers.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
If a conftest plugin implements this hook, it will be called immediately
|
||||
when the conftest is registered.
|
||||
|
||||
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||
"""
|
||||
|
||||
|
||||
@@ -139,17 +126,16 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
|
||||
def pytest_configure(config: "Config") -> None:
|
||||
"""Allow plugins and conftest files to perform initial configuration.
|
||||
|
||||
This hook is called for every plugin and initial conftest file
|
||||
after command line options have been parsed.
|
||||
|
||||
After that, the hook is called for other conftest files as they are
|
||||
imported.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with hook wrappers.
|
||||
|
||||
:param config: The pytest config object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is called for every :ref:`initial conftest <pluginorder>` file
|
||||
after command line options have been parsed. After that, the hook is called
|
||||
for other conftest files as they are registered.
|
||||
:param pytest.Config config: The pytest config object.
|
||||
"""
|
||||
|
||||
|
||||
@@ -168,54 +154,55 @@ def pytest_cmdline_parse(
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
.. note::
|
||||
This hook is only called for plugin classes passed to the
|
||||
This hook will only be called for plugin classes passed to the
|
||||
``plugins`` arg when using `pytest.main`_ to perform an in-process
|
||||
test run.
|
||||
|
||||
:param pluginmanager: The pytest plugin manager.
|
||||
:param args: List of arguments passed on the command line.
|
||||
:returns: A pytest config object.
|
||||
"""
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is not called for conftest files.
|
||||
@hookspec(warn_on_impl=WARNING_CMDLINE_PREPARSE_HOOK)
|
||||
def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None:
|
||||
"""(**Deprecated**) modify command line arguments before option parsing.
|
||||
|
||||
This hook is considered deprecated and will be removed in a future pytest version. Consider
|
||||
using :hook:`pytest_load_initial_conftests` instead.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param config: The pytest config object.
|
||||
:param args: Arguments passed on the command line.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
|
||||
"""Called for performing the main command line action. The default
|
||||
implementation will invoke the configure hooks and runtest_mainloop.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
:param config: The pytest config object.
|
||||
:returns: The exit code.
|
||||
"""
|
||||
|
||||
|
||||
def pytest_load_initial_conftests(
|
||||
early_config: "Config", parser: "Parser", args: List[str]
|
||||
) -> None:
|
||||
"""Called to implement the loading of :ref:`initial conftest files
|
||||
<pluginorder>` ahead of command line option parsing.
|
||||
"""Called to implement the loading of initial conftest files ahead
|
||||
of command line option parsing.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param early_config: The pytest config object.
|
||||
:param args: Arguments passed on the command line.
|
||||
:param parser: To add command line options.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is not called for conftest files.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
|
||||
"""Called for performing the main command line action.
|
||||
|
||||
The default implementation will invoke the configure hooks and
|
||||
:hook:`pytest_runtestloop`.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
:param config: The pytest config object.
|
||||
:returns: The exit code.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||
"""
|
||||
|
||||
|
||||
@@ -258,11 +245,6 @@ def pytest_collection(session: "Session") -> Optional[object]:
|
||||
counter (and returns `None`).
|
||||
|
||||
:param session: The pytest session object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||
"""
|
||||
|
||||
|
||||
@@ -275,11 +257,6 @@ def pytest_collection_modifyitems(
|
||||
:param session: The pytest session object.
|
||||
:param config: The pytest config object.
|
||||
:param items: List of item objects.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -287,16 +264,13 @@ def pytest_collection_finish(session: "Session") -> None:
|
||||
"""Called after collection has been performed and modified.
|
||||
|
||||
:param session: The pytest session object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
This hook is consulted for all files and directories prior to calling
|
||||
@@ -310,18 +284,8 @@ def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[b
|
||||
|
||||
.. versionchanged:: 7.0.0
|
||||
The ``collection_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``path`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``path`` parameter has been removed.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collection path, only
|
||||
conftest files in parent directories of the collection path are consulted
|
||||
(if the path is a directory, its own conftest file is *not* consulted - a
|
||||
directory cannot ignore itself!).
|
||||
equivalent of the ``path`` parameter. The ``path`` parameter
|
||||
has been deprecated.
|
||||
"""
|
||||
|
||||
|
||||
@@ -343,18 +307,12 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
|
||||
|
||||
See :ref:`custom directory collectors` for a simple example of use of this
|
||||
hook.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collection path, only
|
||||
conftest files in parent directories of the collection path are consulted
|
||||
(if the path is a directory, its own conftest file is *not* consulted - a
|
||||
directory cannot collect itself!).
|
||||
"""
|
||||
|
||||
|
||||
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.
|
||||
|
||||
For best results, the returned collector should be a subclass of
|
||||
@@ -367,16 +325,8 @@ def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Colle
|
||||
|
||||
.. versionchanged:: 7.0.0
|
||||
The ``file_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``path`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``path`` parameter was removed.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given file path, only
|
||||
conftest files in parent directories of the file path are consulted.
|
||||
equivalent of the ``path`` parameter. The ``path`` parameter
|
||||
has been deprecated.
|
||||
"""
|
||||
|
||||
|
||||
@@ -388,13 +338,6 @@ def pytest_collectstart(collector: "Collector") -> None:
|
||||
|
||||
:param collector:
|
||||
The collector.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collector, only
|
||||
conftest files in the collector's directory and its parent directories are
|
||||
consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -403,12 +346,6 @@ def pytest_itemcollected(item: "Item") -> None:
|
||||
|
||||
:param item:
|
||||
The item.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -417,13 +354,6 @@ def pytest_collectreport(report: "CollectReport") -> None:
|
||||
|
||||
:param report:
|
||||
The collect report.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collector, only
|
||||
conftest files in the collector's directory and its parent directories are
|
||||
consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -434,11 +364,6 @@ def pytest_deselected(items: Sequence["Item"]) -> None:
|
||||
|
||||
:param items:
|
||||
The items.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -451,13 +376,6 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
|
||||
|
||||
:param collector:
|
||||
The collector.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collector, only
|
||||
conftest files in the collector's directory and its parent directories are
|
||||
consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -467,7 +385,9 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
This hook will be called for each matching test module path.
|
||||
@@ -483,15 +403,7 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]
|
||||
The ``module_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``path`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``path`` parameter has been removed in favor of ``module_path``.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given parent collector,
|
||||
only conftest files in the collector's directory and its parent directories
|
||||
are consulted.
|
||||
The ``path`` parameter has been deprecated in favor of ``fspath``.
|
||||
"""
|
||||
|
||||
|
||||
@@ -511,13 +423,6 @@ def pytest_pycollect_makeitem(
|
||||
The object.
|
||||
:returns:
|
||||
The created items/collectors.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given collector, only
|
||||
conftest files in the collector's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -529,13 +434,6 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
|
||||
|
||||
:param pyfuncitem:
|
||||
The function item.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only
|
||||
conftest files in the item's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -544,13 +442,6 @@ def pytest_generate_tests(metafunc: "Metafunc") -> None:
|
||||
|
||||
:param metafunc:
|
||||
The :class:`~pytest.Metafunc` helper for the test function.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given function definition,
|
||||
only conftest files in the functions's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -568,12 +459,7 @@ def pytest_make_parametrize_id(
|
||||
|
||||
:param config: The pytest config object.
|
||||
:param val: The parametrized value.
|
||||
:param argname: The automatic parameter name produced by pytest.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
:param str argname: The automatic parameter name produced by pytest.
|
||||
"""
|
||||
|
||||
|
||||
@@ -600,11 +486,6 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
The return value is not used, but only stops further processing.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -643,11 +524,6 @@ def pytest_runtest_protocol(
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
The return value is not used, but only stops further processing.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -662,12 +538,6 @@ def pytest_runtest_logstart(
|
||||
:param location: A tuple of ``(filename, lineno, testname)``
|
||||
where ``filename`` is a file path relative to ``config.rootpath``
|
||||
and ``lineno`` is 0-based.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -682,12 +552,6 @@ def pytest_runtest_logfinish(
|
||||
:param location: A tuple of ``(filename, lineno, testname)``
|
||||
where ``filename`` is a file path relative to ``config.rootpath``
|
||||
and ``lineno`` is 0-based.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -701,12 +565,6 @@ def pytest_runtest_setup(item: "Item") -> None:
|
||||
|
||||
:param item:
|
||||
The item.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -717,12 +575,6 @@ def pytest_runtest_call(item: "Item") -> None:
|
||||
|
||||
:param item:
|
||||
The item.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -741,12 +593,6 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
|
||||
scheduled). This argument is used to perform exact teardowns, i.e.
|
||||
calling just enough finalizers so that nextitem only needs to call
|
||||
setup functions.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -763,12 +609,6 @@ def pytest_runtest_makereport(
|
||||
:param call: The :class:`~pytest.CallInfo` for the phase.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -777,12 +617,6 @@ def pytest_runtest_logreport(report: "TestReport") -> None:
|
||||
of the setup, call and teardown runtest phases of an item.
|
||||
|
||||
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -796,12 +630,6 @@ def pytest_report_to_serializable(
|
||||
|
||||
:param config: The pytest config object.
|
||||
:param report: The report.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. The exact details may depend
|
||||
on the plugin which calls the hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -814,12 +642,6 @@ def pytest_report_from_serializable(
|
||||
:hook:`pytest_report_to_serializable`.
|
||||
|
||||
:param config: The pytest config object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. The exact details may depend
|
||||
on the plugin which calls the hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -847,13 +669,6 @@ def pytest_fixture_setup(
|
||||
If the fixture function returns None, other implementations of
|
||||
this hook function will continue to be called, according to the
|
||||
behavior of the :ref:`firstresult` option.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given fixture, only
|
||||
conftest files in the fixture scope's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -868,13 +683,6 @@ def pytest_fixture_post_finalizer(
|
||||
The fixture definition object.
|
||||
:param request:
|
||||
The fixture request object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given fixture, only
|
||||
conftest files in the fixture scope's directory and its parent directories
|
||||
are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -888,11 +696,6 @@ def pytest_sessionstart(session: "Session") -> None:
|
||||
and entering the run test loop.
|
||||
|
||||
:param session: The pytest session object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||
"""
|
||||
|
||||
|
||||
@@ -904,11 +707,6 @@ def pytest_sessionfinish(
|
||||
|
||||
:param session: The pytest session object.
|
||||
:param exitstatus: The status which pytest will return to the system.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -916,11 +714,6 @@ def pytest_unconfigure(config: "Config") -> None:
|
||||
"""Called before test process is exited.
|
||||
|
||||
:param config: The pytest config object.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -943,12 +736,6 @@ def pytest_assertrepr_compare(
|
||||
:param op: The operator, e.g. `"=="`, `"!="`, `"not in"`.
|
||||
:param left: The left operand.
|
||||
:param right: The right operand.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -977,12 +764,6 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
|
||||
:param lineno: Line number of the assert statement.
|
||||
:param orig: String with the original assertion.
|
||||
:param expl: String with the assert explanation.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in the item's directory and its parent directories are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -992,7 +773,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
|
||||
|
||||
|
||||
def pytest_report_header( # type:ignore[empty-body]
|
||||
config: "Config", start_path: Path
|
||||
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
|
||||
) -> Union[str, List[str]]:
|
||||
"""Return a string or list of strings to be displayed as header info for terminal reporting.
|
||||
|
||||
@@ -1007,23 +788,23 @@ def pytest_report_header( # type:ignore[empty-body]
|
||||
If you want to have your line(s) displayed first, use
|
||||
:ref:`trylast=True <plugin-hookorder>`.
|
||||
|
||||
.. note::
|
||||
|
||||
This function should be implemented only in plugins or ``conftest.py``
|
||||
files situated at the tests root directory due to how pytest
|
||||
:ref:`discovers plugins during startup <pluginorder>`.
|
||||
|
||||
.. versionchanged:: 7.0.0
|
||||
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``startdir`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``startdir`` parameter has been removed.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
This hook is only called for :ref:`initial conftests <pluginorder>`.
|
||||
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
|
||||
has been deprecated.
|
||||
"""
|
||||
|
||||
|
||||
def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||
config: "Config",
|
||||
start_path: Path,
|
||||
startdir: "LEGACY_PATH",
|
||||
items: Sequence["Item"],
|
||||
) -> Union[str, List[str]]:
|
||||
"""Return a string or list of strings to be displayed after collection
|
||||
@@ -1047,15 +828,8 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||
|
||||
.. versionchanged:: 7.0.0
|
||||
The ``start_path`` parameter was added as a :class:`pathlib.Path`
|
||||
equivalent of the ``startdir`` parameter.
|
||||
|
||||
.. versionchanged:: 8.0.0
|
||||
The ``startdir`` parameter has been removed.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
|
||||
has been deprecated.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1084,11 +858,6 @@ def pytest_report_teststatus( # type:ignore[empty-body]
|
||||
:returns: The test status.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1105,11 +874,6 @@ def pytest_terminal_summary(
|
||||
|
||||
.. versionadded:: 4.2
|
||||
The ``config`` parameter.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1134,8 +898,7 @@ def pytest_warning_recorded(
|
||||
* ``"runtest"``: during test execution.
|
||||
|
||||
:param nodeid:
|
||||
Full id of the item. Empty string for warnings that are not specific to
|
||||
a particular node.
|
||||
Full id of the item.
|
||||
|
||||
:param location:
|
||||
When available, holds information about the execution context of the captured
|
||||
@@ -1143,13 +906,6 @@ def pytest_warning_recorded(
|
||||
when the execution context is at the module level.
|
||||
|
||||
.. versionadded:: 6.0
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. If the warning is specific to a
|
||||
particular node, only conftest files in parent directories of the node are
|
||||
consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1173,12 +929,6 @@ def pytest_markeval_namespace( # type:ignore[empty-body]
|
||||
|
||||
:param config: The pytest config object.
|
||||
:returns: A dictionary of additional globals to add.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given item, only conftest
|
||||
files in parent directories of the item are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1198,11 +948,6 @@ def pytest_internalerror(
|
||||
|
||||
:param excrepr: The exception repr object.
|
||||
:param excinfo: The exception info.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1212,11 +957,6 @@ def pytest_keyboard_interrupt(
|
||||
"""Called for keyboard interrupt.
|
||||
|
||||
:param excinfo: The exception info.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1243,12 +983,6 @@ def pytest_exception_interact(
|
||||
The call information. Contains the exception.
|
||||
:param report:
|
||||
The collection or test report.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest file can implement this hook. For a given node, only conftest
|
||||
files in parent directories of the node are consulted.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1260,11 +994,6 @@ def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
|
||||
|
||||
:param config: The pytest config object.
|
||||
:param pdb: The Pdb instance.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
|
||||
@@ -1276,9 +1005,4 @@ def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
|
||||
|
||||
:param config: The pytest config object.
|
||||
:param pdb: The Pdb instance.
|
||||
|
||||
Use in conftest plugins
|
||||
=======================
|
||||
|
||||
Any conftest plugin can implement this hook.
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Report test results in JUnit-XML format, for use with Jenkins and build
|
||||
integration servers.
|
||||
|
||||
@@ -7,6 +6,7 @@ Based on initial code from Ross Lawley.
|
||||
Output conforms to
|
||||
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import os
|
||||
@@ -375,7 +375,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
|
||||
|
||||
xml = request.config.stash.get(xml_key, None)
|
||||
if xml is not None:
|
||||
record_func = xml.add_global_property
|
||||
record_func = xml.add_global_property # noqa
|
||||
return record_func
|
||||
|
||||
|
||||
@@ -624,7 +624,7 @@ class LogXML:
|
||||
def update_testcase_duration(self, report: TestReport) -> None:
|
||||
"""Accumulate total duration for nodeid from given report and update
|
||||
the Junit.testcase with the new total if already created."""
|
||||
if self.report_duration in {"total", report.when}:
|
||||
if self.report_duration == "total" or report.when == self.report_duration:
|
||||
reporter = self.node_reporter(report)
|
||||
reporter.duration += getattr(report, "duration", 0.0)
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Add backward compatibility support for the legacy py path type."""
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
@@ -14,9 +13,9 @@ from typing import Union
|
||||
|
||||
from iniconfig import SectionWrapper
|
||||
|
||||
import py
|
||||
|
||||
from _pytest.cacheprovider import Cache
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.compat import legacy_path
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config import PytestPluginManager
|
||||
@@ -39,20 +38,6 @@ if TYPE_CHECKING:
|
||||
import pexpect
|
||||
|
||||
|
||||
#: constant to prepare valuing pylib path replacements/lazy proxies later on
|
||||
# intended for removal in pytest 8.0 or 9.0
|
||||
|
||||
# fmt: off
|
||||
# intentional space to create a fake difference for the verification
|
||||
LEGACY_PATH = py.path. local
|
||||
# fmt: on
|
||||
|
||||
|
||||
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
|
||||
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
|
||||
return LEGACY_PATH(path)
|
||||
|
||||
|
||||
@final
|
||||
class Testdir:
|
||||
"""
|
||||
@@ -329,8 +314,8 @@ class LegacyTmpdirPlugin:
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See
|
||||
:ref:`temporary directory location and retention`.
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a `legacy_path`_ object.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Access and control log capturing."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from contextlib import nullcontext
|
||||
from datetime import datetime
|
||||
@@ -298,13 +298,6 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
default=None,
|
||||
help="Path to a file when logging will be written to",
|
||||
)
|
||||
add_option_ini(
|
||||
"--log-file-mode",
|
||||
dest="log_file_mode",
|
||||
default="w",
|
||||
choices=["w", "a"],
|
||||
help="Log file open mode",
|
||||
)
|
||||
add_option_ini(
|
||||
"--log-file-level",
|
||||
dest="log_file_level",
|
||||
@@ -676,10 +669,7 @@ class LoggingPlugin:
|
||||
if not os.path.isdir(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
self.log_file_mode = get_option_ini(config, "log_file_mode") or "w"
|
||||
self.log_file_handler = _FileHandler(
|
||||
log_file, mode=self.log_file_mode, encoding="UTF-8"
|
||||
)
|
||||
self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
|
||||
log_file_format = get_option_ini(config, "log_file_format", "log_format")
|
||||
log_file_date_format = get_option_ini(
|
||||
config, "log_file_date_format", "log_date_format"
|
||||
@@ -756,7 +746,7 @@ class LoggingPlugin:
|
||||
fpath.parent.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# https://github.com/python/mypy/issues/11193
|
||||
stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment]
|
||||
stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment]
|
||||
old_stream = self.log_file_handler.setStream(stream)
|
||||
if old_stream:
|
||||
old_stream.close()
|
||||
|
||||
@@ -5,7 +5,6 @@ import dataclasses
|
||||
import fnmatch
|
||||
import functools
|
||||
import importlib
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
@@ -22,7 +21,6 @@ from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
@@ -37,6 +35,7 @@ from _pytest.config import hookimpl
|
||||
from _pytest.config import PytestPluginManager
|
||||
from _pytest.config import UsageError
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.config.compat import PathAwareHookProxy
|
||||
from _pytest.fixtures import FixtureManager
|
||||
from _pytest.outcomes import exit
|
||||
from _pytest.pathlib import absolutepath
|
||||
@@ -51,10 +50,6 @@ from _pytest.runner import SetupState
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
parser.addini(
|
||||
"norecursedirs",
|
||||
@@ -222,12 +217,6 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
help="Prepend/append to sys.path when importing test modules and conftest "
|
||||
"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.addoption(
|
||||
@@ -389,9 +378,6 @@ def _in_venv(path: Path) -> bool:
|
||||
|
||||
|
||||
def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
|
||||
if collection_path.name == "__pycache__":
|
||||
return True
|
||||
|
||||
ignore_paths = config._getconftest_pathlist(
|
||||
"collect_ignore", path=collection_path.parent
|
||||
)
|
||||
@@ -503,16 +489,16 @@ class Dir(nodes.Directory):
|
||||
@classmethod
|
||||
def from_parent( # type: ignore[override]
|
||||
cls,
|
||||
parent: nodes.Collector,
|
||||
parent: nodes.Collector, # type: ignore[override]
|
||||
*,
|
||||
path: Path,
|
||||
) -> "Self":
|
||||
) -> "Dir":
|
||||
"""The public constructor.
|
||||
|
||||
:param parent: The parent collector of this Dir.
|
||||
:param path: The directory's path.
|
||||
"""
|
||||
return super().from_parent(parent=parent, path=path)
|
||||
return super().from_parent(parent=parent, path=path) # type: ignore[no-any-return]
|
||||
|
||||
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
||||
config = self.config
|
||||
@@ -521,6 +507,8 @@ class Dir(nodes.Directory):
|
||||
ihook = self.ihook
|
||||
for direntry in scandir(self.path):
|
||||
if direntry.is_dir():
|
||||
if direntry.name == "__pycache__":
|
||||
continue
|
||||
path = Path(direntry.path)
|
||||
if not self.session.isinitpath(path, with_parents=True):
|
||||
if ihook.pytest_ignore_collect(collection_path=path, config=config):
|
||||
@@ -557,6 +545,7 @@ class Session(nodes.Collector):
|
||||
super().__init__(
|
||||
name="",
|
||||
path=config.rootpath,
|
||||
fspath=None,
|
||||
parent=None,
|
||||
config=config,
|
||||
session=self,
|
||||
@@ -570,7 +559,7 @@ class Session(nodes.Collector):
|
||||
self._initialpaths: FrozenSet[Path] = frozenset()
|
||||
self._initialpaths_with_parents: FrozenSet[Path] = frozenset()
|
||||
self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = []
|
||||
self._initial_parts: List[CollectionArgument] = []
|
||||
self._initial_parts: List[Tuple[Path, List[str]]] = []
|
||||
self._collection_cache: Dict[nodes.Collector, CollectReport] = {}
|
||||
self.items: List[nodes.Item] = []
|
||||
|
||||
@@ -694,7 +683,7 @@ class Session(nodes.Collector):
|
||||
proxy: pluggy.HookRelay
|
||||
if remove_mods:
|
||||
# One or more conftests are not in use at this path.
|
||||
proxy = FSHookProxy(pm, remove_mods) # type: ignore[arg-type,assignment]
|
||||
proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment]
|
||||
else:
|
||||
# All plugins are active for this fspath.
|
||||
proxy = self.config.hook
|
||||
@@ -738,12 +727,12 @@ class Session(nodes.Collector):
|
||||
...
|
||||
|
||||
@overload
|
||||
def perform_collect(
|
||||
def perform_collect( # noqa: F811
|
||||
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
|
||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
||||
...
|
||||
|
||||
def perform_collect(
|
||||
def perform_collect( # noqa: F811
|
||||
self, args: Optional[Sequence[str]] = None, genitems: bool = True
|
||||
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
|
||||
"""Perform the collection phase for this session.
|
||||
@@ -776,15 +765,15 @@ class Session(nodes.Collector):
|
||||
initialpaths: List[Path] = []
|
||||
initialpaths_with_parents: List[Path] = []
|
||||
for arg in args:
|
||||
collection_argument = resolve_collection_argument(
|
||||
fspath, parts = resolve_collection_argument(
|
||||
self.config.invocation_params.dir,
|
||||
arg,
|
||||
as_pypath=self.config.option.pyargs,
|
||||
)
|
||||
self._initial_parts.append(collection_argument)
|
||||
initialpaths.append(collection_argument.path)
|
||||
initialpaths_with_parents.append(collection_argument.path)
|
||||
initialpaths_with_parents.extend(collection_argument.path.parents)
|
||||
self._initial_parts.append((fspath, parts))
|
||||
initialpaths.append(fspath)
|
||||
initialpaths_with_parents.append(fspath)
|
||||
initialpaths_with_parents.extend(fspath.parents)
|
||||
self._initialpaths = frozenset(initialpaths)
|
||||
self._initialpaths_with_parents = frozenset(initialpaths_with_parents)
|
||||
|
||||
@@ -846,35 +835,21 @@ class Session(nodes.Collector):
|
||||
|
||||
pm = self.config.pluginmanager
|
||||
|
||||
for collection_argument in self._initial_parts:
|
||||
self.trace("processing argument", collection_argument)
|
||||
for argpath, names in self._initial_parts:
|
||||
self.trace("processing argument", (argpath, names))
|
||||
self.trace.root.indent += 1
|
||||
|
||||
argpath = collection_argument.path
|
||||
names = collection_argument.parts
|
||||
module_name = collection_argument.module_name
|
||||
|
||||
# resolve_collection_argument() ensures this.
|
||||
if argpath.is_dir():
|
||||
assert not names, f"invalid arg {(argpath, names)!r}"
|
||||
|
||||
paths = [argpath]
|
||||
# Add relevant parents of the path, from the root, e.g.
|
||||
# Match the argpath from the root, e.g.
|
||||
# /a/b/c.py -> [/, /a, /a/b, /a/b/c.py]
|
||||
if module_name is None:
|
||||
# Paths outside of the confcutdir should not be considered.
|
||||
for path in argpath.parents:
|
||||
if not pm._is_in_confcutdir(path):
|
||||
break
|
||||
paths.insert(0, path)
|
||||
else:
|
||||
# For --pyargs arguments, only consider paths matching the module
|
||||
# name. Paths beyond the package hierarchy are not included.
|
||||
module_name_parts = module_name.split(".")
|
||||
for i, path in enumerate(argpath.parents, 2):
|
||||
if i > len(module_name_parts) or path.stem != module_name_parts[-i]:
|
||||
break
|
||||
paths.insert(0, path)
|
||||
paths = [*reversed(argpath.parents), argpath]
|
||||
# Paths outside of the confcutdir should not be considered, unless
|
||||
# it's the argpath itself.
|
||||
while len(paths) > 1 and not pm._is_in_confcutdir(paths[0]):
|
||||
paths = paths[1:]
|
||||
|
||||
# Start going over the parts from the root, collecting each level
|
||||
# and discarding all nodes which don't match the level's part.
|
||||
@@ -882,7 +857,7 @@ class Session(nodes.Collector):
|
||||
notfound_collectors = []
|
||||
work: List[
|
||||
Tuple[Union[nodes.Collector, nodes.Item], List[Union[Path, str]]]
|
||||
] = [(self, [*paths, *names])]
|
||||
] = [(self, paths + names)]
|
||||
while work:
|
||||
matchnode, matchparts = work.pop()
|
||||
|
||||
@@ -930,14 +905,7 @@ class Session(nodes.Collector):
|
||||
if sys.platform == "win32" and not is_match:
|
||||
# In case the file paths do not match, fallback to samefile() to
|
||||
# account for short-paths on Windows (#11895).
|
||||
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])
|
||||
)
|
||||
|
||||
is_match = os.path.samefile(node.path, matchparts[0])
|
||||
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
|
||||
else:
|
||||
# TODO: Remove parametrized workaround once collection structure contains
|
||||
@@ -981,36 +949,26 @@ class Session(nodes.Collector):
|
||||
node.ihook.pytest_collectreport(report=rep)
|
||||
|
||||
|
||||
def search_pypath(module_name: str) -> Optional[str]:
|
||||
"""Search sys.path for the given a dotted module name, and return its file
|
||||
system path if found."""
|
||||
def search_pypath(module_name: str) -> str:
|
||||
"""Search sys.path for the given a dotted module name, and return its file system path."""
|
||||
try:
|
||||
spec = importlib.util.find_spec(module_name)
|
||||
# AttributeError: looks like package module, but actually filename
|
||||
# ImportError: module does not exist
|
||||
# ValueError: not a module name
|
||||
except (AttributeError, ImportError, ValueError):
|
||||
return None
|
||||
return module_name
|
||||
if spec is None or spec.origin is None or spec.origin == "namespace":
|
||||
return None
|
||||
return module_name
|
||||
elif spec.submodule_search_locations:
|
||||
return os.path.dirname(spec.origin)
|
||||
else:
|
||||
return spec.origin
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CollectionArgument:
|
||||
"""A resolved collection argument."""
|
||||
|
||||
path: Path
|
||||
parts: Sequence[str]
|
||||
module_name: Optional[str]
|
||||
|
||||
|
||||
def resolve_collection_argument(
|
||||
invocation_path: Path, arg: str, *, as_pypath: bool = False
|
||||
) -> CollectionArgument:
|
||||
) -> Tuple[Path, List[str]]:
|
||||
"""Parse path arguments optionally containing selection parts and return (fspath, names).
|
||||
|
||||
Command-line arguments can point to files and/or directories, and optionally contain
|
||||
@@ -1018,13 +976,9 @@ def resolve_collection_argument(
|
||||
|
||||
"pkg/tests/test_foo.py::TestClass::test_foo"
|
||||
|
||||
This function ensures the path exists, and returns a resolved `CollectionArgument`:
|
||||
This function ensures the path exists, and returns a tuple:
|
||||
|
||||
CollectionArgument(
|
||||
path=Path("/full/path/to/pkg/tests/test_foo.py"),
|
||||
parts=["TestClass", "test_foo"],
|
||||
module_name=None,
|
||||
)
|
||||
(Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
|
||||
|
||||
When as_pypath is True, expects that the command-line argument actually contains
|
||||
module paths instead of file-system paths:
|
||||
@@ -1032,13 +986,7 @@ def resolve_collection_argument(
|
||||
"pkg.tests.test_foo::TestClass::test_foo"
|
||||
|
||||
In which case we search sys.path for a matching module, and then return the *path* to the
|
||||
found module, which may look like this:
|
||||
|
||||
CollectionArgument(
|
||||
path=Path("/home/u/myvenv/lib/site-packages/pkg/tests/test_foo.py"),
|
||||
parts=["TestClass", "test_foo"],
|
||||
module_name="pkg.tests.test_foo",
|
||||
)
|
||||
found module.
|
||||
|
||||
If the path doesn't exist, raise UsageError.
|
||||
If the path is a directory and selection parts are present, raise UsageError.
|
||||
@@ -1047,12 +995,8 @@ def resolve_collection_argument(
|
||||
strpath, *parts = base.split("::")
|
||||
if parts:
|
||||
parts[-1] = f"{parts[-1]}{squacket}{rest}"
|
||||
module_name = None
|
||||
if as_pypath:
|
||||
pyarg_strpath = search_pypath(strpath)
|
||||
if pyarg_strpath is not None:
|
||||
module_name = strpath
|
||||
strpath = pyarg_strpath
|
||||
strpath = search_pypath(strpath)
|
||||
fspath = invocation_path / strpath
|
||||
fspath = absolutepath(fspath)
|
||||
if not safe_exists(fspath):
|
||||
@@ -1069,8 +1013,4 @@ def resolve_collection_argument(
|
||||
else "directory argument cannot contain :: selection parts: {arg}"
|
||||
)
|
||||
raise UsageError(msg.format(arg=arg))
|
||||
return CollectionArgument(
|
||||
path=fspath,
|
||||
parts=parts,
|
||||
module_name=module_name,
|
||||
)
|
||||
return fspath, parts
|
||||
|
||||
@@ -107,7 +107,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
help="show markers (builtin, plugin and per-project ones).",
|
||||
)
|
||||
|
||||
parser.addini("markers", "Register new markers for test functions", "linelist")
|
||||
parser.addini("markers", "Markers for test functions", "linelist")
|
||||
parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets")
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import collections.abc
|
||||
import dataclasses
|
||||
import inspect
|
||||
@@ -355,7 +354,7 @@ class MarkDecorator:
|
||||
func = args[0]
|
||||
is_class = inspect.isclass(func)
|
||||
if len(args) == 1 and (istestfunc(func) or is_class):
|
||||
store_mark(func, self.mark, stacklevel=3)
|
||||
store_mark(func, self.mark)
|
||||
return func
|
||||
return self.with_args(*args, **kwargs)
|
||||
|
||||
@@ -406,11 +405,11 @@ def normalize_mark_list(
|
||||
for mark in mark_list:
|
||||
mark_obj = getattr(mark, "mark", mark)
|
||||
if not isinstance(mark_obj, Mark):
|
||||
raise TypeError(f"got {mark_obj!r} instead of Mark")
|
||||
raise TypeError(f"got {repr(mark_obj)} instead of Mark")
|
||||
yield mark_obj
|
||||
|
||||
|
||||
def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
|
||||
def store_mark(obj, mark: Mark) -> None:
|
||||
"""Store a Mark on an object.
|
||||
|
||||
This is used to implement the Mark declarations/decorators correctly.
|
||||
@@ -420,7 +419,7 @@ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
|
||||
from ..fixtures import getfixturemarker
|
||||
|
||||
if getfixturemarker(obj) is not None:
|
||||
warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel)
|
||||
warnings.warn(MARKED_FIXTURE, stacklevel=2)
|
||||
|
||||
# Always reassign name to avoid updating pytestmark in a reference that
|
||||
# was only borrowed.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Monkeypatching and mocking functionality."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
import re
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import abc
|
||||
from functools import cached_property
|
||||
from inspect import signature
|
||||
import os
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
@@ -11,7 +11,6 @@ from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import MutableMapping
|
||||
from typing import NoReturn
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Set
|
||||
@@ -29,8 +28,12 @@ from _pytest._code import getfslineno
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._code.code import Traceback
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ConftestImportFailure
|
||||
from _pytest.config.compat import _check_path
|
||||
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
|
||||
from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
|
||||
from _pytest.mark.structures import Mark
|
||||
from _pytest.mark.structures import MarkDecorator
|
||||
from _pytest.mark.structures import NodeKeywords
|
||||
@@ -42,8 +45,6 @@ from _pytest.warning_types import PytestWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
# Imported here due to circular import.
|
||||
from _pytest._code.code import _TracebackStyle
|
||||
from _pytest.main import Session
|
||||
@@ -54,7 +55,72 @@ SEP = "/"
|
||||
tracebackcutdir = Path(_pytest.__file__).parent
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
def iterparentnodeids(nodeid: str) -> Iterator[str]:
|
||||
"""Return the parent node IDs of a given node ID, inclusive.
|
||||
|
||||
For the node ID
|
||||
|
||||
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
|
||||
|
||||
the result would be
|
||||
|
||||
""
|
||||
"testing"
|
||||
"testing/code"
|
||||
"testing/code/test_excinfo.py"
|
||||
"testing/code/test_excinfo.py::TestFormattedExcinfo"
|
||||
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
|
||||
|
||||
Note that / components are only considered until the first ::.
|
||||
"""
|
||||
pos = 0
|
||||
first_colons: Optional[int] = nodeid.find("::")
|
||||
if first_colons == -1:
|
||||
first_colons = None
|
||||
# The root Session node - always present.
|
||||
yield ""
|
||||
# Eagerly consume SEP parts until first colons.
|
||||
while True:
|
||||
at = nodeid.find(SEP, pos, first_colons)
|
||||
if at == -1:
|
||||
break
|
||||
if at > 0:
|
||||
yield nodeid[:at]
|
||||
pos = at + len(SEP)
|
||||
# Eagerly consume :: parts.
|
||||
while True:
|
||||
at = nodeid.find("::", pos)
|
||||
if at == -1:
|
||||
break
|
||||
if at > 0:
|
||||
yield nodeid[:at]
|
||||
pos = at + len("::")
|
||||
# The node ID itself.
|
||||
if nodeid:
|
||||
yield nodeid
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
@@ -73,33 +139,33 @@ class NodeMeta(abc.ABCMeta):
|
||||
progress on detangling the :class:`Node` classes.
|
||||
"""
|
||||
|
||||
def __call__(cls, *k, **kw) -> NoReturn:
|
||||
def __call__(self, *k, **kw):
|
||||
msg = (
|
||||
"Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
|
||||
"See "
|
||||
"https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
|
||||
" for more details."
|
||||
).format(name=f"{cls.__module__}.{cls.__name__}")
|
||||
).format(name=f"{self.__module__}.{self.__name__}")
|
||||
fail(msg, pytrace=False)
|
||||
|
||||
def _create(cls: Type[_T], *k, **kw) -> _T:
|
||||
def _create(self, *k, **kw):
|
||||
try:
|
||||
return super().__call__(*k, **kw) # type: ignore[no-any-return,misc]
|
||||
return super().__call__(*k, **kw)
|
||||
except TypeError:
|
||||
sig = signature(getattr(cls, "__init__"))
|
||||
sig = signature(getattr(self, "__init__"))
|
||||
known_kw = {k: v for k, v in kw.items() if k in sig.parameters}
|
||||
from .warning_types import PytestDeprecationWarning
|
||||
|
||||
warnings.warn(
|
||||
PytestDeprecationWarning(
|
||||
f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
|
||||
f"{self} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
|
||||
"See https://docs.pytest.org/en/stable/deprecations.html"
|
||||
"#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
|
||||
"for more details."
|
||||
)
|
||||
)
|
||||
|
||||
return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc]
|
||||
return super().__call__(*k, **known_kw)
|
||||
|
||||
|
||||
class Node(abc.ABC, metaclass=NodeMeta):
|
||||
@@ -110,6 +176,13 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
leaf nodes.
|
||||
"""
|
||||
|
||||
# Implemented in the legacypath plugin.
|
||||
#: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
|
||||
#: for methods not migrated to ``pathlib.Path`` yet, such as
|
||||
#: :meth:`Item.reportinfo <pytest.Item.reportinfo>`. Will be deprecated in
|
||||
fspath: LEGACY_PATH
|
||||
#: a future release, prefer using :attr:`path` instead.
|
||||
|
||||
# Use __slots__ to make attribute access faster.
|
||||
# Note that __dict__ is still available.
|
||||
__slots__ = (
|
||||
@@ -129,6 +202,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
parent: "Optional[Node]" = None,
|
||||
config: Optional[Config] = None,
|
||||
session: "Optional[Session]" = None,
|
||||
fspath: Optional[LEGACY_PATH] = None,
|
||||
path: Optional[Path] = None,
|
||||
nodeid: Optional[str] = None,
|
||||
) -> None:
|
||||
@@ -154,11 +228,10 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
raise TypeError("session or parent must be provided")
|
||||
self.session = parent.session
|
||||
|
||||
if path is None:
|
||||
if path is None and fspath is None:
|
||||
path = getattr(parent, "path", None)
|
||||
assert path is not None
|
||||
#: Filesystem path where this node was collected from (can be None).
|
||||
self.path = path
|
||||
self.path: pathlib.Path = _imply_path(type(self), path, fspath=fspath)
|
||||
|
||||
# The explicit annotation is to avoid publicly exposing NodeKeywords.
|
||||
#: Keywords/markers collected from all scopes.
|
||||
@@ -185,7 +258,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
self._store = self.stash
|
||||
|
||||
@classmethod
|
||||
def from_parent(cls, parent: "Node", **kw) -> "Self":
|
||||
def from_parent(cls, parent: "Node", **kw):
|
||||
"""Public constructor for Nodes.
|
||||
|
||||
This indirection got introduced in order to enable removing
|
||||
@@ -261,20 +334,12 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
def teardown(self) -> None:
|
||||
pass
|
||||
|
||||
def iter_parents(self) -> Iterator["Node"]:
|
||||
"""Iterate over all parent collectors starting from and including self
|
||||
up to the root of the collection tree.
|
||||
|
||||
.. versionadded:: 8.1
|
||||
"""
|
||||
parent: Optional[Node] = self
|
||||
while parent is not None:
|
||||
yield parent
|
||||
parent = parent.parent
|
||||
|
||||
def listchain(self) -> List["Node"]:
|
||||
"""Return a list of all parent collectors starting from the root of the
|
||||
collection tree down to and including self."""
|
||||
"""Return list of all parent collectors up to self, starting from
|
||||
the root of collection tree.
|
||||
|
||||
:returns: The nodes.
|
||||
"""
|
||||
chain = []
|
||||
item: Optional[Node] = self
|
||||
while item is not None:
|
||||
@@ -323,7 +388,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
:param name: If given, filter the results by the name attribute.
|
||||
:returns: An iterator of (node, mark) tuples.
|
||||
"""
|
||||
for node in self.iter_parents():
|
||||
for node in reversed(self.listchain()):
|
||||
for mark in node.own_markers:
|
||||
if name is None or getattr(mark, "name", None) == name:
|
||||
yield node, mark
|
||||
@@ -367,16 +432,17 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
self.session._setupstate.addfinalizer(fin, self)
|
||||
|
||||
def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]:
|
||||
"""Get the closest parent node (including self) which is an instance of
|
||||
"""Get the next parent node (including self) which is an instance of
|
||||
the given class.
|
||||
|
||||
:param cls: The node class to search for.
|
||||
:returns: The node, if found.
|
||||
"""
|
||||
for node in self.iter_parents():
|
||||
if isinstance(node, cls):
|
||||
return node
|
||||
return None
|
||||
current: Optional[Node] = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
assert current is None or isinstance(current, cls)
|
||||
return current
|
||||
|
||||
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||
return excinfo.traceback
|
||||
@@ -389,7 +455,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
|
||||
from _pytest.fixtures import FixtureLookupError
|
||||
|
||||
if isinstance(excinfo.value, ConftestImportFailure):
|
||||
excinfo = ExceptionInfo.from_exception(excinfo.value.cause)
|
||||
excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo)
|
||||
if isinstance(excinfo.value, fail.Exception):
|
||||
if not excinfo.value.pytrace:
|
||||
style = "value"
|
||||
@@ -455,7 +521,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
|
||||
|
||||
* "location": a pair (path, lineno)
|
||||
* "obj": a Python object that the node wraps.
|
||||
* "path": just a path
|
||||
* "fspath": just a path
|
||||
|
||||
:rtype: A tuple of (str|Path, int) with filename and 0-based line number.
|
||||
"""
|
||||
@@ -466,7 +532,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
|
||||
obj = getattr(node, "obj", None)
|
||||
if obj is not None:
|
||||
return getfslineno(obj)
|
||||
return getattr(node, "path", "unknown location"), -1
|
||||
return getattr(node, "fspath", "unknown location"), -1
|
||||
|
||||
|
||||
class Collector(Node, abc.ABC):
|
||||
@@ -529,6 +595,7 @@ class FSCollector(Collector, abc.ABC):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fspath: Optional[LEGACY_PATH] = None,
|
||||
path_or_parent: Optional[Union[Path, Node]] = None,
|
||||
path: Optional[Path] = None,
|
||||
name: Optional[str] = None,
|
||||
@@ -544,8 +611,8 @@ class FSCollector(Collector, abc.ABC):
|
||||
elif isinstance(path_or_parent, Path):
|
||||
assert path is None
|
||||
path = path_or_parent
|
||||
assert path is not None
|
||||
|
||||
path = _imply_path(type(self), path, fspath=fspath)
|
||||
if name is None:
|
||||
name = path.name
|
||||
if parent is not None and parent.path != path:
|
||||
@@ -585,11 +652,20 @@ class FSCollector(Collector, abc.ABC):
|
||||
cls,
|
||||
parent,
|
||||
*,
|
||||
fspath: Optional[LEGACY_PATH] = None,
|
||||
path: Optional[Path] = None,
|
||||
**kw,
|
||||
) -> "Self":
|
||||
):
|
||||
"""The public constructor."""
|
||||
return super().from_parent(parent=parent, path=path, **kw)
|
||||
return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
|
||||
|
||||
def gethookproxy(self, fspath: "os.PathLike[str]"):
|
||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
|
||||
return self.session.gethookproxy(fspath)
|
||||
|
||||
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
|
||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
|
||||
return self.session.isinitpath(path)
|
||||
|
||||
|
||||
class File(FSCollector, abc.ABC):
|
||||
|
||||
51
src/_pytest/nose.py
Normal file
51
src/_pytest/nose.py
Normal file
@@ -0,0 +1,51 @@
|
||||
"""Run testsuites written for nose."""
|
||||
|
||||
import warnings
|
||||
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.deprecated import NOSE_SUPPORT
|
||||
from _pytest.fixtures import getfixturemarker
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.python import Function
|
||||
from _pytest.unittest import TestCaseFunction
|
||||
|
||||
|
||||
@hookimpl(trylast=True)
|
||||
def pytest_runtest_setup(item: Item) -> None:
|
||||
if not isinstance(item, Function):
|
||||
return
|
||||
# Don't do nose style setup/teardown on direct unittest style classes.
|
||||
if isinstance(item, TestCaseFunction):
|
||||
return
|
||||
|
||||
# Capture the narrowed type of item for the teardown closure,
|
||||
# see https://github.com/python/mypy/issues/2608
|
||||
func = item
|
||||
|
||||
call_optional(func.obj, "setup", func.nodeid)
|
||||
func.addfinalizer(lambda: call_optional(func.obj, "teardown", func.nodeid))
|
||||
|
||||
# NOTE: Module- and class-level fixtures are handled in python.py
|
||||
# with `pluginmanager.has_plugin("nose")` checks.
|
||||
# It would have been nicer to implement them outside of core, but
|
||||
# it's not straightforward.
|
||||
|
||||
|
||||
def call_optional(obj: object, name: str, nodeid: str) -> bool:
|
||||
method = getattr(obj, name, None)
|
||||
if method is None:
|
||||
return False
|
||||
is_fixture = getfixturemarker(method) is not None
|
||||
if is_fixture:
|
||||
return False
|
||||
if not callable(method):
|
||||
return False
|
||||
# Warn about deprecation of this plugin.
|
||||
method_name = getattr(method, "__name__", str(method))
|
||||
warnings.warn(
|
||||
NOSE_SUPPORT.format(nodeid=nodeid, method=method_name, stage=name), stacklevel=2
|
||||
)
|
||||
# If there are any problems allow the exception to raise rather than
|
||||
# silently ignoring it.
|
||||
method()
|
||||
return True
|
||||
@@ -10,6 +10,9 @@ from typing import Optional
|
||||
from typing import Protocol
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
import warnings
|
||||
|
||||
from _pytest.deprecated import KEYWORD_MSG_ARG
|
||||
|
||||
|
||||
class OutcomeException(BaseException):
|
||||
@@ -101,8 +104,7 @@ def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _E
|
||||
|
||||
@_with_exception(Exit)
|
||||
def exit(
|
||||
reason: str = "",
|
||||
returncode: Optional[int] = None,
|
||||
reason: str = "", returncode: Optional[int] = None, *, msg: Optional[str] = None
|
||||
) -> NoReturn:
|
||||
"""Exit testing process.
|
||||
|
||||
@@ -112,16 +114,28 @@ def exit(
|
||||
|
||||
:param returncode:
|
||||
Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
|
||||
|
||||
:param msg:
|
||||
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
from _pytest.config import UsageError
|
||||
|
||||
if reason and msg:
|
||||
raise UsageError(
|
||||
"cannot pass reason and msg to exit(), `msg` is deprecated, use `reason`."
|
||||
)
|
||||
if not reason:
|
||||
if msg is None:
|
||||
raise UsageError("exit() requires a reason argument")
|
||||
warnings.warn(KEYWORD_MSG_ARG.format(func="exit"), stacklevel=2)
|
||||
reason = msg
|
||||
raise Exit(reason, returncode)
|
||||
|
||||
|
||||
@_with_exception(Skipped)
|
||||
def skip(
|
||||
reason: str = "",
|
||||
*,
|
||||
allow_module_level: bool = False,
|
||||
reason: str = "", *, allow_module_level: bool = False, msg: Optional[str] = None
|
||||
) -> NoReturn:
|
||||
"""Skip an executing test with the given message.
|
||||
|
||||
@@ -140,6 +154,9 @@ def skip(
|
||||
|
||||
Defaults to False.
|
||||
|
||||
:param msg:
|
||||
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
||||
|
||||
.. note::
|
||||
It is better to use the :ref:`pytest.mark.skipif ref` marker when
|
||||
possible to declare a test to be skipped under certain conditions
|
||||
@@ -148,11 +165,12 @@ def skip(
|
||||
to skip a doctest statically.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
reason = _resolve_msg_to_reason("skip", reason, msg)
|
||||
raise Skipped(msg=reason, allow_module_level=allow_module_level)
|
||||
|
||||
|
||||
@_with_exception(Failed)
|
||||
def fail(reason: str = "", pytrace: bool = True) -> NoReturn:
|
||||
def fail(reason: str = "", pytrace: bool = True, msg: Optional[str] = None) -> NoReturn:
|
||||
"""Explicitly fail an executing test with the given message.
|
||||
|
||||
:param reason:
|
||||
@@ -161,11 +179,51 @@ def fail(reason: str = "", pytrace: bool = True) -> NoReturn:
|
||||
:param pytrace:
|
||||
If False, msg represents the full failure information and no
|
||||
python traceback will be reported.
|
||||
|
||||
:param msg:
|
||||
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
reason = _resolve_msg_to_reason("fail", reason, msg)
|
||||
raise Failed(msg=reason, pytrace=pytrace)
|
||||
|
||||
|
||||
def _resolve_msg_to_reason(
|
||||
func_name: str, reason: str, msg: Optional[str] = None
|
||||
) -> str:
|
||||
"""
|
||||
Handles converting the deprecated msg parameter if provided into
|
||||
reason, raising a deprecation warning. This function will be removed
|
||||
when the optional msg argument is removed from here in future.
|
||||
|
||||
:param str func_name:
|
||||
The name of the offending function, this is formatted into the deprecation message.
|
||||
|
||||
:param str reason:
|
||||
The reason= passed into either pytest.fail() or pytest.skip()
|
||||
|
||||
:param str msg:
|
||||
The msg= passed into either pytest.fail() or pytest.skip(). This will
|
||||
be converted into reason if it is provided to allow pytest.skip(msg=) or
|
||||
pytest.fail(msg=) to continue working in the interim period.
|
||||
|
||||
:returns:
|
||||
The value to use as reason.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if msg is not None:
|
||||
if reason:
|
||||
from pytest import UsageError
|
||||
|
||||
raise UsageError(
|
||||
f"Passing both ``reason`` and ``msg`` to pytest.{func_name}(...) is not permitted."
|
||||
)
|
||||
warnings.warn(KEYWORD_MSG_ARG.format(func=func_name), stacklevel=3)
|
||||
reason = msg
|
||||
return reason
|
||||
|
||||
|
||||
class XFailed(Failed):
|
||||
"""Raised from an explicit call to pytest.xfail()."""
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Submit failure or test session information to a pastebin service."""
|
||||
|
||||
from io import StringIO
|
||||
import tempfile
|
||||
from typing import IO
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import atexit
|
||||
import contextlib
|
||||
from enum import Enum
|
||||
@@ -171,23 +170,23 @@ def rm_rf(path: Path) -> None:
|
||||
shutil.rmtree(str(path), onerror=onerror)
|
||||
|
||||
|
||||
def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]:
|
||||
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
||||
"""Find all elements in root that begin with the prefix, case insensitive."""
|
||||
l_prefix = prefix.lower()
|
||||
for x in os.scandir(root):
|
||||
for x in root.iterdir():
|
||||
if x.name.lower().startswith(l_prefix):
|
||||
yield x
|
||||
|
||||
|
||||
def extract_suffixes(iter: Iterable["os.DirEntry[str]"], prefix: str) -> Iterator[str]:
|
||||
def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]:
|
||||
"""Return the parts of the paths following the prefix.
|
||||
|
||||
:param iter: Iterator over path names.
|
||||
:param prefix: Expected prefix of the path names.
|
||||
"""
|
||||
p_len = len(prefix)
|
||||
for entry in iter:
|
||||
yield entry.name[p_len:]
|
||||
for p in iter:
|
||||
yield p.name[p_len:]
|
||||
|
||||
|
||||
def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
|
||||
@@ -346,12 +345,12 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||
"""List candidates for numbered directories to be removed - follows py.path."""
|
||||
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
|
||||
max_delete = max_existing - keep
|
||||
entries = find_prefixed(root, prefix)
|
||||
entries, entries2 = itertools.tee(entries)
|
||||
numbers = map(parse_num, extract_suffixes(entries2, prefix))
|
||||
for entry, number in zip(entries, numbers):
|
||||
paths = find_prefixed(root, prefix)
|
||||
paths, paths2 = itertools.tee(paths)
|
||||
numbers = map(parse_num, extract_suffixes(paths2, prefix))
|
||||
for path, number in zip(paths, numbers):
|
||||
if number <= max_delete:
|
||||
yield Path(entry)
|
||||
yield path
|
||||
|
||||
|
||||
def cleanup_dead_symlinks(root: Path):
|
||||
@@ -484,86 +483,73 @@ class ImportPathMismatchError(ImportError):
|
||||
|
||||
|
||||
def import_path(
|
||||
path: Union[str, "os.PathLike[str]"],
|
||||
p: Union[str, "os.PathLike[str]"],
|
||||
*,
|
||||
mode: Union[str, ImportMode] = ImportMode.prepend,
|
||||
root: Path,
|
||||
consider_namespace_packages: bool,
|
||||
) -> 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).
|
||||
|
||||
:param path:
|
||||
Path to the file to import.
|
||||
The import mechanism used is controlled by the `mode` parameter:
|
||||
|
||||
:param mode:
|
||||
Controls the underlying import mechanism that will be used:
|
||||
* `mode == ImportMode.prepend`: the directory containing the module (or package, taking
|
||||
`__init__.py` files into account) will be put at the *start* of `sys.path` before
|
||||
being imported with `importlib.import_module`.
|
||||
|
||||
* ImportMode.prepend: the directory containing the module (or package, taking
|
||||
`__init__.py` files into account) will be put at the *start* of `sys.path` before
|
||||
being imported with `importlib.import_module`.
|
||||
* `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
|
||||
to the end of `sys.path`, if not already in `sys.path`.
|
||||
|
||||
* ImportMode.append: same as `prepend`, but the directory will be appended
|
||||
to the end of `sys.path`, if not already in `sys.path`.
|
||||
|
||||
* 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.
|
||||
* `mode == 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:
|
||||
Used as an anchor when mode == ImportMode.importlib to obtain
|
||||
a unique name for the module being imported so it can safely be stored
|
||||
into ``sys.modules``.
|
||||
|
||||
:param consider_namespace_packages:
|
||||
If True, consider namespace packages when resolving module names.
|
||||
|
||||
:raises ImportPathMismatchError:
|
||||
If after importing the given `path` and the module `__file__`
|
||||
are different. Only raised in `prepend` and `append` modes.
|
||||
"""
|
||||
path = Path(path)
|
||||
mode = ImportMode(mode)
|
||||
|
||||
path = Path(p)
|
||||
|
||||
if not path.exists():
|
||||
raise ImportError(path)
|
||||
|
||||
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:
|
||||
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)
|
||||
with contextlib.suppress(KeyError):
|
||||
return sys.modules[module_name]
|
||||
|
||||
mod = _import_module_using_spec(
|
||||
module_name, path, path.parent, insert_modules=True
|
||||
)
|
||||
if mod is None:
|
||||
for meta_importer in sys.meta_path:
|
||||
spec = meta_importer.find_spec(module_name, [str(path.parent)])
|
||||
if spec is not None:
|
||||
break
|
||||
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}")
|
||||
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
|
||||
|
||||
try:
|
||||
pkg_root, module_name = resolve_pkg_root_and_module_name(
|
||||
path, consider_namespace_packages=consider_namespace_packages
|
||||
)
|
||||
except CouldNotResolvePathError:
|
||||
pkg_root, module_name = path.parent, path.stem
|
||||
pkg_path = resolve_package_path(path)
|
||||
if pkg_path is not None:
|
||||
pkg_root = pkg_path.parent
|
||||
names = list(path.with_suffix("").relative_to(pkg_root).parts)
|
||||
if names[-1] == "__init__":
|
||||
names.pop()
|
||||
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
|
||||
# problems because of delayed imports: for example, a conftest.py file imported by this function
|
||||
@@ -605,40 +591,6 @@ def import_path(
|
||||
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
|
||||
# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
|
||||
if sys.platform.startswith("win"):
|
||||
@@ -675,11 +627,6 @@ def module_name_from_path(path: Path, root: Path) -> str:
|
||||
if len(path_parts) >= 2 and path_parts[-1] == "__init__":
|
||||
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)
|
||||
|
||||
|
||||
@@ -741,60 +688,6 @@ def resolve_package_path(path: Path) -> Optional[Path]:
|
||||
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(
|
||||
path: Union[str, "os.PathLike[str]"],
|
||||
sort_key: Callable[["os.DirEntry[str]"], object] = lambda entry: entry.name,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""(Disabled by default) support for testing pytest and pytest plugins.
|
||||
|
||||
PYTEST_DONT_REWRITE
|
||||
"""
|
||||
|
||||
import collections.abc
|
||||
import contextlib
|
||||
from fnmatch import fnmatch
|
||||
@@ -1061,7 +1061,7 @@ class Pytester:
|
||||
:param cmdlineargs: Any extra command line arguments to use.
|
||||
"""
|
||||
p = self.makepyfile(source)
|
||||
values = [*list(cmdlineargs), p]
|
||||
values = list(cmdlineargs) + [p]
|
||||
return self.inline_run(*values)
|
||||
|
||||
def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
|
||||
@@ -1491,10 +1491,10 @@ class Pytester:
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
|
||||
args = ("--basetemp=%s" % p, *args)
|
||||
args = ("--basetemp=%s" % p,) + args
|
||||
plugins = [x for x in self.plugins if isinstance(x, str)]
|
||||
if plugins:
|
||||
args = ("-p", plugins[0], *args)
|
||||
args = ("-p", plugins[0]) + args
|
||||
args = self._getpytestargs() + args
|
||||
return self.run(*args, timeout=timeout)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Python test discovery, setup and run of test functions."""
|
||||
|
||||
import abc
|
||||
from collections import Counter
|
||||
from collections import defaultdict
|
||||
@@ -11,6 +11,7 @@ import inspect
|
||||
import itertools
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import types
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
@@ -27,7 +28,6 @@ from typing import Pattern
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
import warnings
|
||||
|
||||
@@ -48,14 +48,18 @@ from _pytest.compat import getimfunc
|
||||
from _pytest.compat import getlocation
|
||||
from _pytest.compat import is_async_function
|
||||
from _pytest.compat import is_generator
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.compat import safe_isclass
|
||||
from _pytest.compat import STRING_TYPES
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import INSTANCE_COLLECTOR
|
||||
from _pytest.deprecated import NOSE_SUPPORT_METHOD
|
||||
from _pytest.fixtures import FixtureDef
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.fixtures import FuncFixtureInfo
|
||||
@@ -82,10 +86,6 @@ from _pytest.warning_types import PytestReturnNotNoneWarning
|
||||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Self
|
||||
|
||||
|
||||
_PYTEST_DIR = Path(_pytest.__file__).parent
|
||||
|
||||
|
||||
@@ -209,7 +209,8 @@ def pytest_collect_directory(
|
||||
) -> Optional[nodes.Collector]:
|
||||
pkginit = path / "__init__.py"
|
||||
if pkginit.is_file():
|
||||
return Package.from_parent(parent, path=path)
|
||||
pkg: Package = Package.from_parent(parent, path=path)
|
||||
return pkg
|
||||
return None
|
||||
|
||||
|
||||
@@ -234,7 +235,8 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
|
||||
|
||||
|
||||
def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
|
||||
return Module.from_parent(parent, path=module_path)
|
||||
mod: Module = Module.from_parent(parent, path=module_path)
|
||||
return mod
|
||||
|
||||
|
||||
@hookimpl(trylast=True)
|
||||
@@ -245,7 +247,8 @@ def pytest_pycollect_makeitem(
|
||||
# Nothing was collected elsewhere, let's do it here.
|
||||
if safe_isclass(obj):
|
||||
if collector.istestclass(obj, name):
|
||||
return Class.from_parent(collector, name=name, obj=obj)
|
||||
klass: Class = Class.from_parent(collector, name=name, obj=obj)
|
||||
return klass
|
||||
elif collector.istestfunction(obj, name):
|
||||
# mock seems to store unbound methods (issue473), normalize it.
|
||||
obj = getattr(obj, "__func__", obj)
|
||||
@@ -264,7 +267,7 @@ def pytest_pycollect_makeitem(
|
||||
)
|
||||
elif getattr(obj, "__test__", True):
|
||||
if is_generator(obj):
|
||||
res = Function.from_parent(collector, name=name)
|
||||
res: Function = Function.from_parent(collector, name=name)
|
||||
reason = (
|
||||
f"yield tests were removed in pytest 4.0 - {name} will be ignored"
|
||||
)
|
||||
@@ -334,8 +337,10 @@ class PyobjMixin(nodes.Node):
|
||||
|
||||
def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
|
||||
"""Return Python path relative to the containing module."""
|
||||
chain = self.listchain()
|
||||
chain.reverse()
|
||||
parts = []
|
||||
for node in self.iter_parents():
|
||||
for node in chain:
|
||||
name = node.name
|
||||
if isinstance(node, Module):
|
||||
name = os.path.splitext(name)[0]
|
||||
@@ -349,8 +354,20 @@ class PyobjMixin(nodes.Node):
|
||||
|
||||
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
|
||||
# XXX caching?
|
||||
path, lineno = getfslineno(self.obj)
|
||||
obj = self.obj
|
||||
compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
|
||||
if isinstance(compat_co_firstlineno, int):
|
||||
# nose compatibility
|
||||
file_path = sys.modules[obj.__module__].__file__
|
||||
assert file_path is not None
|
||||
if file_path.endswith(".pyc"):
|
||||
file_path = file_path[:-1]
|
||||
path: Union["os.PathLike[str]", str] = file_path
|
||||
lineno = compat_co_firstlineno
|
||||
else:
|
||||
path, lineno = getfslineno(obj)
|
||||
modpath = self.getmodpath()
|
||||
assert isinstance(lineno, int)
|
||||
return path, lineno, modpath
|
||||
|
||||
|
||||
@@ -359,7 +376,7 @@ class PyobjMixin(nodes.Node):
|
||||
# hook is not called for them.
|
||||
# fmt: off
|
||||
class _EmptyClass: pass # noqa: E701
|
||||
IGNORED_ATTRIBUTES = frozenset.union(
|
||||
IGNORED_ATTRIBUTES = frozenset.union( # noqa: E305
|
||||
frozenset(),
|
||||
# Module.
|
||||
dir(types.ModuleType("empty_module")),
|
||||
@@ -467,7 +484,9 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
|
||||
clscol = self.getparent(Class)
|
||||
cls = clscol and clscol.obj or None
|
||||
|
||||
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
|
||||
definition: FunctionDefinition = FunctionDefinition.from_parent(
|
||||
self, name=name, callobj=funcobj
|
||||
)
|
||||
fixtureinfo = definition._fixtureinfo
|
||||
|
||||
# pytest_generate_tests impls call metafunc.parametrize() which fills
|
||||
@@ -516,12 +535,7 @@ def importtestmodule(
|
||||
# We assume we are only called once per module.
|
||||
importmode = config.getoption("--import-mode")
|
||||
try:
|
||||
mod = import_path(
|
||||
path,
|
||||
mode=importmode,
|
||||
root=config.rootpath,
|
||||
consider_namespace_packages=config.getini("consider_namespace_packages"),
|
||||
)
|
||||
mod = import_path(path, mode=importmode, root=config.rootpath)
|
||||
except SyntaxError as e:
|
||||
raise nodes.Collector.CollectError(
|
||||
ExceptionInfo.from_current().getrepr(style="short")
|
||||
@@ -529,12 +543,12 @@ def importtestmodule(
|
||||
except ImportPathMismatchError as e:
|
||||
raise nodes.Collector.CollectError(
|
||||
"import file mismatch:\n"
|
||||
"imported module {!r} has this __file__ attribute:\n"
|
||||
" {}\n"
|
||||
"imported module %r has this __file__ attribute:\n"
|
||||
" %s\n"
|
||||
"which is not the same as the test file we want to collect:\n"
|
||||
" {}\n"
|
||||
" %s\n"
|
||||
"HINT: remove __pycache__ / .pyc files and/or use a "
|
||||
"unique basename for your test file modules".format(*e.args)
|
||||
"unique basename for your test file modules" % e.args
|
||||
) from e
|
||||
except ImportError as e:
|
||||
exc_info = ExceptionInfo.from_current()
|
||||
@@ -572,47 +586,56 @@ class Module(nodes.File, PyCollector):
|
||||
return importtestmodule(self.path, self.config)
|
||||
|
||||
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
||||
self._register_setup_module_fixture()
|
||||
self._register_setup_function_fixture()
|
||||
self._inject_setup_module_fixture()
|
||||
self._inject_setup_function_fixture()
|
||||
self.session._fixturemanager.parsefactories(self)
|
||||
return super().collect()
|
||||
|
||||
def _register_setup_module_fixture(self) -> None:
|
||||
"""Register an autouse, module-scoped fixture for the collected module object
|
||||
def _inject_setup_module_fixture(self) -> None:
|
||||
"""Inject a hidden autouse, module scoped fixture into the collected module object
|
||||
that invokes setUpModule/tearDownModule if either or both are available.
|
||||
|
||||
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
||||
other fixtures (#517).
|
||||
"""
|
||||
has_nose = self.config.pluginmanager.has_plugin("nose")
|
||||
setup_module = _get_first_non_fixture_func(
|
||||
self.obj, ("setUpModule", "setup_module")
|
||||
)
|
||||
if setup_module is None and has_nose:
|
||||
# The name "setup" is too common - only treat as fixture if callable.
|
||||
setup_module = _get_first_non_fixture_func(self.obj, ("setup",))
|
||||
if not callable(setup_module):
|
||||
setup_module = None
|
||||
teardown_module = _get_first_non_fixture_func(
|
||||
self.obj, ("tearDownModule", "teardown_module")
|
||||
)
|
||||
if teardown_module is None and has_nose:
|
||||
teardown_module = _get_first_non_fixture_func(self.obj, ("teardown",))
|
||||
# Same as "setup" above - only treat as fixture if callable.
|
||||
if not callable(teardown_module):
|
||||
teardown_module = None
|
||||
|
||||
if setup_module is None and teardown_module is None:
|
||||
return
|
||||
|
||||
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
|
||||
module = request.module
|
||||
if setup_module is not None:
|
||||
_call_with_optional_argument(setup_module, module)
|
||||
yield
|
||||
if teardown_module is not None:
|
||||
_call_with_optional_argument(teardown_module, module)
|
||||
|
||||
self.session._fixturemanager._register_fixture(
|
||||
@fixtures.fixture(
|
||||
autouse=True,
|
||||
scope="module",
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"_xunit_setup_module_fixture_{self.obj.__name__}",
|
||||
func=xunit_setup_module_fixture,
|
||||
nodeid=self.nodeid,
|
||||
scope="module",
|
||||
autouse=True,
|
||||
)
|
||||
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
|
||||
if setup_module is not None:
|
||||
_call_with_optional_argument(setup_module, request.module)
|
||||
yield
|
||||
if teardown_module is not None:
|
||||
_call_with_optional_argument(teardown_module, request.module)
|
||||
|
||||
def _register_setup_function_fixture(self) -> None:
|
||||
"""Register an autouse, function-scoped fixture for the collected module object
|
||||
self.obj.__pytest_setup_module = xunit_setup_module_fixture
|
||||
|
||||
def _inject_setup_function_fixture(self) -> None:
|
||||
"""Inject a hidden autouse, function scoped fixture into the collected module object
|
||||
that invokes setup_function/teardown_function if either or both are available.
|
||||
|
||||
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
||||
@@ -625,27 +648,25 @@ class Module(nodes.File, PyCollector):
|
||||
if setup_function is None and teardown_function is None:
|
||||
return
|
||||
|
||||
@fixtures.fixture(
|
||||
autouse=True,
|
||||
scope="function",
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
|
||||
)
|
||||
def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
|
||||
if request.instance is not None:
|
||||
# in this case we are bound to an instance, so we need to let
|
||||
# setup_method handle this
|
||||
yield
|
||||
return
|
||||
function = request.function
|
||||
if setup_function is not None:
|
||||
_call_with_optional_argument(setup_function, function)
|
||||
_call_with_optional_argument(setup_function, request.function)
|
||||
yield
|
||||
if teardown_function is not None:
|
||||
_call_with_optional_argument(teardown_function, function)
|
||||
_call_with_optional_argument(teardown_function, request.function)
|
||||
|
||||
self.session._fixturemanager._register_fixture(
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
|
||||
func=xunit_setup_function_fixture,
|
||||
nodeid=self.nodeid,
|
||||
scope="function",
|
||||
autouse=True,
|
||||
)
|
||||
self.obj.__pytest_setup_function = xunit_setup_function_fixture
|
||||
|
||||
|
||||
class Package(nodes.Directory):
|
||||
@@ -665,6 +686,7 @@ class Package(nodes.Directory):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fspath: Optional[LEGACY_PATH],
|
||||
parent: nodes.Collector,
|
||||
# NOTE: following args are unused:
|
||||
config=None,
|
||||
@@ -676,6 +698,7 @@ class Package(nodes.Directory):
|
||||
# super().__init__(self, fspath, parent=parent)
|
||||
session = parent.session
|
||||
super().__init__(
|
||||
fspath=fspath,
|
||||
path=path,
|
||||
parent=parent,
|
||||
config=config,
|
||||
@@ -712,6 +735,8 @@ class Package(nodes.Directory):
|
||||
ihook = self.ihook
|
||||
for direntry in scandir(self.path, sort_key):
|
||||
if direntry.is_dir():
|
||||
if direntry.name == "__pycache__":
|
||||
continue
|
||||
path = Path(direntry.path)
|
||||
if not self.session.isinitpath(path, with_parents=True):
|
||||
if ihook.pytest_ignore_collect(collection_path=path, config=config):
|
||||
@@ -756,7 +781,7 @@ class Class(PyCollector):
|
||||
"""Collector for test methods (and nested classes) in a Python class."""
|
||||
|
||||
@classmethod
|
||||
def from_parent(cls, parent, *, name, obj=None, **kw) -> "Self": # type: ignore[override]
|
||||
def from_parent(cls, parent, *, name, obj=None, **kw):
|
||||
"""The public constructor."""
|
||||
return super().from_parent(name=name, parent=parent, **kw)
|
||||
|
||||
@@ -770,8 +795,9 @@ class Class(PyCollector):
|
||||
assert self.parent is not None
|
||||
self.warn(
|
||||
PytestCollectionWarning(
|
||||
f"cannot collect test class {self.obj.__name__!r} because it has a "
|
||||
f"__init__ constructor (from: {self.parent.nodeid})"
|
||||
"cannot collect test class %r because it has a "
|
||||
"__init__ constructor (from: %s)"
|
||||
% (self.obj.__name__, self.parent.nodeid)
|
||||
)
|
||||
)
|
||||
return []
|
||||
@@ -779,21 +805,22 @@ class Class(PyCollector):
|
||||
assert self.parent is not None
|
||||
self.warn(
|
||||
PytestCollectionWarning(
|
||||
f"cannot collect test class {self.obj.__name__!r} because it has a "
|
||||
f"__new__ constructor (from: {self.parent.nodeid})"
|
||||
"cannot collect test class %r because it has a "
|
||||
"__new__ constructor (from: %s)"
|
||||
% (self.obj.__name__, self.parent.nodeid)
|
||||
)
|
||||
)
|
||||
return []
|
||||
|
||||
self._register_setup_class_fixture()
|
||||
self._register_setup_method_fixture()
|
||||
self._inject_setup_class_fixture()
|
||||
self._inject_setup_method_fixture()
|
||||
|
||||
self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid)
|
||||
|
||||
return super().collect()
|
||||
|
||||
def _register_setup_class_fixture(self) -> None:
|
||||
"""Register an autouse, class scoped fixture into the collected class object
|
||||
def _inject_setup_class_fixture(self) -> None:
|
||||
"""Inject a hidden autouse, class scoped fixture into the collected class object
|
||||
that invokes setup_class/teardown_class if either or both are available.
|
||||
|
||||
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
|
||||
@@ -804,58 +831,93 @@ class Class(PyCollector):
|
||||
if setup_class is None and teardown_class is None:
|
||||
return
|
||||
|
||||
def xunit_setup_class_fixture(request) -> Generator[None, None, None]:
|
||||
cls = request.cls
|
||||
@fixtures.fixture(
|
||||
autouse=True,
|
||||
scope="class",
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
|
||||
)
|
||||
def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
|
||||
if setup_class is not None:
|
||||
func = getimfunc(setup_class)
|
||||
_call_with_optional_argument(func, cls)
|
||||
_call_with_optional_argument(func, self.obj)
|
||||
yield
|
||||
if teardown_class is not None:
|
||||
func = getimfunc(teardown_class)
|
||||
_call_with_optional_argument(func, cls)
|
||||
_call_with_optional_argument(func, self.obj)
|
||||
|
||||
self.session._fixturemanager._register_fixture(
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
|
||||
func=xunit_setup_class_fixture,
|
||||
nodeid=self.nodeid,
|
||||
scope="class",
|
||||
autouse=True,
|
||||
)
|
||||
self.obj.__pytest_setup_class = xunit_setup_class_fixture
|
||||
|
||||
def _register_setup_method_fixture(self) -> None:
|
||||
"""Register an autouse, function scoped fixture into the collected class object
|
||||
def _inject_setup_method_fixture(self) -> None:
|
||||
"""Inject a hidden autouse, function scoped fixture into the collected class object
|
||||
that invokes setup_method/teardown_method if either or both are available.
|
||||
|
||||
Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with
|
||||
other fixtures (#517).
|
||||
"""
|
||||
has_nose = self.config.pluginmanager.has_plugin("nose")
|
||||
setup_name = "setup_method"
|
||||
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
|
||||
emit_nose_setup_warning = False
|
||||
if setup_method is None and has_nose:
|
||||
setup_name = "setup"
|
||||
emit_nose_setup_warning = True
|
||||
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
|
||||
teardown_name = "teardown_method"
|
||||
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
|
||||
emit_nose_teardown_warning = False
|
||||
if teardown_method is None and has_nose:
|
||||
teardown_name = "teardown"
|
||||
emit_nose_teardown_warning = True
|
||||
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
|
||||
if setup_method is None and teardown_method is None:
|
||||
return
|
||||
|
||||
def xunit_setup_method_fixture(request) -> Generator[None, None, None]:
|
||||
instance = request.instance
|
||||
method = request.function
|
||||
if setup_method is not None:
|
||||
func = getattr(instance, setup_name)
|
||||
_call_with_optional_argument(func, method)
|
||||
yield
|
||||
if teardown_method is not None:
|
||||
func = getattr(instance, teardown_name)
|
||||
_call_with_optional_argument(func, method)
|
||||
|
||||
self.session._fixturemanager._register_fixture(
|
||||
@fixtures.fixture(
|
||||
autouse=True,
|
||||
scope="function",
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}",
|
||||
func=xunit_setup_method_fixture,
|
||||
nodeid=self.nodeid,
|
||||
scope="function",
|
||||
autouse=True,
|
||||
)
|
||||
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
|
||||
method = request.function
|
||||
if setup_method is not None:
|
||||
func = getattr(self, setup_name)
|
||||
_call_with_optional_argument(func, method)
|
||||
if emit_nose_setup_warning:
|
||||
warnings.warn(
|
||||
NOSE_SUPPORT_METHOD.format(
|
||||
nodeid=request.node.nodeid, method="setup"
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
yield
|
||||
if teardown_method is not None:
|
||||
func = getattr(self, teardown_name)
|
||||
_call_with_optional_argument(func, method)
|
||||
if emit_nose_teardown_warning:
|
||||
warnings.warn(
|
||||
NOSE_SUPPORT_METHOD.format(
|
||||
nodeid=request.node.nodeid, method="teardown"
|
||||
),
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
self.obj.__pytest_setup_method = xunit_setup_method_fixture
|
||||
|
||||
|
||||
class InstanceDummy:
|
||||
"""Instance used to be a node type between Class and Function. It has been
|
||||
removed in pytest 7.0. Some plugins exist which reference `pytest.Instance`
|
||||
only to ignore it; this dummy class keeps them working. This will be removed
|
||||
in pytest 8."""
|
||||
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
if name == "Instance":
|
||||
warnings.warn(INSTANCE_COLLECTOR, 2)
|
||||
return InstanceDummy
|
||||
raise AttributeError(f"module {__name__} has no attribute {name}")
|
||||
|
||||
|
||||
def hasinit(obj: object) -> bool:
|
||||
@@ -1002,7 +1064,7 @@ class IdMaker:
|
||||
def _idval_from_value(self, val: object) -> Optional[str]:
|
||||
"""Try to make an ID for a parameter in a ParameterSet from its value,
|
||||
if the value type is supported."""
|
||||
if isinstance(val, (str, bytes)):
|
||||
if isinstance(val, STRING_TYPES):
|
||||
return _ascii_escaped_by_config(val, self.config)
|
||||
elif val is None or isinstance(val, (float, int, bool, complex)):
|
||||
return str(val)
|
||||
@@ -1272,6 +1334,7 @@ class Metafunc:
|
||||
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
|
||||
# artificial "pseudo" FixtureDef's so that later at test execution time we can
|
||||
# rely on a proper FixtureDef to exist for fixture setup.
|
||||
arg2fixturedefs = self._arg2fixturedefs
|
||||
node = None
|
||||
# If we have a scope that is higher than function, we need
|
||||
# to make sure we only ever create an according fixturedef on
|
||||
@@ -1285,7 +1348,7 @@ class Metafunc:
|
||||
# If used class scope and there is no class, use module-level
|
||||
# collector (for now).
|
||||
if scope_ is Scope.Class:
|
||||
assert isinstance(collector, Module)
|
||||
assert isinstance(collector, _pytest.python.Module)
|
||||
node = collector
|
||||
# If used package scope and there is no package, use session
|
||||
# (for now).
|
||||
@@ -1308,7 +1371,7 @@ class Metafunc:
|
||||
fixturedef = name2pseudofixturedef[argname]
|
||||
else:
|
||||
fixturedef = FixtureDef(
|
||||
config=self.config,
|
||||
fixturemanager=self.definition.session._fixturemanager,
|
||||
baseid="",
|
||||
argname=argname,
|
||||
func=get_direct_param_fixture_func,
|
||||
@@ -1320,7 +1383,7 @@ class Metafunc:
|
||||
)
|
||||
if name2pseudofixturedef is not None:
|
||||
name2pseudofixturedef[argname] = fixturedef
|
||||
self._arg2fixturedefs[argname] = [fixturedef]
|
||||
arg2fixturedefs[argname] = [fixturedef]
|
||||
|
||||
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
|
||||
# more than once) then we accumulate those calls generating the cartesian product
|
||||
@@ -1440,8 +1503,7 @@ class Metafunc:
|
||||
arg_directness[arg] = "indirect"
|
||||
else:
|
||||
fail(
|
||||
f"In {self.function.__name__}: expected Sequence or boolean"
|
||||
f" for indirect, got {type(indirect).__name__}",
|
||||
f"In {self.function.__name__}: expected Sequence or boolean for indirect, got {type(indirect).__name__}",
|
||||
pytrace=False,
|
||||
)
|
||||
return arg_directness
|
||||
@@ -1522,13 +1584,14 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -
|
||||
return val if escape_option else ascii_escaped(val) # type: ignore
|
||||
|
||||
|
||||
def _pretty_fixture_path(invocation_dir: Path, func) -> str:
|
||||
loc = Path(getlocation(func, invocation_dir))
|
||||
def _pretty_fixture_path(func) -> str:
|
||||
cwd = Path.cwd()
|
||||
loc = Path(getlocation(func, str(cwd)))
|
||||
prefix = Path("...", "_pytest")
|
||||
try:
|
||||
return str(prefix / loc.relative_to(_PYTEST_DIR))
|
||||
except ValueError:
|
||||
return bestrelpath(invocation_dir, loc)
|
||||
return bestrelpath(cwd, loc)
|
||||
|
||||
|
||||
def show_fixtures_per_test(config):
|
||||
@@ -1541,19 +1604,19 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None:
|
||||
import _pytest.config
|
||||
|
||||
session.perform_collect()
|
||||
invocation_dir = config.invocation_params.dir
|
||||
curdir = Path.cwd()
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
verbose = config.getvalue("verbose")
|
||||
|
||||
def get_best_relpath(func) -> str:
|
||||
loc = getlocation(func, invocation_dir)
|
||||
return bestrelpath(invocation_dir, Path(loc))
|
||||
loc = getlocation(func, str(curdir))
|
||||
return bestrelpath(curdir, Path(loc))
|
||||
|
||||
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
|
||||
argname = fixture_def.argname
|
||||
if verbose <= 0 and argname.startswith("_"):
|
||||
return
|
||||
prettypath = _pretty_fixture_path(invocation_dir, fixture_def.func)
|
||||
prettypath = _pretty_fixture_path(fixture_def.func)
|
||||
tw.write(f"{argname}", green=True)
|
||||
tw.write(f" -- {prettypath}", yellow=True)
|
||||
tw.write("\n")
|
||||
@@ -1597,7 +1660,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
|
||||
import _pytest.config
|
||||
|
||||
session.perform_collect()
|
||||
invocation_dir = config.invocation_params.dir
|
||||
curdir = Path.cwd()
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
verbose = config.getvalue("verbose")
|
||||
|
||||
@@ -1611,7 +1674,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
|
||||
if not fixturedefs:
|
||||
continue
|
||||
for fixturedef in fixturedefs:
|
||||
loc = getlocation(fixturedef.func, invocation_dir)
|
||||
loc = getlocation(fixturedef.func, str(curdir))
|
||||
if (fixturedef.argname, loc) in seen:
|
||||
continue
|
||||
seen.add((fixturedef.argname, loc))
|
||||
@@ -1619,7 +1682,7 @@ def _showfixtures_main(config: Config, session: Session) -> None:
|
||||
(
|
||||
len(fixturedef.baseid),
|
||||
fixturedef.func.__module__,
|
||||
_pretty_fixture_path(invocation_dir, fixturedef.func),
|
||||
_pretty_fixture_path(fixturedef.func),
|
||||
fixturedef.argname,
|
||||
fixturedef,
|
||||
)
|
||||
@@ -1735,9 +1798,8 @@ class Function(PyobjMixin, nodes.Item):
|
||||
self.fixturenames = fixtureinfo.names_closure
|
||||
self._initrequest()
|
||||
|
||||
# todo: determine sound type limitations
|
||||
@classmethod
|
||||
def from_parent(cls, parent, **kw) -> "Self":
|
||||
def from_parent(cls, parent, **kw): # todo: determine sound type limitations
|
||||
"""The public constructor."""
|
||||
return super().from_parent(parent=parent, **kw)
|
||||
|
||||
@@ -1791,10 +1853,11 @@ class Function(PyobjMixin, nodes.Item):
|
||||
if len(ntraceback) > 2:
|
||||
ntraceback = Traceback(
|
||||
(
|
||||
ntraceback[0],
|
||||
*(t.with_repr_style("short") for t in ntraceback[1:-1]),
|
||||
ntraceback[-1],
|
||||
entry
|
||||
if i == 0 or i == len(ntraceback) - 1
|
||||
else entry.with_repr_style("short")
|
||||
)
|
||||
for i, entry in enumerate(ntraceback)
|
||||
)
|
||||
|
||||
return ntraceback
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from collections.abc import Collection
|
||||
from collections.abc import Sized
|
||||
from decimal import Decimal
|
||||
@@ -24,6 +23,7 @@ from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
import _pytest._code
|
||||
from _pytest.compat import STRING_TYPES
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
|
||||
@@ -720,11 +720,16 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
|
||||
elif (
|
||||
hasattr(expected, "__getitem__")
|
||||
and isinstance(expected, Sized)
|
||||
and not isinstance(expected, (str, bytes))
|
||||
# Type ignored because the error is wrong -- not unreachable.
|
||||
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
|
||||
):
|
||||
cls = ApproxSequenceLike
|
||||
elif isinstance(expected, Collection) and not isinstance(expected, (str, bytes)):
|
||||
msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}"
|
||||
elif (
|
||||
isinstance(expected, Collection)
|
||||
# Type ignored because the error is wrong -- not unreachable.
|
||||
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
|
||||
):
|
||||
msg = f"pytest.approx() only supports ordered sequences, but got: {repr(expected)}"
|
||||
raise TypeError(msg)
|
||||
else:
|
||||
cls = ApproxScalar
|
||||
@@ -774,7 +779,7 @@ def raises(
|
||||
|
||||
|
||||
@overload
|
||||
def raises(
|
||||
def raises( # noqa: F811
|
||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
||||
func: Callable[..., Any],
|
||||
*args: Any,
|
||||
@@ -783,7 +788,7 @@ def raises(
|
||||
...
|
||||
|
||||
|
||||
def raises(
|
||||
def raises( # noqa: F811
|
||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
|
||||
) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
|
||||
r"""Assert that a code block/function call raises an exception type, or one of its subclasses.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Record warnings during test function execution."""
|
||||
|
||||
from pprint import pformat
|
||||
import re
|
||||
from types import TracebackType
|
||||
@@ -19,6 +19,7 @@ from typing import Union
|
||||
import warnings
|
||||
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import WARNS_NONE_ARG
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.outcomes import Exit
|
||||
from _pytest.outcomes import fail
|
||||
@@ -48,11 +49,13 @@ def deprecated_call(
|
||||
|
||||
|
||||
@overload
|
||||
def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T:
|
||||
def deprecated_call( # noqa: F811
|
||||
func: Callable[..., T], *args: Any, **kwargs: Any
|
||||
) -> T:
|
||||
...
|
||||
|
||||
|
||||
def deprecated_call(
|
||||
def deprecated_call( # noqa: F811
|
||||
func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any
|
||||
) -> Union["WarningsRecorder", Any]:
|
||||
"""Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``.
|
||||
@@ -80,7 +83,7 @@ def deprecated_call(
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if func is not None:
|
||||
args = (func, *args)
|
||||
args = (func,) + args
|
||||
return warns(
|
||||
(DeprecationWarning, PendingDeprecationWarning, FutureWarning), *args, **kwargs
|
||||
)
|
||||
@@ -96,7 +99,7 @@ def warns(
|
||||
|
||||
|
||||
@overload
|
||||
def warns(
|
||||
def warns( # noqa: F811
|
||||
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]],
|
||||
func: Callable[..., T],
|
||||
*args: Any,
|
||||
@@ -105,7 +108,7 @@ def warns(
|
||||
...
|
||||
|
||||
|
||||
def warns(
|
||||
def warns( # noqa: F811
|
||||
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning,
|
||||
*args: Any,
|
||||
match: Optional[Union[str, Pattern[str]]] = None,
|
||||
@@ -113,7 +116,7 @@ def warns(
|
||||
) -> Union["WarningsChecker", Any]:
|
||||
r"""Assert that code raises a particular class of warning.
|
||||
|
||||
Specifically, the parameter ``expected_warning`` can be a warning class or tuple
|
||||
Specifically, the parameter ``expected_warning`` can be a warning class or sequence
|
||||
of warning classes, and the code inside the ``with`` block must issue at least one
|
||||
warning of that class or classes.
|
||||
|
||||
@@ -263,7 +266,9 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
|
||||
class WarningsChecker(WarningsRecorder):
|
||||
def __init__(
|
||||
self,
|
||||
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning,
|
||||
expected_warning: Optional[
|
||||
Union[Type[Warning], Tuple[Type[Warning], ...]]
|
||||
] = Warning,
|
||||
match_expr: Optional[Union[str, Pattern[str]]] = None,
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
@@ -272,14 +277,15 @@ class WarningsChecker(WarningsRecorder):
|
||||
super().__init__(_ispytest=True)
|
||||
|
||||
msg = "exceptions must be derived from Warning, not %s"
|
||||
if isinstance(expected_warning, tuple):
|
||||
if expected_warning is None:
|
||||
warnings.warn(WARNS_NONE_ARG, stacklevel=4)
|
||||
expected_warning_tup = None
|
||||
elif isinstance(expected_warning, tuple):
|
||||
for exc in expected_warning:
|
||||
if not issubclass(exc, Warning):
|
||||
raise TypeError(msg % type(exc))
|
||||
expected_warning_tup = expected_warning
|
||||
elif isinstance(expected_warning, type) and issubclass(
|
||||
expected_warning, Warning
|
||||
):
|
||||
elif issubclass(expected_warning, Warning):
|
||||
expected_warning_tup = (expected_warning,)
|
||||
else:
|
||||
raise TypeError(msg % type(expected_warning))
|
||||
@@ -303,6 +309,10 @@ class WarningsChecker(WarningsRecorder):
|
||||
|
||||
__tracebackhide__ = True
|
||||
|
||||
if self.expected_warning is None:
|
||||
# nothing to do in this deprecated case, see WARNS_NONE_ARG above
|
||||
return
|
||||
|
||||
# BaseExceptions like pytest.{skip,fail,xfail,exit} or Ctrl-C within
|
||||
# pytest.warns should *not* trigger "DID NOT WARN" and get suppressed
|
||||
# when the warning doesn't happen. Control-flow exceptions should always
|
||||
@@ -314,7 +324,7 @@ class WarningsChecker(WarningsRecorder):
|
||||
):
|
||||
return
|
||||
|
||||
def found_str() -> str:
|
||||
def found_str():
|
||||
return pformat([record.message for record in self], indent=2)
|
||||
|
||||
try:
|
||||
@@ -341,30 +351,3 @@ class WarningsChecker(WarningsRecorder):
|
||||
module=w.__module__,
|
||||
source=w.source,
|
||||
)
|
||||
|
||||
# Currently in Python it is possible to pass other types than an
|
||||
# `str` message when creating `Warning` instances, however this
|
||||
# causes an exception when :func:`warnings.filterwarnings` is used
|
||||
# to filter those warnings. See
|
||||
# https://github.com/python/cpython/issues/103577 for a discussion.
|
||||
# While this can be considered a bug in CPython, we put guards in
|
||||
# pytest as the error message produced without this check in place
|
||||
# is confusing (#10865).
|
||||
for w in self:
|
||||
if type(w.message) is not UserWarning:
|
||||
# If the warning was of an incorrect type then `warnings.warn()`
|
||||
# creates a UserWarning. Any other warning must have been specified
|
||||
# explicitly.
|
||||
continue
|
||||
if not w.message.args:
|
||||
# UserWarning() without arguments must have been specified explicitly.
|
||||
continue
|
||||
msg = w.message.args[0]
|
||||
if isinstance(msg, str):
|
||||
continue
|
||||
# It's possible that UserWarning was explicitly specified, and
|
||||
# its first argument was not a string. But that case can't be
|
||||
# distinguished from an invalid type.
|
||||
raise TypeError(
|
||||
f"Warning must be str or Warning, got {msg!r} (type {type(msg).__name__})"
|
||||
)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import dataclasses
|
||||
from io import StringIO
|
||||
import os
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Basic collect and runtest protocol implementations."""
|
||||
|
||||
import bdb
|
||||
import dataclasses
|
||||
import os
|
||||
@@ -164,8 +164,6 @@ def pytest_runtest_call(item: Item) -> None:
|
||||
del sys.last_type
|
||||
del sys.last_value
|
||||
del sys.last_traceback
|
||||
if sys.version_info >= (3, 12, 0):
|
||||
del sys.last_exc # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
@@ -174,8 +172,6 @@ def pytest_runtest_call(item: Item) -> None:
|
||||
# Store trace info to allow postmortem debugging
|
||||
sys.last_type = type(e)
|
||||
sys.last_value = e
|
||||
if sys.version_info >= (3, 12, 0):
|
||||
sys.last_exc = e # type: ignore[attr-defined]
|
||||
assert e.__traceback__ is not None
|
||||
# Skip *this* frame
|
||||
sys.last_traceback = e.__traceback__.tb_next
|
||||
@@ -224,26 +220,13 @@ def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str
|
||||
def call_and_report(
|
||||
item: Item, when: Literal["setup", "call", "teardown"], log: bool = True, **kwds
|
||||
) -> TestReport:
|
||||
ihook = item.ihook
|
||||
if when == "setup":
|
||||
runtest_hook: Callable[..., None] = ihook.pytest_runtest_setup
|
||||
elif when == "call":
|
||||
runtest_hook = ihook.pytest_runtest_call
|
||||
elif when == "teardown":
|
||||
runtest_hook = ihook.pytest_runtest_teardown
|
||||
else:
|
||||
assert False, f"Unhandled runtest hook case: {when}"
|
||||
reraise: Tuple[Type[BaseException], ...] = (Exit,)
|
||||
if not item.config.getoption("usepdb", False):
|
||||
reraise += (KeyboardInterrupt,)
|
||||
call = CallInfo.from_call(
|
||||
lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
|
||||
)
|
||||
report: TestReport = ihook.pytest_runtest_makereport(item=item, call=call)
|
||||
call = call_runtest_hook(item, when, **kwds)
|
||||
hook = item.ihook
|
||||
report: TestReport = hook.pytest_runtest_makereport(item=item, call=call)
|
||||
if log:
|
||||
ihook.pytest_runtest_logreport(report=report)
|
||||
hook.pytest_runtest_logreport(report=report)
|
||||
if check_interactive_exception(call, report):
|
||||
ihook.pytest_exception_interact(node=item, call=call, report=report)
|
||||
hook.pytest_exception_interact(node=item, call=call, report=report)
|
||||
return report
|
||||
|
||||
|
||||
@@ -262,6 +245,25 @@ def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) ->
|
||||
return True
|
||||
|
||||
|
||||
def call_runtest_hook(
|
||||
item: Item, when: Literal["setup", "call", "teardown"], **kwds
|
||||
) -> "CallInfo[None]":
|
||||
if when == "setup":
|
||||
ihook: Callable[..., None] = item.ihook.pytest_runtest_setup
|
||||
elif when == "call":
|
||||
ihook = item.ihook.pytest_runtest_call
|
||||
elif when == "teardown":
|
||||
ihook = item.ihook.pytest_runtest_teardown
|
||||
else:
|
||||
assert False, f"Unhandled runtest hook case: {when}"
|
||||
reraise: Tuple[Type[BaseException], ...] = (Exit,)
|
||||
if not item.config.getoption("usepdb", False):
|
||||
reraise += (KeyboardInterrupt,)
|
||||
return CallInfo.from_call(
|
||||
lambda: ihook(item=item, **kwds), when=when, reraise=reraise
|
||||
)
|
||||
|
||||
|
||||
TResult = TypeVar("TResult", covariant=True)
|
||||
|
||||
|
||||
@@ -380,9 +382,6 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
|
||||
collector.path,
|
||||
collector.config.getoption("importmode"),
|
||||
rootpath=collector.config.rootpath,
|
||||
consider_namespace_packages=collector.config.getini(
|
||||
"consider_namespace_packages"
|
||||
),
|
||||
)
|
||||
|
||||
return list(collector.collect())
|
||||
|
||||
@@ -47,23 +47,20 @@ def pytest_fixture_setup(
|
||||
else:
|
||||
param = request.param
|
||||
fixturedef.cached_param = param # type: ignore[attr-defined]
|
||||
_show_fixture_action(fixturedef, request.config, "SETUP")
|
||||
_show_fixture_action(fixturedef, "SETUP")
|
||||
|
||||
|
||||
def pytest_fixture_post_finalizer(
|
||||
fixturedef: FixtureDef[object], request: SubRequest
|
||||
) -> None:
|
||||
def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None:
|
||||
if fixturedef.cached_result is not None:
|
||||
config = request.config
|
||||
config = fixturedef._fixturemanager.config
|
||||
if config.option.setupshow:
|
||||
_show_fixture_action(fixturedef, request.config, "TEARDOWN")
|
||||
_show_fixture_action(fixturedef, "TEARDOWN")
|
||||
if hasattr(fixturedef, "cached_param"):
|
||||
del fixturedef.cached_param # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def _show_fixture_action(
|
||||
fixturedef: FixtureDef[object], config: Config, msg: str
|
||||
) -> None:
|
||||
def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None:
|
||||
config = fixturedef._fixturemanager.config
|
||||
capman = config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
capman.suspend_global_capture()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Support for skip/xfail functions and markers."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import dataclasses
|
||||
import os
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Terminal reporting of the full testing process.
|
||||
|
||||
This is a good source for looking at the various reporting hooks.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from collections import Counter
|
||||
import dataclasses
|
||||
@@ -255,14 +255,6 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"progress even when capture=no)",
|
||||
default="progress",
|
||||
)
|
||||
Config._add_verbosity_ini(
|
||||
parser,
|
||||
Config.VERBOSITY_TEST_CASES,
|
||||
help=(
|
||||
"Specify a verbosity level for test case execution, overriding the main level. "
|
||||
"Higher levels will provide more detailed information about each test case executed."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config: Config) -> None:
|
||||
@@ -389,7 +381,7 @@ class TerminalReporter:
|
||||
if self.config.getoption("setupshow", False):
|
||||
return False
|
||||
cfg: str = self.config.getini("console_output_style")
|
||||
if cfg in {"progress", "progress-even-when-capture-no"}:
|
||||
if cfg == "progress" or cfg == "progress-even-when-capture-no":
|
||||
return "progress"
|
||||
elif cfg == "count":
|
||||
return "count"
|
||||
@@ -416,7 +408,7 @@ class TerminalReporter:
|
||||
@property
|
||||
def showfspath(self) -> bool:
|
||||
if self._showfspath is None:
|
||||
return self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) >= 0
|
||||
return self.verbosity >= 0
|
||||
return self._showfspath
|
||||
|
||||
@showfspath.setter
|
||||
@@ -425,7 +417,7 @@ class TerminalReporter:
|
||||
|
||||
@property
|
||||
def showlongtestinfo(self) -> bool:
|
||||
return self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) > 0
|
||||
return self.verbosity > 0
|
||||
|
||||
def hasopt(self, char: str) -> bool:
|
||||
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
|
||||
@@ -603,7 +595,7 @@ class TerminalReporter:
|
||||
markup = {"yellow": True}
|
||||
else:
|
||||
markup = {}
|
||||
if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0:
|
||||
if self.verbosity <= 0:
|
||||
self._tw.write(letter, **markup)
|
||||
else:
|
||||
self._progress_nodeids_reported.add(rep.nodeid)
|
||||
@@ -612,7 +604,7 @@ class TerminalReporter:
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
if rep.skipped or hasattr(report, "wasxfail"):
|
||||
reason = _get_raw_skip_reason(rep)
|
||||
if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) < 2:
|
||||
if self.config.option.verbose < 2:
|
||||
available_width = (
|
||||
(self._tw.fullwidth - self._tw.width_of_current_line)
|
||||
- len(" [100%]")
|
||||
@@ -649,10 +641,7 @@ class TerminalReporter:
|
||||
|
||||
def pytest_runtest_logfinish(self, nodeid: str) -> None:
|
||||
assert self._session
|
||||
if (
|
||||
self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0
|
||||
and self._show_progress_info
|
||||
):
|
||||
if self.verbosity <= 0 and self._show_progress_info:
|
||||
if self._show_progress_info == "count":
|
||||
num_tests = self._session.testscollected
|
||||
progress_length = len(f" [{num_tests}/{num_tests}]")
|
||||
@@ -830,9 +819,8 @@ class TerminalReporter:
|
||||
rep.toterminal(self._tw)
|
||||
|
||||
def _printcollecteditems(self, items: Sequence[Item]) -> None:
|
||||
test_cases_verbosity = self.config.get_verbosity(Config.VERBOSITY_TEST_CASES)
|
||||
if test_cases_verbosity < 0:
|
||||
if test_cases_verbosity < -1:
|
||||
if self.config.option.verbose < 0:
|
||||
if self.config.option.verbose < -1:
|
||||
counts = Counter(item.nodeid.split("::", 1)[0] for item in items)
|
||||
for name, count in sorted(counts.items()):
|
||||
self._tw.line("%s: %d" % (name, count))
|
||||
@@ -852,7 +840,7 @@ class TerminalReporter:
|
||||
stack.append(col)
|
||||
indent = (len(stack) - 1) * " "
|
||||
self._tw.line(f"{indent}{col}")
|
||||
if test_cases_verbosity >= 1:
|
||||
if self.config.option.verbose >= 1:
|
||||
obj = getattr(col, "obj", None)
|
||||
doc = inspect.getdoc(obj) if obj else None
|
||||
if doc:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Support for providing temporary directories to test functions."""
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -264,8 +264,8 @@ def tmp_path(
|
||||
and old bases are removed after 3 sessions, to aid in debugging.
|
||||
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See
|
||||
:ref:`temporary directory location and retention`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a :class:`pathlib.Path` object.
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Discover and run std-library "unittest" style tests."""
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
import types
|
||||
@@ -29,6 +29,7 @@ from _pytest.python import Class
|
||||
from _pytest.python import Function
|
||||
from _pytest.python import Module
|
||||
from _pytest.runner import CallInfo
|
||||
from _pytest.scope import Scope
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -55,7 +56,8 @@ def pytest_pycollect_makeitem(
|
||||
except Exception:
|
||||
return None
|
||||
# Yes, so let's collect it.
|
||||
return UnitTestCase.from_parent(collector, name=name, obj=obj)
|
||||
item: UnitTestCase = UnitTestCase.from_parent(collector, name=name, obj=obj)
|
||||
return item
|
||||
|
||||
|
||||
class UnitTestCase(Class):
|
||||
@@ -72,9 +74,8 @@ class UnitTestCase(Class):
|
||||
|
||||
skipped = _is_skipped(cls)
|
||||
if not skipped:
|
||||
self._register_unittest_setup_method_fixture(cls)
|
||||
self._register_unittest_setup_class_fixture(cls)
|
||||
self._register_setup_class_fixture()
|
||||
self._inject_setup_teardown_fixtures(cls)
|
||||
self._inject_setup_class_fixture()
|
||||
|
||||
self.session._fixturemanager.parsefactories(self, unittest=True)
|
||||
loader = TestLoader()
|
||||
@@ -95,75 +96,91 @@ class UnitTestCase(Class):
|
||||
if ut is None or runtest != ut.TestCase.runTest: # type: ignore
|
||||
yield TestCaseFunction.from_parent(self, name="runTest")
|
||||
|
||||
def _register_unittest_setup_class_fixture(self, cls: type) -> None:
|
||||
"""Register an auto-use fixture to invoke setUpClass and
|
||||
tearDownClass (#517)."""
|
||||
setup = getattr(cls, "setUpClass", None)
|
||||
teardown = getattr(cls, "tearDownClass", None)
|
||||
if setup is None and teardown is None:
|
||||
return None
|
||||
cleanup = getattr(cls, "doClassCleanups", lambda: None)
|
||||
def _inject_setup_teardown_fixtures(self, cls: type) -> None:
|
||||
"""Injects a hidden auto-use fixture to invoke setUpClass/setup_method and corresponding
|
||||
teardown functions (#517)."""
|
||||
class_fixture = _make_xunit_fixture(
|
||||
cls,
|
||||
"setUpClass",
|
||||
"tearDownClass",
|
||||
"doClassCleanups",
|
||||
scope=Scope.Class,
|
||||
pass_self=False,
|
||||
)
|
||||
if class_fixture:
|
||||
cls.__pytest_class_setup = class_fixture # type: ignore[attr-defined]
|
||||
|
||||
def unittest_setup_class_fixture(
|
||||
request: FixtureRequest,
|
||||
) -> Generator[None, None, None]:
|
||||
cls = request.cls
|
||||
if _is_skipped(cls):
|
||||
reason = cls.__unittest_skip_why__
|
||||
raise pytest.skip.Exception(reason, _use_item_location=True)
|
||||
if setup is not None:
|
||||
try:
|
||||
setup()
|
||||
# unittest does not call the cleanup function for every BaseException, so we
|
||||
# follow this here.
|
||||
except Exception:
|
||||
cleanup()
|
||||
raise
|
||||
yield
|
||||
method_fixture = _make_xunit_fixture(
|
||||
cls,
|
||||
"setup_method",
|
||||
"teardown_method",
|
||||
None,
|
||||
scope=Scope.Function,
|
||||
pass_self=True,
|
||||
)
|
||||
if method_fixture:
|
||||
cls.__pytest_method_setup = method_fixture # type: ignore[attr-defined]
|
||||
|
||||
|
||||
def _make_xunit_fixture(
|
||||
obj: type,
|
||||
setup_name: str,
|
||||
teardown_name: str,
|
||||
cleanup_name: Optional[str],
|
||||
scope: Scope,
|
||||
pass_self: bool,
|
||||
):
|
||||
setup = getattr(obj, setup_name, None)
|
||||
teardown = getattr(obj, teardown_name, None)
|
||||
if setup is None and teardown is None:
|
||||
return None
|
||||
|
||||
if cleanup_name:
|
||||
cleanup = getattr(obj, cleanup_name, lambda *args: None)
|
||||
else:
|
||||
|
||||
def cleanup(*args):
|
||||
pass
|
||||
|
||||
@pytest.fixture(
|
||||
scope=scope.value,
|
||||
autouse=True,
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"_unittest_{setup_name}_fixture_{obj.__qualname__}",
|
||||
)
|
||||
def fixture(self, request: FixtureRequest) -> Generator[None, None, None]:
|
||||
if _is_skipped(self):
|
||||
reason = self.__unittest_skip_why__
|
||||
raise pytest.skip.Exception(reason, _use_item_location=True)
|
||||
if setup is not None:
|
||||
try:
|
||||
if teardown is not None:
|
||||
if pass_self:
|
||||
setup(self, request.function)
|
||||
else:
|
||||
setup()
|
||||
# unittest does not call the cleanup function for every BaseException, so we
|
||||
# follow this here.
|
||||
except Exception:
|
||||
if pass_self:
|
||||
cleanup(self)
|
||||
else:
|
||||
cleanup()
|
||||
|
||||
raise
|
||||
yield
|
||||
try:
|
||||
if teardown is not None:
|
||||
if pass_self:
|
||||
teardown(self, request.function)
|
||||
else:
|
||||
teardown()
|
||||
finally:
|
||||
finally:
|
||||
if pass_self:
|
||||
cleanup(self)
|
||||
else:
|
||||
cleanup()
|
||||
|
||||
self.session._fixturemanager._register_fixture(
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"_unittest_setUpClass_fixture_{cls.__qualname__}",
|
||||
func=unittest_setup_class_fixture,
|
||||
nodeid=self.nodeid,
|
||||
scope="class",
|
||||
autouse=True,
|
||||
)
|
||||
|
||||
def _register_unittest_setup_method_fixture(self, cls: type) -> None:
|
||||
"""Register an auto-use fixture to invoke setup_method and
|
||||
teardown_method (#517)."""
|
||||
setup = getattr(cls, "setup_method", None)
|
||||
teardown = getattr(cls, "teardown_method", None)
|
||||
if setup is None and teardown is None:
|
||||
return None
|
||||
|
||||
def unittest_setup_method_fixture(
|
||||
request: FixtureRequest,
|
||||
) -> Generator[None, None, None]:
|
||||
self = request.instance
|
||||
if _is_skipped(self):
|
||||
reason = self.__unittest_skip_why__
|
||||
raise pytest.skip.Exception(reason, _use_item_location=True)
|
||||
if setup is not None:
|
||||
setup(self, request.function)
|
||||
yield
|
||||
if teardown is not None:
|
||||
teardown(self, request.function)
|
||||
|
||||
self.session._fixturemanager._register_fixture(
|
||||
# Use a unique name to speed up lookup.
|
||||
name=f"_unittest_setup_method_fixture_{cls.__qualname__}",
|
||||
func=unittest_setup_method_fixture,
|
||||
nodeid=self.nodeid,
|
||||
scope="function",
|
||||
autouse=True,
|
||||
)
|
||||
return fixture
|
||||
|
||||
|
||||
class TestCaseFunction(Function):
|
||||
@@ -208,8 +225,8 @@ class TestCaseFunction(Function):
|
||||
)
|
||||
# Invoke the attributes to trigger storing the traceback
|
||||
# trial causes some issue there.
|
||||
_ = excinfo.value
|
||||
_ = excinfo.traceback
|
||||
excinfo.value
|
||||
excinfo.traceback
|
||||
except TypeError:
|
||||
try:
|
||||
try:
|
||||
@@ -225,7 +242,7 @@ class TestCaseFunction(Function):
|
||||
except BaseException:
|
||||
fail(
|
||||
"ERROR: Unknown Incompatible Exception "
|
||||
f"representation:\n{rawexcinfo!r}",
|
||||
"representation:\n%r" % (rawexcinfo,),
|
||||
pytrace=False,
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
@@ -360,21 +377,14 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
|
||||
|
||||
|
||||
# Twisted trial support.
|
||||
classImplements_has_run = False
|
||||
|
||||
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
|
||||
if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules:
|
||||
ut: Any = sys.modules["twisted.python.failure"]
|
||||
global classImplements_has_run
|
||||
Failure__init__ = ut.Failure.__init__
|
||||
if not classImplements_has_run:
|
||||
from twisted.trial.itrial import IReporter
|
||||
from zope.interface import classImplements
|
||||
|
||||
classImplements(TestCaseFunction, IReporter)
|
||||
classImplements_has_run = True
|
||||
check_testcase_implements_trial_reporter()
|
||||
|
||||
def excstore(
|
||||
self, exc_value=None, exc_type=None, exc_tb=None, captureVars=None
|
||||
@@ -402,6 +412,16 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
|
||||
return res
|
||||
|
||||
|
||||
def check_testcase_implements_trial_reporter(done: List[int] = []) -> None:
|
||||
if done:
|
||||
return
|
||||
from twisted.trial.itrial import IReporter
|
||||
from zope.interface import classImplements
|
||||
|
||||
classImplements(TestCaseFunction, IReporter)
|
||||
done.append(1)
|
||||
|
||||
|
||||
def _is_skipped(obj) -> bool:
|
||||
"""Return True if the given object has been marked with @unittest.skip."""
|
||||
return bool(getattr(obj, "__unittest_skip__", False))
|
||||
|
||||
@@ -49,6 +49,12 @@ class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
|
||||
__module__ = "pytest"
|
||||
|
||||
|
||||
class PytestRemovedIn8Warning(PytestDeprecationWarning):
|
||||
"""Warning class for features that will be removed in pytest 8."""
|
||||
|
||||
__module__ = "pytest"
|
||||
|
||||
|
||||
class PytestRemovedIn9Warning(PytestDeprecationWarning):
|
||||
"""Warning class for features that will be removed in pytest 9."""
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from contextlib import contextmanager
|
||||
import sys
|
||||
from typing import Generator
|
||||
@@ -47,8 +46,7 @@ def catch_warnings_for_item(
|
||||
warnings.filterwarnings("always", category=DeprecationWarning)
|
||||
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
||||
|
||||
# To be enabled in pytest 9.0.0.
|
||||
# warnings.filterwarnings("error", category=pytest.PytestRemovedIn9Warning)
|
||||
warnings.filterwarnings("error", category=pytest.PytestRemovedIn8Warning)
|
||||
|
||||
apply_warning_filters(config_filters, cmdline_filters)
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
"""pytest: unit and functional testing with Python."""
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from _pytest import __version__
|
||||
from _pytest import version_tuple
|
||||
from _pytest._code import ExceptionInfo
|
||||
@@ -73,6 +75,7 @@ from _pytest.warning_types import PytestCollectionWarning
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import PytestDeprecationWarning
|
||||
from _pytest.warning_types import PytestExperimentalApiWarning
|
||||
from _pytest.warning_types import PytestRemovedIn8Warning
|
||||
from _pytest.warning_types import PytestRemovedIn9Warning
|
||||
from _pytest.warning_types import PytestReturnNotNoneWarning
|
||||
from _pytest.warning_types import PytestUnhandledCoroutineWarning
|
||||
@@ -137,6 +140,7 @@ __all__ = [
|
||||
"PytestConfigWarning",
|
||||
"PytestDeprecationWarning",
|
||||
"PytestExperimentalApiWarning",
|
||||
"PytestRemovedIn8Warning",
|
||||
"PytestRemovedIn9Warning",
|
||||
"PytestReturnNotNoneWarning",
|
||||
"Pytester",
|
||||
@@ -167,3 +171,13 @@ __all__ = [
|
||||
"xfail",
|
||||
"yield_fixture",
|
||||
]
|
||||
|
||||
if not TYPE_CHECKING:
|
||||
|
||||
def __getattr__(name: str) -> object:
|
||||
if name == "Instance":
|
||||
# The import emits a deprecation warning.
|
||||
from _pytest.python import Instance
|
||||
|
||||
return Instance
|
||||
raise AttributeError(f"module {__name__} has no attribute {name}")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import contextlib
|
||||
import multiprocessing
|
||||
import os
|
||||
@@ -17,7 +16,7 @@ import pytest
|
||||
def ignore_encoding_warning():
|
||||
with warnings.catch_warnings():
|
||||
with contextlib.suppress(NameError): # new in 3.10
|
||||
warnings.simplefilter("ignore", EncodingWarning) # type: ignore [name-defined]
|
||||
warnings.simplefilter("ignore", EncodingWarning) # type: ignore [name-defined] # noqa: F821
|
||||
yield
|
||||
|
||||
|
||||
@@ -1241,9 +1240,9 @@ class TestWINLocalPath:
|
||||
|
||||
def test_owner_group_not_implemented(self, path1):
|
||||
with pytest.raises(NotImplementedError):
|
||||
_ = path1.stat().owner
|
||||
path1.stat().owner
|
||||
with pytest.raises(NotImplementedError):
|
||||
_ = path1.stat().group
|
||||
path1.stat().group
|
||||
|
||||
def test_chmod_simple_int(self, path1):
|
||||
mode = path1.stat().mode
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import dataclasses
|
||||
import importlib.metadata
|
||||
import os
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import re
|
||||
import sys
|
||||
from types import FrameType
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
@@ -180,7 +179,7 @@ class TestTraceback_f_g_h:
|
||||
def test_traceback_cut_excludepath(self, pytester: Pytester) -> None:
|
||||
p = pytester.makepyfile("def f(): raise ValueError")
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
import_path(p, root=pytester.path, consider_namespace_packages=False).f() # type: ignore[attr-defined]
|
||||
import_path(p, root=pytester.path).f() # type: ignore[attr-defined]
|
||||
basedir = Path(pytest.__file__).parent
|
||||
newtraceback = excinfo.traceback.cut(excludepath=basedir)
|
||||
for x in newtraceback:
|
||||
@@ -387,7 +386,7 @@ def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
|
||||
excinfo = pytest.raises(ValueError, template.render, h=h)
|
||||
for item in excinfo.traceback:
|
||||
print(item) # XXX: for some reason jinja.Template.render is printed in full
|
||||
_ = item.source # shouldn't fail
|
||||
item.source # shouldn't fail
|
||||
if isinstance(item.path, Path) and item.path.name == "test.txt":
|
||||
assert str(item.source) == "{{ h()}}:"
|
||||
|
||||
@@ -418,7 +417,7 @@ def test_codepath_Queue_example() -> None:
|
||||
|
||||
def test_match_succeeds():
|
||||
with pytest.raises(ZeroDivisionError) as excinfo:
|
||||
_ = 0 // 0
|
||||
0 // 0
|
||||
excinfo.match(r".*zero.*")
|
||||
|
||||
|
||||
@@ -543,9 +542,7 @@ class TestFormattedExcinfo:
|
||||
tmp_path.joinpath("__init__.py").touch()
|
||||
modpath.write_text(source, encoding="utf-8")
|
||||
importlib.invalidate_caches()
|
||||
return import_path(
|
||||
modpath, root=tmp_path, consider_namespace_packages=False
|
||||
)
|
||||
return import_path(modpath, root=tmp_path)
|
||||
|
||||
return importasmod
|
||||
|
||||
@@ -586,7 +583,7 @@ class TestFormattedExcinfo:
|
||||
try:
|
||||
|
||||
def f():
|
||||
_ = 1 / 0
|
||||
1 / 0
|
||||
|
||||
f()
|
||||
|
||||
@@ -603,7 +600,7 @@ class TestFormattedExcinfo:
|
||||
print(line)
|
||||
assert lines == [
|
||||
" def f():",
|
||||
"> _ = 1 / 0",
|
||||
"> 1 / 0",
|
||||
"E ZeroDivisionError: division by zero",
|
||||
]
|
||||
|
||||
@@ -640,7 +637,7 @@ raise ValueError()
|
||||
pr = FormattedExcinfo()
|
||||
|
||||
try:
|
||||
_ = 1 / 0
|
||||
1 / 0
|
||||
except ZeroDivisionError:
|
||||
excinfo = ExceptionInfo.from_current()
|
||||
|
||||
@@ -1584,7 +1581,7 @@ def test_no_recursion_index_on_recursion_error():
|
||||
return getattr(self, "_" + attr)
|
||||
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
_ = RecursionDepthError().trigger
|
||||
RecursionDepthError().trigger
|
||||
assert "maximum recursion" in str(excinfo.getrepr())
|
||||
|
||||
|
||||
@@ -1741,7 +1738,7 @@ def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester)
|
||||
def add_note(err: BaseException, msg: str) -> None:
|
||||
"""Adds a note to an exception inplace."""
|
||||
if sys.version_info < (3, 11):
|
||||
err.__notes__ = [*getattr(err, "__notes__", []), msg] # type: ignore[attr-defined]
|
||||
err.__notes__ = getattr(err, "__notes__", []) + [msg] # type: ignore[attr-defined]
|
||||
else:
|
||||
err.add_note(msg)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
# flake8: noqa
|
||||
# disable flake check on this file because some constructs are strange
|
||||
# or redundant on purpose and can't be disable on a line-by-line basis
|
||||
@@ -296,7 +295,7 @@ def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) -
|
||||
)
|
||||
path = tmp_path.joinpath("a.py")
|
||||
path.write_text(str(source), encoding="utf-8")
|
||||
mod: Any = import_path(path, root=tmp_path, consider_namespace_packages=False)
|
||||
mod: Any = import_path(path, root=tmp_path)
|
||||
s2 = Source(mod.A)
|
||||
assert str(source).strip() == str(s2).strip()
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# mypy: allow-untyped-defs
|
||||
import dataclasses
|
||||
import re
|
||||
import sys
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# mypy: allow-untyped-defs
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
from _pytest import deprecated
|
||||
from _pytest.compat import legacy_path
|
||||
from _pytest.pytester import Pytester
|
||||
import pytest
|
||||
from pytest import PytestDeprecationWarning
|
||||
@@ -63,6 +68,50 @@ def test_hookimpl_via_function_attributes_are_deprecated():
|
||||
assert record.filename == __file__
|
||||
|
||||
|
||||
def test_fscollector_gethookproxy_isinitpath(pytester: Pytester) -> None:
|
||||
module = pytester.getmodulecol(
|
||||
"""
|
||||
def test_foo(): pass
|
||||
""",
|
||||
withinit=True,
|
||||
)
|
||||
assert isinstance(module, pytest.Module)
|
||||
package = module.parent
|
||||
assert isinstance(package, pytest.Package)
|
||||
|
||||
with pytest.warns(pytest.PytestDeprecationWarning, match="gethookproxy"):
|
||||
package.gethookproxy(pytester.path)
|
||||
|
||||
with pytest.warns(pytest.PytestDeprecationWarning, match="isinitpath"):
|
||||
package.isinitpath(pytester.path)
|
||||
|
||||
# The methods on Session are *not* deprecated.
|
||||
session = module.session
|
||||
with warnings.catch_warnings(record=True) as rec:
|
||||
session.gethookproxy(pytester.path)
|
||||
session.isinitpath(pytester.path)
|
||||
assert len(rec) == 0
|
||||
|
||||
|
||||
def test_strict_option_is_deprecated(pytester: Pytester) -> None:
|
||||
"""--strict is a deprecated alias to --strict-markers (#7530)."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
@pytest.mark.unknown
|
||||
def test_foo(): pass
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("--strict", "-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"'unknown' not found in `markers` configuration option",
|
||||
"*PytestRemovedIn8Warning: The --strict option is deprecated, use --strict-markers instead.",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_yield_fixture_is_deprecated() -> None:
|
||||
with pytest.warns(DeprecationWarning, match=r"yield_fixture is deprecated"):
|
||||
|
||||
@@ -85,6 +134,159 @@ def test_private_is_deprecated() -> None:
|
||||
PrivateInit(10, _ispytest=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("hooktype", ["hook", "ihook"])
|
||||
def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request):
|
||||
path = legacy_path(tmp_path)
|
||||
|
||||
PATH_WARN_MATCH = r".*path: py\.path\.local\) argument is deprecated, please use \(collection_path: pathlib\.Path.*"
|
||||
if hooktype == "ihook":
|
||||
hooks = request.node.ihook
|
||||
else:
|
||||
hooks = request.config.hook
|
||||
|
||||
with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
|
||||
l1 = sys._getframe().f_lineno
|
||||
hooks.pytest_ignore_collect(
|
||||
config=request.config, path=path, collection_path=tmp_path
|
||||
)
|
||||
l2 = sys._getframe().f_lineno
|
||||
|
||||
(record,) = r
|
||||
assert record.filename == __file__
|
||||
assert l1 < record.lineno < l2
|
||||
|
||||
hooks.pytest_ignore_collect(config=request.config, collection_path=tmp_path)
|
||||
|
||||
# Passing entirely *different* paths is an outright error.
|
||||
with pytest.raises(ValueError, match=r"path.*fspath.*need to be equal"):
|
||||
with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r:
|
||||
hooks.pytest_ignore_collect(
|
||||
config=request.config, path=path, collection_path=Path("/bla/bla")
|
||||
)
|
||||
|
||||
|
||||
def test_warns_none_is_deprecated():
|
||||
with pytest.warns(
|
||||
PytestDeprecationWarning,
|
||||
match=re.escape(
|
||||
"Passing None has been deprecated.\n"
|
||||
"See https://docs.pytest.org/en/latest/how-to/capture-warnings.html"
|
||||
"#additional-use-cases-of-warnings-in-tests"
|
||||
" for alternatives in common use cases."
|
||||
),
|
||||
):
|
||||
with pytest.warns(None): # type: ignore[call-overload]
|
||||
pass
|
||||
|
||||
|
||||
class TestSkipMsgArgumentDeprecated:
|
||||
def test_skip_with_msg_is_deprecated(self, pytester: Pytester) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
def test_skipping_msg():
|
||||
pytest.skip(msg="skippedmsg")
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*PytestRemovedIn8Warning: pytest.skip(msg=...) is now deprecated, "
|
||||
"use pytest.skip(reason=...) instead",
|
||||
'*pytest.skip(msg="skippedmsg")*',
|
||||
]
|
||||
)
|
||||
result.assert_outcomes(skipped=1, warnings=1)
|
||||
|
||||
def test_fail_with_msg_is_deprecated(self, pytester: Pytester) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
def test_failing_msg():
|
||||
pytest.fail(msg="failedmsg")
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*PytestRemovedIn8Warning: pytest.fail(msg=...) is now deprecated, "
|
||||
"use pytest.fail(reason=...) instead",
|
||||
'*pytest.fail(msg="failedmsg")',
|
||||
]
|
||||
)
|
||||
result.assert_outcomes(failed=1, warnings=1)
|
||||
|
||||
def test_exit_with_msg_is_deprecated(self, pytester: Pytester) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
def test_exit_msg():
|
||||
pytest.exit(msg="exitmsg")
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*PytestRemovedIn8Warning: pytest.exit(msg=...) is now deprecated, "
|
||||
"use pytest.exit(reason=...) instead",
|
||||
]
|
||||
)
|
||||
result.assert_outcomes(warnings=1)
|
||||
|
||||
|
||||
def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None:
|
||||
pytester.makeconftest(
|
||||
"""
|
||||
def pytest_cmdline_preparse(config, args):
|
||||
...
|
||||
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*PytestRemovedIn8Warning: The pytest_cmdline_preparse hook is deprecated*",
|
||||
"*Please use pytest_load_initial_conftests hook instead.*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
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_importing_instance_is_deprecated(pytester: Pytester) -> None:
|
||||
with pytest.warns(
|
||||
pytest.PytestDeprecationWarning,
|
||||
match=re.escape("The pytest.Instance collector type is deprecated"),
|
||||
):
|
||||
pytest.Instance # type:ignore[attr-defined]
|
||||
|
||||
with pytest.warns(
|
||||
pytest.PytestDeprecationWarning,
|
||||
match=re.escape("The pytest.Instance collector type is deprecated"),
|
||||
):
|
||||
from _pytest.python import Instance # noqa: F401
|
||||
|
||||
|
||||
def test_fixture_disallow_on_marked_functions():
|
||||
"""Test that applying @pytest.fixture to a marked function warns (#3364)."""
|
||||
with pytest.warns(
|
||||
@@ -118,8 +320,6 @@ def test_fixture_disallow_marks_on_fixtures():
|
||||
raise NotImplementedError()
|
||||
|
||||
assert len(record) == 2 # one for each mark decorator
|
||||
# should point to this file
|
||||
assert all(rec.filename == __file__ for rec in record)
|
||||
|
||||
|
||||
def test_fixture_disallowed_between_marks():
|
||||
@@ -136,3 +336,62 @@ def test_fixture_disallowed_between_marks():
|
||||
raise NotImplementedError()
|
||||
|
||||
assert len(record) == 2 # one for each mark decorator
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_nose_deprecated_with_setup(pytester: Pytester) -> None:
|
||||
pytest.importorskip("nose")
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
from nose.tools import with_setup
|
||||
|
||||
def setup_fn_no_op():
|
||||
...
|
||||
|
||||
def teardown_fn_no_op():
|
||||
...
|
||||
|
||||
@with_setup(setup_fn_no_op, teardown_fn_no_op)
|
||||
def test_omits_warnings():
|
||||
...
|
||||
"""
|
||||
)
|
||||
output = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||
message = [
|
||||
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
|
||||
"*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `setup_fn_no_op` (setup)",
|
||||
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
|
||||
"*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `teardown_fn_no_op` (teardown)",
|
||||
]
|
||||
output.stdout.fnmatch_lines(message)
|
||||
output.assert_outcomes(passed=1)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_nose_deprecated_setup_teardown(pytester: Pytester) -> None:
|
||||
pytest.importorskip("nose")
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
class Test:
|
||||
|
||||
def setup(self):
|
||||
...
|
||||
|
||||
def teardown(self):
|
||||
...
|
||||
|
||||
def test(self):
|
||||
...
|
||||
"""
|
||||
)
|
||||
output = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning")
|
||||
message = [
|
||||
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
|
||||
"*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `setup(self)`",
|
||||
"*To remove this warning, rename it to `setup_method(self)`",
|
||||
"*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.",
|
||||
"*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `teardown(self)`",
|
||||
"*To remove this warning, rename it to `teardown_method(self)`",
|
||||
]
|
||||
output.stdout.fnmatch_lines(message)
|
||||
output.assert_outcomes(passed=1)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# mypy: allow-untyped-defs
|
||||
"""Reproduces issue #3774"""
|
||||
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
# mypy: allow-untyped-defs
|
||||
def test_init():
|
||||
pass
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user