Merge branch 'main' into test-case-verbosity

This commit is contained in:
Patrick Lannigan 2024-02-24 10:45:00 -05:00
commit 6bae32378e
No known key found for this signature in database
GPG Key ID: BBF5D9DED1E4AAF9
265 changed files with 4704 additions and 3311 deletions

View File

@ -23,6 +23,11 @@ afc607cfd81458d4e4f3b1f3cf8cc931b933907e
5f95dce95602921a70bfbc7d8de2f7712c5e4505 5f95dce95602921a70bfbc7d8de2f7712c5e4505
# ran pyupgrade-docs again # ran pyupgrade-docs again
75d0b899bbb56d6849e9d69d83a9426ed3f43f8b 75d0b899bbb56d6849e9d69d83a9426ed3f43f8b
# move argument parser to own file # move argument parser to own file
c9df77cbd6a365dcb73c39618e4842711817e871 c9df77cbd6a365dcb73c39618e4842711817e871
# Replace reorder-python-imports by isort due to black incompatibility (#11896)
8b54596639f41dfac070030ef20394b9001fe63c
# Run blacken-docs with black's 2024's style
4546d5445aaefe6a03957db028c263521dfb5c4b
# Migration to ruff / ruff format
4588653b2497ed25976b7aaff225b889fb476756

View File

@ -26,7 +26,7 @@ jobs:
persist-credentials: false persist-credentials: false
- name: Build and Check Package - name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.0.0 uses: hynek/build-and-inspect-python-package@v2.0.1
deploy: deploy:
if: github.repository == 'pytest-dev/pytest' if: github.repository == 'pytest-dev/pytest'
@ -72,6 +72,12 @@ jobs:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Download Package
uses: actions/download-artifact@v4
with:
name: Packages
path: dist
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:

View File

@ -35,7 +35,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Build and Check Package - name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.0.0 uses: hynek/build-and-inspect-python-package@v2.0.1
build: build:
needs: [package] needs: [package]
@ -205,7 +205,7 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: "matrix.use_coverage" if: "matrix.use_coverage"
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v4
continue-on-error: true continue-on-error: true
with: with:
fail_ci_if_error: true fail_ci_if_error: true

View File

@ -30,7 +30,7 @@ jobs:
python-version: "3.11" python-version: "3.11"
cache: pip cache: pip
- name: requests-cache - name: requests-cache
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.cache/pytest-plugin-list/ path: ~/.cache/pytest-plugin-list/
key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well
@ -46,7 +46,7 @@ jobs:
run: python scripts/update-plugin-list.py run: python scripts/update-plugin-list.py
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50
with: with:
commit-message: '[automated] Update plugin list' commit-message: '[automated] Update plugin list'
author: 'pytest bot <pytestbot@users.noreply.github.com>' author: 'pytest bot <pytestbot@users.noreply.github.com>'

View File

@ -1,14 +1,10 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/astral-sh/ruff-pre-commit
rev: 23.12.1 rev: "v0.2.2"
hooks: hooks:
- id: black - id: ruff
args: [--safe, --quiet] args: ["--fix"]
- repo: https://github.com/asottile/blacken-docs - id: ruff-format
rev: 1.16.0
hooks:
- id: blacken-docs
additional_dependencies: [black==23.7.0]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0 rev: v4.5.0
hooks: hooks:
@ -20,37 +16,11 @@ repos:
- id: debug-statements - id: debug-statements
exclude: _pytest/(debugging|hookspec).py exclude: _pytest/(debugging|hookspec).py
language_version: python3 language_version: python3
- repo: https://github.com/PyCQA/autoflake - repo: https://github.com/adamchainz/blacken-docs
rev: v2.2.1 rev: 1.16.0
hooks: hooks:
- id: autoflake - id: blacken-docs
name: autoflake additional_dependencies: [black==24.1.1]
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
language: python
files: \.py$
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
language_version: python3
additional_dependencies:
- flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.12.0
hooks:
- id: reorder-python-imports
args: ['--application-directories=.:src', --py38-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.5.0
hooks:
- id: setup-cfg-fmt
args: ["--max-py-version=3.12", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0 rev: v1.10.0
hooks: hooks:
@ -64,6 +34,7 @@ repos:
additional_dependencies: additional_dependencies:
- iniconfig>=1.1.0 - iniconfig>=1.1.0
- attrs>=19.2.0 - attrs>=19.2.0
- pluggy>=1.4.0
- packaging - packaging
- tomli - tomli
- types-pkg_resources - types-pkg_resources
@ -71,6 +42,12 @@ repos:
# for mypy running on python>=3.11 since exceptiongroup is only a dependency # for mypy running on python>=3.11 since exceptiongroup is only a dependency
# on <3.11 # on <3.11
- exceptiongroup>=1.0.0rc8 - exceptiongroup>=1.0.0rc8
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "1.7.0"
hooks:
- id: pyproject-fmt
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
additional_dependencies: ["tox>=4.9"]
- repo: local - repo: local
hooks: hooks:
- id: rst - id: rst

View File

@ -56,6 +56,7 @@ Babak Keyvani
Barney Gale Barney Gale
Ben Brown Ben Brown
Ben Gartner Ben Gartner
Ben Leith
Ben Webb Ben Webb
Benjamin Peterson Benjamin Peterson
Benjamin Schubert Benjamin Schubert
@ -93,6 +94,7 @@ Christopher Dignam
Christopher Gilling Christopher Gilling
Claire Cecil Claire Cecil
Claudio Madotto Claudio Madotto
Clément M.T. Robert
CrazyMerlyn CrazyMerlyn
Cristian Vera Cristian Vera
Cyrus Maden Cyrus Maden
@ -126,6 +128,8 @@ Edison Gustavo Muenz
Edoardo Batini Edoardo Batini
Edson Tadeu M. Manoel Edson Tadeu M. Manoel
Eduardo Schettino Eduardo Schettino
Edward Haigh
Eero Vaher
Eli Boyarski Eli Boyarski
Elizaveta Shashkova Elizaveta Shashkova
Éloi Rivard Éloi Rivard
@ -141,6 +145,7 @@ Evgeny Seliverstov
Fabian Sturm Fabian Sturm
Fabien Zarifian Fabien Zarifian
Fabio Zadrozny Fabio Zadrozny
faph
Felix Hofstätter Felix Hofstätter
Felix Nieuwenhuizen Felix Nieuwenhuizen
Feng Ma Feng Ma
@ -339,6 +344,7 @@ Ronny Pfannschmidt
Ross Lawley Ross Lawley
Ruaridh Williamson Ruaridh Williamson
Russel Winder Russel Winder
Russell Martin
Ryan Puddephatt Ryan Puddephatt
Ryan Wooden Ryan Wooden
Sadra Barikbin Sadra Barikbin
@ -413,6 +419,7 @@ Vivaan Verma
Vlad Dragos Vlad Dragos
Vlad Radziuk Vlad Radziuk
Vladyslav Rachek Vladyslav Rachek
Volodymyr Kochetkov
Volodymyr Piskun Volodymyr Piskun
Wei Lin Wei Lin
Wil Cooley Wil Cooley

View File

@ -27,9 +27,6 @@
:target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main :target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main
:alt: pre-commit.ci status :alt: pre-commit.ci status
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg .. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
:target: https://www.codetriage.com/pytest-dev/pytest :target: https://www.codetriage.com/pytest-dev/pytest

View File

@ -23,7 +23,6 @@ members of the `contributors team`_ interested in receiving funding.
The current list of contributors receiving funding are: The current list of contributors receiving funding are:
* `@asottile`_
* `@nicoddemus`_ * `@nicoddemus`_
* `@The-Compiler`_ * `@The-Compiler`_
@ -55,6 +54,5 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the
.. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members .. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members
.. _`agreement`: https://tidelift.com/docs/lifting/agreement .. _`agreement`: https://tidelift.com/docs/lifting/agreement
.. _`@asottile`: https://github.com/asottile
.. _`@nicoddemus`: https://github.com/nicoddemus .. _`@nicoddemus`: https://github.com/nicoddemus
.. _`@The-Compiler`: https://github.com/The-Compiler .. _`@The-Compiler`: https://github.com/The-Compiler

View File

@ -1,10 +1,12 @@
import sys import sys
if __name__ == "__main__": if __name__ == "__main__":
import cProfile import cProfile
import pytest # NOQA
import pstats import pstats
import pytest # noqa: F401
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"] script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
cProfile.run("pytest.cmdline.main(%r)" % script, "prof") cProfile.run("pytest.cmdline.main(%r)" % script, "prof")
p = pstats.Stats("prof") p = pstats.Stats("prof")

View File

@ -4,6 +4,7 @@
# FastFilesCompleter 0.7383 1.0760 # FastFilesCompleter 0.7383 1.0760
import timeit import timeit
imports = [ imports = [
"from argcomplete.completers import FilesCompleter as completer", "from argcomplete.completers import FilesCompleter as completer",
"from _pytest._argcomplete import FastFilesCompleter as completer", "from _pytest._argcomplete import FastFilesCompleter as completer",

View File

@ -1,5 +1,6 @@
import pytest import pytest
SKIP = True SKIP = True

View File

@ -1,5 +1,6 @@
from unittest import TestCase # noqa: F401 from unittest import TestCase # noqa: F401
for i in range(15000): for i in range(15000):
exec( exec(
f""" f"""

View File

@ -0,0 +1,3 @@
:func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`.
Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion).
While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing.

View File

@ -1,5 +0,0 @@
Improvements to how ``-r`` for xfailures and xpasses:
* Report tracebacks for xfailures when ``-rx`` is set.
* Report captured output for xpasses when ``-rX`` is set.
* For xpasses, add ``-`` in summary between test name and reason, to match how xfail is displayed.

View File

@ -0,0 +1,4 @@
When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
as the relative directory.
Previoulsy this would raise an :class:`AssertionError`.

View File

@ -1 +0,0 @@
Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.

View File

@ -1,2 +0,0 @@
Fixed ``IndexError: string index out of range`` crash in ``if highlighted[-1] == "\n" and source[-1] != "\n"``.
This bug was introduced in pytest 8.0.0rc1.

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

@ -0,0 +1 @@
Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail.

View File

@ -1,2 +1,2 @@
Added the :func:`iterparents() <_pytest.nodes.Node.iterparents>` helper method on nodes. Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes.
It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list. It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list.

View File

@ -0,0 +1 @@
Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12.

View File

@ -0,0 +1 @@
Fix collection on Windows where initial paths contain the short version of a path (for example ``c:\PROGRA~1\tests``).

View File

@ -0,0 +1 @@
Fix an ``IndexError`` crash raising from ``getstatementrange_ast``.

View File

@ -0,0 +1 @@
In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``.

View File

@ -0,0 +1,3 @@
Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``.
Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging.

View File

@ -0,0 +1 @@
Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed.

View File

@ -0,0 +1 @@
Fix the ``stacklevel`` used when warning about marks used on fixtures.

View File

@ -0,0 +1 @@
Reverted a fix to `--maxfail` handling in pytest 8.0.0 because it caused a regression in pytest-xdist whereby session fixture teardowns may get executed multiple times when the max-fails is reached.

View File

@ -5,11 +5,10 @@
<div id="searchbox" style="display: none" role="search"> <div id="searchbox" style="display: none" role="search">
<div class="searchformwrapper"> <div class="searchformwrapper">
<form class="search" action="{{ pathto('search') }}" method="get"> <form class="search" action="{{ pathto('search') }}" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" <input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
placeholder="Search"/>
<input type="submit" value="{{ _('Go') }}" /> <input type="submit" value="{{ _('Go') }}" />
</form> </form>
</div> </div>
</div> </div>
<script type="text/javascript">$('#searchbox').show(0);</script> <script>document.getElementById('searchbox').style.display = "block"</script>
{%- endif %} {%- endif %}

View File

@ -6,6 +6,9 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-8.0.1
release-8.0.0
release-8.0.0rc2
release-8.0.0rc1 release-8.0.0rc1
release-7.4.4 release-7.4.4
release-7.4.3 release-7.4.3

View File

@ -0,0 +1,26 @@
pytest-8.0.0
=======================================
The pytest team is proud to announce the 8.0.0 release!
This release contains new features, improvements, bug fixes, and breaking changes, so users
are encouraged to take a look at the CHANGELOG carefully:
https://docs.pytest.org/en/stable/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/stable/
As usual, you can upgrade from PyPI via:
pip install -U pytest
Thanks to all of the contributors to this release:
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team

View File

@ -0,0 +1,32 @@
pytest-8.0.0rc2
=======================================
The pytest team is proud to announce the 8.0.0rc2 prerelease!
This is a prerelease, not intended for production use, but to test the upcoming features and improvements
in order to catch any major problems before the final version is released to the major public.
We appreciate your help testing this out before the final release, making sure to report any
regressions to our issue tracker:
https://github.com/pytest-dev/pytest/issues
When doing so, please include the string ``[prerelease]`` in the title.
You can upgrade from PyPI via:
pip install pytest==8.0.0rc2
Users are encouraged to take a look at the CHANGELOG carefully:
https://docs.pytest.org/en/release-8.0.0rc2/changelog.html
Thanks to all the contributors to this release:
* Ben Brown
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team

View File

@ -0,0 +1,21 @@
pytest-8.0.1
=======================================
pytest 8.0.1 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Bruno Oliveira
* Clément Robert
* Pierre Sassoulas
* Ran Benita
Happy testing,
The pytest Development Team

View File

@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
cachedir: .pytest_cache cachedir: .pytest_cache
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 0 items collected 0 items
cache -- .../_pytest/cacheprovider.py:526 cache -- .../_pytest/cacheprovider.py:527
Return a cache object that can persist state between testing sessions. Return a cache object that can persist state between testing sessions.
cache.get(key, default) cache.get(key, default)
@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Values can be any object handled by the json stdlib module. Values can be any object handled by the json stdlib module.
capsysbinary -- .../_pytest/capture.py:1008 capsysbinary -- .../_pytest/capture.py:1007
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsysbinary.readouterr()`` The captured output is made available via ``capsysbinary.readouterr()``
@ -43,7 +43,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example: Example:
.. code-block:: python .. code-block:: python
def test_output(capsysbinary): def test_output(capsysbinary):
@ -51,7 +50,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsysbinary.readouterr() captured = capsysbinary.readouterr()
assert captured.out == b"hello\n" assert captured.out == b"hello\n"
capfd -- .../_pytest/capture.py:1036 capfd -- .../_pytest/capture.py:1034
Enable text capturing of writes to file descriptors ``1`` and ``2``. Enable text capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method The captured output is made available via ``capfd.readouterr()`` method
@ -61,7 +60,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example: Example:
.. code-block:: python .. code-block:: python
def test_system_echo(capfd): def test_system_echo(capfd):
@ -69,7 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capfd.readouterr() captured = capfd.readouterr()
assert captured.out == "hello\n" assert captured.out == "hello\n"
capfdbinary -- .../_pytest/capture.py:1064 capfdbinary -- .../_pytest/capture.py:1061
Enable bytes capturing of writes to file descriptors ``1`` and ``2``. Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method The captured output is made available via ``capfd.readouterr()`` method
@ -79,7 +77,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example: Example:
.. code-block:: python .. code-block:: python
def test_system_echo(capfdbinary): def test_system_echo(capfdbinary):
@ -97,7 +94,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example: Example:
.. code-block:: python .. code-block:: python
def test_output(capsys): def test_output(capsys):
@ -105,7 +101,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsys.readouterr() captured = capsys.readouterr()
assert captured.out == "hello\n" assert captured.out == "hello\n"
doctest_namespace [session scope] -- .../_pytest/doctest.py:743 doctest_namespace [session scope] -- .../_pytest/doctest.py:745
Fixture that returns a :py:class:`dict` that will be injected into the Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests. namespace of doctests.
@ -119,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`. For more details: :ref:`doctest_namespace`.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1365 pytestconfig [session scope] -- .../_pytest/fixtures.py:1354
Session-scoped fixture that returns the session's :class:`pytest.Config` Session-scoped fixture that returns the session's :class:`pytest.Config`
object. object.
@ -129,7 +125,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
if pytestconfig.getoption("verbose") > 0: if pytestconfig.getoption("verbose") > 0:
... ...
record_property -- .../_pytest/junitxml.py:282 record_property -- .../_pytest/junitxml.py:283
Add extra properties to the calling test. Add extra properties to the calling test.
User properties become part of the test report and are available to the User properties become part of the test report and are available to the
@ -143,13 +139,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
def test_function(record_property): def test_function(record_property):
record_property("example_key", 1) record_property("example_key", 1)
record_xml_attribute -- .../_pytest/junitxml.py:305 record_xml_attribute -- .../_pytest/junitxml.py:306
Add extra xml attributes to the tag for the calling test. Add extra xml attributes to the tag for the calling test.
The fixture is callable with ``name, value``. The value is The fixture is callable with ``name, value``. The value is
automatically XML-encoded. automatically XML-encoded.
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:343 record_testsuite_property [session scope] -- .../_pytest/junitxml.py:344
Record a new ``<property>`` tag as child of the root ``<testsuite>``. Record a new ``<property>`` tag as child of the root ``<testsuite>``.
This is suitable to writing global information regarding the entire test This is suitable to writing global information regarding the entire test
@ -174,10 +170,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See `pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
:issue:`7767` for details. :issue:`7767` for details.
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:300 tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
Return a :class:`pytest.TempdirFactory` instance for the test session. Return a :class:`pytest.TempdirFactory` instance for the test session.
tmpdir -- .../_pytest/legacypath.py:307 tmpdir -- .../_pytest/legacypath.py:309
Return a temporary directory path object which is unique to each test Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary function invocation, created as a sub directory of the base temporary
directory. directory.
@ -196,7 +192,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:593 caplog -- .../_pytest/logging.py:594
Access and control log capturing. Access and control log capturing.
Captured logs are available through the following properties/methods:: Captured logs are available through the following properties/methods::
@ -207,7 +203,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
* caplog.record_tuples -> list of (logger_name, level, message) tuples * caplog.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string * caplog.clear() -> clear captured records and formatted log output string
monkeypatch -- .../_pytest/monkeypatch.py:30 monkeypatch -- .../_pytest/monkeypatch.py:32
A convenient fixture for monkey-patching. A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or The fixture provides these methods to modify objects, dictionaries, or
@ -231,16 +227,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
To undo modifications done by the fixture in a contained scope, To undo modifications done by the fixture in a contained scope,
use :meth:`context() <pytest.MonkeyPatch.context>`. use :meth:`context() <pytest.MonkeyPatch.context>`.
recwarn -- .../_pytest/recwarn.py:30 recwarn -- .../_pytest/recwarn.py:32
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
on warning categories. on warning categories.
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:239 tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:241
Return a :class:`pytest.TempPathFactory` instance for the test session. Return a :class:`pytest.TempPathFactory` instance for the test session.
tmp_path -- .../_pytest/tmpdir.py:254 tmp_path -- .../_pytest/tmpdir.py:256
Return a temporary directory path object which is unique to each test Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary function invocation, created as a sub directory of the base temporary
directory. directory.

View File

@ -28,6 +28,75 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
pytest 8.0.1 (2024-02-16)
=========================
Bug Fixes
---------
- `#11875 <https://github.com/pytest-dev/pytest/issues/11875>`_: Correctly handle errors from :func:`getpass.getuser` in Python 3.13.
- `#11879 <https://github.com/pytest-dev/pytest/issues/11879>`_: Fix an edge case where ``ExceptionInfo._stringify_exception`` could crash :func:`pytest.raises`.
- `#11906 <https://github.com/pytest-dev/pytest/issues/11906>`_: Fix regression with :func:`pytest.warns` using custom warning subclasses which have more than one parameter in their `__init__`.
- `#11907 <https://github.com/pytest-dev/pytest/issues/11907>`_: Fix a regression in pytest 8.0.0 whereby calling :func:`pytest.skip` and similar control-flow exceptions within a :func:`pytest.warns()` block would get suppressed instead of propagating.
- `#11929 <https://github.com/pytest-dev/pytest/issues/11929>`_: Fix a regression in pytest 8.0.0 whereby autouse fixtures defined in a module get ignored by the doctests in the module.
- `#11937 <https://github.com/pytest-dev/pytest/issues/11937>`_: Fix a regression in pytest 8.0.0 whereby items would be collected in reverse order in some circumstances.
pytest 8.0.0 (2024-01-27)
=========================
Bug Fixes
---------
- `#11842 <https://github.com/pytest-dev/pytest/issues/11842>`_: Properly escape the ``reason`` of a :ref:`skip <pytest.mark.skip ref>` mark when writing JUnit XML files.
- `#11861 <https://github.com/pytest-dev/pytest/issues/11861>`_: Avoid microsecond exceeds ``1_000_000`` when using ``log-date-format`` with ``%f`` specifier, which might cause the test suite to crash.
pytest 8.0.0rc2 (2024-01-17)
============================
Improvements
------------
- `#11233 <https://github.com/pytest-dev/pytest/issues/11233>`_: Improvements to ``-r`` for xfailures and xpasses:
* Report tracebacks for xfailures when ``-rx`` is set.
* Report captured output for xpasses when ``-rX`` is set.
* For xpasses, add ``-`` in summary between test name and reason, to match how xfail is displayed.
- `#11825 <https://github.com/pytest-dev/pytest/issues/11825>`_: The :hook:`pytest_plugin_registered` hook has a new ``plugin_name`` parameter containing the name by which ``plugin`` is registered.
Bug Fixes
---------
- `#11706 <https://github.com/pytest-dev/pytest/issues/11706>`_: Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.
NOTE: This change was reverted in pytest 8.0.2 to fix a `regression <https://github.com/pytest-dev/pytest-xdist/issues/1024>`_ it caused in pytest-xdist.
- `#11758 <https://github.com/pytest-dev/pytest/issues/11758>`_: Fixed ``IndexError: string index out of range`` crash in ``if highlighted[-1] == "\n" and source[-1] != "\n"``.
This bug was introduced in pytest 8.0.0rc1.
- `#9765 <https://github.com/pytest-dev/pytest/issues/9765>`_, `#11816 <https://github.com/pytest-dev/pytest/issues/11816>`_: Fixed a frustrating bug that afflicted some users with the only error being ``assert mod not in mods``. The issue was caused by the fact that ``str(Path(mod))`` and ``mod.__file__`` don't necessarily produce the same string, and was being erroneously used interchangably in some places in the code.
This fix also broke the internal API of ``PytestPluginManager.consider_conftest`` by introducing a new parameter -- we mention this in case it is being used by external code, even if marked as *private*.
pytest 8.0.0rc1 (2023-12-30) pytest 8.0.0rc1 (2023-12-30)
============================ ============================
@ -223,6 +292,10 @@ These are breaking changes where deprecation was not possible.
therefore fail on the newly-re-emitted warnings. therefore fail on the newly-re-emitted warnings.
- The internal ``FixtureManager.getfixtureclosure`` method has changed. Plugins which use this method or
which subclass ``FixtureManager`` and overwrite that method will need to adapt to the change.
Deprecations Deprecations
------------ ------------

View File

@ -23,6 +23,7 @@ from typing import TYPE_CHECKING
from _pytest import __version__ as version from _pytest import __version__ as version
if TYPE_CHECKING: if TYPE_CHECKING:
import sphinx.application import sphinx.application
@ -440,9 +441,10 @@ intersphinx_mapping = {
def configure_logging(app: "sphinx.application.Sphinx") -> None: def configure_logging(app: "sphinx.application.Sphinx") -> None:
"""Configure Sphinx's WarningHandler to handle (expected) missing include.""" """Configure Sphinx's WarningHandler to handle (expected) missing include."""
import sphinx.util.logging
import logging import logging
import sphinx.util.logging
class WarnLogFilter(logging.Filter): class WarnLogFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool: def filter(self, record: logging.LogRecord) -> bool:
"""Ignore warnings about missing include with "only" directive. """Ignore warnings about missing include with "only" directive.

View File

@ -33,13 +33,11 @@ have been available since years and should be used instead.
.. code-block:: python .. code-block:: python
@pytest.mark.tryfirst @pytest.mark.tryfirst
def pytest_runtest_call(): def pytest_runtest_call(): ...
...
# or # or
def pytest_runtest_call(): def pytest_runtest_call(): ...
...
pytest_runtest_call.tryfirst = True pytest_runtest_call.tryfirst = True
@ -49,8 +47,7 @@ should be changed to:
.. code-block:: python .. code-block:: python
@pytest.hookimpl(tryfirst=True) @pytest.hookimpl(tryfirst=True)
def pytest_runtest_call(): def pytest_runtest_call(): ...
...
Changed ``hookimpl`` attributes: Changed ``hookimpl`` attributes:
@ -146,8 +143,7 @@ Applying a mark to a fixture function never had any effect, but it is a common u
@pytest.mark.usefixtures("clean_database") @pytest.mark.usefixtures("clean_database")
@pytest.fixture @pytest.fixture
def user() -> User: def user() -> User: ...
...
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all. Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
@ -308,11 +304,9 @@ they are in fact part of the ``nose`` support.
def teardown(self): def teardown(self):
self.resource.close() self.resource.close()
def test_foo(self): def test_foo(self): ...
...
def test_bar(self): def test_bar(self): ...
...
@ -327,11 +321,9 @@ Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`x
def teardown_method(self): def teardown_method(self):
self.resource.close() self.resource.close()
def test_foo(self): def test_foo(self): ...
...
def test_bar(self): def test_bar(self): ...
...
This is easy to do in an entire code base by doing a simple find/replace. This is easy to do in an entire code base by doing a simple find/replace.
@ -346,17 +338,14 @@ Code using `@with_setup <with-setup-nose>`_ such as this:
from nose.tools import with_setup from nose.tools import with_setup
def setup_some_resource(): def setup_some_resource(): ...
...
def teardown_some_resource(): def teardown_some_resource(): ...
...
@with_setup(setup_some_resource, teardown_some_resource) @with_setup(setup_some_resource, teardown_some_resource)
def test_foo(): def test_foo(): ...
...
Will also need to be ported to a supported pytest style. One way to do it is using a fixture: Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
@ -365,12 +354,10 @@ Will also need to be ported to a supported pytest style. One way to do it is usi
import pytest import pytest
def setup_some_resource(): def setup_some_resource(): ...
...
def teardown_some_resource(): def teardown_some_resource(): ...
...
@pytest.fixture @pytest.fixture
@ -380,8 +367,7 @@ Will also need to be ported to a supported pytest style. One way to do it is usi
teardown_some_resource() teardown_some_resource()
def test_foo(some_resource): def test_foo(some_resource): ...
...
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup .. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
@ -500,8 +486,7 @@ Implement the :hook:`pytest_load_initial_conftests` hook instead.
.. code-block:: python .. code-block:: python
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None: def pytest_cmdline_preparse(config: Config, args: List[str]) -> None: ...
...
# becomes: # becomes:
@ -509,8 +494,7 @@ Implement the :hook:`pytest_load_initial_conftests` hook instead.
def pytest_load_initial_conftests( def pytest_load_initial_conftests(
early_config: Config, parser: Parser, args: List[str] early_config: Config, parser: Parser, args: List[str]
) -> None: ) -> None: ...
...
Collection changes in pytest 8 Collection changes in pytest 8
@ -925,8 +909,7 @@ Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated
(50, 500), (50, 500),
], ],
) )
def test_foo(a, b): def test_foo(a, b): ...
...
This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization
call. call.
@ -949,8 +932,7 @@ To update the code, use ``pytest.param``:
(50, 500), (50, 500),
], ],
) )
def test_foo(a, b): def test_foo(a, b): ...
...
.. _pytest_funcarg__ prefix deprecated: .. _pytest_funcarg__ prefix deprecated:
@ -1101,15 +1083,13 @@ This is just a matter of renaming the fixture as the API is the same:
.. code-block:: python .. code-block:: python
def test_foo(record_xml_property): def test_foo(record_xml_property): ...
...
Change to: Change to:
.. code-block:: python .. code-block:: python
def test_foo(record_property): def test_foo(record_property): ...
...
.. _passing command-line string to pytest.main deprecated: .. _passing command-line string to pytest.main deprecated:
@ -1271,8 +1251,7 @@ Example of usage:
.. code-block:: python .. code-block:: python
class MySymbol: class MySymbol: ...
...
def pytest_namespace(): def pytest_namespace():

View File

@ -172,7 +172,7 @@ class TestRaises:
raise ValueError("demo error") raise ValueError("demo error")
def test_tupleerror(self): def test_tupleerror(self):
a, b = [1] # NOQA a, b = [1] # noqa: F841
def test_reinterpret_fails_with_print_for_the_fun_of_it(self): def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
items = [1, 2, 3] items = [1, 2, 3]
@ -180,7 +180,7 @@ class TestRaises:
a, b = items.pop() a, b = items.pop()
def test_some_error(self): def test_some_error(self):
if namenotexi: # NOQA if namenotexi: # noqa: F821
pass pass
def func1(self): def func1(self):

View File

@ -2,6 +2,7 @@ import os.path
import pytest import pytest
mydir = os.path.dirname(__file__) mydir = os.path.dirname(__file__)

View File

@ -1,6 +1,7 @@
import os.path import os.path
import shutil import shutil
failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py") failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py")
pytest_plugins = ("pytester",) pytest_plugins = ("pytester",)

View File

@ -1,5 +1,6 @@
"""Module containing a parametrized tests testing cross-python serialization """Module containing a parametrized tests testing cross-python serialization
via the pickle module.""" via the pickle module."""
import shutil import shutil
import subprocess import subprocess
import textwrap import textwrap
@ -32,14 +33,12 @@ class Python:
dumpfile = self.picklefile.with_name("dump.py") dumpfile = self.picklefile.with_name("dump.py")
dumpfile.write_text( dumpfile.write_text(
textwrap.dedent( textwrap.dedent(
r""" rf"""
import pickle import pickle
f = open({!r}, 'wb') f = open({str(self.picklefile)!r}, 'wb')
s = pickle.dump({!r}, f, protocol=2) s = pickle.dump({obj!r}, f, protocol=2)
f.close() f.close()
""".format( """
str(self.picklefile), obj
)
) )
) )
subprocess.run((self.pythonpath, str(dumpfile)), check=True) subprocess.run((self.pythonpath, str(dumpfile)), check=True)
@ -48,17 +47,15 @@ class Python:
loadfile = self.picklefile.with_name("load.py") loadfile = self.picklefile.with_name("load.py")
loadfile.write_text( loadfile.write_text(
textwrap.dedent( textwrap.dedent(
r""" rf"""
import pickle import pickle
f = open({!r}, 'rb') f = open({str(self.picklefile)!r}, 'rb')
obj = pickle.load(f) obj = pickle.load(f)
f.close() f.close()
res = eval({!r}) res = eval({expression!r})
if not res: if not res:
raise SystemExit(1) raise SystemExit(1)
""".format( """
str(self.picklefile), expression
)
) )
) )
print(loadfile) print(loadfile)

View File

@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 8 items collected 8 items
<Dir parametrize.rst-189> <Dir parametrize.rst-193>
<Module test_time.py> <Module test_time.py>
<Function test_timedistance_v0[a0-b0-expected0]> <Function test_timedistance_v0[a0-b0-expected0]>
<Function test_timedistance_v0[a1-b1-expected1]> <Function test_timedistance_v0[a1-b1-expected1]>
@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 4 items collected 4 items
<Dir parametrize.rst-189> <Dir parametrize.rst-193>
<Module test_scenarios.py> <Module test_scenarios.py>
<Class TestSampleWithScenarios> <Class TestSampleWithScenarios>
<Function test_demo1[basic]> <Function test_demo1[basic]>
@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 2 items collected 2 items
<Dir parametrize.rst-189> <Dir parametrize.rst-193>
<Module test_backends.py> <Module test_backends.py>
<Function test_db_initialized[d1]> <Function test_db_initialized[d1]>
<Function test_db_initialized[d2]> <Function test_db_initialized[d2]>
@ -505,8 +505,8 @@ Running it results in some skips if we don't have all the python interpreters in
. $ pytest -rs -q multipython.py . $ pytest -rs -q multipython.py
ssssssssssss...ssssssssssss [100%] ssssssssssss...ssssssssssss [100%]
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIPPED [12] multipython.py:68: 'python3.9' not found SKIPPED [12] multipython.py:65: 'python3.9' not found
SKIPPED [12] multipython.py:68: 'python3.11' not found SKIPPED [12] multipython.py:65: 'python3.11' not found
3 passed, 24 skipped in 0.12s 3 passed, 24 skipped in 0.12s
Parametrization of optional implementations/imports Parametrization of optional implementations/imports

View File

@ -152,7 +152,7 @@ The test collection would look like this:
configfile: pytest.ini configfile: pytest.ini
collected 2 items collected 2 items
<Dir pythoncollection.rst-190> <Dir pythoncollection.rst-194>
<Module check_myapp.py> <Module check_myapp.py>
<Class CheckMyApp> <Class CheckMyApp>
<Function simple_check> <Function simple_check>
@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
configfile: pytest.ini configfile: pytest.ini
collected 3 items collected 3 items
<Dir pythoncollection.rst-190> <Dir pythoncollection.rst-194>
<Dir CWD> <Dir CWD>
<Module pythoncollection.py> <Module pythoncollection.py>
<Function test_function> <Function test_function>

View File

@ -660,6 +660,31 @@ If we run this:
E assert 0 E assert 0
test_step.py:11: AssertionError test_step.py:11: AssertionError
================================ XFAILURES =================================
______________________ TestUserHandling.test_deletion ______________________
item = <Function test_deletion>
def pytest_runtest_setup(item):
if "incremental" in item.keywords:
# retrieve the class name of the test
cls_name = str(item.cls)
# check if a previous test has failed for this class
if cls_name in _test_failed_incremental:
# retrieve the index of the test (if parametrize is used in combination with incremental)
parametrize_index = (
tuple(item.callspec.indices.values())
if hasattr(item, "callspec")
else ()
)
# retrieve the name of the first test function to fail for this class name and index
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
# if name found, test has failed for the combination of class name & test name
if test_name is not None:
> pytest.xfail(f"previous test failed ({test_name})")
E _pytest.outcomes.XFailed: previous test failed (test_modification)
conftest.py:47: XFailed
========================= short test summary info ========================== ========================= short test summary info ==========================
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification) XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
================== 1 failed, 2 passed, 1 xfailed in 0.12s ================== ================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================

View File

@ -1,5 +1,6 @@
import pytest import pytest
xfail = pytest.mark.xfail xfail = pytest.mark.xfail

View File

@ -99,8 +99,7 @@ sets. pytest-2.3 introduces a decorator for use on the factory itself:
.. code-block:: python .. code-block:: python
@pytest.fixture(params=["mysql", "pg"]) @pytest.fixture(params=["mysql", "pg"])
def db(request): def db(request): ... # use request.param
... # use request.param
Here the factory will be invoked twice (with the respective "mysql" Here the factory will be invoked twice (with the respective "mysql"
and "pg" values set as ``request.param`` attributes) and all of and "pg" values set as ``request.param`` attributes) and all of
@ -141,8 +140,7 @@ argument:
.. code-block:: python .. code-block:: python
@pytest.fixture() @pytest.fixture()
def db(request): def db(request): ...
...
The name under which the funcarg resource can be requested is ``db``. The name under which the funcarg resource can be requested is ``db``.
@ -151,8 +149,7 @@ aka:
.. code-block:: python .. code-block:: python
def pytest_funcarg__db(request): def pytest_funcarg__db(request): ...
...
But it is then not possible to define scoping and parametrization. But it is then not possible to define scoping and parametrization.

View File

@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash .. code-block:: bash
$ pytest --version $ pytest --version
pytest 8.0.0rc1 pytest 8.0.1
.. _`simpletest`: .. _`simpletest`:

View File

@ -227,8 +227,7 @@ to use strings:
@pytest.mark.skipif("sys.version_info >= (3,3)") @pytest.mark.skipif("sys.version_info >= (3,3)")
def test_function(): def test_function(): ...
...
During test function setup the skipif condition is evaluated by calling During test function setup the skipif condition is evaluated by calling
``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains ``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains
@ -262,8 +261,7 @@ configuration value which you might have added:
.. code-block:: python .. code-block:: python
@pytest.mark.skipif("not config.getvalue('db')") @pytest.mark.skipif("not config.getvalue('db')")
def test_function(): def test_function(): ...
...
The equivalent with "boolean conditions" is: The equivalent with "boolean conditions" is:

View File

@ -154,7 +154,7 @@ method to test for exceptions returned as part of an :class:`ExceptionGroup`:
.. code-block:: python .. code-block:: python
def test_exception_in_group(): def test_exception_in_group():
with pytest.raises(RuntimeError) as excinfo: with pytest.raises(ExceptionGroup) as excinfo:
raise ExceptionGroup( raise ExceptionGroup(
"Group message", "Group message",
[ [
@ -176,7 +176,7 @@ exception at a specific level; exceptions contained directly in the top
.. code-block:: python .. code-block:: python
def test_exception_in_group_at_given_depth(): def test_exception_in_group_at_given_depth():
with pytest.raises(RuntimeError) as excinfo: with pytest.raises(ExceptionGroup) as excinfo:
raise ExceptionGroup( raise ExceptionGroup(
"Group message", "Group message",
[ [

View File

@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 12 items collected 12 items
<Dir fixtures.rst-208> <Dir fixtures.rst-212>
<Module test_anothersmtp.py> <Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]> <Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]> <Function test_showhelo[mail.python.org]>
@ -1721,8 +1721,7 @@ You can specify multiple fixtures like this:
.. code-block:: python .. code-block:: python
@pytest.mark.usefixtures("cleandir", "anotherfixture") @pytest.mark.usefixtures("cleandir", "anotherfixture")
def test(): def test(): ...
...
and you may specify fixture usage at the test module level using :globalvar:`pytestmark`: and you may specify fixture usage at the test module level using :globalvar:`pytestmark`:
@ -1750,8 +1749,7 @@ into an ini-file:
@pytest.mark.usefixtures("my_other_fixture") @pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture @pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture(): def my_fixture_that_sadly_wont_use_my_other_fixture(): ...
...
This generates a deprecation warning, and will become an error in Pytest 8. This generates a deprecation warning, and will become an error in Pytest 8.

View File

@ -206,8 +206,9 @@ option names are:
* ``log_cli_date_format`` * ``log_cli_date_format``
If you need to record the whole test suite logging calls to a file, you can pass If you need to record the whole test suite logging calls to a file, you can pass
``--log-file=/path/to/log/file``. This log file is opened in write mode which ``--log-file=/path/to/log/file``. This log file is opened in write mode by default which
means that it will be overwritten at each run tests session. means that it will be overwritten at each run tests session.
If you'd like the file opened in append mode instead, then you can pass ``--log-file-mode=a``.
Note that relative paths for the log-file location, whether passed on the CLI or declared in a Note that relative paths for the log-file location, whether passed on the CLI or declared in a
config file, are always resolved relative to the current working directory. config file, are always resolved relative to the current working directory.
@ -223,12 +224,13 @@ All of the log file options can also be set in the configuration INI file. The
option names are: option names are:
* ``log_file`` * ``log_file``
* ``log_file_mode``
* ``log_file_level`` * ``log_file_level``
* ``log_file_format`` * ``log_file_format``
* ``log_file_date_format`` * ``log_file_date_format``
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
is considered **experimental**. is considered **experimental**. Note that ``set_log_path()`` respects the ``log_file_mode`` option.
.. _log_colors: .. _log_colors:

View File

@ -406,10 +406,19 @@ Example:
E assert 0 E assert 0
test_example.py:14: AssertionError test_example.py:14: AssertionError
================================ XFAILURES =================================
________________________________ test_xfail ________________________________
def test_xfail():
> pytest.xfail("xfailing this test")
E _pytest.outcomes.XFailed: xfailing this test
test_example.py:26: XFailed
================================= XPASSES ==================================
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIPPED [1] test_example.py:22: skipping this test SKIPPED [1] test_example.py:22: skipping this test
XFAIL test_example.py::test_xfail - reason: xfailing this test XFAIL test_example.py::test_xfail - reason: xfailing this test
XPASS test_example.py::test_xpass always xfail XPASS test_example.py::test_xpass - always xfail
ERROR test_example.py::test_error - assert 0 ERROR test_example.py::test_error - assert 0
FAILED test_example.py::test_fail - assert 0 FAILED test_example.py::test_fail - assert 0
== 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s ===

View File

@ -47,8 +47,7 @@ which may be passed an optional ``reason``:
.. code-block:: python .. code-block:: python
@pytest.mark.skip(reason="no way of currently testing this") @pytest.mark.skip(reason="no way of currently testing this")
def test_the_unknown(): def test_the_unknown(): ...
...
Alternatively, it is also possible to skip imperatively during test execution or setup Alternatively, it is also possible to skip imperatively during test execution or setup
@ -93,8 +92,7 @@ when run on an interpreter earlier than Python3.10:
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher") @pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
def test_function(): def test_function(): ...
...
If the condition evaluates to ``True`` during collection, the test function will be skipped, If the condition evaluates to ``True`` during collection, the test function will be skipped,
with the specified reason appearing in the summary when using ``-rs``. with the specified reason appearing in the summary when using ``-rs``.
@ -112,8 +110,7 @@ You can share ``skipif`` markers between modules. Consider this test module:
@minversion @minversion
def test_function(): def test_function(): ...
...
You can import the marker and reuse it in another test module: You can import the marker and reuse it in another test module:
@ -124,8 +121,7 @@ You can import the marker and reuse it in another test module:
@minversion @minversion
def test_anotherfunction(): def test_anotherfunction(): ...
...
For larger test suites it's usually a good idea to have one file For larger test suites it's usually a good idea to have one file
where you define the markers which you then consistently apply where you define the markers which you then consistently apply
@ -232,8 +228,7 @@ expect a test to fail:
.. code-block:: python .. code-block:: python
@pytest.mark.xfail @pytest.mark.xfail
def test_function(): def test_function(): ...
...
This test will run but no traceback will be reported when it fails. Instead, terminal This test will run but no traceback will be reported when it fails. Instead, terminal
reporting will list it in the "expected to fail" (``XFAIL``) or "unexpectedly reporting will list it in the "expected to fail" (``XFAIL``) or "unexpectedly
@ -275,8 +270,7 @@ that condition as the first parameter:
.. code-block:: python .. code-block:: python
@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library") @pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
def test_function(): def test_function(): ...
...
Note that you have to pass a reason as well (see the parameter description at Note that you have to pass a reason as well (see the parameter description at
:ref:`pytest.mark.xfail ref`). :ref:`pytest.mark.xfail ref`).
@ -289,8 +283,7 @@ You can specify the motive of an expected failure with the ``reason`` parameter:
.. code-block:: python .. code-block:: python
@pytest.mark.xfail(reason="known parser issue") @pytest.mark.xfail(reason="known parser issue")
def test_function(): def test_function(): ...
...
``raises`` parameter ``raises`` parameter
@ -302,8 +295,7 @@ a single exception, or a tuple of exceptions, in the ``raises`` argument.
.. code-block:: python .. code-block:: python
@pytest.mark.xfail(raises=RuntimeError) @pytest.mark.xfail(raises=RuntimeError)
def test_function(): def test_function(): ...
...
Then the test will be reported as a regular failure if it fails with an Then the test will be reported as a regular failure if it fails with an
exception not mentioned in ``raises``. exception not mentioned in ``raises``.
@ -317,8 +309,7 @@ even executed, use the ``run`` parameter as ``False``:
.. code-block:: python .. code-block:: python
@pytest.mark.xfail(run=False) @pytest.mark.xfail(run=False)
def test_function(): def test_function(): ...
...
This is specially useful for xfailing tests that are crashing the interpreter and should be This is specially useful for xfailing tests that are crashing the interpreter and should be
investigated later. investigated later.
@ -334,8 +325,7 @@ You can change this by setting the ``strict`` keyword-only parameter to ``True``
.. code-block:: python .. code-block:: python
@pytest.mark.xfail(strict=True) @pytest.mark.xfail(strict=True)
def test_function(): def test_function(): ...
...
This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite. This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite.

View File

@ -8,9 +8,8 @@ How to use temporary directories and files in tests
The ``tmp_path`` fixture The ``tmp_path`` fixture
------------------------ ------------------------
You can use the ``tmp_path`` fixture which will You can use the ``tmp_path`` fixture which will provide a temporary directory
provide a temporary directory unique to the current test, unique to each test function.
created in the `base temporary directory`_.
``tmp_path`` is a :class:`pathlib.Path` object. Here is an example test usage: ``tmp_path`` is a :class:`pathlib.Path` object. Here is an example test usage:
@ -62,6 +61,11 @@ Running this would result in a passed test except for the last
FAILED test_tmp_path.py::test_create_file - assert 0 FAILED test_tmp_path.py::test_create_file - assert 0
============================ 1 failed in 0.12s ============================= ============================ 1 failed in 0.12s =============================
By default, ``pytest`` retains the temporary directory for the last 3 ``pytest``
invocations. Concurrent invocations of the same test function are supported by
configuring the base temporary directory to be unique for each concurrent
run. See `temporary directory location and retention`_ for details.
.. _`tmp_path_factory example`: .. _`tmp_path_factory example`:
The ``tmp_path_factory`` fixture The ``tmp_path_factory`` fixture
@ -100,7 +104,7 @@ See :ref:`tmp_path_factory API <tmp_path_factory factory api>` for details.
.. _tmpdir: .. _tmpdir:
The ``tmpdir`` and ``tmpdir_factory`` fixtures The ``tmpdir`` and ``tmpdir_factory`` fixtures
--------------------------------------------------- ----------------------------------------------
The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path`` The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path``
and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects
@ -124,10 +128,10 @@ See :fixture:`tmpdir <tmpdir>` :fixture:`tmpdir_factory <tmpdir_factory>`
API for details. API for details.
.. _`base temporary directory`: .. _`temporary directory location and retention`:
The default base temporary directory Temporary directory location and retention
----------------------------------------------- ------------------------------------------
Temporary directories are by default created as sub-directories of Temporary directories are by default created as sub-directories of
the system temporary directory. The base name will be ``pytest-NUM`` where the system temporary directory. The base name will be ``pytest-NUM`` where
@ -152,7 +156,7 @@ You can override the default temporary directory setting like this:
for that purpose only. for that purpose only.
When distributing tests on the local machine using ``pytest-xdist``, care is taken to When distributing tests on the local machine using ``pytest-xdist``, care is taken to
automatically configure a basetemp directory for the sub processes such that all temporary automatically configure a `basetemp` directory for the sub processes such that all temporary
data lands below a single per-test run basetemp directory. data lands below a single per-test run temporary directory.
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html .. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html

View File

@ -46,24 +46,18 @@ Plugin discovery order at tool startup
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable. 5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
6. by loading all :file:`conftest.py` files as inferred by the command line 6. by loading all "initial ":file:`conftest.py` files:
invocation:
- if no test paths are specified, use the current dir as a test path - determine the test paths: specified on the command line, otherwise in
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative :confval:`testpaths` if defined and running from the rootdir, otherwise the
to the directory part of the first test path. After the ``conftest.py`` current dir
file is loaded, load all plugins specified in its - for each test path, load ``conftest.py`` and ``test*/conftest.py`` relative
:globalvar:`pytest_plugins` variable if present. to the directory part of the test path, if exist. Before a ``conftest.py``
file is loaded, load ``conftest.py`` files in all of its parent directories.
Note that pytest does not find ``conftest.py`` files in deeper nested After a ``conftest.py`` file is loaded, recursively load all plugins specified
sub directories at tool startup. It is usually a good idea to keep in its :globalvar:`pytest_plugins` variable if present.
your ``conftest.py`` file in the top level test or project root directory.
7. by recursively loading all plugins specified by the
:globalvar:`pytest_plugins` variable in ``conftest.py`` files.
.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/
.. _`conftest.py plugins`: .. _`conftest.py plugins`:
.. _`localplugin`: .. _`localplugin`:
.. _`local conftest plugins`: .. _`local conftest plugins`:
@ -108,9 +102,9 @@ Here is how you might run it::
See also: :ref:`pythonpath`. See also: :ref:`pythonpath`.
.. note:: .. note::
Some hooks should be implemented only in plugins or conftest.py files situated at the Some hooks cannot be implemented in conftest.py files which are not
tests root directory due to how pytest discovers plugins during startup, :ref:`initial <pluginorder>` due to how pytest discovers plugins during
see the documentation of each hook for details. startup. See the documentation of each hook for details.
Writing your own plugin Writing your own plugin
----------------------- -----------------------

View File

@ -1,8 +1,11 @@
:orphan: :orphan:
.. sidebar:: Next Open Trainings .. sidebar:: Next Open Trainings and Events
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote** - `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
* **June 11th to 13th 2024**, Remote
* **March 4th to 6th 2025**, Leipzig, Germany / Remote
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, June 2024 (`date poll <https://nuudel.digitalcourage.de/2tEsEpRcwMNcAXVO>`_)
Also see :doc:`previous talks and blogposts <talks>`. Also see :doc:`previous talks and blogposts <talks>`.

View File

@ -177,13 +177,20 @@ Files will only be matched for configuration if:
* ``tox.ini``: contains a ``[pytest]`` section. * ``tox.ini``: contains a ``[pytest]`` section.
* ``setup.cfg``: contains a ``[tool:pytest]`` section. * ``setup.cfg``: contains a ``[tool:pytest]`` section.
Finally, a ``pyproject.toml`` file will be considered the ``configfile`` if no other match was found, in this case
even if it does not contain a ``[tool.pytest.ini_options]`` table (this was added in ``8.1``).
The files are considered in the order above. Options from multiple ``configfiles`` candidates The files are considered in the order above. Options from multiple ``configfiles`` candidates
are never merged - the first match wins. are never merged - the first match wins.
The configuration file also determines the value of the ``rootpath``.
The :class:`Config <pytest.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture) The :class:`Config <pytest.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
will subsequently carry these attributes: will subsequently carry these attributes:
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist. - :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist. It is used as
a reference directory for constructing test addresses ("nodeids") and can be used also by plugins for storing
per-testrun information.
- :attr:`config.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None`` - :attr:`config.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None``
(it is named ``inipath`` for historical reasons). (it is named ``inipath`` for historical reasons).
@ -193,9 +200,7 @@ will subsequently carry these attributes:
versions of the older ``config.rootdir`` and ``config.inifile``, which have type versions of the older ``config.rootdir`` and ``config.inifile``, which have type
``py.path.local``, and still exist for backward compatibility. ``py.path.local``, and still exist for backward compatibility.
The ``rootdir`` is used as a reference directory for constructing test
addresses ("nodeids") and can be used also by plugins for storing
per-testrun information.
Example: Example:

File diff suppressed because it is too large Load Diff

View File

@ -164,8 +164,7 @@ Add warning filters to marked test items.
.. code-block:: python .. code-block:: python
@pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning") @pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning")
def test_foo(): def test_foo(): ...
...
.. _`pytest.mark.parametrize ref`: .. _`pytest.mark.parametrize ref`:
@ -276,8 +275,7 @@ For example:
.. code-block:: python .. code-block:: python
@pytest.mark.timeout(10, "slow", method="thread") @pytest.mark.timeout(10, "slow", method="thread")
def test_function(): def test_function(): ...
...
Will create and attach a :class:`Mark <pytest.Mark>` object to the collected Will create and attach a :class:`Mark <pytest.Mark>` object to the collected
:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with :class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
@ -294,8 +292,7 @@ Example for using multiple custom markers:
@pytest.mark.timeout(10, "slow", method="thread") @pytest.mark.timeout(10, "slow", method="thread")
@pytest.mark.slow @pytest.mark.slow
def test_function(): def test_function(): ...
...
When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers_with_node <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``. When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers_with_node <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``.
@ -2044,7 +2041,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
failure failure
--doctest-glob=pat Doctests file matching pattern, default: test*.txt --doctest-glob=pat Doctests file matching pattern, default: test*.txt
--doctest-ignore-import-errors --doctest-ignore-import-errors
Ignore doctest ImportErrors Ignore doctest collection errors
--doctest-continue-on-failure --doctest-continue-on-failure
For a given doctest, continue to run after the first For a given doctest, continue to run after the first
failure failure

View File

@ -2,7 +2,7 @@ pallets-sphinx-themes
pluggy>=1.2.0 pluggy>=1.2.0
pygments-pytest>=2.3.0 pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0 sphinx-removed-in>=0.2.0
sphinx>=5,<8 sphinx>=7
sphinxcontrib-trio sphinxcontrib-trio
sphinxcontrib-svg2pdfconverter sphinxcontrib-svg2pdfconverter
# Pin packaging because it no longer handles 'latest' version, which # Pin packaging because it no longer handles 'latest' version, which

View File

@ -3,6 +3,7 @@ from pathlib import Path
import requests import requests
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues" issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"

View File

@ -1,13 +1,178 @@
[build-system] [project]
requires = [ name = "pytest"
"setuptools>=45.0", description = "pytest: simple powerful testing with Python"
"setuptools-scm[toml]>=6.2.3", 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" build-backend = "setuptools.build_meta"
requires = [
"setuptools>=61",
"setuptools-scm[toml]>=6.2.3",
]
[tool.setuptools.package-data]
"_pytest" = ["py.typed"]
"pytest" = ["py.typed"]
[tool.setuptools_scm] [tool.setuptools_scm]
write_to = "src/_pytest/_version.py" write_to = "src/_pytest/_version.py"
[tool.black]
target-version = ['py38']
[tool.ruff]
src = ["src"]
line-length = 88
[tool.ruff.format]
docstring-code-format = true
[tool.ruff.lint]
select = [
"B", # bugbear
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"PYI", # flake8-pyi
"UP", # pyupgrade
"RUF", # ruff
"W", # pycodestyle
"PIE", # flake8-pie
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
"PLE", # pylint error
"PLW", # pylint warning
"PLR1714", # Consider merging multiple comparisons
]
ignore = [
# bugbear ignore
"B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
"B007", # Loop control variable `i` not used within loop body
"B009", # Do not call `getattr` with a constant attribute value
"B010", # [*] Do not call `setattr` with a constant attribute value.
"B011", # Do not `assert False` (`python -O` removes these calls)
"B028", # No explicit `stacklevel` keyword argument found
# pycodestyle ignore
# pytest can do weird low-level things, and we usually know
# what we're doing when we use type(..) is ...
"E721", # Do not compare types, use `isinstance()`
# pydocstyle ignore
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D102", # Missing docstring in public method
"D103", # Missing docstring in public function
"D104", # Missing docstring in public package
"D105", # Missing docstring in magic method
"D106", # Missing docstring in public nested class
"D107", # Missing docstring in `__init__`
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
"D205", # 1 blank line required between summary line and description
"D400", # First line should end with a period
"D401", # First line of docstring should be in imperative mood
"D402", # First line should not be the function's signature
"D404", # First word of the docstring should not be "This"
"D415", # First line should end with a period, question mark, or exclamation point
# ruff ignore
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
# pylint ignore
"PLW0603", # Using the global statement
"PLW0120", # remove the else and dedent its contents
"PLW2901", # for loop variable overwritten by assignment target
"PLR5501", # Use `elif` instead of `else` then `if`
]
[tool.ruff.lint.pycodestyle]
# In order to be able to format for 88 char in ruff format
max-line-length = 120
[tool.ruff.lint.pydocstyle]
convention = "pep257"
[tool.ruff.lint.isort]
force-single-line = true
combine-as-imports = true
force-sort-within-sections = true
order-by-type = false
known-local-folder = ["pytest", "_pytest"]
lines-after-imports = 2
[tool.ruff.lint.per-file-ignores]
"src/_pytest/_py/**/*.py" = ["B", "PYI"]
"src/_pytest/_version.py" = ["I001"]
"testing/python/approx.py" = ["B015"]
[tool.check-wheel-contents]
# check-wheel-contents is executed by the build-and-inspect-python-package action.
# W009: Wheel contains multiple toplevel library entries
ignore = "W009"
[tool.pyproject-fmt]
indent = 4
[tool.pytest.ini_options] [tool.pytest.ini_options]
minversion = "2.0" minversion = "2.0"
addopts = "-rfEX -p pytester --strict-markers" addopts = "-rfEX -p pytester --strict-markers"
@ -67,7 +232,6 @@ markers = [
"uses_pexpect", "uses_pexpect",
] ]
[tool.towncrier] [tool.towncrier]
package = "pytest" package = "pytest"
package_dir = "src" package_dir = "src"
@ -116,10 +280,16 @@ template = "changelog/_template.rst"
name = "Trivial/Internal Changes" name = "Trivial/Internal Changes"
showcontent = true showcontent = true
[tool.black] [tool.mypy]
target-version = ['py38'] mypy_path = ["src"]
check_untyped_defs = true
# check-wheel-contents is executed by the build-and-inspect-python-package action. disallow_any_generics = true
[tool.check-wheel-contents] disallow_untyped_defs = true
# W009: Wheel contains multiple toplevel library entries ignore_missing_imports = true
ignore = "W009" show_error_codes = true
strict_equality = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
no_implicit_reexport = true

View File

@ -8,9 +8,9 @@ our CHANGELOG) into Markdown (which is required by GitHub Releases).
Requires Python3.6+. Requires Python3.6+.
""" """
from pathlib import Path
import re import re
import sys import sys
from pathlib import Path
from typing import Sequence from typing import Sequence
import pypandoc import pypandoc

View File

@ -14,8 +14,8 @@ After that, it will create a release using the `release` tox environment, and pu
`pytest bot <pytestbot@gmail.com>` commit author. `pytest bot <pytestbot@gmail.com>` commit author.
""" """
import argparse import argparse
import re
from pathlib import Path from pathlib import Path
import re
from subprocess import check_call from subprocess import check_call
from subprocess import check_output from subprocess import check_output
from subprocess import run from subprocess import run
@ -79,7 +79,7 @@ def prepare_release_pr(
) )
except InvalidFeatureRelease as e: except InvalidFeatureRelease as e:
print(f"{Fore.RED}{e}") print(f"{Fore.RED}{e}")
raise SystemExit(1) raise SystemExit(1) from None
print(f"Version: {Fore.CYAN}{version}") print(f"Version: {Fore.CYAN}{version}")

View File

@ -107,7 +107,7 @@ def pre_release(
def changelog(version: str, write_out: bool = False) -> None: def changelog(version: str, write_out: bool = False) -> None:
addopts = [] if write_out else ["--draft"] addopts = [] if write_out else ["--draft"]
check_call(["towncrier", "--yes", "--version", version] + addopts) check_call(["towncrier", "--yes", "--version", version, *addopts])
def main() -> None: def main() -> None:

View File

@ -1,6 +1,6 @@
# mypy: disallow-untyped-defs # mypy: disallow-untyped-defs
import sys
from subprocess import call from subprocess import call
import sys
def main() -> int: def main() -> int:

View File

@ -11,13 +11,13 @@ from typing import TypedDict
import packaging.version import packaging.version
import platformdirs import platformdirs
import tabulate
import wcwidth
from requests_cache import CachedResponse from requests_cache import CachedResponse
from requests_cache import CachedSession from requests_cache import CachedSession
from requests_cache import OriginalResponse from requests_cache import OriginalResponse
from requests_cache import SQLiteCache from requests_cache import SQLiteCache
import tabulate
from tqdm import tqdm from tqdm import tqdm
import wcwidth
FILE_HEAD = r""" FILE_HEAD = r"""
@ -29,7 +29,7 @@ Pytest Plugin List
================== ==================
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_. Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects. It includes PyPI projects whose names begin with "pytest-" or "pytest_" and a handful of manually selected projects.
Packages classified as inactive are excluded. Packages classified as inactive are excluded.
For detailed insights into how this list is generated, For detailed insights into how this list is generated,
@ -61,6 +61,7 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
) )
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert", "logassert",
"logot",
"nuts", "nuts",
"flask_fixture", "flask_fixture",
} }
@ -86,7 +87,6 @@ def project_response_with_refresh(
force refresh in case of last serial mismatch force refresh in case of last serial mismatch
""" """
response = session.get(f"https://pypi.org/pypi/{name}/json") response = session.get(f"https://pypi.org/pypi/{name}/json")
if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial: if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial:
response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True) response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True)
@ -110,7 +110,10 @@ def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
return { return {
name: p["_last-serial"] name: p["_last-serial"]
for p in response.json()["projects"] for p in response.json()["projects"]
if (name := p["name"]).startswith("pytest-") or name in ADDITIONAL_PROJECTS if (
(name := p["name"]).startswith(("pytest-", "pytest_"))
or name in ADDITIONAL_PROJECTS
)
} }
@ -185,7 +188,6 @@ def iter_plugins() -> Iterator[PluginInfo]:
def plugin_definitions(plugins: Iterable[PluginInfo]) -> Iterator[str]: def plugin_definitions(plugins: Iterable[PluginInfo]) -> Iterator[str]:
"""Return RST for the plugin list that fits better on a vertical page.""" """Return RST for the plugin list that fits better on a vertical page."""
for plugin in plugins: for plugin in plugins:
yield dedent( yield dedent(
f""" f"""
@ -210,7 +212,7 @@ def main() -> None:
f.write(f"This list contains {len(plugins)} plugins.\n\n") f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(".. only:: not latex\n\n") f.write(".. only:: not latex\n\n")
wcwidth # reference library that must exist for tabulate to work _ = wcwidth # reference library that must exist for tabulate to work
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst") plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
f.write(indent(plugin_table, " ")) f.write(indent(plugin_table, " "))
f.write("\n\n") f.write("\n\n")

104
setup.cfg
View File

@ -1,104 +0,0 @@
[metadata]
name = pytest
description = pytest: simple powerful testing with Python
long_description = file: README.rst
long_description_content_type = text/x-rst
url = https://docs.pytest.org/en/latest/
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
license = MIT
license_files = LICENSE
platforms = unix, linux, osx, cygwin, win32
classifiers =
Development Status :: 6 - Mature
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: MacOS :: MacOS X
Operating System :: Microsoft :: Windows
Operating System :: POSIX
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Software Development :: Libraries
Topic :: Software Development :: Testing
Topic :: Utilities
keywords = test, unittest
project_urls =
Changelog=https://docs.pytest.org/en/stable/changelog.html
Twitter=https://twitter.com/pytestdotorg
Source=https://github.com/pytest-dev/pytest
Tracker=https://github.com/pytest-dev/pytest/issues
[options]
packages =
_pytest
_pytest._code
_pytest._io
_pytest._py
_pytest.assertion
_pytest.config
_pytest.mark
pytest
py_modules = py
install_requires =
iniconfig
packaging
pluggy>=1.3.0,<2.0
colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11"
tomli>=1.0.0;python_version<"3.11"
python_requires = >=3.8
package_dir =
=src
setup_requires =
setuptools
setuptools-scm>=6.0
zip_safe = no
[options.entry_points]
console_scripts =
pytest=pytest:console_main
py.test=pytest:console_main
[options.extras_require]
testing =
argcomplete
attrs>=19.2.0
hypothesis>=3.56
mock
pygments>=2.7.2
requests
setuptools
xmlschema
[options.package_data]
_pytest = py.typed
pytest = py.typed
[build_sphinx]
source_dir = doc/en/
build_dir = doc/build
all_files = 1
[check-manifest]
ignore =
src/_pytest/_version.py
[devpi:upload]
formats = sdist.tgz,bdist_wheel
[mypy]
mypy_path = src
check_untyped_defs = True
disallow_any_generics = True
ignore_missing_imports = True
show_error_codes = True
strict_equality = True
warn_redundant_casts = True
warn_return_any = True
warn_unreachable = True
warn_unused_configs = True
no_implicit_reexport = True

View File

@ -1,7 +1,8 @@
__all__ = ["__version__", "version_tuple"] __all__ = ["__version__", "version_tuple"]
try: try:
from ._version import version as __version__, version_tuple from ._version import version as __version__
from ._version import version_tuple
except ImportError: # pragma: no cover except ImportError: # pragma: no cover
# broken installation, we don't even try # broken installation, we don't even try
# unknown only works because we do poor mans version compare # unknown only works because we do poor mans version compare

View File

@ -61,10 +61,11 @@ If things do not work right away:
which should throw a KeyError: 'COMPLINE' (which is properly set by the which should throw a KeyError: 'COMPLINE' (which is properly set by the
global argcomplete script). global argcomplete script).
""" """
import argparse import argparse
from glob import glob
import os import os
import sys import sys
from glob import glob
from typing import Any from typing import Any
from typing import List from typing import List
from typing import Optional from typing import Optional

View File

@ -1,4 +1,5 @@
"""Python inspection/code generation API.""" """Python inspection/code generation API."""
from .code import Code from .code import Code
from .code import ExceptionInfo from .code import ExceptionInfo
from .code import filter_traceback from .code import filter_traceback
@ -9,6 +10,7 @@ from .code import TracebackEntry
from .source import getrawcode from .source import getrawcode
from .source import Source from .source import Source
__all__ = [ __all__ = [
"Code", "Code",
"ExceptionInfo", "ExceptionInfo",

View File

@ -1,14 +1,15 @@
# mypy: allow-untyped-defs
import ast import ast
import dataclasses import dataclasses
import inspect import inspect
import os
import re
import sys
import traceback
from inspect import CO_VARARGS from inspect import CO_VARARGS
from inspect import CO_VARKEYWORDS from inspect import CO_VARKEYWORDS
from io import StringIO from io import StringIO
import os
from pathlib import Path from pathlib import Path
import re
import sys
import traceback
from traceback import format_exception_only from traceback import format_exception_only
from types import CodeType from types import CodeType
from types import FrameType from types import FrameType
@ -50,6 +51,7 @@ from _pytest.deprecated import check_ispytest
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
if sys.version_info[:2] < (3, 11): if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup from exceptiongroup import BaseExceptionGroup
@ -486,9 +488,10 @@ class ExceptionInfo(Generic[E]):
.. versionadded:: 7.4 .. versionadded:: 7.4
""" """
assert ( assert exception.__traceback__, (
exception.__traceback__ "Exceptions passed to ExcInfo.from_exception(...)"
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__." " must have a non-None __traceback__."
)
exc_info = (type(exception), exception, exception.__traceback__) exc_info = (type(exception), exception, exception.__traceback__)
return cls.from_exc_info(exc_info, exprinfo) return cls.from_exc_info(exc_info, exprinfo)
@ -587,9 +590,7 @@ class ExceptionInfo(Generic[E]):
def __repr__(self) -> str: def __repr__(self) -> str:
if self._excinfo is None: if self._excinfo is None:
return "<ExceptionInfo for raises contextmanager>" return "<ExceptionInfo for raises contextmanager>"
return "<{} {} tblen={}>".format( return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
)
def exconly(self, tryshort: bool = False) -> str: def exconly(self, tryshort: bool = False) -> str:
"""Return the exception as a string. """Return the exception as a string.
@ -698,10 +699,21 @@ class ExceptionInfo(Generic[E]):
return fmt.repr_excinfo(self) return fmt.repr_excinfo(self)
def _stringify_exception(self, exc: BaseException) -> str: def _stringify_exception(self, exc: BaseException) -> str:
try:
notes = getattr(exc, "__notes__", [])
except KeyError:
# Workaround for https://github.com/python/cpython/issues/98778 on
# Python <= 3.9, and some 3.10 and 3.11 patch versions.
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
if sys.version_info[:2] <= (3, 11) and isinstance(exc, HTTPError):
notes = []
else:
raise
return "\n".join( return "\n".join(
[ [
str(exc), str(exc),
*getattr(exc, "__notes__", []), *notes,
] ]
) )
@ -1006,13 +1018,8 @@ class FormattedExcinfo:
extraline: Optional[str] = ( extraline: Optional[str] = (
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n" "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
" The following exception happened when comparing locals in the stack frame:\n" " The following exception happened when comparing locals in the stack frame:\n"
" {exc_type}: {exc_msg}\n" f" {type(e).__name__}: {e!s}\n"
" Displaying first and last {max_frames} stack frames out of {total}." f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
).format(
exc_type=type(e).__name__,
exc_msg=str(e),
max_frames=max_frames,
total=len(traceback),
) )
# Type ignored because adding two instances of a List subtype # Type ignored because adding two instances of a List subtype
# currently incorrectly has type List instead of the subtype. # currently incorrectly has type List instead of the subtype.
@ -1219,7 +1226,6 @@ class ReprEntry(TerminalRepr):
the "E" prefix) using syntax highlighting, taking care to not highlighting the ">" the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
character, as doing so might break line continuations. character, as doing so might break line continuations.
""" """
if not self.lines: if not self.lines:
return return

View File

@ -1,10 +1,10 @@
# mypy: allow-untyped-defs
import ast import ast
from bisect import bisect_right
import inspect import inspect
import textwrap import textwrap
import tokenize import tokenize
import types import types
import warnings
from bisect import bisect_right
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List from typing import List
@ -12,6 +12,7 @@ from typing import Optional
from typing import overload from typing import overload
from typing import Tuple from typing import Tuple
from typing import Union from typing import Union
import warnings
class Source: class Source:
@ -196,7 +197,9 @@ def getstatementrange_ast(
# by using the BlockFinder helper used which inspect.getsource() uses itself. # by using the BlockFinder helper used which inspect.getsource() uses itself.
block_finder = inspect.BlockFinder() block_finder = inspect.BlockFinder()
# If we start with an indented line, put blockfinder to "started" mode. # If we start with an indented line, put blockfinder to "started" mode.
block_finder.started = source.lines[start][0].isspace() block_finder.started = (
bool(source.lines[start]) and source.lines[start][0].isspace()
)
it = ((x + "\n") for x in source.lines[start:end]) it = ((x + "\n") for x in source.lines[start:end])
try: try:
for tok in tokenize.generate_tokens(lambda: next(it)): for tok in tokenize.generate_tokens(lambda: next(it)):

View File

@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
# This module was imported from the cpython standard library # This module was imported from the cpython standard library
# (https://github.com/python/cpython/) at commit # (https://github.com/python/cpython/) at commit
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12). # c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
@ -14,9 +15,9 @@
# useful, thank small children who sleep at night. # useful, thank small children who sleep at night.
import collections as _collections import collections as _collections
import dataclasses as _dataclasses import dataclasses as _dataclasses
from io import StringIO as _StringIO
import re import re
import types as _types import types as _types
from io import StringIO as _StringIO
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict

View File

@ -19,8 +19,8 @@ def _format_repr_exception(exc: BaseException, obj: object) -> str:
raise raise
except BaseException as exc: except BaseException as exc:
exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})" exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})"
return "<[{} raised in repr()] {} object at 0x{:x}>".format( return (
exc_info, type(obj).__name__, id(obj) f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
) )
@ -108,7 +108,6 @@ def saferepr(
This function is a wrapper around the Repr/reprlib functionality of the This function is a wrapper around the Repr/reprlib functionality of the
stdlib. stdlib.
""" """
return SafeRepr(maxsize, use_ascii).repr(obj) return SafeRepr(maxsize, use_ascii).repr(obj)

View File

@ -1,4 +1,5 @@
"""Helper functions for writing to terminals and files.""" """Helper functions for writing to terminals and files."""
import os import os
import shutil import shutil
import sys import sys
@ -183,9 +184,7 @@ class TerminalWriter:
""" """
if indents and len(indents) != len(lines): if indents and len(indents) != len(lines):
raise ValueError( raise ValueError(
"indents size ({}) should have same size as lines ({})".format( f"indents size ({len(indents)}) should have same size as lines ({len(lines)})"
len(indents), len(lines)
)
) )
if not indents: if not indents:
indents = [""] * len(lines) indents = [""] * len(lines)
@ -233,17 +232,17 @@ class TerminalWriter:
# which may lead to the previous color being propagated to the # which may lead to the previous color being propagated to the
# start of the expression, so reset first. # start of the expression, so reset first.
return "\x1b[0m" + highlighted return "\x1b[0m" + highlighted
except pygments.util.ClassNotFound: except pygments.util.ClassNotFound as e:
raise UsageError( raise UsageError(
"PYTEST_THEME environment variable had an invalid value: '{}'. " "PYTEST_THEME environment variable had an invalid value: '{}'. "
"Only valid pygment styles are allowed.".format( "Only valid pygment styles are allowed.".format(
os.getenv("PYTEST_THEME") os.getenv("PYTEST_THEME")
) )
) ) from e
except pygments.util.OptionError: except pygments.util.OptionError as e:
raise UsageError( raise UsageError(
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. " "PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
"The only allowed values are 'dark' and 'light'.".format( "The only allowed values are 'dark' and 'light'.".format(
os.getenv("PYTEST_THEME_MODE") os.getenv("PYTEST_THEME_MODE")
) )
) ) from e

View File

@ -1,5 +1,5 @@
import unicodedata
from functools import lru_cache from functools import lru_cache
import unicodedata
@lru_cache(100) @lru_cache(100)

View File

@ -1,4 +1,5 @@
"""create errno-specific classes for IO or os calls.""" """create errno-specific classes for IO or os calls."""
from __future__ import annotations from __future__ import annotations
import errno import errno
@ -8,6 +9,7 @@ from typing import Callable
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import ParamSpec from typing_extensions import ParamSpec

View File

@ -1,16 +1,13 @@
# mypy: allow-untyped-defs
"""local path implementation.""" """local path implementation."""
from __future__ import annotations from __future__ import annotations
import atexit import atexit
from contextlib import contextmanager
import fnmatch import fnmatch
import importlib.util import importlib.util
import io import io
import os import os
import posixpath
import sys
import uuid
import warnings
from contextlib import contextmanager
from os.path import abspath from os.path import abspath
from os.path import dirname from os.path import dirname
from os.path import exists from os.path import exists
@ -19,18 +16,23 @@ from os.path import isdir
from os.path import isfile from os.path import isfile
from os.path import islink from os.path import islink
from os.path import normpath from os.path import normpath
import posixpath
from stat import S_ISDIR from stat import S_ISDIR
from stat import S_ISLNK from stat import S_ISLNK
from stat import S_ISREG from stat import S_ISREG
import sys
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Literal from typing import Literal
from typing import overload from typing import overload
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
import uuid
import warnings
from . import error from . import error
# Moved from local.py. # Moved from local.py.
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt") iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
@ -450,7 +452,7 @@ class LocalPath:
def ensure_dir(self, *args): def ensure_dir(self, *args):
"""Ensure the path joined with args is a directory.""" """Ensure the path joined with args is a directory."""
return self.ensure(*args, **{"dir": True}) return self.ensure(*args, dir=True)
def bestrelpath(self, dest): def bestrelpath(self, dest):
"""Return a string which is a relative path from self """Return a string which is a relative path from self
@ -675,7 +677,7 @@ class LocalPath:
else: else:
kw.setdefault("dirname", dirname) kw.setdefault("dirname", dirname)
kw.setdefault("sep", self.sep) kw.setdefault("sep", self.sep)
obj.strpath = normpath("%(dirname)s%(sep)s%(basename)s" % kw) obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw))
return obj return obj
def _getbyspec(self, spec: str) -> list[str]: def _getbyspec(self, spec: str) -> list[str]:
@ -760,7 +762,10 @@ class LocalPath:
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type] # expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
# Which seems incorrect, given io.open supports the given argument types. # Which seems incorrect, given io.open supports the given argument types.
return error.checked_call( return error.checked_call(
io.open, self.strpath, mode, encoding=encoding # type:ignore[arg-type] io.open,
self.strpath,
mode,
encoding=encoding, # type:ignore[arg-type]
) )
return error.checked_call(open, self.strpath, mode) return error.checked_call(open, self.strpath, mode)
@ -779,11 +784,11 @@ class LocalPath:
valid checkers:: valid checkers::
file=1 # is a file file = 1 # is a file
file=0 # is not a file (may not even exist) file = 0 # is not a file (may not even exist)
dir=1 # is a dir dir = 1 # is a dir
link=1 # is a link link = 1 # is a link
exists=1 # exists exists = 1 # exists
You can specify multiple checker definitions, for example:: You can specify multiple checker definitions, for example::
@ -1100,9 +1105,7 @@ class LocalPath:
modname = self.purebasename modname = self.purebasename
spec = importlib.util.spec_from_file_location(modname, str(self)) spec = importlib.util.spec_from_file_location(modname, str(self))
if spec is None or spec.loader is None: if spec is None or spec.loader is None:
raise ImportError( raise ImportError(f"Can't find module {modname} at location {self!s}")
f"Can't find module {modname} at location {str(self)}"
)
mod = importlib.util.module_from_spec(spec) mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod) spec.loader.exec_module(mod)
return mod return mod
@ -1167,7 +1170,8 @@ class LocalPath:
where the 'self' path points to executable. where the 'self' path points to executable.
The process is directly invoked and not through a system shell. The process is directly invoked and not through a system shell.
""" """
from subprocess import Popen, PIPE from subprocess import PIPE
from subprocess import Popen
popen_opts.pop("stdout", None) popen_opts.pop("stdout", None)
popen_opts.pop("stderr", None) popen_opts.pop("stderr", None)
@ -1277,7 +1281,8 @@ class LocalPath:
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type] # error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types. # Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
path = error.checked_call( path = error.checked_call(
tempfile.mkdtemp, dir=str(rootdir) # type:ignore[arg-type] tempfile.mkdtemp,
dir=str(rootdir), # type:ignore[arg-type]
) )
return cls(path) return cls(path)

View File

@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Support for presenting detailed information in failing assertions.""" """Support for presenting detailed information in failing assertions."""
import sys import sys
from typing import Any from typing import Any
@ -15,6 +16,7 @@ from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.nodes import Item from _pytest.nodes import Item
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.main import Session from _pytest.main import Session
@ -128,7 +130,6 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
reporting via the pytest_assertrepr_compare hook. This sets up this custom reporting via the pytest_assertrepr_compare hook. This sets up this custom
comparison for the test. comparison for the test.
""" """
ihook = item.ihook ihook = item.ihook
def callbinrepr(op, left: object, right: object) -> Optional[str]: def callbinrepr(op, left: object, right: object) -> Optional[str]:

View File

@ -1,5 +1,7 @@
"""Rewrite assertion AST to produce nice error messages.""" """Rewrite assertion AST to produce nice error messages."""
import ast import ast
from collections import defaultdict
import errno import errno
import functools import functools
import importlib.abc import importlib.abc
@ -9,13 +11,12 @@ import io
import itertools import itertools
import marshal import marshal
import os import os
from pathlib import Path
from pathlib import PurePath
import struct import struct
import sys import sys
import tokenize import tokenize
import types import types
from collections import defaultdict
from pathlib import Path
from pathlib import PurePath
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import IO from typing import IO
@ -33,15 +34,17 @@ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest._version import version from _pytest._version import version
from _pytest.assertion import util from _pytest.assertion import util
from _pytest.assertion.util import ( # noqa: F401
format_explanation as _format_explanation,
)
from _pytest.config import Config from _pytest.config import Config
from _pytest.main import Session from _pytest.main import Session
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import fnmatch_ex
from _pytest.stash import StashKey from _pytest.stash import StashKey
# fmt: off
from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip
# fmt:on
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.assertion import AssertionState from _pytest.assertion import AssertionState
@ -858,9 +861,10 @@ class AssertionRewriter(ast.NodeVisitor):
the expression is false. the expression is false.
""" """
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1: if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
from _pytest.warning_types import PytestAssertRewriteWarning
import warnings import warnings
from _pytest.warning_types import PytestAssertRewriteWarning
# TODO: This assert should not be needed. # TODO: This assert should not be needed.
assert self.module_path is not None assert self.module_path is not None
warnings.warn_explicit( warnings.warn_explicit(
@ -921,7 +925,7 @@ class AssertionRewriter(ast.NodeVisitor):
# If any hooks implement assert_pass hook # If any hooks implement assert_pass hook
hook_impl_test = ast.If( hook_impl_test = ast.If(
self.helper("_check_if_assertion_pass_impl"), self.helper("_check_if_assertion_pass_impl"),
self.expl_stmts + [hook_call_pass], [*self.expl_stmts, hook_call_pass],
[], [],
) )
statements_pass = [hook_impl_test] statements_pass = [hook_impl_test]
@ -1002,7 +1006,7 @@ class AssertionRewriter(ast.NodeVisitor):
if i: if i:
fail_inner: List[ast.stmt] = [] fail_inner: List[ast.stmt] = []
# cond is set in a prior loop iteration below # cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
self.expl_stmts = fail_inner self.expl_stmts = fail_inner
# Check if the left operand is a ast.NamedExpr and the value has already been visited # Check if the left operand is a ast.NamedExpr and the value has already been visited
if ( if (
@ -1016,9 +1020,7 @@ class AssertionRewriter(ast.NodeVisitor):
] ]
): ):
pytest_temp = self.variable() pytest_temp = self.variable()
self.variables_overwrite[self.scope][ self.variables_overwrite[self.scope][v.left.target.id] = v.left # type:ignore[assignment]
v.left.target.id
] = v.left # type:ignore[assignment]
v.left.target.id = pytest_temp v.left.target.id = pytest_temp
self.push_format_context() self.push_format_context()
res, expl = self.visit(v) res, expl = self.visit(v)
@ -1062,9 +1064,7 @@ class AssertionRewriter(ast.NodeVisitor):
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get( if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
self.scope, {} self.scope, {}
): ):
arg = self.variables_overwrite[self.scope][ arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
arg.id
] # type:ignore[assignment]
res, expl = self.visit(arg) res, expl = self.visit(arg)
arg_expls.append(expl) arg_expls.append(expl)
new_args.append(res) new_args.append(res)
@ -1072,9 +1072,7 @@ class AssertionRewriter(ast.NodeVisitor):
if isinstance( if isinstance(
keyword.value, ast.Name keyword.value, ast.Name
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}): ) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
keyword.value = self.variables_overwrite[self.scope][ keyword.value = self.variables_overwrite[self.scope][keyword.value.id] # type:ignore[assignment]
keyword.value.id
] # type:ignore[assignment]
res, expl = self.visit(keyword.value) res, expl = self.visit(keyword.value)
new_kwargs.append(ast.keyword(keyword.arg, res)) new_kwargs.append(ast.keyword(keyword.arg, res))
if keyword.arg: if keyword.arg:
@ -1111,13 +1109,9 @@ class AssertionRewriter(ast.NodeVisitor):
if isinstance( if isinstance(
comp.left, ast.Name comp.left, ast.Name
) and comp.left.id in self.variables_overwrite.get(self.scope, {}): ) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
comp.left = self.variables_overwrite[self.scope][ comp.left = self.variables_overwrite[self.scope][comp.left.id] # type:ignore[assignment]
comp.left.id
] # type:ignore[assignment]
if isinstance(comp.left, ast.NamedExpr): if isinstance(comp.left, ast.NamedExpr):
self.variables_overwrite[self.scope][ self.variables_overwrite[self.scope][comp.left.target.id] = comp.left # type:ignore[assignment]
comp.left.target.id
] = comp.left # type:ignore[assignment]
left_res, left_expl = self.visit(comp.left) left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)): if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})" left_expl = f"({left_expl})"
@ -1135,9 +1129,7 @@ class AssertionRewriter(ast.NodeVisitor):
and next_operand.target.id == left_res.id and next_operand.target.id == left_res.id
): ):
next_operand.target.id = self.variable() next_operand.target.id = self.variable()
self.variables_overwrite[self.scope][ self.variables_overwrite[self.scope][left_res.id] = next_operand # type:ignore[assignment]
left_res.id
] = next_operand # type:ignore[assignment]
next_res, next_expl = self.visit(next_operand) next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)): if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = f"({next_expl})" next_expl = f"({next_expl})"

View File

@ -3,6 +3,7 @@
Current default behaviour is to truncate assertion explanations at Current default behaviour is to truncate assertion explanations at
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI. terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
""" """
from typing import List from typing import List
from typing import Optional from typing import Optional
@ -91,7 +92,8 @@ def _truncate_explanation(
else: else:
# Add proper ellipsis when we were able to fit a full line exactly # Add proper ellipsis when we were able to fit a full line exactly
truncated_explanation[-1] = "..." truncated_explanation[-1] = "..."
return truncated_explanation + [ return [
*truncated_explanation,
"", "",
f"...Full output truncated ({truncated_line_count} line" f"...Full output truncated ({truncated_line_count} line"
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}", f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",

View File

@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Utilities for assertion debugging.""" """Utilities for assertion debugging."""
import collections.abc import collections.abc
import os import os
@ -14,13 +15,14 @@ from typing import Protocol
from typing import Sequence from typing import Sequence
from unicodedata import normalize from unicodedata import normalize
import _pytest._code
from _pytest import outcomes from _pytest import outcomes
import _pytest._code
from _pytest._io.pprint import PrettyPrinter from _pytest._io.pprint import PrettyPrinter
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited from _pytest._io.saferepr import saferepr_unlimited
from _pytest.config import Config from _pytest.config import Config
# The _reprcompare attribute on the util module is used by the new assertion # The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was # interpretation code and assertion rewriter to detect this plugin was
# loaded and in turn call the hooks defined here as part of the # loaded and in turn call the hooks defined here as part of the
@ -231,8 +233,8 @@ def assertrepr_compare(
return None return None
if explanation[0] != "": if explanation[0] != "":
explanation = [""] + explanation explanation = ["", *explanation]
return [summary] + explanation return [summary, *explanation]
def _compare_eq_any( def _compare_eq_any(
@ -301,8 +303,8 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
if i > 42: if i > 42:
i -= 10 # Provide some context i -= 10 # Provide some context
explanation += [ explanation += [
"Skipping {} identical trailing " f"Skipping {i} identical trailing "
"characters in diff, use -v to show".format(i) "characters in diff, use -v to show"
] ]
left = left[:-i] left = left[:-i]
right = right[:-i] right = right[:-i]

View File

@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Implementation of the cache provider.""" """Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external # This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version. # pytest-cache version.
@ -31,6 +32,7 @@ from _pytest.nodes import Directory
from _pytest.nodes import File from _pytest.nodes import File
from _pytest.reports import TestReport from _pytest.reports import TestReport
README_CONTENT = """\ README_CONTENT = """\
# pytest cache directory # # pytest cache directory #
@ -111,6 +113,7 @@ class Cache:
""" """
check_ispytest(_ispytest) check_ispytest(_ispytest)
import warnings import warnings
from _pytest.warning_types import PytestCacheWarning from _pytest.warning_types import PytestCacheWarning
warnings.warn( warnings.warn(
@ -366,15 +369,13 @@ class LFPlugin:
noun = "failure" if self._previously_failed_count == 1 else "failures" noun = "failure" if self._previously_failed_count == 1 else "failures"
suffix = " first" if self.config.getoption("failedfirst") else "" suffix = " first" if self.config.getoption("failedfirst") else ""
self._report_status = "rerun previous {count} {noun}{suffix}".format( self._report_status = (
count=self._previously_failed_count, suffix=suffix, noun=noun f"rerun previous {self._previously_failed_count} {noun}{suffix}"
) )
if self._skipped_files > 0: if self._skipped_files > 0:
files_noun = "file" if self._skipped_files == 1 else "files" files_noun = "file" if self._skipped_files == 1 else "files"
self._report_status += " (skipped {files} {files_noun})".format( self._report_status += f" (skipped {self._skipped_files} {files_noun})"
files=self._skipped_files, files_noun=files_noun
)
else: else:
self._report_status = "no previously failed tests, " self._report_status = "no previously failed tests, "
if self.config.getoption("last_failed_no_failures") == "none": if self.config.getoption("last_failed_no_failures") == "none":

View File

@ -1,11 +1,12 @@
# mypy: allow-untyped-defs
"""Per-test stdout/stderr capturing mechanism.""" """Per-test stdout/stderr capturing mechanism."""
import abc import abc
import collections import collections
import contextlib import contextlib
import io import io
from io import UnsupportedOperation
import os import os
import sys import sys
from io import UnsupportedOperation
from tempfile import TemporaryFile from tempfile import TemporaryFile
from types import TracebackType from types import TracebackType
from typing import Any from typing import Any
@ -38,6 +39,7 @@ from _pytest.nodes import File
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.reports import CollectReport from _pytest.reports import CollectReport
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
@ -596,7 +598,8 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
else: else:
class CaptureResult( class CaptureResult(
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr] collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
Generic[AnyStr],
): ):
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`.""" """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
@ -789,9 +792,7 @@ class CaptureManager:
current_fixture = self._capture_fixture.request.fixturename current_fixture = self._capture_fixture.request.fixturename
requested_fixture = capture_fixture.request.fixturename requested_fixture = capture_fixture.request.fixturename
capture_fixture.request.raiseerror( capture_fixture.request.raiseerror(
"cannot use {} and {} at the same time".format( f"cannot use {requested_fixture} and {current_fixture} at the same time"
requested_fixture, current_fixture
)
) )
self._capture_fixture = capture_fixture self._capture_fixture = capture_fixture
@ -987,7 +988,6 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example: Example:
.. code-block:: python .. code-block:: python
def test_output(capsys): def test_output(capsys):
@ -1015,7 +1015,6 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example: Example:
.. code-block:: python .. code-block:: python
def test_output(capsysbinary): def test_output(capsysbinary):
@ -1043,7 +1042,6 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`. Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example: Example:
.. code-block:: python .. code-block:: python
def test_system_echo(capfd): def test_system_echo(capfd):
@ -1071,7 +1069,6 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`. Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example: Example:
.. code-block:: python .. code-block:: python
def test_system_echo(capfdbinary): def test_system_echo(capfdbinary):

View File

@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Python version compatibility code.""" """Python version compatibility code."""
from __future__ import annotations from __future__ import annotations
@ -5,20 +6,15 @@ import dataclasses
import enum import enum
import functools import functools
import inspect import inspect
import os
import sys
from inspect import Parameter from inspect import Parameter
from inspect import signature from inspect import signature
import os
from pathlib import Path from pathlib import Path
import sys
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Final from typing import Final
from typing import NoReturn from typing import NoReturn
from typing import TypeVar
_T = TypeVar("_T")
_S = TypeVar("_S")
# fmt: off # fmt: off
@ -26,7 +22,7 @@ _S = TypeVar("_S")
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum): class NotSetType(enum.Enum):
token = 0 token = 0
NOTSET: Final = NotSetType.token # noqa: E305 NOTSET: Final = NotSetType.token
# fmt: on # fmt: on
@ -176,25 +172,13 @@ _non_printable_ascii_translate_table.update(
) )
def _translate_non_printable(s: str) -> str:
return s.translate(_non_printable_ascii_translate_table)
STRING_TYPES = bytes, str
def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace")
def ascii_escaped(val: bytes | str) -> str: def ascii_escaped(val: bytes | str) -> str:
r"""If val is pure ASCII, return it as an str, otherwise, escape r"""If val is pure ASCII, return it as an str, otherwise, escape
bytes objects into a sequence of escaped bytes: bytes objects into a sequence of escaped bytes:
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6' b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
and escapes unicode objects into a sequence of escaped unicode and escapes strings into a sequence of escaped unicode ids, e.g.:
ids, e.g.:
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944' r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
@ -205,10 +189,10 @@ def ascii_escaped(val: bytes | str) -> str:
a UTF-8 string. a UTF-8 string.
""" """
if isinstance(val, bytes): if isinstance(val, bytes):
ret = _bytes_to_ascii(val) ret = val.decode("ascii", "backslashreplace")
else: else:
ret = val.encode("unicode_escape").decode("ascii") ret = val.encode("unicode_escape").decode("ascii")
return _translate_non_printable(ret) return ret.translate(_non_printable_ascii_translate_table)
@dataclasses.dataclass @dataclasses.dataclass
@ -243,9 +227,7 @@ def get_real_func(obj):
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
raise ValueError( raise ValueError(
("could not find real function of {start}\nstopped at {current}").format( f"could not find real function of {saferepr(start_obj)}\nstopped at {saferepr(obj)}"
start=saferepr(start_obj), current=saferepr(obj)
)
) )
if isinstance(obj, functools.partial): if isinstance(obj, functools.partial):
obj = obj.func obj = obj.func
@ -307,7 +289,7 @@ def get_user_id() -> int | None:
# mypy follows the version and platform checking expectation of PEP 484: # mypy follows the version and platform checking expectation of PEP 484:
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks # https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
# Containment checks are too complex for mypy v1.5.0 and cause failure. # Containment checks are too complex for mypy v1.5.0 and cause failure.
if sys.platform == "win32" or sys.platform == "emscripten": if sys.platform == "win32" or sys.platform == "emscripten": # noqa: PLR1714
# win32 does not have a getuid() function. # win32 does not have a getuid() function.
# Emscripten has a return 0 stub. # Emscripten has a return 0 stub.
return None return None

View File

@ -1,23 +1,22 @@
# mypy: allow-untyped-defs
"""Command line options, ini-file and conftest.py processing.""" """Command line options, ini-file and conftest.py processing."""
import argparse import argparse
import collections.abc import collections.abc
import copy import copy
import dataclasses import dataclasses
import enum import enum
from functools import lru_cache
import glob import glob
import importlib.metadata import importlib.metadata
import inspect import inspect
import os import os
from pathlib import Path
import re import re
import shlex import shlex
import sys import sys
import types
import warnings
from functools import lru_cache
from pathlib import Path
from textwrap import dedent from textwrap import dedent
import types
from types import FunctionType from types import FunctionType
from types import TracebackType
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
@ -37,6 +36,7 @@ from typing import Tuple
from typing import Type from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import warnings
from pluggy import HookimplMarker from pluggy import HookimplMarker
from pluggy import HookimplOpts from pluggy import HookimplOpts
@ -44,15 +44,15 @@ from pluggy import HookspecMarker
from pluggy import HookspecOpts from pluggy import HookspecOpts
from pluggy import PluginManager from pluggy import PluginManager
import _pytest._code
import _pytest.deprecated
import _pytest.hookspec
from .exceptions import PrintHelp as PrintHelp from .exceptions import PrintHelp as PrintHelp
from .exceptions import UsageError as UsageError from .exceptions import UsageError as UsageError
from .findpaths import determine_setup from .findpaths import determine_setup
import _pytest._code
from _pytest._code import ExceptionInfo from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
import _pytest.deprecated
import _pytest.hookspec
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
@ -65,10 +65,12 @@ from _pytest.stash import Stash
from _pytest.warning_types import PytestConfigWarning from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING: if TYPE_CHECKING:
from .argparsing import Argument
from .argparsing import Parser
from _pytest._code.code import _TracebackStyle from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
from .argparsing import Argument, Parser
_PluggyPlugin = object _PluggyPlugin = object
@ -112,16 +114,14 @@ class ConftestImportFailure(Exception):
def __init__( def __init__(
self, self,
path: Path, path: Path,
excinfo: Tuple[Type[Exception], Exception, TracebackType], *,
cause: Exception,
) -> None: ) -> None:
super().__init__(path, excinfo)
self.path = path self.path = path
self.excinfo = excinfo self.cause = cause
def __str__(self) -> str: def __str__(self) -> str:
return "{}: {} (from {})".format( return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
self.excinfo[0].__name__, self.excinfo[1], self.path
)
def filter_traceback_for_conftest_import_failure( def filter_traceback_for_conftest_import_failure(
@ -152,7 +152,7 @@ def main(
try: try:
config = _prepareconfig(args, plugins) config = _prepareconfig(args, plugins)
except ConftestImportFailure as e: except ConftestImportFailure as e:
exc_info = ExceptionInfo.from_exc_info(e.excinfo) exc_info = ExceptionInfo.from_exception(e.cause)
tw = TerminalWriter(sys.stderr) tw = TerminalWriter(sys.stderr)
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True) tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
exc_info.traceback = exc_info.traceback.filter( exc_info.traceback = exc_info.traceback.filter(
@ -238,7 +238,8 @@ essential_plugins = (
"helpconfig", # Provides -p. "helpconfig", # Provides -p.
) )
default_plugins = essential_plugins + ( default_plugins = (
*essential_plugins,
"python", "python",
"terminal", "terminal",
"debugging", "debugging",
@ -493,15 +494,19 @@ class PytestPluginManager(PluginManager):
) )
) )
return None return None
ret: Optional[str] = super().register(plugin, name) plugin_name = super().register(plugin, name)
if ret: if plugin_name is not None:
self.hook.pytest_plugin_registered.call_historic( self.hook.pytest_plugin_registered.call_historic(
kwargs=dict(plugin=plugin, manager=self) kwargs=dict(
plugin=plugin,
plugin_name=plugin_name,
manager=self,
)
) )
if isinstance(plugin, types.ModuleType): if isinstance(plugin, types.ModuleType):
self.consider_module(plugin) self.consider_module(plugin)
return ret return plugin_name
def getplugin(self, name: str): def getplugin(self, name: str):
# Support deprecated naming because plugins (xdist e.g.) use it. # Support deprecated naming because plugins (xdist e.g.) use it.
@ -540,6 +545,7 @@ class PytestPluginManager(PluginManager):
noconftest: bool, noconftest: bool,
rootpath: Path, rootpath: Path,
confcutdir: Optional[Path], confcutdir: Optional[Path],
invocation_dir: Path,
importmode: Union[ImportMode, str], importmode: Union[ImportMode, str],
) -> None: ) -> None:
"""Load initial conftest files given a preparsed "namespace". """Load initial conftest files given a preparsed "namespace".
@ -549,8 +555,9 @@ class PytestPluginManager(PluginManager):
All builtin and 3rd party plugins will have been loaded, however, so All builtin and 3rd party plugins will have been loaded, however, so
common options will not confuse our logic here. common options will not confuse our logic here.
""" """
current = Path.cwd() self._confcutdir = (
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None absolutepath(invocation_dir / confcutdir) if confcutdir else None
)
self._noconftest = noconftest self._noconftest = noconftest
self._using_pyargs = pyargs self._using_pyargs = pyargs
foundanchor = False foundanchor = False
@ -560,7 +567,7 @@ class PytestPluginManager(PluginManager):
i = path.find("::") i = path.find("::")
if i != -1: if i != -1:
path = path[:i] path = path[:i]
anchor = absolutepath(current / path) anchor = absolutepath(invocation_dir / path)
# Ensure we do not break if what appears to be an anchor # Ensure we do not break if what appears to be an anchor
# is in fact a very long option (#10169, #11394). # is in fact a very long option (#10169, #11394).
@ -568,15 +575,21 @@ class PytestPluginManager(PluginManager):
self._try_load_conftest(anchor, importmode, rootpath) self._try_load_conftest(anchor, importmode, rootpath)
foundanchor = True foundanchor = True
if not foundanchor: if not foundanchor:
self._try_load_conftest(current, importmode, rootpath) self._try_load_conftest(invocation_dir, importmode, rootpath)
def _is_in_confcutdir(self, path: Path) -> bool: def _is_in_confcutdir(self, path: Path) -> bool:
"""Whether a path is within the confcutdir. """Whether to consider the given path to load conftests from."""
When false, should not load conftest.
"""
if self._confcutdir is None: if self._confcutdir is None:
return True return True
# The semantics here are literally:
# Do not load a conftest if it is found upwards from confcut dir.
# But this is *not* the same as:
# Load only conftests from confcutdir or below.
# At first glance they might seem the same thing, however we do support use cases where
# we want to load conftests that are not found in confcutdir or below, but are found
# in completely different directory hierarchies like packages installed
# in out-of-source trees.
# (see #9767 for a regression where the logic was inverted).
return path not in self._confcutdir.parents return path not in self._confcutdir.parents
def _try_load_conftest( def _try_load_conftest(
@ -602,9 +615,6 @@ class PytestPluginManager(PluginManager):
if directory in self._dirpath2confmods: if directory in self._dirpath2confmods:
return return
# XXX these days we may rather want to use config.rootpath
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir.
clist = [] clist = []
for parent in reversed((directory, *directory.parents)): for parent in reversed((directory, *directory.parents)):
if self._is_in_confcutdir(parent): if self._is_in_confcutdir(parent):
@ -634,7 +644,8 @@ class PytestPluginManager(PluginManager):
def _importconftest( def _importconftest(
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> types.ModuleType: ) -> types.ModuleType:
existing = self.get_plugin(str(conftestpath)) conftestpath_plugin_name = str(conftestpath)
existing = self.get_plugin(conftestpath_plugin_name)
if existing is not None: if existing is not None:
return cast(types.ModuleType, existing) return cast(types.ModuleType, existing)
@ -653,8 +664,7 @@ class PytestPluginManager(PluginManager):
mod = import_path(conftestpath, mode=importmode, root=rootpath) mod = import_path(conftestpath, mode=importmode, root=rootpath)
except Exception as e: except Exception as e:
assert e.__traceback__ is not None assert e.__traceback__ is not None
exc_info = (type(e), e, e.__traceback__) raise ConftestImportFailure(conftestpath, cause=e) from e
raise ConftestImportFailure(conftestpath, exc_info) from e
self._check_non_top_pytest_plugins(mod, conftestpath) self._check_non_top_pytest_plugins(mod, conftestpath)
@ -663,10 +673,15 @@ class PytestPluginManager(PluginManager):
if dirpath in self._dirpath2confmods: if dirpath in self._dirpath2confmods:
for path, mods in self._dirpath2confmods.items(): for path, mods in self._dirpath2confmods.items():
if dirpath in path.parents or path == dirpath: if dirpath in path.parents or path == dirpath:
assert mod not in mods if mod in mods:
raise AssertionError(
f"While trying to load conftest path {conftestpath!s}, "
f"found that the module {mod} is already loaded with path {mod.__file__}. "
"This is not supposed to happen. Please report this issue to pytest."
)
mods.append(mod) mods.append(mod)
self.trace(f"loading conftestmodule {mod!r}") self.trace(f"loading conftestmodule {mod!r}")
self.consider_conftest(mod) self.consider_conftest(mod, registration_name=conftestpath_plugin_name)
return mod return mod
def _check_non_top_pytest_plugins( def _check_non_top_pytest_plugins(
@ -737,18 +752,17 @@ class PytestPluginManager(PluginManager):
self.set_blocked("pytest_" + name) self.set_blocked("pytest_" + name)
else: else:
name = arg name = arg
# Unblock the plugin. None indicates that it has been blocked. # Unblock the plugin.
# There is no interface with pluggy for this. self.unblock(name)
if self._name2plugin.get(name, -1) is None:
del self._name2plugin[name]
if not name.startswith("pytest_"): if not name.startswith("pytest_"):
if self._name2plugin.get("pytest_" + name, -1) is None: self.unblock("pytest_" + name)
del self._name2plugin["pytest_" + name]
self.import_plugin(arg, consider_entry_points=True) self.import_plugin(arg, consider_entry_points=True)
def consider_conftest(self, conftestmodule: types.ModuleType) -> None: def consider_conftest(
self, conftestmodule: types.ModuleType, registration_name: str
) -> None:
""":meta private:""" """:meta private:"""
self.register(conftestmodule, name=conftestmodule.__file__) self.register(conftestmodule, name=registration_name)
def consider_env(self) -> None: def consider_env(self) -> None:
""":meta private:""" """:meta private:"""
@ -804,7 +818,7 @@ class PytestPluginManager(PluginManager):
def _get_plugin_specs_as_list( def _get_plugin_specs_as_list(
specs: Union[None, types.ModuleType, str, Sequence[str]] specs: Union[None, types.ModuleType, str, Sequence[str]],
) -> List[str]: ) -> List[str]:
"""Parse a plugins specification into a list of plugin names.""" """Parse a plugins specification into a list of plugin names."""
# None means empty. # None means empty.
@ -965,7 +979,8 @@ class Config:
*, *,
invocation_params: Optional[InvocationParams] = None, invocation_params: Optional[InvocationParams] = None,
) -> None: ) -> None:
from .argparsing import Parser, FILE_OR_DIR from .argparsing import FILE_OR_DIR
from .argparsing import Parser
if invocation_params is None: if invocation_params is None:
invocation_params = self.InvocationParams( invocation_params = self.InvocationParams(
@ -1160,6 +1175,7 @@ class Config:
noconftest=early_config.known_args_namespace.noconftest, noconftest=early_config.known_args_namespace.noconftest,
rootpath=early_config.rootpath, rootpath=early_config.rootpath,
confcutdir=early_config.known_args_namespace.confcutdir, confcutdir=early_config.known_args_namespace.confcutdir,
invocation_dir=early_config.invocation_params.dir,
importmode=early_config.known_args_namespace.importmode, importmode=early_config.known_args_namespace.importmode,
) )
@ -1168,8 +1184,8 @@ class Config:
args, namespace=copy.copy(self.option) args, namespace=copy.copy(self.option)
) )
rootpath, inipath, inicfg = determine_setup( rootpath, inipath, inicfg = determine_setup(
ns.inifilename, inifile=ns.inifilename,
ns.file_or_dir + unknown_args, args=ns.file_or_dir + unknown_args,
rootdir_cmd_arg=ns.rootdir or None, rootdir_cmd_arg=ns.rootdir or None,
invocation_dir=self.invocation_params.dir, invocation_dir=self.invocation_params.dir,
) )
@ -1253,6 +1269,8 @@ class Config:
"""Decide the args (initial paths/nodeids) to use given the relevant inputs. """Decide the args (initial paths/nodeids) to use given the relevant inputs.
:param warn: Whether can issue warnings. :param warn: Whether can issue warnings.
:returns: The args and the args source. Guaranteed to be non-empty.
""" """
if args: if args:
source = Config.ArgsSource.ARGS source = Config.ArgsSource.ARGS
@ -1361,12 +1379,7 @@ class Config:
if Version(minver) > Version(pytest.__version__): if Version(minver) > Version(pytest.__version__):
raise pytest.UsageError( raise pytest.UsageError(
"%s: 'minversion' requires pytest-%s, actual pytest-%s'" f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
% (
self.inipath,
minver,
pytest.__version__,
)
) )
def _validate_config_options(self) -> None: def _validate_config_options(self) -> None:
@ -1379,8 +1392,9 @@ class Config:
return return
# Imported lazily to improve start-up time. # Imported lazily to improve start-up time.
from packaging.requirements import InvalidRequirement
from packaging.requirements import Requirement
from packaging.version import Version from packaging.version import Version
from packaging.requirements import InvalidRequirement, Requirement
plugin_info = self.pluginmanager.list_plugin_distinfo() plugin_info = self.pluginmanager.list_plugin_distinfo()
plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info} plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
@ -1552,9 +1566,11 @@ class Config:
# in this case, we already have a list ready to use. # in this case, we already have a list ready to use.
# #
if type == "paths": if type == "paths":
# TODO: This assert is probably not valid in all cases. dp = (
assert self.inipath is not None self.inipath.parent
dp = self.inipath.parent if self.inipath is not None
else self.invocation_params.dir
)
input_values = shlex.split(value) if isinstance(value, str) else value input_values = shlex.split(value) if isinstance(value, str) else value
return [dp / x for x in input_values] return [dp / x for x in input_values]
elif type == "args": elif type == "args":
@ -1600,9 +1616,7 @@ class Config:
key, user_ini_value = ini_config.split("=", 1) key, user_ini_value = ini_config.split("=", 1)
except ValueError as e: except ValueError as e:
raise UsageError( raise UsageError(
"-o/--override-ini expects option=value style (got: {!r}).".format( f"-o/--override-ini expects option=value style (got: {ini_config!r})."
ini_config
)
) from e ) from e
else: else:
if key == name: if key == name:
@ -1662,7 +1676,6 @@ class Config:
can be used to explicitly use the global verbosity level. can be used to explicitly use the global verbosity level.
Example: Example:
.. code-block:: ini .. code-block:: ini
# content of pytest.ini # content of pytest.ini
@ -1842,13 +1855,13 @@ def parse_warning_filter(
try: try:
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined] action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
except warnings._OptionError as e: except warnings._OptionError as e:
raise UsageError(error_template.format(error=str(e))) raise UsageError(error_template.format(error=str(e))) from None
try: try:
category: Type[Warning] = _resolve_warning_category(category_) category: Type[Warning] = _resolve_warning_category(category_)
except Exception: except Exception:
exc_info = ExceptionInfo.from_current() exc_info = ExceptionInfo.from_current()
exception_text = exc_info.getrepr(style="native") exception_text = exc_info.getrepr(style="native")
raise UsageError(error_template.format(error=exception_text)) raise UsageError(error_template.format(error=exception_text)) from None
if message and escape: if message and escape:
message = re.escape(message) message = re.escape(message)
if module and escape: if module and escape:
@ -1861,7 +1874,7 @@ def parse_warning_filter(
except ValueError as e: except ValueError as e:
raise UsageError( raise UsageError(
error_template.format(error=f"invalid lineno {lineno_!r}: {e}") error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
) ) from None
else: else:
lineno = 0 lineno = 0
return action, message, category, module, lineno return action, message, category, module, lineno

View File

@ -1,7 +1,8 @@
# mypy: allow-untyped-defs
import argparse import argparse
from gettext import gettext
import os import os
import sys import sys
from gettext import gettext
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
@ -20,6 +21,7 @@ import _pytest._io
from _pytest.config.exceptions import UsageError from _pytest.config.exceptions import UsageError
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
FILE_OR_DIR = "file_or_dir" FILE_OR_DIR = "file_or_dir"
@ -120,7 +122,7 @@ class Parser:
from _pytest._argcomplete import filescompleter from _pytest._argcomplete import filescompleter
optparser = MyOptionParser(self, self.extra_info, prog=self.prog) optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
groups = self._groups + [self._anonymous] groups = [*self._groups, self._anonymous]
for group in groups: for group in groups:
if group.options: if group.options:
desc = group.description or group.name desc = group.description or group.name
@ -196,9 +198,16 @@ class Parser:
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell * ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
* ``pathlist``: a list of ``py.path``, separated as in a shell * ``pathlist``: a list of ``py.path``, separated as in a shell
For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file.
In case the execution is happening without an ini-file defined,
they will be considered relative to the current working directory (for example with ``--override-ini``).
.. versionadded:: 7.0 .. versionadded:: 7.0
The ``paths`` variable type. The ``paths`` variable type.
.. versionadded:: 8.1
Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of an ini-file.
Defaults to ``string`` if ``None`` or not passed. Defaults to ``string`` if ``None`` or not passed.
:param default: :param default:
Default value if no ini-file option exists but is queried. Default value if no ini-file option exists but is queried.
@ -215,7 +224,7 @@ class Parser:
def get_ini_default_for_type( def get_ini_default_for_type(
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]] type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]],
) -> Any: ) -> Any:
""" """
Used by addini to get the default value for a given ini-option type, when Used by addini to get the default value for a given ini-option type, when
@ -447,7 +456,7 @@ class MyOptionParser(argparse.ArgumentParser):
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]: ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
if not arg_string: if not arg_string:
return None return None
if not arg_string[0] in self.prefix_chars: if arg_string[0] not in self.prefix_chars:
return None return None
if arg_string in self._option_string_actions: if arg_string in self._option_string_actions:
action = self._option_string_actions[arg_string] action = self._option_string_actions[arg_string]

View File

@ -1,6 +1,6 @@
import os import os
import sys
from pathlib import Path from pathlib import Path
import sys
from typing import Dict from typing import Dict
from typing import Iterable from typing import Iterable
from typing import List from typing import List
@ -37,7 +37,6 @@ def load_config_dict_from_file(
Return None if the file does not contain valid pytest configuration. Return None if the file does not contain valid pytest configuration.
""" """
# Configuration from ini files are obtained from the [pytest] section, if present. # Configuration from ini files are obtained from the [pytest] section, if present.
if filepath.suffix == ".ini": if filepath.suffix == ".ini":
iniconfig = _parse_ini_config(filepath) iniconfig = _parse_ini_config(filepath)
@ -87,6 +86,7 @@ def load_config_dict_from_file(
def locate_config( def locate_config(
invocation_dir: Path,
args: Iterable[Path], args: Iterable[Path],
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]: ) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
"""Search in the list of arguments for a valid ini-file for pytest, """Search in the list of arguments for a valid ini-file for pytest,
@ -100,20 +100,28 @@ def locate_config(
] ]
args = [x for x in args if not str(x).startswith("-")] args = [x for x in args if not str(x).startswith("-")]
if not args: if not args:
args = [Path.cwd()] args = [invocation_dir]
found_pyproject_toml: Optional[Path] = None
for arg in args: for arg in args:
argpath = absolutepath(arg) argpath = absolutepath(arg)
for base in (argpath, *argpath.parents): for base in (argpath, *argpath.parents):
for config_name in config_names: for config_name in config_names:
p = base / config_name p = base / config_name
if p.is_file(): if p.is_file():
if p.name == "pyproject.toml" and found_pyproject_toml is None:
found_pyproject_toml = p
ini_config = load_config_dict_from_file(p) ini_config = load_config_dict_from_file(p)
if ini_config is not None: if ini_config is not None:
return base, p, ini_config return base, p, ini_config
if found_pyproject_toml is not None:
return found_pyproject_toml.parent, found_pyproject_toml, {}
return None, None, {} return None, None, {}
def get_common_ancestor(paths: Iterable[Path]) -> Path: def get_common_ancestor(
invocation_dir: Path,
paths: Iterable[Path],
) -> Path:
common_ancestor: Optional[Path] = None common_ancestor: Optional[Path] = None
for path in paths: for path in paths:
if not path.exists(): if not path.exists():
@ -130,7 +138,7 @@ def get_common_ancestor(paths: Iterable[Path]) -> Path:
if shared is not None: if shared is not None:
common_ancestor = shared common_ancestor = shared
if common_ancestor is None: if common_ancestor is None:
common_ancestor = Path.cwd() common_ancestor = invocation_dir
elif common_ancestor.is_file(): elif common_ancestor.is_file():
common_ancestor = common_ancestor.parent common_ancestor = common_ancestor.parent
return common_ancestor return common_ancestor
@ -162,10 +170,11 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte
def determine_setup( def determine_setup(
*,
inifile: Optional[str], inifile: Optional[str],
args: Sequence[str], args: Sequence[str],
rootdir_cmd_arg: Optional[str] = None, rootdir_cmd_arg: Optional[str],
invocation_dir: Optional[Path] = None, invocation_dir: Path,
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]: ) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
"""Determine the rootdir, inifile and ini configuration values from the """Determine the rootdir, inifile and ini configuration values from the
command line arguments. command line arguments.
@ -177,8 +186,7 @@ def determine_setup(
:param rootdir_cmd_arg: :param rootdir_cmd_arg:
The `--rootdir` command line argument, if given. The `--rootdir` command line argument, if given.
:param invocation_dir: :param invocation_dir:
The working directory when pytest was invoked, if known. The working directory when pytest was invoked.
If not known, the current working directory is used.
""" """
rootdir = None rootdir = None
dirs = get_dirs_from_args(args) dirs = get_dirs_from_args(args)
@ -189,8 +197,8 @@ def determine_setup(
if rootdir_cmd_arg is None: if rootdir_cmd_arg is None:
rootdir = inipath_.parent rootdir = inipath_.parent
else: else:
ancestor = get_common_ancestor(dirs) ancestor = get_common_ancestor(invocation_dir, dirs)
rootdir, inipath, inicfg = locate_config([ancestor]) rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor])
if rootdir is None and rootdir_cmd_arg is None: if rootdir is None and rootdir_cmd_arg is None:
for possible_rootdir in (ancestor, *ancestor.parents): for possible_rootdir in (ancestor, *ancestor.parents):
if (possible_rootdir / "setup.py").is_file(): if (possible_rootdir / "setup.py").is_file():
@ -198,22 +206,18 @@ def determine_setup(
break break
else: else:
if dirs != [ancestor]: if dirs != [ancestor]:
rootdir, inipath, inicfg = locate_config(dirs) rootdir, inipath, inicfg = locate_config(invocation_dir, dirs)
if rootdir is None: if rootdir is None:
if invocation_dir is not None: rootdir = get_common_ancestor(
cwd = invocation_dir invocation_dir, [invocation_dir, ancestor]
else: )
cwd = Path.cwd()
rootdir = get_common_ancestor([cwd, ancestor])
if is_fs_root(rootdir): if is_fs_root(rootdir):
rootdir = ancestor rootdir = ancestor
if rootdir_cmd_arg: if rootdir_cmd_arg:
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
if not rootdir.is_dir(): if not rootdir.is_dir():
raise UsageError( raise UsageError(
"Directory '{}' not found. Check your '--rootdir' option.".format( f"Directory '{rootdir}' not found. Check your '--rootdir' option."
rootdir
)
) )
assert rootdir is not None assert rootdir is not None
return rootdir, inipath, inicfg or {} return rootdir, inipath, inicfg or {}

View File

@ -1,9 +1,9 @@
# mypy: allow-untyped-defs
"""Interactive debugging with PDB, the Python Debugger.""" """Interactive debugging with PDB, the Python Debugger."""
import argparse import argparse
import functools import functools
import sys import sys
import types import types
import unittest
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Generator from typing import Generator
@ -13,6 +13,7 @@ from typing import Tuple
from typing import Type from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import unittest
from _pytest import outcomes from _pytest import outcomes
from _pytest._code import ExceptionInfo from _pytest._code import ExceptionInfo
@ -25,6 +26,7 @@ from _pytest.config.exceptions import UsageError
from _pytest.nodes import Node from _pytest.nodes import Node
from _pytest.reports import BaseReport from _pytest.reports import BaseReport
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.capture import CaptureManager from _pytest.capture import CaptureManager
from _pytest.runner import CallInfo from _pytest.runner import CallInfo
@ -263,8 +265,7 @@ class pytestPDB:
elif capturing: elif capturing:
tw.sep( tw.sep(
">", ">",
"PDB %s (IO-capturing turned off for %s)" f"PDB {method} (IO-capturing turned off for {capturing})",
% (method, capturing),
) )
else: else:
tw.sep(">", f"PDB {method}") tw.sep(">", f"PDB {method}")
@ -377,7 +378,8 @@ def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.Traceb
elif isinstance(excinfo.value, ConftestImportFailure): elif isinstance(excinfo.value, ConftestImportFailure):
# A config.ConftestImportFailure is not useful for post_mortem. # A config.ConftestImportFailure is not useful for post_mortem.
# Use the underlying exception instead: # Use the underlying exception instead:
return excinfo.value.excinfo[2] assert excinfo.value.cause.__traceback__ is not None
return excinfo.value.cause.__traceback__
else: else:
assert excinfo._excinfo is not None assert excinfo._excinfo is not None
return excinfo._excinfo[2] return excinfo._excinfo[2]

View File

@ -8,12 +8,14 @@ All constants defined in this module should be either instances of
:class:`PytestWarning`, or :class:`UnformattedWarning` :class:`PytestWarning`, or :class:`UnformattedWarning`
in case of warnings which need to format their messages. in case of warnings which need to format their messages.
""" """
from warnings import warn from warnings import warn
from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import PytestRemovedIn9Warning from _pytest.warning_types import PytestRemovedIn9Warning
from _pytest.warning_types import UnformattedWarning from _pytest.warning_types import UnformattedWarning
# set of plugins which have been integrated into the core; we use this list to ignore # set of plugins which have been integrated into the core; we use this list to ignore
# them during registration to avoid conflicts # them during registration to avoid conflicts
DEPRECATED_EXTERNAL_PLUGINS = { DEPRECATED_EXTERNAL_PLUGINS = {

View File

@ -1,15 +1,15 @@
# mypy: allow-untyped-defs
"""Discover and run doctests in modules and test files.""" """Discover and run doctests in modules and test files."""
import bdb import bdb
from contextlib import contextmanager
import functools import functools
import inspect import inspect
import os import os
from pathlib import Path
import platform import platform
import sys import sys
import traceback import traceback
import types import types
import warnings
from contextlib import contextmanager
from pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
@ -23,6 +23,7 @@ from typing import Tuple
from typing import Type from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import warnings
from _pytest import outcomes from _pytest import outcomes
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
@ -39,13 +40,14 @@ from _pytest.nodes import Item
from _pytest.outcomes import OutcomeException from _pytest.outcomes import OutcomeException
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path
from _pytest.python import Module from _pytest.python import Module
from _pytest.python_api import approx from _pytest.python_api import approx
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
if TYPE_CHECKING: if TYPE_CHECKING:
import doctest import doctest
from typing import Self
DOCTEST_REPORT_CHOICE_NONE = "none" DOCTEST_REPORT_CHOICE_NONE = "none"
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
@ -105,7 +107,7 @@ def pytest_addoption(parser: Parser) -> None:
"--doctest-ignore-import-errors", "--doctest-ignore-import-errors",
action="store_true", action="store_true",
default=False, default=False,
help="Ignore doctest ImportErrors", help="Ignore doctest collection errors",
dest="doctest_ignore_import_errors", dest="doctest_ignore_import_errors",
) )
group.addoption( group.addoption(
@ -132,11 +134,9 @@ def pytest_collect_file(
if config.option.doctestmodules and not any( if config.option.doctestmodules and not any(
(_is_setup_py(file_path), _is_main_py(file_path)) (_is_setup_py(file_path), _is_main_py(file_path))
): ):
mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path) return DoctestModule.from_parent(parent, path=file_path)
return mod
elif _is_doctest(config, file_path, parent): elif _is_doctest(config, file_path, parent):
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path) return DoctestTextfile.from_parent(parent, path=file_path)
return txt
return None return None
@ -271,14 +271,14 @@ class DoctestItem(Item):
self._initrequest() self._initrequest()
@classmethod @classmethod
def from_parent( # type: ignore def from_parent( # type: ignore[override]
cls, cls,
parent: "Union[DoctestTextfile, DoctestModule]", parent: "Union[DoctestTextfile, DoctestModule]",
*, *,
name: str, name: str,
runner: "doctest.DocTestRunner", runner: "doctest.DocTestRunner",
dtest: "doctest.DocTest", dtest: "doctest.DocTest",
): ) -> "Self":
# incompatible signature due to imposed limits on subclass # incompatible signature due to imposed limits on subclass
"""The public named constructor.""" """The public named constructor."""
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
@ -485,9 +485,9 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func)) return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
except Exception as e: except Exception as e:
warnings.warn( warnings.warn(
"Got %r when unwrapping %r. This is usually caused " f"Got {e!r} when unwrapping {func!r}. This is usually caused "
"by a violation of Python's object protocol; see e.g. " "by a violation of Python's object protocol; see e.g. "
"https://github.com/pytest-dev/pytest/issues/5080" % (e, func), "https://github.com/pytest-dev/pytest/issues/5080",
PytestWarning, PytestWarning,
) )
raise raise
@ -558,24 +558,18 @@ class DoctestModule(Module):
else: # pragma: no cover else: # pragma: no cover
pass pass
if self.path.name == "conftest.py": try:
module = self.config.pluginmanager._importconftest( module = self.obj
self.path, except Collector.CollectError:
self.config.getoption("importmode"), if self.config.getvalue("doctest_ignore_import_errors"):
rootpath=self.config.rootpath, skip("unable to import module %r" % self.path)
) else:
else: raise
try:
module = import_path( # While doctests currently don't support fixtures directly, we still
self.path, # need to pick up autouse fixtures.
root=self.config.rootpath, self.session._fixturemanager.parsefactories(self)
mode=self.config.getoption("importmode"),
)
except ImportError:
if self.config.getvalue("doctest_ignore_import_errors"):
skip("unable to import module %r" % self.path)
else:
raise
# Uses internal doctest module parsing mechanism. # Uses internal doctest module parsing mechanism.
finder = MockAwareDocTestFinder() finder = MockAwareDocTestFinder()
optionflags = get_optionflags(self.config) optionflags = get_optionflags(self.config)

View File

@ -2,11 +2,11 @@ import os
import sys import sys
from typing import Generator from typing import Generator
import pytest
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.stash import StashKey from _pytest.stash import StashKey
import pytest
fault_handler_original_stderr_fd_key = StashKey[int]() fault_handler_original_stderr_fd_key = StashKey[int]()

View File

@ -1,12 +1,11 @@
# mypy: allow-untyped-defs
import abc import abc
from collections import defaultdict
from collections import deque
import dataclasses import dataclasses
import functools import functools
import inspect import inspect
import os import os
import warnings
from collections import defaultdict
from collections import deque
from contextlib import suppress
from pathlib import Path from pathlib import Path
from typing import AbstractSet from typing import AbstractSet
from typing import Any from typing import Any
@ -30,6 +29,7 @@ from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import warnings
import _pytest import _pytest
from _pytest import nodes from _pytest import nodes
@ -119,7 +119,7 @@ def get_scope_package(
) -> Optional[nodes.Node]: ) -> Optional[nodes.Node]:
from _pytest.python import Package from _pytest.python import Package
for parent in node.iterparents(): for parent in node.iter_parents():
if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid: if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid:
return parent return parent
return node.session return node.session
@ -168,33 +168,28 @@ def get_parametrized_fixture_keys(
the specified scope.""" the specified scope."""
assert scope is not Scope.Function assert scope is not Scope.Function
try: try:
callspec = item.callspec # type: ignore[attr-defined] callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
except AttributeError: except AttributeError:
pass return
else: for argname in callspec.indices:
cs: CallSpec2 = callspec if callspec._arg2scope[argname] != scope:
# cs.indices is random order of argnames. Need to continue
# 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 item_cls = None
if scope is Scope.Session: if scope is Scope.Session:
scoped_item_path = None scoped_item_path = None
elif scope is Scope.Package: elif scope is Scope.Package:
scoped_item_path = item.path scoped_item_path = item.path
elif scope is Scope.Module: elif scope is Scope.Module:
scoped_item_path = item.path scoped_item_path = item.path
elif scope is Scope.Class: elif scope is Scope.Class:
scoped_item_path = item.path scoped_item_path = item.path
item_cls = item.cls # type: ignore[attr-defined] item_cls = item.cls # type: ignore[attr-defined]
else: else:
assert_never(scope) assert_never(scope)
param_index = cs.indices[argname] param_index = callspec.indices[argname]
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls) yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
# Algorithm for sorting on a per-parametrized resource setup basis. # Algorithm for sorting on a per-parametrized resource setup basis.
@ -582,7 +577,6 @@ class FixtureRequest(abc.ABC):
# (latter managed by fixturedef) # (latter managed by fixturedef)
argname = fixturedef.argname argname = fixturedef.argname
funcitem = self._pyfuncitem funcitem = self._pyfuncitem
scope = fixturedef._scope
try: try:
callspec = funcitem.callspec callspec = funcitem.callspec
except AttributeError: except AttributeError:
@ -590,24 +584,20 @@ class FixtureRequest(abc.ABC):
if callspec is not None and argname in callspec.params: if callspec is not None and argname in callspec.params:
param = callspec.params[argname] param = callspec.params[argname]
param_index = callspec.indices[argname] param_index = callspec.indices[argname]
# If a parametrize invocation set a scope it will override # The parametrize invocation scope overrides the fixture's scope.
# the static scope defined with the fixture function. scope = callspec._arg2scope[argname]
with suppress(KeyError):
scope = callspec._arg2scope[argname]
else: else:
param = NOTSET param = NOTSET
param_index = 0 param_index = 0
scope = fixturedef._scope
has_params = fixturedef.params is not None has_params = fixturedef.params is not None
fixtures_not_supported = getattr(funcitem, "nofuncargs", False) fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
if has_params and fixtures_not_supported: if has_params and fixtures_not_supported:
msg = ( msg = (
"{name} does not support fixtures, maybe unittest.TestCase subclass?\n" f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
"Node id: {nodeid}\n" f"Node id: {funcitem.nodeid}\n"
"Function type: {typename}" f"Function type: {type(funcitem).__name__}"
).format(
name=funcitem.name,
nodeid=funcitem.nodeid,
typename=type(funcitem).__name__,
) )
fail(msg, pytrace=False) fail(msg, pytrace=False)
if has_params: if has_params:
@ -740,9 +730,7 @@ class SubRequest(FixtureRequest):
if node is None and scope is Scope.Class: if node is None and scope is Scope.Class:
# Fallback to function item itself. # Fallback to function item itself.
node = self._pyfuncitem node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format( assert node, f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}'
scope, self._pyfuncitem
)
return node return node
def _check_scope( def _check_scope(
@ -845,8 +833,8 @@ class FixtureLookupError(LookupError):
if faclist: if faclist:
available.add(name) available.add(name)
if self.argname in available: if self.argname in available:
msg = " recursive dependency involving fixture '{}' detected".format( msg = (
self.argname f" recursive dependency involving fixture '{self.argname}' detected"
) )
else: else:
msg = f"fixture '{self.argname}' not found" msg = f"fixture '{self.argname}' not found"
@ -940,15 +928,13 @@ def _eval_scope_callable(
result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg] result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg]
except Exception as e: except Exception as e:
raise TypeError( raise TypeError(
"Error evaluating {} while defining fixture '{}'.\n" f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n"
"Expected a function with the signature (*, fixture_name, config)".format( "Expected a function with the signature (*, fixture_name, config)"
scope_callable, fixture_name
)
) from e ) from e
if not isinstance(result, str): if not isinstance(result, str):
fail( fail(
"Expected {} to return a 'str' while defining fixture '{}', but it returned:\n" f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n"
"{!r}".format(scope_callable, fixture_name, result), f"{result!r}",
pytrace=False, pytrace=False,
) )
return result return result
@ -1090,9 +1076,7 @@ class FixtureDef(Generic[FixtureValue]):
return request.param_index if not hasattr(request, "param") else request.param return request.param_index if not hasattr(request, "param") else request.param
def __repr__(self) -> str: def __repr__(self) -> str:
return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format( return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"
self.argname, self.scope, self.baseid
)
def resolve_fixture_function( def resolve_fixture_function(
@ -1113,7 +1097,8 @@ def resolve_fixture_function(
# Handle the case where fixture is defined not in a test class, but some other class # Handle the case where fixture is defined not in a test class, but some other class
# (for example a plugin class with a fixture), see #2270. # (for example a plugin class with a fixture), see #2270.
if hasattr(fixturefunc, "__self__") and not isinstance( if hasattr(fixturefunc, "__self__") and not isinstance(
request.instance, fixturefunc.__self__.__class__ # type: ignore[union-attr] request.instance,
fixturefunc.__self__.__class__, # type: ignore[union-attr]
): ):
return fixturefunc return fixturefunc
fixturefunc = getimfunc(fixturedef.func) fixturefunc = getimfunc(fixturedef.func)
@ -1196,7 +1181,7 @@ class FixtureFunctionMarker:
if getattr(function, "_pytestfixturefunction", False): if getattr(function, "_pytestfixturefunction", False):
raise ValueError( raise ValueError(
"fixture is being applied more than once to the same function" f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}"
) )
if hasattr(function, "pytestmark"): if hasattr(function, "pytestmark"):
@ -1208,9 +1193,7 @@ class FixtureFunctionMarker:
if name == "request": if name == "request":
location = getlocation(function) location = getlocation(function)
fail( fail(
"'request' is a reserved word for fixtures, use another name:\n {}".format( f"'request' is a reserved word for fixtures, use another name:\n {location}",
location
),
pytrace=False, pytrace=False,
) )
@ -1235,7 +1218,7 @@ def fixture(
@overload @overload
def fixture( # noqa: F811 def fixture(
fixture_function: None = ..., fixture_function: None = ...,
*, *,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
@ -1249,7 +1232,7 @@ def fixture( # noqa: F811
... ...
def fixture( # noqa: F811 def fixture(
fixture_function: Optional[FixtureFunction] = None, fixture_function: Optional[FixtureFunction] = None,
*, *,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function", scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
@ -1483,25 +1466,27 @@ class FixtureManager:
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
def pytest_plugin_registered(self, plugin: _PluggyPlugin) -> None: def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> None:
nodeid = None # Fixtures defined in conftest plugins are only visible to within the
try: # conftest's directory. This is unlike fixtures in non-conftest plugins
p = absolutepath(plugin.__file__) # type: ignore[attr-defined] # which have global visibility. So for conftests, construct the base
except AttributeError: # nodeid from the plugin name (which is the conftest path).
pass if plugin_name and plugin_name.endswith("conftest.py"):
# Note: we explicitly do *not* use `plugin.__file__` here -- The
# difference is that plugin_name has the correct capitalization on
# case-insensitive systems (Windows) and other normalization issues
# (issue #11816).
conftestpath = absolutepath(plugin_name)
try:
nodeid = str(conftestpath.parent.relative_to(self.config.rootpath))
except ValueError:
nodeid = ""
if nodeid == ".":
nodeid = ""
if os.sep != nodes.SEP:
nodeid = nodeid.replace(os.sep, nodes.SEP)
else: else:
# Construct the base nodeid which is later used to check nodeid = None
# what fixtures are visible for particular tests (as denoted
# by their test id).
if p.name == "conftest.py":
try:
nodeid = str(p.parent.relative_to(self.config.rootpath))
except ValueError:
nodeid = ""
if nodeid == ".":
nodeid = ""
if os.sep != nodes.SEP:
nodeid = nodeid.replace(os.sep, nodes.SEP)
self.parsefactories(plugin, nodeid) self.parsefactories(plugin, nodeid)
@ -1681,7 +1666,7 @@ class FixtureManager:
raise NotImplementedError() raise NotImplementedError()
@overload @overload
def parsefactories( # noqa: F811 def parsefactories(
self, self,
node_or_obj: object, node_or_obj: object,
nodeid: Optional[str], nodeid: Optional[str],
@ -1690,7 +1675,7 @@ class FixtureManager:
) -> None: ) -> None:
raise NotImplementedError() raise NotImplementedError()
def parsefactories( # noqa: F811 def parsefactories(
self, self,
node_or_obj: Union[nodes.Node, object], node_or_obj: Union[nodes.Node, object],
nodeid: Union[str, NotSetType, None] = NOTSET, nodeid: Union[str, NotSetType, None] = NOTSET,
@ -1775,7 +1760,7 @@ class FixtureManager:
def _matchfactories( def _matchfactories(
self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
) -> Iterator[FixtureDef[Any]]: ) -> Iterator[FixtureDef[Any]]:
parentnodeids = {n.nodeid for n in node.iterparents()} parentnodeids = {n.nodeid for n in node.iter_parents()}
for fixturedef in fixturedefs: for fixturedef in fixturedefs:
if fixturedef.baseid in parentnodeids: if fixturedef.baseid in parentnodeids:
yield fixturedef yield fixturedef

View File

@ -1,5 +1,6 @@
"""Provides a function to report all internal modules for using freezing """Provides a function to report all internal modules for using freezing
tools.""" tools."""
import types import types
from typing import Iterator from typing import Iterator
from typing import List from typing import List

View File

@ -1,18 +1,19 @@
# mypy: allow-untyped-defs
"""Version info, help messages, tracing configuration.""" """Version info, help messages, tracing configuration."""
from argparse import Action
import os import os
import sys import sys
from argparse import Action
from typing import Generator from typing import Generator
from typing import List from typing import List
from typing import Optional from typing import Optional
from typing import Union from typing import Union
import pytest
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import PrintHelp from _pytest.config import PrintHelp
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
import pytest
class HelpAction(Action): class HelpAction(Action):
@ -108,11 +109,11 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
path = config.option.debug path = config.option.debug
debugfile = open(path, "w", encoding="utf-8") debugfile = open(path, "w", encoding="utf-8")
debugfile.write( debugfile.write(
"versions pytest-%s, " "versions pytest-{}, "
"python-%s\ncwd=%s\nargs=%s\n\n" "python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format(
% (
pytest.__version__, pytest.__version__,
".".join(map(str, sys.version_info)), ".".join(map(str, sys.version_info)),
config.invocation_params.dir,
os.getcwd(), os.getcwd(),
config.invocation_params.args, config.invocation_params.args,
) )
@ -135,9 +136,7 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
def showversion(config: Config) -> None: def showversion(config: Config) -> None:
if config.option.version > 1: if config.option.version > 1:
sys.stdout.write( sys.stdout.write(
"This is pytest version {}, imported from {}\n".format( f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n"
pytest.__version__, pytest.__file__
)
) )
plugininfo = getpluginversioninfo(config) plugininfo = getpluginversioninfo(config)
if plugininfo: if plugininfo:

View File

@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Hook specifications for pytest plugins which are invoked by pytest itself """Hook specifications for pytest plugins which are invoked by pytest itself
and by builtin plugins.""" and by builtin plugins."""
from pathlib import Path from pathlib import Path
@ -13,17 +14,18 @@ from typing import Union
from pluggy import HookspecMarker from pluggy import HookspecMarker
if TYPE_CHECKING: if TYPE_CHECKING:
import pdb import pdb
import warnings
from typing import Literal from typing import Literal
import warnings
from _pytest._code.code import ExceptionRepr
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ExceptionRepr
from _pytest.config import _PluggyPlugin
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import PytestPluginManager from _pytest.config import PytestPluginManager
from _pytest.config import _PluggyPlugin
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureDef
from _pytest.fixtures import SubRequest from _pytest.fixtures import SubRequest
@ -54,24 +56,41 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
"""Called at plugin registration time to allow adding new hooks via a call to """Called at plugin registration time to allow adding new hooks via a call to
:func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`. :func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`.
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager. :param pluginmanager: The pytest plugin manager.
.. note:: .. note::
This hook is incompatible with hook wrappers. This hook is incompatible with hook wrappers.
Use in conftest plugins
=======================
If a conftest plugin implements this hook, it will be called immediately
when the conftest is registered.
""" """
@hookspec(historic=True) @hookspec(historic=True)
def pytest_plugin_registered( def pytest_plugin_registered(
plugin: "_PluggyPlugin", manager: "PytestPluginManager" plugin: "_PluggyPlugin",
plugin_name: str,
manager: "PytestPluginManager",
) -> None: ) -> None:
"""A new pytest plugin got registered. """A new pytest plugin got registered.
:param plugin: The plugin module or instance. :param plugin: The plugin module or instance.
:param pytest.PytestPluginManager manager: pytest plugin manager. :param plugin_name: The name by which the plugin is registered.
:param manager: The pytest plugin manager.
.. note:: .. note::
This hook is incompatible with hook wrappers. This hook is incompatible with hook wrappers.
Use in conftest plugins
=======================
If a conftest plugin implements this hook, it will be called immediately
when the conftest is registered, once for each plugin registered thus far
(including itself!), and for all plugins thereafter when they are
registered.
""" """
@ -80,19 +99,13 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
"""Register argparse-style options and ini-style config values, """Register argparse-style options and ini-style config values,
called once at the beginning of a test run. called once at the beginning of a test run.
.. note:: :param parser:
This function should be implemented only in plugins or ``conftest.py``
files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`.
:param pytest.Parser parser:
To add command line options, call To add command line options, call
:py:func:`parser.addoption(...) <pytest.Parser.addoption>`. :py:func:`parser.addoption(...) <pytest.Parser.addoption>`.
To add ini-file values call :py:func:`parser.addini(...) To add ini-file values call :py:func:`parser.addini(...)
<pytest.Parser.addini>`. <pytest.Parser.addini>`.
:param pytest.PytestPluginManager pluginmanager: :param pluginmanager:
The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks
to change how command line options are added. to change how command line options are added.
@ -111,6 +124,14 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
.. note:: .. note::
This hook is incompatible with hook wrappers. This hook is incompatible with hook wrappers.
Use in conftest plugins
=======================
If a conftest plugin implements this hook, it will be called immediately
when the conftest is registered.
This hook is only called for :ref:`initial conftests <pluginorder>`.
""" """
@ -118,16 +139,17 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
def pytest_configure(config: "Config") -> None: def pytest_configure(config: "Config") -> None:
"""Allow plugins and conftest files to perform initial configuration. """Allow plugins and conftest files to perform initial configuration.
This hook is called for every plugin and initial conftest file
after command line options have been parsed.
After that, the hook is called for other conftest files as they are
imported.
.. note:: .. note::
This hook is incompatible with hook wrappers. This hook is incompatible with hook wrappers.
:param pytest.Config config: The pytest config object. :param config: The pytest config object.
Use in conftest plugins
=======================
This hook is called for every :ref:`initial conftest <pluginorder>` file
after command line options have been parsed. After that, the hook is called
for other conftest files as they are registered.
""" """
@ -146,40 +168,54 @@ def pytest_cmdline_parse(
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
.. note:: .. note::
This hook will only be called for plugin classes passed to the This hook is only called for plugin classes passed to the
``plugins`` arg when using `pytest.main`_ to perform an in-process ``plugins`` arg when using `pytest.main`_ to perform an in-process
test run. test run.
:param pluginmanager: The pytest plugin manager. :param pluginmanager: The pytest plugin manager.
:param args: List of arguments passed on the command line. :param args: List of arguments passed on the command line.
:returns: A pytest config object. :returns: A pytest config object.
"""
Use in conftest plugins
=======================
@hookspec(firstresult=True) This hook is not called for conftest files.
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
"""Called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop.
Stops at first non-None result, see :ref:`firstresult`.
:param config: The pytest config object.
:returns: The exit code.
""" """
def pytest_load_initial_conftests( def pytest_load_initial_conftests(
early_config: "Config", parser: "Parser", args: List[str] early_config: "Config", parser: "Parser", args: List[str]
) -> None: ) -> None:
"""Called to implement the loading of initial conftest files ahead """Called to implement the loading of :ref:`initial conftest files
of command line option parsing. <pluginorder>` ahead of command line option parsing.
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
:param early_config: The pytest config object. :param early_config: The pytest config object.
:param args: Arguments passed on the command line. :param args: Arguments passed on the command line.
:param parser: To add command line options. :param parser: To add command line options.
Use in conftest plugins
=======================
This hook is not called for conftest files.
"""
@hookspec(firstresult=True)
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
"""Called for performing the main command line action.
The default implementation will invoke the configure hooks and
:hook:`pytest_runtestloop`.
Stops at first non-None result, see :ref:`firstresult`.
:param config: The pytest config object.
:returns: The exit code.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
""" """
@ -222,6 +258,11 @@ def pytest_collection(session: "Session") -> Optional[object]:
counter (and returns `None`). counter (and returns `None`).
:param session: The pytest session object. :param session: The pytest session object.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
""" """
@ -234,6 +275,11 @@ def pytest_collection_modifyitems(
:param session: The pytest session object. :param session: The pytest session object.
:param config: The pytest config object. :param config: The pytest config object.
:param items: List of item objects. :param items: List of item objects.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
""" """
@ -241,6 +287,11 @@ def pytest_collection_finish(session: "Session") -> None:
"""Called after collection has been performed and modified. """Called after collection has been performed and modified.
:param session: The pytest session object. :param session: The pytest session object.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
""" """
@ -263,6 +314,14 @@ def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[b
.. versionchanged:: 8.0.0 .. versionchanged:: 8.0.0
The ``path`` parameter has been removed. The ``path`` parameter has been removed.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collection path, only
conftest files in parent directories of the collection path are consulted
(if the path is a directory, its own conftest file is *not* consulted - a
directory cannot ignore itself!).
""" """
@ -284,6 +343,14 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
See :ref:`custom directory collectors` for a simple example of use of this See :ref:`custom directory collectors` for a simple example of use of this
hook. hook.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collection path, only
conftest files in parent directories of the collection path are consulted
(if the path is a directory, its own conftest file is *not* consulted - a
directory cannot collect itself!).
""" """
@ -304,6 +371,12 @@ def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Colle
.. versionchanged:: 8.0.0 .. versionchanged:: 8.0.0
The ``path`` parameter was removed. The ``path`` parameter was removed.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given file path, only
conftest files in parent directories of the file path are consulted.
""" """
@ -315,6 +388,13 @@ def pytest_collectstart(collector: "Collector") -> None:
:param collector: :param collector:
The collector. The collector.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories are
consulted.
""" """
@ -323,6 +403,12 @@ def pytest_itemcollected(item: "Item") -> None:
:param item: :param item:
The item. The item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
""" """
@ -331,6 +417,13 @@ def pytest_collectreport(report: "CollectReport") -> None:
:param report: :param report:
The collect report. The collect report.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories are
consulted.
""" """
@ -341,6 +434,11 @@ def pytest_deselected(items: Sequence["Item"]) -> None:
:param items: :param items:
The items. The items.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
""" """
@ -353,6 +451,13 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
:param collector: :param collector:
The collector. The collector.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories are
consulted.
""" """
@ -380,6 +485,13 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]
.. versionchanged:: 8.0.0 .. versionchanged:: 8.0.0
The ``path`` parameter has been removed in favor of ``module_path``. The ``path`` parameter has been removed in favor of ``module_path``.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given parent collector,
only conftest files in the collector's directory and its parent directories
are consulted.
""" """
@ -399,6 +511,13 @@ def pytest_pycollect_makeitem(
The object. The object.
:returns: :returns:
The created items/collectors. The created items/collectors.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories
are consulted.
""" """
@ -410,6 +529,13 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
:param pyfuncitem: :param pyfuncitem:
The function item. The function item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only
conftest files in the item's directory and its parent directories
are consulted.
""" """
@ -418,6 +544,13 @@ def pytest_generate_tests(metafunc: "Metafunc") -> None:
:param metafunc: :param metafunc:
The :class:`~pytest.Metafunc` helper for the test function. The :class:`~pytest.Metafunc` helper for the test function.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given function definition,
only conftest files in the functions's directory and its parent directories
are consulted.
""" """
@ -435,7 +568,12 @@ def pytest_make_parametrize_id(
:param config: The pytest config object. :param config: The pytest config object.
:param val: The parametrized value. :param val: The parametrized value.
:param str argname: The automatic parameter name produced by pytest. :param argname: The automatic parameter name produced by pytest.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
""" """
@ -462,6 +600,11 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
The return value is not used, but only stops further processing. The return value is not used, but only stops further processing.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
""" """
@ -500,6 +643,11 @@ def pytest_runtest_protocol(
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
The return value is not used, but only stops further processing. The return value is not used, but only stops further processing.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
""" """
@ -514,6 +662,12 @@ def pytest_runtest_logstart(
:param location: A tuple of ``(filename, lineno, testname)`` :param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath`` where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based. and ``lineno`` is 0-based.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
""" """
@ -528,6 +682,12 @@ def pytest_runtest_logfinish(
:param location: A tuple of ``(filename, lineno, testname)`` :param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath`` where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based. and ``lineno`` is 0-based.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
""" """
@ -541,6 +701,12 @@ def pytest_runtest_setup(item: "Item") -> None:
:param item: :param item:
The item. The item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
""" """
@ -551,6 +717,12 @@ def pytest_runtest_call(item: "Item") -> None:
:param item: :param item:
The item. The item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
""" """
@ -569,6 +741,12 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
scheduled). This argument is used to perform exact teardowns, i.e. scheduled). This argument is used to perform exact teardowns, i.e.
calling just enough finalizers so that nextitem only needs to call calling just enough finalizers so that nextitem only needs to call
setup functions. setup functions.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
""" """
@ -585,6 +763,12 @@ def pytest_runtest_makereport(
:param call: The :class:`~pytest.CallInfo` for the phase. :param call: The :class:`~pytest.CallInfo` for the phase.
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
""" """
@ -593,6 +777,12 @@ def pytest_runtest_logreport(report: "TestReport") -> None:
of the setup, call and teardown runtest phases of an item. of the setup, call and teardown runtest phases of an item.
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
""" """
@ -606,6 +796,12 @@ def pytest_report_to_serializable(
:param config: The pytest config object. :param config: The pytest config object.
:param report: The report. :param report: The report.
Use in conftest plugins
=======================
Any conftest file can implement this hook. The exact details may depend
on the plugin which calls the hook.
""" """
@ -618,6 +814,12 @@ def pytest_report_from_serializable(
:hook:`pytest_report_to_serializable`. :hook:`pytest_report_to_serializable`.
:param config: The pytest config object. :param config: The pytest config object.
Use in conftest plugins
=======================
Any conftest file can implement this hook. The exact details may depend
on the plugin which calls the hook.
""" """
@ -645,6 +847,13 @@ def pytest_fixture_setup(
If the fixture function returns None, other implementations of If the fixture function returns None, other implementations of
this hook function will continue to be called, according to the this hook function will continue to be called, according to the
behavior of the :ref:`firstresult` option. behavior of the :ref:`firstresult` option.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given fixture, only
conftest files in the fixture scope's directory and its parent directories
are consulted.
""" """
@ -659,6 +868,13 @@ def pytest_fixture_post_finalizer(
The fixture definition object. The fixture definition object.
:param request: :param request:
The fixture request object. The fixture request object.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given fixture, only
conftest files in the fixture scope's directory and its parent directories
are consulted.
""" """
@ -672,6 +888,11 @@ def pytest_sessionstart(session: "Session") -> None:
and entering the run test loop. and entering the run test loop.
:param session: The pytest session object. :param session: The pytest session object.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
""" """
@ -683,6 +904,11 @@ def pytest_sessionfinish(
:param session: The pytest session object. :param session: The pytest session object.
:param exitstatus: The status which pytest will return to the system. :param exitstatus: The status which pytest will return to the system.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
""" """
@ -690,6 +916,11 @@ def pytest_unconfigure(config: "Config") -> None:
"""Called before test process is exited. """Called before test process is exited.
:param config: The pytest config object. :param config: The pytest config object.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
""" """
@ -712,6 +943,12 @@ def pytest_assertrepr_compare(
:param op: The operator, e.g. `"=="`, `"!="`, `"not in"`. :param op: The operator, e.g. `"=="`, `"!="`, `"not in"`.
:param left: The left operand. :param left: The left operand.
:param right: The right operand. :param right: The right operand.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
""" """
@ -740,6 +977,12 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
:param lineno: Line number of the assert statement. :param lineno: Line number of the assert statement.
:param orig: String with the original assertion. :param orig: String with the original assertion.
:param expl: String with the assert explanation. :param expl: String with the assert explanation.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
""" """
@ -764,18 +1007,17 @@ def pytest_report_header( # type:ignore[empty-body]
If you want to have your line(s) displayed first, use If you want to have your line(s) displayed first, use
:ref:`trylast=True <plugin-hookorder>`. :ref:`trylast=True <plugin-hookorder>`.
.. note::
This function should be implemented only in plugins or ``conftest.py``
files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`.
.. versionchanged:: 7.0.0 .. versionchanged:: 7.0.0
The ``start_path`` parameter was added as a :class:`pathlib.Path` The ``start_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter. equivalent of the ``startdir`` parameter.
.. versionchanged:: 8.0.0 .. versionchanged:: 8.0.0
The ``startdir`` parameter has been removed. The ``startdir`` parameter has been removed.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
""" """
@ -809,6 +1051,11 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
.. versionchanged:: 8.0.0 .. versionchanged:: 8.0.0
The ``startdir`` parameter has been removed. The ``startdir`` parameter has been removed.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
""" """
@ -837,6 +1084,11 @@ def pytest_report_teststatus( # type:ignore[empty-body]
:returns: The test status. :returns: The test status.
Stops at first non-None result, see :ref:`firstresult`. Stops at first non-None result, see :ref:`firstresult`.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
""" """
@ -853,6 +1105,11 @@ def pytest_terminal_summary(
.. versionadded:: 4.2 .. versionadded:: 4.2
The ``config`` parameter. The ``config`` parameter.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
""" """
@ -877,7 +1134,8 @@ def pytest_warning_recorded(
* ``"runtest"``: during test execution. * ``"runtest"``: during test execution.
:param nodeid: :param nodeid:
Full id of the item. Full id of the item. Empty string for warnings that are not specific to
a particular node.
:param location: :param location:
When available, holds information about the execution context of the captured When available, holds information about the execution context of the captured
@ -885,6 +1143,13 @@ def pytest_warning_recorded(
when the execution context is at the module level. when the execution context is at the module level.
.. versionadded:: 6.0 .. versionadded:: 6.0
Use in conftest plugins
=======================
Any conftest file can implement this hook. If the warning is specific to a
particular node, only conftest files in parent directories of the node are
consulted.
""" """
@ -908,6 +1173,12 @@ def pytest_markeval_namespace( # type:ignore[empty-body]
:param config: The pytest config object. :param config: The pytest config object.
:returns: A dictionary of additional globals to add. :returns: A dictionary of additional globals to add.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in parent directories of the item are consulted.
""" """
@ -927,6 +1198,11 @@ def pytest_internalerror(
:param excrepr: The exception repr object. :param excrepr: The exception repr object.
:param excinfo: The exception info. :param excinfo: The exception info.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
""" """
@ -936,6 +1212,11 @@ def pytest_keyboard_interrupt(
"""Called for keyboard interrupt. """Called for keyboard interrupt.
:param excinfo: The exception info. :param excinfo: The exception info.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
""" """
@ -962,6 +1243,12 @@ def pytest_exception_interact(
The call information. Contains the exception. The call information. Contains the exception.
:param report: :param report:
The collection or test report. The collection or test report.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given node, only conftest
files in parent directories of the node are consulted.
""" """
@ -973,6 +1260,11 @@ def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
:param config: The pytest config object. :param config: The pytest config object.
:param pdb: The Pdb instance. :param pdb: The Pdb instance.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
""" """
@ -984,4 +1276,9 @@ def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
:param config: The pytest config object. :param config: The pytest config object.
:param pdb: The Pdb instance. :param pdb: The Pdb instance.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
""" """

View File

@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Report test results in JUnit-XML format, for use with Jenkins and build """Report test results in JUnit-XML format, for use with Jenkins and build
integration servers. integration servers.
@ -6,12 +7,11 @@ Based on initial code from Ross Lawley.
Output conforms to Output conforms to
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
""" """
from datetime import datetime
import functools import functools
import os import os
import platform import platform
import re import re
import xml.etree.ElementTree as ET
from datetime import datetime
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import List from typing import List
@ -19,8 +19,8 @@ from typing import Match
from typing import Optional from typing import Optional
from typing import Tuple from typing import Tuple
from typing import Union from typing import Union
import xml.etree.ElementTree as ET
import pytest
from _pytest import nodes from _pytest import nodes
from _pytest import timing from _pytest import timing
from _pytest._code.code import ExceptionRepr from _pytest._code.code import ExceptionRepr
@ -32,6 +32,7 @@ from _pytest.fixtures import FixtureRequest
from _pytest.reports import TestReport from _pytest.reports import TestReport
from _pytest.stash import StashKey from _pytest.stash import StashKey
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
import pytest
xml_key = StashKey["LogXML"]() xml_key = StashKey["LogXML"]()
@ -248,7 +249,9 @@ class _NodeReporter:
skipreason = skipreason[9:] skipreason = skipreason[9:]
details = f"{filename}:{lineno}: {skipreason}" details = f"{filename}:{lineno}: {skipreason}"
skipped = ET.Element("skipped", type="pytest.skip", message=skipreason) skipped = ET.Element(
"skipped", type="pytest.skip", message=bin_xml_escape(skipreason)
)
skipped.text = bin_xml_escape(details) skipped.text = bin_xml_escape(details)
self.append(skipped) self.append(skipped)
self.write_captured_output(report) self.write_captured_output(report)
@ -271,9 +274,7 @@ def _warn_incompatibility_with_xunit2(
if xml is not None and xml.family not in ("xunit1", "legacy"): if xml is not None and xml.family not in ("xunit1", "legacy"):
request.node.warn( request.node.warn(
PytestWarning( PytestWarning(
"{fixture_name} is incompatible with junit_family '{family}' (use 'legacy' or 'xunit1')".format( f"{fixture_name} is incompatible with junit_family '{xml.family}' (use 'legacy' or 'xunit1')"
fixture_name=fixture_name, family=xml.family
)
) )
) )
@ -365,7 +366,6 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See `pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
:issue:`7767` for details. :issue:`7767` for details.
""" """
__tracebackhide__ = True __tracebackhide__ = True
def record_func(name: str, value: object) -> None: def record_func(name: str, value: object) -> None:
@ -375,7 +375,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
xml = request.config.stash.get(xml_key, None) xml = request.config.stash.get(xml_key, None)
if xml is not None: if xml is not None:
record_func = xml.add_global_property # noqa record_func = xml.add_global_property
return record_func return record_func
@ -624,7 +624,7 @@ class LogXML:
def update_testcase_duration(self, report: TestReport) -> None: def update_testcase_duration(self, report: TestReport) -> None:
"""Accumulate total duration for nodeid from given report and update """Accumulate total duration for nodeid from given report and update
the Junit.testcase with the new total if already created.""" the Junit.testcase with the new total if already created."""
if self.report_duration == "total" or report.when == self.report_duration: if self.report_duration in {"total", report.when}:
reporter = self.node_reporter(report) reporter = self.node_reporter(report)
reporter.duration += getattr(report, "duration", 0.0) reporter.duration += getattr(report, "duration", 0.0)

View File

@ -1,9 +1,10 @@
# mypy: allow-untyped-defs
"""Add backward compatibility support for the legacy py path type.""" """Add backward compatibility support for the legacy py path type."""
import dataclasses import dataclasses
import os import os
from pathlib import Path
import shlex import shlex
import subprocess import subprocess
from pathlib import Path
from typing import Final from typing import Final
from typing import final from typing import final
from typing import List from typing import List
@ -14,6 +15,7 @@ from typing import Union
from iniconfig import SectionWrapper from iniconfig import SectionWrapper
import py import py
from _pytest.cacheprovider import Cache from _pytest.cacheprovider import Cache
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import hookimpl from _pytest.config import hookimpl
@ -32,6 +34,7 @@ from _pytest.pytester import RunResult
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
from _pytest.tmpdir import TempPathFactory from _pytest.tmpdir import TempPathFactory
if TYPE_CHECKING: if TYPE_CHECKING:
import pexpect import pexpect
@ -326,8 +329,8 @@ class LegacyTmpdirPlugin:
By default, a new base temporary directory is created each test session, By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See :ref:`base ``--basetemp`` is used then it is cleared each session. See
temporary directory`. :ref:`temporary directory location and retention`.
The returned object is a `legacy_path`_ object. The returned object is a `legacy_path`_ object.

View File

@ -1,16 +1,17 @@
# mypy: allow-untyped-defs
"""Access and control log capturing.""" """Access and control log capturing."""
import io
import logging
import os
import re
from contextlib import contextmanager from contextlib import contextmanager
from contextlib import nullcontext from contextlib import nullcontext
from datetime import datetime from datetime import datetime
from datetime import timedelta from datetime import timedelta
from datetime import timezone from datetime import timezone
import io
from io import StringIO from io import StringIO
import logging
from logging import LogRecord from logging import LogRecord
import os
from pathlib import Path from pathlib import Path
import re
from types import TracebackType from types import TracebackType
from typing import AbstractSet from typing import AbstractSet
from typing import Dict from typing import Dict
@ -43,6 +44,7 @@ from _pytest.main import Session
from _pytest.stash import StashKey from _pytest.stash import StashKey
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
if TYPE_CHECKING: if TYPE_CHECKING:
logging_StreamHandler = logging.StreamHandler[StringIO] logging_StreamHandler = logging.StreamHandler[StringIO]
else: else:
@ -71,7 +73,8 @@ class DatetimeFormatter(logging.Formatter):
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone) tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
# Construct `datetime.datetime` object from `struct_time` # Construct `datetime.datetime` object from `struct_time`
# and msecs information from `record` # and msecs information from `record`
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz) # Using int() instead of round() to avoid it exceeding 1_000_000 and causing a ValueError (#11861).
dt = datetime(*ct[0:6], microsecond=int(record.msecs * 1000), tzinfo=tz)
return dt.strftime(datefmt) return dt.strftime(datefmt)
# Use `logging.Formatter` for non-microsecond formats # Use `logging.Formatter` for non-microsecond formats
return super().formatTime(record, datefmt) return super().formatTime(record, datefmt)
@ -114,7 +117,6 @@ class ColoredLevelFormatter(DatetimeFormatter):
.. warning:: .. warning::
This is an experimental API. This is an experimental API.
""" """
assert self._fmt is not None assert self._fmt is not None
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt) levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
if not levelname_fmt_match: if not levelname_fmt_match:
@ -181,7 +183,6 @@ class PercentStyleMultiline(logging.PercentStyle):
0 (auto-indent turned off) or 0 (auto-indent turned off) or
>0 (explicitly set indentation position). >0 (explicitly set indentation position).
""" """
if auto_indent_option is None: if auto_indent_option is None:
return 0 return 0
elif isinstance(auto_indent_option, bool): elif isinstance(auto_indent_option, bool):
@ -297,6 +298,13 @@ def pytest_addoption(parser: Parser) -> None:
default=None, default=None,
help="Path to a file when logging will be written to", help="Path to a file when logging will be written to",
) )
add_option_ini(
"--log-file-mode",
dest="log_file_mode",
default="w",
choices=["w", "a"],
help="Log file open mode",
)
add_option_ini( add_option_ini(
"--log-file-level", "--log-file-level",
dest="log_file_level", dest="log_file_level",
@ -623,9 +631,9 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i
except ValueError as e: except ValueError as e:
# Python logging does not recognise this as a logging level # Python logging does not recognise this as a logging level
raise UsageError( raise UsageError(
"'{}' is not recognized as a logging level name for " f"'{log_level}' is not recognized as a logging level name for "
"'{}'. Please consider passing the " f"'{setting_name}'. Please consider passing the "
"logging level num instead.".format(log_level, setting_name) "logging level num instead."
) from e ) from e
@ -668,7 +676,10 @@ class LoggingPlugin:
if not os.path.isdir(directory): if not os.path.isdir(directory):
os.makedirs(directory) os.makedirs(directory)
self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8") self.log_file_mode = get_option_ini(config, "log_file_mode") or "w"
self.log_file_handler = _FileHandler(
log_file, mode=self.log_file_mode, encoding="UTF-8"
)
log_file_format = get_option_ini(config, "log_file_format", "log_format") log_file_format = get_option_ini(config, "log_file_format", "log_format")
log_file_date_format = get_option_ini( log_file_date_format = get_option_ini(
config, "log_file_date_format", "log_date_format" config, "log_file_date_format", "log_date_format"
@ -745,7 +756,7 @@ class LoggingPlugin:
fpath.parent.mkdir(exist_ok=True, parents=True) fpath.parent.mkdir(exist_ok=True, parents=True)
# https://github.com/python/mypy/issues/11193 # https://github.com/python/mypy/issues/11193
stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment] stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment]
old_stream = self.log_file_handler.setStream(stream) old_stream = self.log_file_handler.setStream(stream)
if old_stream: if old_stream:
old_stream.close() old_stream.close()

View File

@ -1,13 +1,13 @@
"""Core implementation of the testing process: init, session, runtest loop.""" """Core implementation of the testing process: init, session, runtest loop."""
import argparse import argparse
import dataclasses import dataclasses
import fnmatch import fnmatch
import functools import functools
import importlib import importlib
import os import os
import sys
import warnings
from pathlib import Path from pathlib import Path
import sys
from typing import AbstractSet from typing import AbstractSet
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
@ -21,12 +21,14 @@ from typing import Optional
from typing import overload from typing import overload
from typing import Sequence from typing import Sequence
from typing import Tuple from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union from typing import Union
import warnings
import pluggy import pluggy
import _pytest._code
from _pytest import nodes from _pytest import nodes
import _pytest._code
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import directory_arg from _pytest.config import directory_arg
from _pytest.config import ExitCode from _pytest.config import ExitCode
@ -48,6 +50,10 @@ from _pytest.runner import SetupState
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
from typing import Self
def pytest_addoption(parser: Parser) -> None: def pytest_addoption(parser: Parser) -> None:
parser.addini( parser.addini(
"norecursedirs", "norecursedirs",
@ -376,6 +382,9 @@ def _in_venv(path: Path) -> bool:
def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]: def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
if collection_path.name == "__pycache__":
return True
ignore_paths = config._getconftest_pathlist( ignore_paths = config._getconftest_pathlist(
"collect_ignore", path=collection_path.parent "collect_ignore", path=collection_path.parent
) )
@ -487,16 +496,16 @@ class Dir(nodes.Directory):
@classmethod @classmethod
def from_parent( # type: ignore[override] def from_parent( # type: ignore[override]
cls, cls,
parent: nodes.Collector, # type: ignore[override] parent: nodes.Collector,
*, *,
path: Path, path: Path,
) -> "Dir": ) -> "Self":
"""The public constructor. """The public constructor.
:param parent: The parent collector of this Dir. :param parent: The parent collector of this Dir.
:param path: The directory's path. :param path: The directory's path.
""" """
return super().from_parent(parent=parent, path=path) # type: ignore[no-any-return] return super().from_parent(parent=parent, path=path)
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
config = self.config config = self.config
@ -505,8 +514,6 @@ class Dir(nodes.Directory):
ihook = self.ihook ihook = self.ihook
for direntry in scandir(self.path): for direntry in scandir(self.path):
if direntry.is_dir(): if direntry.is_dir():
if direntry.name == "__pycache__":
continue
path = Path(direntry.path) path = Path(direntry.path)
if not self.session.isinitpath(path, with_parents=True): if not self.session.isinitpath(path, with_parents=True):
if ihook.pytest_ignore_collect(collection_path=path, config=config): if ihook.pytest_ignore_collect(collection_path=path, config=config):
@ -724,12 +731,12 @@ class Session(nodes.Collector):
... ...
@overload @overload
def perform_collect( # noqa: F811 def perform_collect(
self, args: Optional[Sequence[str]] = ..., genitems: bool = ... self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
) -> Sequence[Union[nodes.Item, nodes.Collector]]: ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
... ...
def perform_collect( # noqa: F811 def perform_collect(
self, args: Optional[Sequence[str]] = None, genitems: bool = True self, args: Optional[Sequence[str]] = None, genitems: bool = True
) -> Sequence[Union[nodes.Item, nodes.Collector]]: ) -> Sequence[Union[nodes.Item, nodes.Collector]]:
"""Perform the collection phase for this session. """Perform the collection phase for this session.
@ -895,10 +902,14 @@ class Session(nodes.Collector):
# Prune this level. # Prune this level.
any_matched_in_collector = False any_matched_in_collector = False
for node in subnodes: for node in reversed(subnodes):
# Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`. # Path part e.g. `/a/b/` in `/a/b/test_file.py::TestIt::test_it`.
if isinstance(matchparts[0], Path): if isinstance(matchparts[0], Path):
is_match = node.path == matchparts[0] is_match = node.path == matchparts[0]
if sys.platform == "win32" and not is_match:
# In case the file paths do not match, fallback to samefile() to
# account for short-paths on Windows (#11895).
is_match = os.path.samefile(node.path, matchparts[0])
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`. # Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
else: else:
# TODO: Remove parametrized workaround once collection structure contains # TODO: Remove parametrized workaround once collection structure contains

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