Merge branch 'main' into Improvement-catch-duplicate-values-when-determining-param-indices-in-metafunc-parametrize

This commit is contained in:
Sadra Barikbin 2023-12-15 23:05:15 +03:30
commit 9b10ae75fd
147 changed files with 4893 additions and 1406 deletions

View File

@ -22,7 +22,7 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: true persist-credentials: true

View File

@ -1,44 +1,60 @@
name: deploy name: deploy
on: on:
push: workflow_dispatch:
tags: inputs:
# These tags are protected, see: version:
# https://github.com/pytest-dev/pytest/settings/tag_protection description: 'Release version'
- "[0-9]+.[0-9]+.[0-9]+" required: true
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" default: '1.2.3'
# Set permissions at the job level. # Set permissions at the job level.
permissions: {} permissions: {}
jobs: jobs:
build: package:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Build and Check Package - name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5 uses: hynek/build-and-inspect-python-package@v1.5.4
deploy: deploy:
if: github.repository == 'pytest-dev/pytest' if: github.repository == 'pytest-dev/pytest'
needs: [build] needs: [package]
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: deploy
timeout-minutes: 30 timeout-minutes: 30
permissions: permissions:
id-token: write id-token: write
contents: write
steps: steps:
- uses: actions/checkout@v4
- name: Download Package - name: Download Package
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: Packages name: Packages
path: dist path: dist
- name: Publish package to PyPI - name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.8 uses: pypa/gh-action-pypi-publish@v1.8.11
- name: Push tag
run: |
git config user.name "pytest bot"
git config user.email "pytestbot@gmail.com"
git tag --annotate --message=v${{ github.event.inputs.version }} ${{ github.event.inputs.version }} ${{ github.sha }}
git push origin ${{ github.event.inputs.version }}
release-notes: release-notes:
@ -51,16 +67,16 @@ jobs:
permissions: permissions:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.11" python-version: "3.11"
- name: Install tox - name: Install tox
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip

View File

@ -27,12 +27,12 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.8" python-version: "3.8"

View File

@ -10,7 +10,7 @@ jobs:
permissions: permissions:
issues: write issues: write
steps: steps:
- uses: actions/stale@v8 - uses: actions/stale@v9
with: with:
debug-only: false debug-only: false
days-before-issue-stale: 14 days-before-issue-stale: 14

View File

@ -27,7 +27,19 @@ concurrency:
permissions: {} permissions: {}
jobs: jobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5.4
build: build:
needs: [package]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
timeout-minutes: 45 timeout-minutes: 45
permissions: permissions:
@ -58,7 +70,6 @@ jobs:
"macos-py310", "macos-py310",
"macos-py312", "macos-py312",
"docs",
"doctesting", "doctesting",
"plugins", "plugins",
] ]
@ -145,14 +156,10 @@ jobs:
tox_env: "py312-xdist" tox_env: "py312-xdist"
- name: "plugins" - name: "plugins"
python: "3.9" python: "3.12"
os: ubuntu-latest os: ubuntu-latest
tox_env: "plugins" tox_env: "plugins"
- name: "docs"
python: "3.8"
os: ubuntu-latest
tox_env: "docs"
- name: "doctesting" - name: "doctesting"
python: "3.8" python: "3.8"
os: ubuntu-latest os: ubuntu-latest
@ -160,13 +167,19 @@ jobs:
use_coverage: true use_coverage: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Download Package
uses: actions/download-artifact@v3
with:
name: Packages
path: dist
- name: Set up Python ${{ matrix.python }} - name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
check-latest: ${{ endsWith(matrix.python, '-dev') }} check-latest: ${{ endsWith(matrix.python, '-dev') }}
@ -178,11 +191,13 @@ jobs:
- name: Test without coverage - name: Test without coverage
if: "! matrix.use_coverage" if: "! matrix.use_coverage"
run: "tox -e ${{ matrix.tox_env }}" shell: bash
run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
- name: Test with coverage - name: Test with coverage
if: "matrix.use_coverage" if: "matrix.use_coverage"
run: "tox -e ${{ matrix.tox_env }}-coverage" shell: bash
run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz`
- name: Generate coverage report - name: Generate coverage report
if: "matrix.use_coverage" if: "matrix.use_coverage"
@ -196,10 +211,3 @@ jobs:
fail_ci_if_error: true fail_ci_if_error: true
files: ./coverage.xml files: ./coverage.xml
verbose: true verbose: true
check-package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5

View File

@ -20,14 +20,14 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v4 uses: actions/setup-python@v5
with: with:
python-version: "3.8" python-version: "3.11"
cache: pip cache: pip
- name: requests-cache - name: requests-cache
uses: actions/cache@v3 uses: actions/cache@v3

View File

@ -1,16 +1,16 @@
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 23.7.0 rev: 23.11.0
hooks: hooks:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs - repo: https://github.com/asottile/blacken-docs
rev: 1.15.0 rev: 1.16.0
hooks: hooks:
- id: blacken-docs - id: blacken-docs
additional_dependencies: [black==23.7.0] additional_dependencies: [black==23.7.0]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v4.5.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@ -21,7 +21,7 @@ repos:
exclude: _pytest/(debugging|hookspec).py exclude: _pytest/(debugging|hookspec).py
language_version: python3 language_version: python3
- repo: https://github.com/PyCQA/autoflake - repo: https://github.com/PyCQA/autoflake
rev: v2.2.0 rev: v2.2.1
hooks: hooks:
- id: autoflake - id: autoflake
name: autoflake name: autoflake
@ -37,17 +37,17 @@ repos:
- flake8-typing-imports==1.12.0 - flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0 - flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder-python-imports - repo: https://github.com/asottile/reorder-python-imports
rev: v3.10.0 rev: v3.12.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: ['--application-directories=.:src', --py38-plus] args: ['--application-directories=.:src', --py38-plus]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.10.1 rev: v3.15.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py38-plus] args: [--py38-plus]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.4.0 rev: v2.5.0
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
args: ["--max-py-version=3.12", "--include-version-classifiers"] args: ["--max-py-version=3.12", "--include-version-classifiers"]
@ -56,7 +56,7 @@ repos:
hooks: hooks:
- id: python-use-type-annotations - id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.4.1 rev: v1.7.1
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/)

View File

@ -9,6 +9,10 @@ python:
path: . path: .
- requirements: doc/en/requirements.txt - requirements: doc/en/requirements.txt
sphinx:
configuration: doc/en/conf.py
fail_on_warning: true
build: build:
os: ubuntu-20.04 os: ubuntu-20.04
tools: tools:

17
AUTHORS
View File

@ -48,6 +48,7 @@ Ariel Pillemer
Armin Rigo Armin Rigo
Aron Coyle Aron Coyle
Aron Curzon Aron Curzon
Arthur Richard
Ashish Kurmi Ashish Kurmi
Aviral Verma Aviral Verma
Aviv Palivoda Aviv Palivoda
@ -56,6 +57,7 @@ Barney Gale
Ben Gartner Ben Gartner
Ben Webb Ben Webb
Benjamin Peterson Benjamin Peterson
Benjamin Schubert
Bernard Pratz Bernard Pratz
Bo Wu Bo Wu
Bob Ippolito Bob Ippolito
@ -143,6 +145,7 @@ Feng Ma
Florian Bruhin Florian Bruhin
Florian Dahlitz Florian Dahlitz
Floris Bruynooghe Floris Bruynooghe
Fraser Stark
Gabriel Landau Gabriel Landau
Gabriel Reis Gabriel Reis
Garvit Shubham Garvit Shubham
@ -170,6 +173,7 @@ Ian Lesperance
Ilya Konstantinov Ilya Konstantinov
Ionuț Turturică Ionuț Turturică
Isaac Virshup Isaac Virshup
Israel Fruchter
Itxaso Aizpurua Itxaso Aizpurua
Iwan Briquemont Iwan Briquemont
Jaap Broekhuizen Jaap Broekhuizen
@ -185,6 +189,7 @@ Javier Romero
Jeff Rackauckas Jeff Rackauckas
Jeff Widman Jeff Widman
Jenni Rinker Jenni Rinker
Jens Tröger
John Eddie Ayson John Eddie Ayson
John Litborn John Litborn
John Towler John Towler
@ -233,6 +238,7 @@ Maho
Maik Figura Maik Figura
Mandeep Bhutani Mandeep Bhutani
Manuel Krebber Manuel Krebber
Marc Mueller
Marc Schlaich Marc Schlaich
Marcelo Duarte Trevisani Marcelo Duarte Trevisani
Marcin Bachry Marcin Bachry
@ -263,6 +269,7 @@ Michal Wajszczuk
Michał Zięba Michał Zięba
Mickey Pashov Mickey Pashov
Mihai Capotă Mihai Capotă
Mihail Milushev
Mike Hoyle (hoylemd) Mike Hoyle (hoylemd)
Mike Lundy Mike Lundy
Milan Lesnek Milan Lesnek
@ -270,6 +277,7 @@ Miro Hrončok
Nathaniel Compton Nathaniel Compton
Nathaniel Waisbrot Nathaniel Waisbrot
Ned Batchelder Ned Batchelder
Neil Martin
Neven Mundar Neven Mundar
Nicholas Devenish Nicholas Devenish
Nicholas Murphy Nicholas Murphy
@ -287,6 +295,7 @@ Ondřej Súkup
Oscar Benjamin Oscar Benjamin
Parth Patel Parth Patel
Patrick Hayes Patrick Hayes
Patrick Lannigan
Paul Müller Paul Müller
Paul Reece Paul Reece
Pauli Virtanen Pauli Virtanen
@ -326,26 +335,32 @@ Ronny Pfannschmidt
Ross Lawley Ross Lawley
Ruaridh Williamson Ruaridh Williamson
Russel Winder Russel Winder
Ryan Puddephatt
Ryan Wooden Ryan Wooden
Sadra Barikbin Sadra Barikbin
Saiprasad Kale Saiprasad Kale
Samuel Colvin Samuel Colvin
Samuel Dion-Girardeau Samuel Dion-Girardeau
Samuel Searles-Bryant Samuel Searles-Bryant
Samuel Therrien (Avasam)
Samuele Pedroni Samuele Pedroni
Sanket Duthade Sanket Duthade
Sankt Petersbug Sankt Petersbug
Saravanan Padmanaban Saravanan Padmanaban
Sean Malloy
Segev Finer Segev Finer
Serhii Mozghovyi Serhii Mozghovyi
Seth Junot Seth Junot
Shantanu Jain Shantanu Jain
Sharad Nair
Shubham Adep Shubham Adep
Simon Blanchard
Simon Gomizelj Simon Gomizelj
Simon Holesch Simon Holesch
Simon Kerr Simon Kerr
Skylar Downes Skylar Downes
Srinivas Reddy Thatiparthy Srinivas Reddy Thatiparthy
Stefaan Lippens
Stefan Farmbauer Stefan Farmbauer
Stefan Scherfke Stefan Scherfke
Stefan Zimmermann Stefan Zimmermann
@ -359,6 +374,7 @@ Tadek Teleżyński
Takafumi Arakaki Takafumi Arakaki
Taneli Hukkinen Taneli Hukkinen
Tanvi Mehta Tanvi Mehta
Tanya Agarwal
Tarcisio Fischer Tarcisio Fischer
Tareq Alayan Tareq Alayan
Tatiana Ovary Tatiana Ovary
@ -379,6 +395,7 @@ Tor Colvin
Trevor Bekolay Trevor Bekolay
Tushar Sadhwani Tushar Sadhwani
Tyler Goodlet Tyler Goodlet
Tyler Smart
Tzu-ping Chung Tzu-ping Chung
Vasily Kuznetsov Vasily Kuznetsov
Victor Maryama Victor Maryama

View File

@ -50,7 +50,7 @@ Fix bugs
-------- --------
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_. Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
See also the `"status: easy" issues <https://github.com/pytest-dev/pytest/labels/status%3A%20easy>`_ See also the `"good first issue" issues <https://github.com/pytest-dev/pytest/labels/good%20first%20issue>`_
that are friendly to new contributors. that are friendly to new contributors.
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going :ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going
@ -197,8 +197,9 @@ Short version
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
#. Fork the repository. #. Fork the repository.
#. Fetch tags from upstream if necessary (if you cloned only main `git fetch --tags https://github.com/pytest-dev/pytest`).
#. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed. #. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed.
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting. #. Follow `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_ for naming.
#. Tests are run using ``tox``:: #. Tests are run using ``tox``::
tox -e linting,py39 tox -e linting,py39
@ -236,6 +237,7 @@ Here is a simple overview, with pytest-specific bits:
$ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git $ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git
$ cd pytest $ cd pytest
$ git fetch --tags https://github.com/pytest-dev/pytest
# now, create your own branch off "main": # now, create your own branch off "main":
$ git checkout -b your-bugfix-branch-name main $ git checkout -b your-bugfix-branch-name main
@ -280,7 +282,7 @@ Here is a simple overview, with pytest-specific bits:
This command will run tests via the "tox" tool against Python 3.9 This command will run tests via the "tox" tool against Python 3.9
and also perform "lint" coding-style checks. and also perform "lint" coding-style checks.
#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming. #. You can now edit your local working copy and run the tests again as necessary. Please follow `PEP-8 <https://www.python.org/dev/peps/pep-0008/>`_ for naming.
You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest
(e.g. enter pdb on failure) to pytest you can do:: (e.g. enter pdb on failure) to pytest you can do::

View File

@ -20,7 +20,7 @@
:target: https://codecov.io/gh/pytest-dev/pytest :target: https://codecov.io/gh/pytest-dev/pytest
:alt: Code coverage Status :alt: Code coverage Status
.. image:: https://github.com/pytest-dev/pytest/workflows/test/badge.svg .. image:: https://github.com/pytest-dev/pytest/actions/workflows/test.yml/badge.svg
:target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest :target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest
.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg .. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg

View File

@ -133,14 +133,12 @@ Releasing
Both automatic and manual processes described above follow the same steps from this point onward. Both automatic and manual processes described above follow the same steps from this point onward.
#. After all tests pass and the PR has been approved, tag the release commit #. After all tests pass and the PR has been approved, trigger the ``deploy`` job
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI:: in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
as source.
git fetch upstream This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH and tag the repository.
git push upstream MAJOR.MINOR.PATCH
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch. #. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.

View File

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

View File

@ -0,0 +1,2 @@
Added :func:`ExceptionInfo.group_contains() <pytest.ExceptionInfo.group_contains>`, an assertion
helper that tests if an `ExceptionGroup` contains a matching exception.

View File

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

View File

@ -0,0 +1 @@
Test functions returning a value other than None will now issue a :class:`pytest.PytestWarning` instead of :class:`pytest.PytestRemovedIn8Warning`, meaning this will stay a warning instead of becoming an error in the future.

View File

@ -0,0 +1,2 @@
Added more comprehensive set assertion rewrites for comparisons other than equality ``==``, with
the following operations now providing better failure messages: ``!=``, ``<=``, ``>=``, ``<``, and ``>``.

View File

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

View File

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

3
changelog/11065.doc.rst Normal file
View File

@ -0,0 +1,3 @@
Use pytestconfig instead of request.config in cache example
to be consistent with the API documentation.

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

@ -0,0 +1 @@
Updated documentation and tests to refer to hyphonated options: replaced ``--junitxml`` with ``--junit-xml`` and ``--collectonly`` with ``--collect-only``.

View File

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

View File

@ -0,0 +1,5 @@
(This entry is meant to assist plugins which access private pytest internals to instantiate ``FixtureRequest`` objects.)
:class:`~pytest.FixtureRequest` is now an abstract class which can't be instantiated directly.
A new concrete ``TopRequest`` subclass of ``FixtureRequest`` has been added for the ``request`` fixture in test functions,
as counterpart to the existing ``SubRequest`` subclass for the ``request`` fixture in fixture functions.

View File

@ -0,0 +1,11 @@
Sanitized the handling of the ``default`` parameter when defining configuration options.
Previously if ``default`` was not supplied for :meth:`parser.addini <pytest.Parser.addini>` and the configuration option value was not defined in a test session, then calls to :func:`config.getini <pytest.Config.getini>` returned an *empty list* or an *empty string* depending on whether ``type`` was supplied or not respectively, which is clearly incorrect. Also, ``None`` was not honored even if ``default=None`` was used explicitly while defining the option.
Now the behavior of :meth:`parser.addini <pytest.Parser.addini>` is as follows:
* If ``default`` is NOT passed but ``type`` is provided, then a type-specific default will be returned. For example ``type=bool`` will return ``False``, ``type=str`` will return ``""``, etc.
* If ``default=None`` is passed and the option is not defined in a test session, then ``None`` will be returned, regardless of the ``type``.
* If neither ``default`` nor ``type`` are provided, assume ``type=str`` and return ``""`` as default (this is as per previous behavior).
The team decided to not introduce a deprecation period for this change, as doing so would be complicated both in terms of communicating this to the community as well as implementing it, and also because the team believes this change should not break existing plugins except in rare cases.

View File

@ -0,0 +1,2 @@
Logging to a file using the ``--log-file`` option will use ``--log-level``, ``--log-format`` and ``--log-date-format`` as fallback
if ``--log-file-level``, ``--log-file-format`` and ``--log-file-date-format`` are not provided respectively.

View File

@ -0,0 +1,3 @@
The :fixture:`pytester` fixture now uses the :fixture:`monkeypatch` fixture to manage the current working directory.
If you use ``pytester`` in combination with :func:`monkeypatch.undo() <pytest.MonkeyPatch.undo>`, the CWD might get restored.
Use :func:`monkeypatch.context() <pytest.MonkeyPatch.context>` instead.

View File

@ -0,0 +1,2 @@
Corrected the spelling of ``Config.ArgsSource.INVOCATION_DIR``.
The previous spelling ``INCOVATION_DIR`` remains as an alias.

View File

@ -0,0 +1 @@
pluggy>=1.3.0 is now required. This adds typing to :class:`~pytest.PytestPluginManager`.

View File

@ -0,0 +1,5 @@
Added the new :confval:`verbosity_assertions` configuration option for fine-grained control of failed assertions verbosity.
See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.
For plugin authors, :attr:`config.get_verbosity <pytest.Config.get_verbosity>` can be used to retrieve the verbosity level for a specific verbosity type.

View File

@ -0,0 +1 @@
:func:`pytest.deprecated_call` now also considers warnings of type :class:`FutureWarning`.

View File

@ -0,0 +1,4 @@
Parametrized tests now *really do* ensure that the ids given to each input are unique - for
example, ``a, a, a0`` now results in ``a1, a2, a0`` instead of the previous (buggy) ``a0, a1, a0``.
This necessarily means changing nodeids where these were previously colliding, and for
readability adds an underscore when non-unique ids end in a number.

View File

@ -0,0 +1,3 @@
Improved very verbose diff output to color it as a diff instead of only red.
Improved the error reporting to better separate each section.

View File

@ -0,0 +1 @@
Fixed crash when using an empty string for the same parametrized value more than once.

View File

@ -0,0 +1 @@
Handle an edge case where :data:`sys.stderr` and :data:`sys.__stderr__` might already be closed when :ref:`faulthandler` is tearing down.

View File

@ -0,0 +1 @@
Improved the documentation and type signature for :func:`pytest.mark.xfail <pytest.mark.xfail>`'s ``condition`` param to use ``False`` as the default value.

View File

@ -0,0 +1,2 @@
Added :func:`LogCaptureFixture.filtering() <pytest.LogCaptureFixture.filtering>` context manager that
adds a given :class:`logging.Filter` object to the caplog fixture.

View File

@ -0,0 +1 @@
Fixed the selftests to pass correctly if ``FORCE_COLOR``, ``NO_COLOR`` or ``PY_COLORS`` is set in the calling environment.

View File

@ -0,0 +1,3 @@
pytest's ``setup.py`` file is removed.
If you relied on this file, e.g. to install pytest using ``setup.py install``,
please see `Why you shouldn't invoke setup.py directly <https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html#summary>`_ for alternatives.

View File

@ -0,0 +1,3 @@
The classes :class:`~_pytest.nodes.Node`, :class:`~pytest.Collector`, :class:`~pytest.Item`, :class:`~pytest.File`, :class:`~_pytest.nodes.FSCollector` are now marked abstract (see :mod:`abc`).
We do not expect this change to affect users and plugin authors, it will only cause errors when the code is already wrong or problematic.

View File

@ -0,0 +1,4 @@
Improved the very verbose diff for every standard library container types: the indentation is now consistent and the markers are on their own separate lines, which should reduce the diffs shown to users.
Previously, the default python pretty printer was used to generate the output, which puts opening and closing
markers on the same line as the first/last entry, in addition to not having consistent indentation.

View File

@ -0,0 +1 @@
Removes unhelpful error message from assertion rewrite mechanism when exceptions raised in __iter__ methods, and instead treats them as un-iterable.

View File

@ -1,4 +1,4 @@
:func:`pytest.warns <warns>` now re-emits unmatched warnings when the context :func:`~pytest.warns` now re-emits unmatched warnings when the context
closes -- previously it would consume all warnings, hiding those that were not closes -- previously it would consume all warnings, hiding those that were not
matched by the function. matched by the function.

View File

@ -14,7 +14,7 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of: ``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
* ``feature``: new user facing features, like new command-line options and new behavior. * ``feature``: new user facing features, like new command-line options and new behavior.
* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc). * ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junit-xml``, improved colors in terminal, etc).
* ``bugfix``: fixes a bug. * ``bugfix``: fixes a bug.
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs. * ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
* ``deprecation``: feature deprecation. * ``deprecation``: feature deprecation.

View File

@ -6,6 +6,9 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-7.4.3
release-7.4.2
release-7.4.1
release-7.4.0 release-7.4.0
release-7.3.2 release-7.3.2
release-7.3.1 release-7.3.1

View File

@ -0,0 +1,20 @@
pytest-7.4.1
=======================================
pytest 7.4.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
* Florian Bruhin
* Ran Benita
Happy testing,
The pytest Development Team

View File

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

View File

@ -0,0 +1,19 @@
pytest-7.4.3
=======================================
pytest 7.4.3 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
* Marc Mueller
Happy testing,
The pytest Development Team

View File

@ -22,7 +22,7 @@ b) transitional: the old and new API don't conflict
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0). We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationwarning`). A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationWarning`).
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g. `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed. When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g. `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.

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:528 cache -- .../_pytest/cacheprovider.py:532
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)
@ -105,7 +105,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:737 doctest_namespace [session scope] -- .../_pytest/doctest.py:757
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.

View File

@ -28,6 +28,63 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
pytest 7.4.3 (2023-10-24)
=========================
Bug Fixes
---------
- `#10447 <https://github.com/pytest-dev/pytest/issues/10447>`_: Markers are now considered in the reverse mro order to ensure base class markers are considered first -- this resolves a regression.
- `#11239 <https://github.com/pytest-dev/pytest/issues/11239>`_: Fixed ``:=`` in asserts impacting unrelated test cases.
- `#11439 <https://github.com/pytest-dev/pytest/issues/11439>`_: Handled an edge case where :data:`sys.stderr` might already be closed when :ref:`faulthandler` is tearing down.
pytest 7.4.2 (2023-09-07)
=========================
Bug Fixes
---------
- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects.
- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.
- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files.
Improved Documentation
----------------------
- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.
pytest 7.4.1 (2023-09-02)
=========================
Bug Fixes
---------
- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug where fake intermediate modules generated by ``--import-mode=importlib`` would not include the
child modules as attributes of the parent modules.
- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
to be imported more than once, causing problems with modules that have import side effects.
pytest 7.4.0 (2023-06-23) pytest 7.4.0 (2023-06-23)
========================= =========================
@ -356,7 +413,7 @@ Improvements
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and - `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
enhance match comparison for :py:func:`_pytest._code.ExceptionInfo.match` as returned by :py:func:`pytest.raises`. enhance match comparison for :py:func:`pytest.ExceptionInfo.match` as returned by :py:func:`pytest.raises`.
- `#8646 <https://github.com/pytest-dev/pytest/issues/8646>`_: Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing - `#8646 <https://github.com/pytest-dev/pytest/issues/8646>`_: Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing
@ -365,7 +422,7 @@ Improvements
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML. - `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
:mod:`tomli` is no longer a dependency on Python 3.11. `tomli` is no longer a dependency on Python 3.11.
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``. - `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
@ -400,7 +457,7 @@ Bug Fixes
When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse. When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse.
When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`pytest.Node.iter_markers` instead. When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` instead.
- `#9159 <https://github.com/pytest-dev/pytest/issues/9159>`_: Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``. - `#9159 <https://github.com/pytest-dev/pytest/issues/9159>`_: Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``.
@ -653,7 +710,7 @@ Bug Fixes
- `#9355 <https://github.com/pytest-dev/pytest/issues/9355>`_: Fixed error message prints function decorators when using assert in Python 3.8 and above. - `#9355 <https://github.com/pytest-dev/pytest/issues/9355>`_: Fixed error message prints function decorators when using assert in Python 3.8 and above.
- `#9396 <https://github.com/pytest-dev/pytest/issues/9396>`_: Ensure :attr:`pytest.Config.inifile` is available during the :func:`pytest_cmdline_main <_pytest.hookspec.pytest_cmdline_main>` hook (regression during ``7.0.0rc1``). - `#9396 <https://github.com/pytest-dev/pytest/issues/9396>`_: Ensure `pytest.Config.inifile` is available during the :hook:`pytest_cmdline_main` hook (regression during ``7.0.0rc1``).
@ -798,7 +855,7 @@ Deprecations
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead. - ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
- `#8447 <https://github.com/pytest-dev/pytest/issues/8447>`_: Defining a custom pytest node type which is both an :class:`pytest.Item <Item>` and a :class:`pytest.Collector <Collector>` (e.g. :class:`pytest.File <File>`) now issues a warning. - `#8447 <https://github.com/pytest-dev/pytest/issues/8447>`_: Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning.
It was never sanely supported and triggers hard to debug errors. It was never sanely supported and triggers hard to debug errors.
See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details. See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details.
@ -840,7 +897,7 @@ Features
- `#7132 <https://github.com/pytest-dev/pytest/issues/7132>`_: Added two environment variables :envvar:`PYTEST_THEME` and :envvar:`PYTEST_THEME_MODE` to let the users customize the pygments theme used. - `#7132 <https://github.com/pytest-dev/pytest/issues/7132>`_: Added two environment variables :envvar:`PYTEST_THEME` and :envvar:`PYTEST_THEME_MODE` to let the users customize the pygments theme used.
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: Added :meth:`cache.mkdir() <pytest.Cache.mkdir>`, which is similar to the existing :meth:`cache.makedir() <pytest.Cache.makedir>`, - `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: Added :meth:`cache.mkdir() <pytest.Cache.mkdir>`, which is similar to the existing ``cache.makedir()``,
but returns a :class:`pathlib.Path` instead of a legacy ``py.path.local``. but returns a :class:`pathlib.Path` instead of a legacy ``py.path.local``.
Added a ``paths`` type to :meth:`parser.addini() <pytest.Parser.addini>`, Added a ``paths`` type to :meth:`parser.addini() <pytest.Parser.addini>`,
@ -866,7 +923,7 @@ Features
- ``pytest.HookRecorder`` for the :class:`HookRecorder <pytest.HookRecorder>` type returned from :class:`~pytest.Pytester`. - ``pytest.HookRecorder`` for the :class:`HookRecorder <pytest.HookRecorder>` type returned from :class:`~pytest.Pytester`.
- ``pytest.RecordedHookCall`` for the :class:`RecordedHookCall <pytest.HookRecorder>` type returned from :class:`~pytest.HookRecorder`. - ``pytest.RecordedHookCall`` for the :class:`RecordedHookCall <pytest.HookRecorder>` type returned from :class:`~pytest.HookRecorder`.
- ``pytest.RunResult`` for the :class:`RunResult <pytest.RunResult>` type returned from :class:`~pytest.Pytester`. - ``pytest.RunResult`` for the :class:`RunResult <pytest.RunResult>` type returned from :class:`~pytest.Pytester`.
- ``pytest.LineMatcher`` for the :class:`LineMatcher <pytest.RunResult>` type used in :class:`~pytest.RunResult` and others. - ``pytest.LineMatcher`` for the :class:`LineMatcher <pytest.LineMatcher>` type used in :class:`~pytest.RunResult` and others.
- ``pytest.TestReport`` for the :class:`TestReport <pytest.TestReport>` type used in various hooks. - ``pytest.TestReport`` for the :class:`TestReport <pytest.TestReport>` type used in various hooks.
- ``pytest.CollectReport`` for the :class:`CollectReport <pytest.CollectReport>` type used in various hooks. - ``pytest.CollectReport`` for the :class:`CollectReport <pytest.CollectReport>` type used in various hooks.
@ -899,7 +956,7 @@ Features
- `#8251 <https://github.com/pytest-dev/pytest/issues/8251>`_: Implement ``Node.path`` as a ``pathlib.Path``. Both the old ``fspath`` and this new attribute gets set no matter whether ``path`` or ``fspath`` (deprecated) is passed to the constructor. It is a replacement for the ``fspath`` attribute (which represents the same path as ``py.path.local``). While ``fspath`` is not deprecated yet - `#8251 <https://github.com/pytest-dev/pytest/issues/8251>`_: Implement ``Node.path`` as a ``pathlib.Path``. Both the old ``fspath`` and this new attribute gets set no matter whether ``path`` or ``fspath`` (deprecated) is passed to the constructor. It is a replacement for the ``fspath`` attribute (which represents the same path as ``py.path.local``). While ``fspath`` is not deprecated yet
due to the ongoing migration of methods like :meth:`~_pytest.Item.reportinfo`, we expect to deprecate it in a future release. due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`, we expect to deprecate it in a future release.
.. note:: .. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
@ -931,7 +988,7 @@ Features
See :ref:`plugin-stash` for details. See :ref:`plugin-stash` for details.
- `#8953 <https://github.com/pytest-dev/pytest/issues/8953>`_: :class:`RunResult <_pytest.pytester.RunResult>` method :meth:`assert_outcomes <_pytest.pytester.RunResult.assert_outcomes>` now accepts a - `#8953 <https://github.com/pytest-dev/pytest/issues/8953>`_: :class:`~pytest.RunResult` method :meth:`~pytest.RunResult.assert_outcomes` now accepts a
``warnings`` argument to assert the total number of warnings captured. ``warnings`` argument to assert the total number of warnings captured.
@ -943,7 +1000,7 @@ Features
used. used.
- `#9113 <https://github.com/pytest-dev/pytest/issues/9113>`_: :class:`RunResult <_pytest.pytester.RunResult>` method :meth:`assert_outcomes <_pytest.pytester.RunResult.assert_outcomes>` now accepts a - `#9113 <https://github.com/pytest-dev/pytest/issues/9113>`_: :class:`~pytest.RunResult` method :meth:`~pytest.RunResult.assert_outcomes` now accepts a
``deselected`` argument to assert the total number of deselected tests. ``deselected`` argument to assert the total number of deselected tests.
@ -956,7 +1013,7 @@ Improvements
- `#7480 <https://github.com/pytest-dev/pytest/issues/7480>`_: A deprecation scheduled to be removed in a major version X (e.g. pytest 7, 8, 9, ...) now uses warning category `PytestRemovedInXWarning`, - `#7480 <https://github.com/pytest-dev/pytest/issues/7480>`_: A deprecation scheduled to be removed in a major version X (e.g. pytest 7, 8, 9, ...) now uses warning category `PytestRemovedInXWarning`,
a subclass of :class:`~pytest.PytestDeprecationWarning`, a subclass of :class:`~pytest.PytestDeprecationWarning`,
instead of :class:`PytestDeprecationWarning` directly. instead of :class:`~pytest.PytestDeprecationWarning` directly.
See :ref:`backwards-compatibility` for more details. See :ref:`backwards-compatibility` for more details.
@ -995,7 +1052,7 @@ Improvements
- `#8803 <https://github.com/pytest-dev/pytest/issues/8803>`_: It is now possible to add colors to custom log levels on cli log. - `#8803 <https://github.com/pytest-dev/pytest/issues/8803>`_: It is now possible to add colors to custom log levels on cli log.
By using :func:`add_color_level <_pytest.logging.add_color_level>` from a ``pytest_configure`` hook, colors can be added:: By using ``add_color_level`` from a :hook:`pytest_configure` hook, colors can be added::
logging_plugin = config.pluginmanager.get_plugin('logging-plugin') logging_plugin = config.pluginmanager.get_plugin('logging-plugin')
logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, 'cyan') logging_plugin.log_cli_handler.formatter.add_color_level(logging.INFO, 'cyan')
@ -1060,7 +1117,7 @@ Bug Fixes
- `#8503 <https://github.com/pytest-dev/pytest/issues/8503>`_: :meth:`pytest.MonkeyPatch.syspath_prepend` no longer fails when - `#8503 <https://github.com/pytest-dev/pytest/issues/8503>`_: :meth:`pytest.MonkeyPatch.syspath_prepend` no longer fails when
``setuptools`` is not installed. ``setuptools`` is not installed.
It now only calls :func:`pkg_resources.fixup_namespace_packages` if It now only calls ``pkg_resources.fixup_namespace_packages`` if
``pkg_resources`` was previously imported, because it is not needed otherwise. ``pkg_resources`` was previously imported, because it is not needed otherwise.
@ -1287,7 +1344,7 @@ Features
This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future. This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future.
Internally, the old :class:`Testdir <_pytest.pytester.Testdir>` is now a thin wrapper around :class:`Pytester <_pytest.pytester.Pytester>`, preserving the old interface. Internally, the old ``pytest.Testdir`` is now a thin wrapper around :class:`~pytest.Pytester`, preserving the old interface.
- :issue:`7695`: A new hook was added, `pytest_markeval_namespace` which should return a dictionary. - :issue:`7695`: A new hook was added, `pytest_markeval_namespace` which should return a dictionary.
@ -1325,7 +1382,7 @@ Features
Improvements Improvements
------------ ------------
- :issue:`1265`: Added an ``__str__`` implementation to the :class:`~pytest.pytester.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method. - :issue:`1265`: Added an ``__str__`` implementation to the :class:`~pytest.LineMatcher` class which is returned from ``pytester.run_pytest().stdout`` and similar. It returns the entire output, like the existing ``str()`` method.
- :issue:`2044`: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS". - :issue:`2044`: Verbose mode now shows the reason that a test was skipped in the test's terminal line after the "SKIPPED", "XFAIL" or "XPASS".
@ -1389,7 +1446,7 @@ Bug Fixes
- :issue:`7911`: Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites. - :issue:`7911`: Directories created by by :fixture:`tmp_path` and :fixture:`tmpdir` are now considered stale after 3 days without modification (previous value was 3 hours) to avoid deleting directories still in use in long running test suites.
- :issue:`7913`: Fixed a crash or hang in :meth:`pytester.spawn <_pytest.pytester.Pytester.spawn>` when the :mod:`readline` module is involved. - :issue:`7913`: Fixed a crash or hang in :meth:`pytester.spawn <pytest.Pytester.spawn>` when the :mod:`readline` module is involved.
- :issue:`7951`: Fixed handling of recursive symlinks when collecting tests. - :issue:`7951`: Fixed handling of recursive symlinks when collecting tests.
@ -1506,7 +1563,7 @@ Deprecations
if you use this and want a replacement. if you use this and want a replacement.
- :issue:`7255`: The :hook:`pytest_warning_captured` hook is deprecated in favor - :issue:`7255`: The ``pytest_warning_captured`` hook is deprecated in favor
of :hook:`pytest_warning_recorded`, and will be removed in a future version. of :hook:`pytest_warning_recorded`, and will be removed in a future version.
@ -1534,8 +1591,8 @@ Improvements
- :issue:`7572`: When a plugin listed in ``required_plugins`` is missing or an unknown config key is used with ``--strict-config``, a simple error message is now shown instead of a stacktrace. - :issue:`7572`: When a plugin listed in ``required_plugins`` is missing or an unknown config key is used with ``--strict-config``, a simple error message is now shown instead of a stacktrace.
- :issue:`7685`: Added two new attributes :attr:`rootpath <_pytest.config.Config.rootpath>` and :attr:`inipath <_pytest.config.Config.inipath>` to :class:`Config <_pytest.config.Config>`. - :issue:`7685`: Added two new attributes :attr:`rootpath <pytest.Config.rootpath>` and :attr:`inipath <pytest.Config.inipath>` to :class:`~pytest.Config`.
These attributes are :class:`pathlib.Path` versions of the existing :attr:`rootdir <_pytest.config.Config.rootdir>` and :attr:`inifile <_pytest.config.Config.inifile>` attributes, These attributes are :class:`pathlib.Path` versions of the existing ``rootdir`` and ``inifile`` attributes,
and should be preferred over them when possible. and should be preferred over them when possible.
@ -1606,7 +1663,7 @@ Trivial/Internal Changes
- :issue:`7587`: The dependency on the ``more-itertools`` package has been removed. - :issue:`7587`: The dependency on the ``more-itertools`` package has been removed.
- :issue:`7631`: The result type of :meth:`capfd.readouterr() <_pytest.capture.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple, - :issue:`7631`: The result type of :meth:`capfd.readouterr() <pytest.CaptureFixture.readouterr>` (and similar) is no longer a namedtuple,
but should behave like one in all respects. This was done for technical reasons. but should behave like one in all respects. This was done for technical reasons.
@ -1984,10 +2041,10 @@ Improvements
- :issue:`7128`: `pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. This is more consistent with how other tools show `--version`. - :issue:`7128`: `pytest --version` now displays just the pytest version, while `pytest --version --version` displays more verbose information including plugins. This is more consistent with how other tools show `--version`.
- :issue:`7133`: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` will now override any :confval:`log_level` set via the CLI or configuration file. - :issue:`7133`: :meth:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` will now override any :confval:`log_level` set via the CLI or configuration file.
- :issue:`7159`: :meth:`caplog.set_level() <_pytest.logging.LogCaptureFixture.set_level>` and :meth:`caplog.at_level() <_pytest.logging.LogCaptureFixture.at_level>` no longer affect - :issue:`7159`: :meth:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :meth:`caplog.at_level() <pytest.LogCaptureFixture.at_level>` no longer affect
the level of logs that are shown in the *Captured log report* report section. the level of logs that are shown in the *Captured log report* report section.
@ -2082,7 +2139,7 @@ Bug Fixes
parameter when Python is called with the ``-bb`` flag. parameter when Python is called with the ``-bb`` flag.
- :issue:`7143`: Fix :meth:`pytest.File.from_parent` so it forwards extra keyword arguments to the constructor. - :issue:`7143`: Fix :meth:`pytest.File.from_parent <_pytest.nodes.Node.from_parent>` so it forwards extra keyword arguments to the constructor.
- :issue:`7145`: Classes with broken ``__getattribute__`` methods are displayed correctly during failures. - :issue:`7145`: Classes with broken ``__getattribute__`` methods are displayed correctly during failures.
@ -2333,7 +2390,7 @@ Improvements
- :issue:`6384`: Make `--showlocals` work also with `--tb=short`. - :issue:`6384`: Make `--showlocals` work also with `--tb=short`.
- :issue:`6653`: Add support for matching lines consecutively with :attr:`LineMatcher <_pytest.pytester.LineMatcher>`'s :func:`~_pytest.pytester.LineMatcher.fnmatch_lines` and :func:`~_pytest.pytester.LineMatcher.re_match_lines`. - :issue:`6653`: Add support for matching lines consecutively with :class:`~pytest.LineMatcher`'s :func:`~pytest.LineMatcher.fnmatch_lines` and :func:`~pytest.LineMatcher.re_match_lines`.
- :issue:`6658`: Code is now highlighted in tracebacks when ``pygments`` is installed. - :issue:`6658`: Code is now highlighted in tracebacks when ``pygments`` is installed.
@ -2401,7 +2458,7 @@ Bug Fixes
- :issue:`6597`: Fix node ids which contain a parametrized empty-string variable. - :issue:`6597`: Fix node ids which contain a parametrized empty-string variable.
- :issue:`6646`: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's :func:`testdir.runpytest <_pytest.pytester.Testdir.runpytest>` etc. - :issue:`6646`: Assertion rewriting hooks are (re)stored for the current item, which fixes them being still used after e.g. pytester's ``testdir.runpytest`` etc.
- :issue:`6660`: :py:func:`pytest.exit` is handled when emitted from the :hook:`pytest_sessionfinish` hook. This includes quitting from a debugger. - :issue:`6660`: :py:func:`pytest.exit` is handled when emitted from the :hook:`pytest_sessionfinish` hook. This includes quitting from a debugger.
@ -2467,7 +2524,7 @@ Bug Fixes
``multiprocessing`` module. ``multiprocessing`` module.
- :issue:`6436`: :class:`FixtureDef <_pytest.fixtures.FixtureDef>` objects now properly register their finalizers with autouse and - :issue:`6436`: :class:`~pytest.FixtureDef` objects now properly register their finalizers with autouse and
parameterized fixtures that execute before them in the fixture stack so they are torn parameterized fixtures that execute before them in the fixture stack so they are torn
down at the right times, and in the right order. down at the right times, and in the right order.
@ -2523,7 +2580,7 @@ Improvements
Bug Fixes Bug Fixes
--------- ---------
- :issue:`5914`: pytester: fix :py:func:`~_pytest.pytester.LineMatcher.no_fnmatch_line` when used after positive matching. - :issue:`5914`: pytester: fix :py:func:`~pytest.LineMatcher.no_fnmatch_line` when used after positive matching.
- :issue:`6082`: Fix line detection for doctest samples inside :py:class:`python:property` docstrings, as a workaround to :bpo:`17446`. - :issue:`6082`: Fix line detection for doctest samples inside :py:class:`python:property` docstrings, as a workaround to :bpo:`17446`.
@ -2587,8 +2644,8 @@ Features
rather than implicitly. rather than implicitly.
- :issue:`5914`: :fixture:`testdir` learned two new functions, :py:func:`~_pytest.pytester.LineMatcher.no_fnmatch_line` and - :issue:`5914`: :fixture:`testdir` learned two new functions, :py:func:`~pytest.LineMatcher.no_fnmatch_line` and
:py:func:`~_pytest.pytester.LineMatcher.no_re_match_line`. :py:func:`~pytest.LineMatcher.no_re_match_line`.
The functions are used to ensure the captured text *does not* match the given The functions are used to ensure the captured text *does not* match the given
pattern. pattern.
@ -6440,7 +6497,7 @@ Changes
* fix :issue:`2013`: turn RecordedWarning into ``namedtuple``, * fix :issue:`2013`: turn RecordedWarning into ``namedtuple``,
to give it a comprehensible repr while preventing unwarranted modification. to give it a comprehensible repr while preventing unwarranted modification.
* fix :issue:`2208`: ensure an iteration limit for _pytest.compat.get_real_func. * fix :issue:`2208`: ensure an iteration limit for ``_pytest.compat.get_real_func``.
Thanks :user:`RonnyPfannschmidt` for the report and PR. Thanks :user:`RonnyPfannschmidt` for the report and PR.
* Hooks are now verified after collection is complete, rather than right after loading installed plugins. This * Hooks are now verified after collection is complete, rather than right after loading installed plugins. This

View File

@ -169,6 +169,50 @@ extlinks = {
} }
nitpicky = True
nitpick_ignore = [
# TODO (fix in pluggy?)
("py:class", "HookCaller"),
("py:class", "HookspecMarker"),
("py:exc", "PluginValidationError"),
# Might want to expose/TODO (https://github.com/pytest-dev/pytest/issues/7469)
("py:class", "ExceptionRepr"),
("py:class", "Exit"),
("py:class", "SubRequest"),
("py:class", "SubRequest"),
("py:class", "TerminalReporter"),
("py:class", "_pytest._code.code.TerminalRepr"),
("py:class", "_pytest.fixtures.FixtureFunctionMarker"),
("py:class", "_pytest.logging.LogCaptureHandler"),
("py:class", "_pytest.mark.structures.ParameterSet"),
# Intentionally undocumented/private
("py:class", "_pytest._code.code.Traceback"),
("py:class", "_pytest._py.path.LocalPath"),
("py:class", "_pytest.capture.CaptureResult"),
("py:class", "_pytest.compat.NotSetType"),
("py:class", "_pytest.python.PyCollector"),
("py:class", "_pytest.python.PyobjMixin"),
("py:class", "_pytest.python_api.RaisesContext"),
("py:class", "_pytest.recwarn.WarningsChecker"),
("py:class", "_pytest.reports.BaseReport"),
# Undocumented third parties
("py:class", "_tracing.TagTracerSub"),
("py:class", "warnings.WarningMessage"),
# Undocumented type aliases
("py:class", "LEGACY_PATH"),
("py:class", "_PluggyPlugin"),
# TypeVars
("py:class", "_pytest._code.code.E"),
("py:class", "_pytest.fixtures.FixtureFunction"),
("py:class", "_pytest.nodes._NodeType"),
("py:class", "_pytest.python_api.E"),
("py:class", "_pytest.recwarn.T"),
("py:class", "_pytest.runner.TResult"),
("py:obj", "_pytest.fixtures.FixtureValue"),
("py:obj", "_pytest.stash.T"),
]
# -- Options for HTML output --------------------------------------------------- # -- Options for HTML output ---------------------------------------------------
sys.path.append(os.path.abspath("_themes")) sys.path.append(os.path.abspath("_themes"))

View File

@ -177,7 +177,7 @@ arguments they only pass on to the superclass.
resolved in future versions as we slowly get rid of the :pypi:`py` resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion). dependency (see :issue:`9283` for a longer discussion).
Due to the ongoing migration of methods like :meth:`~_pytest.Item.reportinfo` Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
which still is expected to return a ``py.path.local`` object, nodes still have which still is expected to return a ``py.path.local`` object, nodes still have
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes, both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
no matter what argument was used in the constructor. We expect to deprecate the no matter what argument was used in the constructor. We expect to deprecate the
@ -336,7 +336,7 @@ Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item`
.. deprecated:: 7.0 .. deprecated:: 7.0
Defining a custom pytest node type which is both an :class:`pytest.Item <Item>` and a :class:`pytest.Collector <Collector>` (e.g. :class:`pytest.File <File>`) now issues a warning. Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning.
It was never sanely supported and triggers hard to debug errors. It was never sanely supported and triggers hard to debug errors.
Some plugins providing linting/code analysis have been using this as a hack. Some plugins providing linting/code analysis have been using this as a hack.
@ -348,8 +348,8 @@ Instead, a separate collector node should be used, which collects the item. See
.. _uncooperative-constructors-deprecated: .. _uncooperative-constructors-deprecated:
Constructors of custom :class:`pytest.Node` subclasses should take ``**kwargs`` Constructors of custom :class:`~_pytest.nodes.Node` subclasses should take ``**kwargs``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0 .. deprecated:: 7.0
@ -645,7 +645,7 @@ By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading
pytest 6.0, where the default format will be ``xunit2``. pytest 6.0, where the default format will be ``xunit2``.
In order to let users know about the transition, pytest will issue a warning in case In order to let users know about the transition, pytest will issue a warning in case
the ``--junitxml`` option is given in the command line but ``junit_family`` is not explicitly the ``--junit-xml`` option is given in the command line but ``junit_family`` is not explicitly
configured in ``pytest.ini``. configured in ``pytest.ini``.
Services known to support the ``xunit2`` format: Services known to support the ``xunit2`` format:

View File

@ -136,7 +136,7 @@ Or select multiple nodes:
Node IDs for failing tests are displayed in the test summary info Node IDs for failing tests are displayed in the test summary info
when running pytest with the ``-rf`` option. You can also when running pytest with the ``-rf`` option. You can also
construct Node IDs from the output of ``pytest --collectonly``. construct Node IDs from the output of ``pytest --collect-only``.
Using ``-k expr`` to select tests based on their name Using ``-k expr`` to select tests based on their name
------------------------------------------------------- -------------------------------------------------------

View File

@ -4,8 +4,6 @@
Parametrizing tests Parametrizing tests
================================================= =================================================
.. currentmodule:: _pytest.python
``pytest`` allows to easily parametrize test functions. ``pytest`` allows to easily parametrize test functions.
For basic docs, see :ref:`parametrize-basics`. For basic docs, see :ref:`parametrize-basics`.
@ -185,7 +183,7 @@ A quick port of "testscenarios"
Here is a quick port to run tests configured with :pypi:`testscenarios`, Here is a quick port to run tests configured with :pypi:`testscenarios`,
an add-on from Robert Collins for the standard unittest framework. We an add-on from Robert Collins for the standard unittest framework. We
only have to work a bit to construct the correct arguments for pytest's only have to work a bit to construct the correct arguments for pytest's
:py:func:`Metafunc.parametrize`: :py:func:`Metafunc.parametrize <pytest.Metafunc.parametrize>`:
.. code-block:: python .. code-block:: python

View File

@ -168,7 +168,7 @@ Now we'll get feedback on a bad argument:
If you need to provide more detailed error messages, you can use the If you need to provide more detailed error messages, you can use the
``type`` parameter and raise ``pytest.UsageError``: ``type`` parameter and raise :exc:`pytest.UsageError`:
.. code-block:: python .. code-block:: python
@ -1090,4 +1090,4 @@ application with standard ``pytest`` command-line options:
.. code-block:: bash .. code-block:: bash
./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/ ./app_main --pytest --verbose --tb=long --junit=xml=results.xml test-suite/

View File

@ -34,7 +34,7 @@ a function/method call.
**Assert** is where we look at that resulting state and check if it looks how **Assert** is where we look at that resulting state and check if it looks how
we'd expect after the dust has settled. It's where we gather evidence to say the we'd expect after the dust has settled. It's where we gather evidence to say the
behavior does or does not aligns with what we expect. The ``assert`` in our test behavior does or does not align with what we expect. The ``assert`` in our test
is where we take that measurement/observation and apply our judgement to it. If is where we take that measurement/observation and apply our judgement to it. If
something should be green, we'd say ``assert thing == "green"``. something should be green, we'd say ``assert thing == "green"``.

View File

@ -162,7 +162,7 @@ A note about fixture cleanup
---------------------------- ----------------------------
pytest does not do any special processing for :data:`SIGTERM <signal.SIGTERM>` and pytest does not do any special processing for :data:`SIGTERM <signal.SIGTERM>` and
:data:`SIGQUIT <signal.SIGQUIT>` signals (:data:`SIGINT <signal.SIGINT>` is handled naturally ``SIGQUIT`` signals (:data:`SIGINT <signal.SIGINT>` is handled naturally
by the Python runtime via :class:`KeyboardInterrupt`), so fixtures that manage external resources which are important by the Python runtime via :class:`KeyboardInterrupt`), so fixtures that manage external resources which are important
to be cleared when the Python process is terminated (by those signals) might leak resources. to be cleared when the Python process is terminated (by those signals) might leak resources.

View File

@ -11,8 +11,6 @@ funcarg mechanism, see :ref:`historical funcargs and pytest.funcargs`.
If you are new to pytest, then you can simply ignore this If you are new to pytest, then you can simply ignore this
section and read the other sections. section and read the other sections.
.. currentmodule:: _pytest
Shortcomings of the previous ``pytest_funcarg__`` mechanism Shortcomings of the previous ``pytest_funcarg__`` mechanism
-------------------------------------------------------------- --------------------------------------------------------------
@ -46,7 +44,7 @@ There are several limitations and difficulties with this approach:
2. parametrizing the "db" resource is not straight forward: 2. parametrizing the "db" resource is not straight forward:
you need to apply a "parametrize" decorator or implement a you need to apply a "parametrize" decorator or implement a
:py:func:`~hookspec.pytest_generate_tests` hook :hook:`pytest_generate_tests` hook
calling :py:func:`~pytest.Metafunc.parametrize` which calling :py:func:`~pytest.Metafunc.parametrize` which
performs parametrization at the places where the resource performs parametrization at the places where the resource
is used. Moreover, you need to modify the factory to use an is used. Moreover, you need to modify the factory to use an
@ -94,7 +92,7 @@ Direct parametrization of funcarg resource factories
Previously, funcarg factories could not directly cause parametrization. Previously, funcarg factories could not directly cause parametrization.
You needed to specify a ``@parametrize`` decorator on your test function You needed to specify a ``@parametrize`` decorator on your test function
or implement a ``pytest_generate_tests`` hook to perform or implement a :hook:`pytest_generate_tests` hook to perform
parametrization, i.e. calling a test multiple times with different value parametrization, i.e. calling a test multiple times with different value
sets. pytest-2.3 introduces a decorator for use on the factory itself: sets. pytest-2.3 introduces a decorator for use on the factory itself:

View File

@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash .. code-block:: bash
$ pytest --version $ pytest --version
pytest 7.4.0 pytest 7.4.3
.. _`simpletest`: .. _`simpletest`:
@ -97,6 +97,30 @@ Use the :ref:`raises <assertraises>` helper to assert that some code raises an e
with pytest.raises(SystemExit): with pytest.raises(SystemExit):
f() f()
You can also use the context provided by :ref:`raises <assertraises>` to
assert that an expected exception is part of a raised ``ExceptionGroup``:
.. code-block:: python
# content of test_exceptiongroup.py
import pytest
def f():
raise ExceptionGroup(
"Group message",
[
RuntimeError(),
],
)
def test_exception_in_group():
with pytest.raises(ExceptionGroup) as excinfo:
f()
assert excinfo.group_contains(RuntimeError)
assert not excinfo.group_contains(TypeError)
Execute the test function with “quiet” reporting mode: Execute the test function with “quiet” reporting mode:
.. code-block:: pytest .. code-block:: pytest

View File

@ -112,7 +112,7 @@ More details can be found in the :pull:`original PR <3317>`.
.. note:: .. note::
in a future major release of pytest we will introduce class based markers, in a future major release of pytest we will introduce class based markers,
at which point markers will no longer be limited to instances of :py:class:`~_pytest.mark.Mark`. at which point markers will no longer be limited to instances of :py:class:`~pytest.Mark`.
cache plugin integrated into the core cache plugin integrated into the core

View File

@ -98,6 +98,27 @@ and if you need to have access to the actual exception info you may use:
the actual exception raised. The main attributes of interest are the actual exception raised. The main attributes of interest are
``.type``, ``.value`` and ``.traceback``. ``.type``, ``.value`` and ``.traceback``.
Note that ``pytest.raises`` will match the exception type or any subclasses (like the standard ``except`` statement).
If you want to check if a block of code is raising an exact exception type, you need to check that explicitly:
.. code-block:: python
def test_foo_not_implemented():
def foo():
raise NotImplementedError
with pytest.raises(RuntimeError) as excinfo:
foo()
assert excinfo.type is RuntimeError
The :func:`pytest.raises` call will succeed, even though the function raises :class:`NotImplementedError`, because
:class:`NotImplementedError` is a subclass of :class:`RuntimeError`; however the following `assert` statement will
catch the problem.
Matching exception messages
~~~~~~~~~~~~~~~~~~~~~~~~~~~
You can pass a ``match`` keyword parameter to the context-manager to test You can pass a ``match`` keyword parameter to the context-manager to test
that a regular expression matches on the string representation of an exception that a regular expression matches on the string representation of an exception
(similar to the ``TestCase.assertRaisesRegex`` method from ``unittest``): (similar to the ``TestCase.assertRaisesRegex`` method from ``unittest``):
@ -115,36 +136,111 @@ that a regular expression matches on the string representation of an exception
with pytest.raises(ValueError, match=r".* 123 .*"): with pytest.raises(ValueError, match=r".* 123 .*"):
myfunc() myfunc()
The regexp parameter of the ``match`` method is matched with the ``re.search`` Notes:
function, so in the above example ``match='123'`` would have worked as
well.
There's an alternate form of the :func:`pytest.raises` function where you pass * The ``match`` parameter is matched with the :func:`re.search`
a function that will be executed with the given ``*args`` and ``**kwargs`` and function, so in the above example ``match='123'`` would have worked as well.
assert that the given exception is raised: * The ``match`` parameter also matches against `PEP-678 <https://peps.python.org/pep-0678/>`__ ``__notes__``.
Matching exception groups
~~~~~~~~~~~~~~~~~~~~~~~~~
You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>`
method to test for exceptions returned as part of an ``ExceptionGroup``:
.. code-block:: python .. code-block:: python
pytest.raises(ExpectedException, func, *args, **kwargs) def test_exception_in_group():
with pytest.raises(RuntimeError) as excinfo:
raise ExceptionGroup(
"Group message",
[
RuntimeError("Exception 123 raised"),
],
)
assert excinfo.group_contains(RuntimeError, match=r".* 123 .*")
assert not excinfo.group_contains(TypeError)
The optional ``match`` keyword parameter works the same way as for
:func:`pytest.raises`.
By default ``group_contains()`` will recursively search for a matching
exception at any level of nested ``ExceptionGroup`` instances. You can
specify a ``depth`` keyword parameter if you only want to match an
exception at a specific level; exceptions contained directly in the top
``ExceptionGroup`` would match ``depth=1``.
.. code-block:: python
def test_exception_in_group_at_given_depth():
with pytest.raises(RuntimeError) as excinfo:
raise ExceptionGroup(
"Group message",
[
RuntimeError(),
ExceptionGroup(
"Nested group",
[
TypeError(),
],
),
],
)
assert excinfo.group_contains(RuntimeError, depth=1)
assert excinfo.group_contains(TypeError, depth=2)
assert not excinfo.group_contains(RuntimeError, depth=2)
assert not excinfo.group_contains(TypeError, depth=1)
Alternate form (legacy)
~~~~~~~~~~~~~~~~~~~~~~~
There is an alternate form where you pass
a function that will be executed, along ``*args`` and ``**kwargs``, and :func:`pytest.raises`
will execute the function with the arguments and assert that the given exception is raised:
.. code-block:: python
def func(x):
if x <= 0:
raise ValueError("x needs to be larger than zero")
pytest.raises(ValueError, func, x=-1)
The reporter will provide you with helpful output in case of failures such as *no The reporter will provide you with helpful output in case of failures such as *no
exception* or *wrong exception*. exception* or *wrong exception*.
Note that it is also possible to specify a "raises" argument to This form was the original :func:`pytest.raises` API, developed before the ``with`` statement was
``pytest.mark.xfail``, which checks that the test is failing in a more added to the Python language. Nowadays, this form is rarely used, with the context-manager form (using ``with``)
being considered more readable.
Nonetheless, this form is fully supported and not deprecated in any way.
xfail mark and pytest.raises
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
It is also possible to specify a ``raises`` argument to
:ref:`pytest.mark.xfail <pytest.mark.xfail ref>`, which checks that the test is failing in a more
specific way than just having any exception raised: specific way than just having any exception raised:
.. code-block:: python .. code-block:: python
def f():
raise IndexError()
@pytest.mark.xfail(raises=IndexError) @pytest.mark.xfail(raises=IndexError)
def test_f(): def test_f():
f() f()
Using :func:`pytest.raises` is likely to be better for cases where you are
testing exceptions your own code is deliberately raising, whereas using This will only "xfail" if the test fails by raising ``IndexError`` or subclasses.
``@pytest.mark.xfail`` with a check function is probably better for something
like documenting unfixed bugs (where the test describes what "should" happen) * Using :ref:`pytest.mark.xfail <pytest.mark.xfail ref>` with the ``raises`` parameter is probably better for something
or bugs in dependencies. like documenting unfixed bugs (where the test describes what "should" happen) or bugs in dependencies.
* Using :func:`pytest.raises` is likely to be better for cases where you are
testing exceptions your own code is deliberately raising, which is the majority of cases.
.. _`assertwarns`: .. _`assertwarns`:

View File

@ -176,14 +176,21 @@ with more recent files coming first.
Behavior when no tests failed in the last run Behavior when no tests failed in the last run
--------------------------------------------- ---------------------------------------------
When no tests failed in the last run, or when no cached ``lastfailed`` data was The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
found, ``pytest`` can be configured either to run all of the tests or no tests, Determines whether to execute tests when there are no previously (known)
using the ``--last-failed-no-failures`` option, which takes one of the following values: failures or when no cached ``lastfailed`` data was found.
There are two options:
* ``all``: when there are no known test failures, runs all tests (the full test suite). This is the default.
* ``none``: when there are no known test failures, just emits a message stating this and exit successfully.
Example:
.. code-block:: bash .. code-block:: bash
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior) pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
pytest --last-failed --last-failed-no-failures none # run no tests and exit pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
The new config.cache object The new config.cache object
-------------------------------- --------------------------------
@ -206,12 +213,12 @@ across pytest invocations:
@pytest.fixture @pytest.fixture
def mydata(request): def mydata(pytestconfig):
val = request.config.cache.get("example/value", None) val = pytestconfig.cache.get("example/value", None)
if val is None: if val is None:
expensive_computation() expensive_computation()
val = 42 val = 42
request.config.cache.set("example/value", val) pytestconfig.cache.set("example/value", val)
return val return val

View File

@ -382,8 +382,6 @@ warnings: a WarningsRecorder instance. To view the recorded warnings, you can
iterate over this instance, call ``len`` on it to get the number of recorded iterate over this instance, call ``len`` on it to get the number of recorded
warnings, or index into it to get a particular recorded warning. warnings, or index into it to get a particular recorded warning.
.. currentmodule:: _pytest.warnings
Full API: :class:`~_pytest.recwarn.WarningsRecorder`. Full API: :class:`~_pytest.recwarn.WarningsRecorder`.
.. _`warns use cases`: .. _`warns use cases`:

View File

@ -1271,7 +1271,7 @@ configured in multiple ways.
Extending the previous example, we can flag the fixture to create two Extending the previous example, we can flag the fixture to create two
``smtp_connection`` fixture instances which will cause all tests using the fixture ``smtp_connection`` fixture instances which will cause all tests using the fixture
to run twice. The fixture function gets access to each parameter to run twice. The fixture function gets access to each parameter
through the special :py:class:`request <FixtureRequest>` object: through the special :py:class:`request <pytest.FixtureRequest>` object:
.. code-block:: python .. code-block:: python

View File

@ -241,7 +241,7 @@ through ``add_color_level()``. Example:
.. code-block:: python .. code-block:: python
@pytest.hookimpl @pytest.hookimpl(trylast=True)
def pytest_configure(config): def pytest_configure(config):
logging_plugin = config.pluginmanager.get_plugin("logging-plugin") logging_plugin = config.pluginmanager.get_plugin("logging-plugin")

View File

@ -16,6 +16,12 @@ Examples for modifying traceback printing:
pytest -l # show local variables (shortcut) pytest -l # show local variables (shortcut)
pytest --no-showlocals # hide local variables (if addopts enables them) pytest --no-showlocals # hide local variables (if addopts enables them)
pytest --capture=fd # default, capture at the file descriptor level
pytest --capture=sys # capture at the sys level
pytest --capture=no # don't capture
pytest -s # don't capture (shortcut)
pytest --capture=tee-sys # capture to logs but also output to sys level streams
pytest --tb=auto # (default) 'long' tracebacks for the first and last pytest --tb=auto # (default) 'long' tracebacks for the first and last
# entry, but 'short' style for the other entries # entry, but 'short' style for the other entries
pytest --tb=long # exhaustive, informative traceback formatting pytest --tb=long # exhaustive, informative traceback formatting
@ -36,6 +42,16 @@ option you make sure a trace is shown.
Verbosity Verbosity
-------------------------------------------------- --------------------------------------------------
Examples for modifying printing verbosity:
.. code-block:: bash
pytest --quiet # quiet - less verbose - mode
pytest -q # quiet - less verbose - mode (shortcut)
pytest -v # increase verbosity, display individual test names
pytest -vv # more verbose, display more details from the test output
pytest -vvv # not a standard , but may be used for even more detail in certain setups
The ``-v`` flag controls the verbosity of pytest output in various aspects: test session progress, assertion The ``-v`` flag controls the verbosity of pytest output in various aspects: test session progress, assertion
details when tests fail, fixtures details with ``--fixtures``, etc. details when tests fail, fixtures details with ``--fixtures``, etc.
@ -270,6 +286,20 @@ situations, for example you are shown even fixtures that start with ``_`` if you
Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment, Using higher verbosity levels (``-vvv``, ``-vvvv``, ...) is supported, but has no effect in pytest itself at the moment,
however some plugins might make use of higher verbosity. however some plugins might make use of higher verbosity.
.. _`pytest.fine_grained_verbosity`:
Fine-grained verbosity
~~~~~~~~~~~~~~~~~~~~~~
In addition to specifying the application wide verbosity level, it is possible to control specific aspects independently.
This is done by setting a verbosity level in the configuration file for the specific aspect of the output.
:confval:`verbosity_assertions`: Controls how verbose the assertion output should be when pytest is executed. Running
``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside
the file is shown by a single character in the output.
(Note: currently this is the only option available, but more might be added in the future).
.. _`pytest.detailed_failed_tests_usage`: .. _`pytest.detailed_failed_tests_usage`:
Producing a detailed summary report Producing a detailed summary report
@ -478,7 +508,7 @@ integration servers, use this invocation:
.. code-block:: bash .. code-block:: bash
pytest --junitxml=path pytest --junit-xml=path
to create an XML file at ``path``. to create an XML file at ``path``.

View File

@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
d = tmp_path / "sub" d = tmp_path / "sub"
d.mkdir() d.mkdir()
p = d / "hello.txt" p = d / "hello.txt"
p.write_text(CONTENT) p.write_text(CONTENT, encoding="utf-8")
assert p.read_text() == CONTENT assert p.read_text(encoding="utf-8") == CONTENT
assert len(list(tmp_path.iterdir())) == 1 assert len(list(tmp_path.iterdir())) == 1
> assert 0 > assert 0
E assert 0 E assert 0

View File

@ -59,10 +59,6 @@ The remaining hook functions will not be called in this case.
hook wrappers: executing around other hooks hook wrappers: executing around other hooks
------------------------------------------------- -------------------------------------------------
.. currentmodule:: _pytest.core
pytest plugins can implement hook wrappers which wrap the execution pytest plugins can implement hook wrappers which wrap the execution
of other hook implementations. A hook wrapper is a generator function of other hook implementations. A hook wrapper is a generator function
which yields exactly once. When pytest invokes hooks it first executes which yields exactly once. When pytest invokes hooks it first executes
@ -165,6 +161,7 @@ Here is the order of execution:
It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
in which case it will influence the ordering of hook wrappers among each other. in which case it will influence the ordering of hook wrappers among each other.
.. _`declaringhooks`:
Declaring new hooks Declaring new hooks
------------------------ ------------------------
@ -174,13 +171,11 @@ Declaring new hooks
This is a quick overview on how to add new hooks and how they work in general, but a more complete This is a quick overview on how to add new hooks and how they work in general, but a more complete
overview can be found in `the pluggy documentation <https://pluggy.readthedocs.io/en/latest/>`__. overview can be found in `the pluggy documentation <https://pluggy.readthedocs.io/en/latest/>`__.
.. currentmodule:: _pytest.hookspec
Plugins and ``conftest.py`` files may declare new hooks that can then be Plugins and ``conftest.py`` files may declare new hooks that can then be
implemented by other plugins in order to alter behaviour or interact with implemented by other plugins in order to alter behaviour or interact with
the new plugin: the new plugin:
.. autofunction:: pytest_addhooks .. autofunction:: _pytest.hookspec.pytest_addhooks
:noindex: :noindex:
Hooks are usually declared as do-nothing functions that contain only Hooks are usually declared as do-nothing functions that contain only

View File

@ -2,7 +2,6 @@
.. sidebar:: Next Open Trainings .. sidebar:: Next Open Trainings
- `pytest: Professionelles Testen (nicht nur) für Python <https://workshoptage.ch/workshops/2023/pytest-professionelles-testen-nicht-nur-fuer-python-2/>`_, at `Workshoptage 2023 <https://workshoptage.ch/>`_, **September 5th**, `OST <https://www.ost.ch/en>`_ Campus **Rapperswil, Switzerland**
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote** - `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
Also see :doc:`previous talks and blogposts <talks>`. Also see :doc:`previous talks and blogposts <talks>`.

View File

@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
setup.cfg setup.cfg
~~~~~~~~~ ~~~~~~~~~
``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <python:distutils/configfile>`, and can also be used to hold pytest configuration ``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration
if they have a ``[tool:pytest]`` section. if they have a ``[tool:pytest]`` section.
.. code-block:: ini .. code-block:: ini

View File

@ -11,9 +11,6 @@ Fixtures reference
.. seealso:: :ref:`about-fixtures` .. seealso:: :ref:`about-fixtures`
.. seealso:: :ref:`how-to-fixtures` .. seealso:: :ref:`how-to-fixtures`
.. currentmodule:: _pytest.python
.. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection .. _`Dependency injection`: https://en.wikipedia.org/wiki/Dependency_injection
@ -76,15 +73,13 @@ Built-in fixtures
:class:`pathlib.Path` objects. :class:`pathlib.Path` objects.
:fixture:`tmpdir` :fixture:`tmpdir`
Provide a :class:`py.path.local` object to a temporary Provide a `py.path.local <https://py.readthedocs.io/en/latest/path.html>`_ object to a temporary
directory which is unique to each test function; directory which is unique to each test function;
replaced by :fixture:`tmp_path`. replaced by :fixture:`tmp_path`.
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html
:fixture:`tmpdir_factory` :fixture:`tmpdir_factory`
Make session-scoped temporary directories and return Make session-scoped temporary directories and return
:class:`py.path.local` objects; ``py.path.local`` objects;
replaced by :fixture:`tmp_path_factory`. replaced by :fixture:`tmp_path_factory`.
@ -98,7 +93,7 @@ Fixture availability is determined from the perspective of the test. A fixture
is only available for tests to request if they are in the scope that fixture is is only available for tests to request if they are in the scope that fixture is
defined in. If a fixture is defined inside a class, it can only be requested by defined in. If a fixture is defined inside a class, it can only be requested by
tests inside that class. But if a fixture is defined inside the global scope of tests inside that class. But if a fixture is defined inside the global scope of
the module, than every test in that module, even if it's defined inside a class, the module, then every test in that module, even if it's defined inside a class,
can request it. can request it.
Similarly, a test can also only be affected by an autouse fixture if that test Similarly, a test can also only be affected by an autouse fixture if that test

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
:tocdepth: 3
.. _`api-reference`: .. _`api-reference`:
API Reference API Reference
@ -77,7 +79,7 @@ pytest.xfail
pytest.exit pytest.exit
~~~~~~~~~~~ ~~~~~~~~~~~
.. autofunction:: pytest.exit(reason, [returncode=False, msg=None]) .. autofunction:: pytest.exit(reason, [returncode=None, msg=None])
pytest.main pytest.main
~~~~~~~~~~~ ~~~~~~~~~~~
@ -237,22 +239,23 @@ pytest.mark.xfail
Marks a test function as *expected to fail*. Marks a test function as *expected to fail*.
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=False) .. py:function:: pytest.mark.xfail(condition=False, *, reason=None, raises=None, run=True, strict=xfail_strict)
:type condition: bool or str :keyword Union[bool, str] condition:
:param condition:
Condition for marking the test function as xfail (``True/False`` or a Condition for marking the test function as xfail (``True/False`` or a
:ref:`condition string <string conditions>`). If a bool, you also have :ref:`condition string <string conditions>`). If a ``bool``, you also have
to specify ``reason`` (see :ref:`condition string <string conditions>`). to specify ``reason`` (see :ref:`condition string <string conditions>`).
:keyword str reason: :keyword str reason:
Reason why the test function is marked as xfail. Reason why the test function is marked as xfail.
:keyword Type[Exception] raises: :keyword Type[Exception] raises:
Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test. Exception class (or tuple of classes) expected to be raised by the test function; other exceptions will fail the test.
Note that subclasses of the classes passed will also result in a match (similar to how the ``except`` statement works).
:keyword bool run: :keyword bool run:
If the test function should actually be executed. If ``False``, the function will always xfail and will Whether the test function should actually be executed. If ``False``, the function will always xfail and will
not be executed (useful if a function is segfaulting). not be executed (useful if a function is segfaulting).
:keyword bool strict: :keyword bool strict:
* If ``False`` (the default) the function will be shown in the terminal output as ``xfailed`` if it fails * If ``False`` the function will be shown in the terminal output as ``xfailed`` if it fails
and as ``xpass`` if it passes. In both cases this will not cause the test suite to fail as a whole. This and as ``xpass`` if it passes. In both cases this will not cause the test suite to fail as a whole. This
is particularly useful to mark *flaky* tests (tests that fail at random) to be tackled later. is particularly useful to mark *flaky* tests (tests that fail at random) to be tackled later.
* If ``True``, the function will be shown in the terminal output as ``xfailed`` if it fails, but if it * If ``True``, the function will be shown in the terminal output as ``xfailed`` if it fails, but if it
@ -260,6 +263,8 @@ Marks a test function as *expected to fail*.
that are always failing and there should be a clear indication if they unexpectedly start to pass (for example that are always failing and there should be a clear indication if they unexpectedly start to pass (for example
a new release of a library fixes a known bug). a new release of a library fixes a known bug).
Defaults to :confval:`xfail_strict`, which is ``False`` by default.
Custom marks Custom marks
~~~~~~~~~~~~ ~~~~~~~~~~~~
@ -607,10 +612,30 @@ Hooks
**Tutorial**: :ref:`writing-plugins` **Tutorial**: :ref:`writing-plugins`
.. currentmodule:: _pytest.hookspec
Reference to all hooks which can be implemented by :ref:`conftest.py files <localplugin>` and :ref:`plugins <plugins>`. Reference to all hooks which can be implemented by :ref:`conftest.py files <localplugin>` and :ref:`plugins <plugins>`.
@pytest.hookimpl
~~~~~~~~~~~~~~~~
.. function:: pytest.hookimpl
:decorator:
pytest's decorator for marking functions as hook implementations.
See :ref:`writinghooks` and :func:`pluggy.HookimplMarker`.
@pytest.hookspec
~~~~~~~~~~~~~~~~
.. function:: pytest.hookspec
:decorator:
pytest's decorator for marking functions as hook specifications.
See :ref:`declaringhooks` and :func:`pluggy.HookspecMarker`.
.. currentmodule:: _pytest.hookspec
Bootstrapping hooks Bootstrapping hooks
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
@ -796,6 +821,7 @@ Node
.. autoclass:: _pytest.nodes.Node() .. autoclass:: _pytest.nodes.Node()
:members: :members:
:show-inheritance:
Collector Collector
~~~~~~~~~ ~~~~~~~~~
@ -978,10 +1004,10 @@ TestShortLogReport
.. autoclass:: pytest.TestShortLogReport() .. autoclass:: pytest.TestShortLogReport()
:members: :members:
_Result Result
~~~~~~~ ~~~~~~~
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information. Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`Result in the pluggy documentation <pluggy.Result>` for more information.
Stash Stash
~~~~~ ~~~~~
@ -1132,7 +1158,10 @@ When set (regardless of value), pytest will use color in terminal output.
Exceptions Exceptions
---------- ----------
.. autoclass:: pytest.UsageError() .. autoexception:: pytest.UsageError()
:show-inheritance:
.. autoexception:: pytest.FixtureLookupError()
:show-inheritance: :show-inheritance:
.. _`warnings ref`: .. _`warnings ref`:
@ -1638,11 +1667,11 @@ passed multiple times. The expected format is ``name=value``. For example::
Additionally, ``pytest`` will attempt to intelligently identify and ignore a Additionally, ``pytest`` will attempt to intelligently identify and ignore a
virtualenv by the presence of an activation script. Any directory deemed to virtualenv by the presence of an activation script. Any directory deemed to
be the root of a virtual environment will not be considered during test be the root of a virtual environment will not be considered during test
collection unless ``collectinvirtualenv`` is given. Note also that collection unless ``--collect-in-virtualenv`` is given. Note also that
``norecursedirs`` takes precedence over ``collectinvirtualenv``; e.g. if ``norecursedirs`` takes precedence over ``--collect-in-virtualenv``; e.g. if
you intend to run tests in a virtualenv with a base directory that matches you intend to run tests in a virtualenv with a base directory that matches
``'.*'`` you *must* override ``norecursedirs`` in addition to using the ``'.*'`` you *must* override ``norecursedirs`` in addition to using the
``collectinvirtualenv`` flag. ``--collect-in-virtualenv`` flag.
.. confval:: python_classes .. confval:: python_classes
@ -1817,6 +1846,19 @@ passed multiple times. The expected format is ``name=value``. For example::
clean_db clean_db
.. confval:: verbosity_assertions
Set a verbosity level specifically for assertion related output, overriding the application wide level.
.. code-block:: ini
[pytest]
verbosity_assertions = 2
Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of
"auto" can be used to explicitly use the global verbosity level.
.. confval:: xfail_strict .. confval:: xfail_strict
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
@ -1890,8 +1932,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
tests. Optional argument: glob (default: '*'). tests. Optional argument: glob (default: '*').
--cache-clear Remove all cache contents at start of test run --cache-clear Remove all cache contents at start of test run
--lfnf={all,none}, --last-failed-no-failures={all,none} --lfnf={all,none}, --last-failed-no-failures={all,none}
Which tests to run with no previously (known) With ``--lf``, determines whether to execute tests
failures when there are no previously (known) failures or
when no cached ``lastfailed`` data was found.
``all`` (the default) runs the full test suite
again. ``none`` just emits a message about no known
failures and exits successfully.
--sw, --stepwise Exit on test failure and continue from last failing --sw, --stepwise Exit on test failure and continue from last failing
test next time test next time
--sw-skip, --stepwise-skip --sw-skip, --stepwise-skip

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,<6 sphinx>=5,<8
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

@ -1,6 +1,5 @@
[build-system] [build-system]
requires = [ requires = [
# sync with setup.py until we discard non-pep-517/518
"setuptools>=45.0", "setuptools>=45.0",
"setuptools-scm[toml]>=6.2.3", "setuptools-scm[toml]>=6.2.3",
] ]
@ -17,7 +16,12 @@ python_classes = ["Test", "Acceptance"]
python_functions = ["test"] python_functions = ["test"]
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". # NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
testpaths = ["testing"] testpaths = ["testing"]
norecursedirs = ["testing/example_scripts"] norecursedirs = [
"testing/example_scripts",
".*",
"build",
"dist",
]
xfail_strict = true xfail_strict = true
filterwarnings = [ filterwarnings = [
"error", "error",

View File

@ -31,10 +31,22 @@ class InvalidFeatureRelease(Exception):
SLUG = "pytest-dev/pytest" SLUG = "pytest-dev/pytest"
PR_BODY = """\ PR_BODY = """\
Created automatically from manual trigger. Created by the [prepare release pr]\
(https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml) workflow.
Once all builds pass and it has been **approved** by one or more maintainers, the build Once all builds pass and it has been **approved** by one or more maintainers, start the \
can be released by pushing a tag `{version}` to this repository. [deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
* `Use workflow from`: `release-{version}`.
* `Release version`: `{version}`.
Or execute on the command line:
```console
gh workflow run deploy.yml -r release-{version} -f version={version}
```
After the workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
""" """

View File

@ -20,14 +20,26 @@ FILE_HEAD = r"""
.. _plugin-list: .. _plugin-list:
Plugin List Pytest Plugin List
=========== ==================
PyPI projects that match "pytest-\*" are considered plugins and are listed Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
automatically together with a manually-maintained list in `the source It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
Packages classified as inactive are excluded. Packages classified as inactive are excluded.
For detailed insights into how this list is generated,
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
.. warning::
Please be aware that this list is not a curated collection of projects
and does not undergo a systematic review process.
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
Do not presume any endorsement from the ``pytest`` project or its developers,
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
.. The following conditional uses a different format for this list when .. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the creating a PDF, because otherwise the table gets far too wide for the
page. page.
@ -44,6 +56,8 @@ 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",
"nuts",
"flask_fixture",
} }

View File

@ -46,7 +46,7 @@ py_modules = py
install_requires = install_requires =
iniconfig iniconfig
packaging packaging
pluggy>=1.2.0,<2.0 pluggy>=1.3.0,<2.0
colorama;sys_platform=="win32" colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11" exceptiongroup>=1.0.0rc8;python_version<"3.11"
tomli>=1.0.0;python_version<"3.11" tomli>=1.0.0;python_version<"3.11"

View File

@ -1,4 +0,0 @@
from setuptools import setup
if __name__ == "__main__":
setup()

View File

@ -697,6 +697,14 @@ class ExceptionInfo(Generic[E]):
) )
return fmt.repr_excinfo(self) return fmt.repr_excinfo(self)
def _stringify_exception(self, exc: BaseException) -> str:
return "\n".join(
[
str(exc),
*getattr(exc, "__notes__", []),
]
)
def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]": def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
"""Check whether the regular expression `regexp` matches the string """Check whether the regular expression `regexp` matches the string
representation of the exception using :func:`python:re.search`. representation of the exception using :func:`python:re.search`.
@ -704,12 +712,7 @@ class ExceptionInfo(Generic[E]):
If it matches `True` is returned, otherwise an `AssertionError` is raised. If it matches `True` is returned, otherwise an `AssertionError` is raised.
""" """
__tracebackhide__ = True __tracebackhide__ = True
value = "\n".join( value = self._stringify_exception(self.value)
[
str(self.value),
*getattr(self.value, "__notes__", []),
]
)
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}" msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
if regexp == value: if regexp == value:
msg += "\n Did you mean to `re.escape()` the regex?" msg += "\n Did you mean to `re.escape()` the regex?"
@ -717,6 +720,69 @@ class ExceptionInfo(Generic[E]):
# Return True to allow for "assert excinfo.match()". # Return True to allow for "assert excinfo.match()".
return True return True
def _group_contains(
self,
exc_group: BaseExceptionGroup[BaseException],
expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]],
match: Union[str, Pattern[str], None],
target_depth: Optional[int] = None,
current_depth: int = 1,
) -> bool:
"""Return `True` if a `BaseExceptionGroup` contains a matching exception."""
if (target_depth is not None) and (current_depth > target_depth):
# already descended past the target depth
return False
for exc in exc_group.exceptions:
if isinstance(exc, BaseExceptionGroup):
if self._group_contains(
exc, expected_exception, match, target_depth, current_depth + 1
):
return True
if (target_depth is not None) and (current_depth != target_depth):
# not at the target depth, no match
continue
if not isinstance(exc, expected_exception):
continue
if match is not None:
value = self._stringify_exception(exc)
if not re.search(match, value):
continue
return True
return False
def group_contains(
self,
expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]],
*,
match: Union[str, Pattern[str], None] = None,
depth: Optional[int] = None,
) -> bool:
"""Check whether a captured exception group contains a matching exception.
:param Type[BaseException] | Tuple[Type[BaseException]] expected_exception:
The expected exception type, or a tuple if one of multiple possible
exception types are expected.
:param str | Pattern[str] | None match:
If specified, a string containing a regular expression,
or a regular expression object, that is tested against the string
representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__`
using :func:`re.search`.
To match a literal string that may contain :ref:`special characters
<re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
:param Optional[int] depth:
If `None`, will search for a matching exception at any nesting depth.
If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
the exceptions contained within the topmost exception group).
"""
msg = "Captured exception is not an instance of `BaseExceptionGroup`"
assert isinstance(self.value, BaseExceptionGroup), msg
msg = "`depth` must be >= 1 if specified"
assert (depth is None) or (depth >= 1), msg
return self._group_contains(self.value, expected_exception, match, depth)
@dataclasses.dataclass @dataclasses.dataclass
class FormattedExcinfo: class FormattedExcinfo:

701
src/_pytest/_io/pprint.py Normal file
View File

@ -0,0 +1,701 @@
# This module was imported from the cpython standard library
# (https://github.com/python/cpython/) at commit
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
#
#
# Original Author: Fred L. Drake, Jr.
# fdrake@acm.org
#
# This is a simple little module I wrote to make life easier. I didn't
# see anything quite like it in the library, though I may have overlooked
# something. I wrote this when I was trying to read some heavily nested
# tuples with fairly non-descriptive content. This is modeled very much
# after Lisp/Scheme - style pretty-printing of lists. If you find it
# useful, thank small children who sleep at night.
import collections as _collections
import dataclasses as _dataclasses
import re
import types as _types
from io import StringIO as _StringIO
from typing import Any
from typing import Callable
from typing import Dict
from typing import IO
from typing import Iterator
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
class _safe_key:
"""Helper function for key functions when sorting unorderable objects.
The wrapped-object will fallback to a Py2.x style comparison for
unorderable types (sorting first comparing the type name and then by
the obj ids). Does not work recursively, so dict.items() must have
_safe_key applied to both the key and the value.
"""
__slots__ = ["obj"]
def __init__(self, obj):
self.obj = obj
def __lt__(self, other):
try:
return self.obj < other.obj
except TypeError:
return (str(type(self.obj)), id(self.obj)) < (
str(type(other.obj)),
id(other.obj),
)
def _safe_tuple(t):
"""Helper function for comparing 2-tuples"""
return _safe_key(t[0]), _safe_key(t[1])
class PrettyPrinter:
def __init__(
self,
indent: int = 4,
width: int = 80,
depth: Optional[int] = None,
*,
sort_dicts: bool = True,
underscore_numbers: bool = False,
) -> None:
"""Handle pretty printing operations onto a stream using a set of
configured parameters.
indent
Number of spaces to indent for each level of nesting.
width
Attempted maximum number of columns in the output.
depth
The maximum depth to print out nested structures.
sort_dicts
If true, dict keys are sorted.
"""
indent = int(indent)
width = int(width)
if indent < 0:
raise ValueError("indent must be >= 0")
if depth is not None and depth <= 0:
raise ValueError("depth must be > 0")
if not width:
raise ValueError("width must be != 0")
self._depth = depth
self._indent_per_level = indent
self._width = width
self._sort_dicts = sort_dicts
self._underscore_numbers = underscore_numbers
def pformat(self, object: Any) -> str:
sio = _StringIO()
self._format(object, sio, 0, 0, set(), 0)
return sio.getvalue()
def _format(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
objid = id(object)
if objid in context:
stream.write(_recursion(object))
return
p = self._dispatch.get(type(object).__repr__, None)
if p is not None:
context.add(objid)
p(self, object, stream, indent, allowance, context, level + 1)
context.remove(objid)
elif (
_dataclasses.is_dataclass(object)
and not isinstance(object, type)
and object.__dataclass_params__.repr
and
# Check dataclass has generated repr method.
hasattr(object.__repr__, "__wrapped__")
and "__create_fn__" in object.__repr__.__wrapped__.__qualname__
):
context.add(objid)
self._pprint_dataclass(
object, stream, indent, allowance, context, level + 1
)
context.remove(objid)
else:
stream.write(self._repr(object, context, level))
def _pprint_dataclass(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
cls_name = object.__class__.__name__
items = [
(f.name, getattr(object, f.name))
for f in _dataclasses.fields(object)
if f.repr
]
stream.write(cls_name + "(")
self._format_namespace_items(items, stream, indent, allowance, context, level)
stream.write(")")
_dispatch: Dict[
Callable[..., str],
Callable[["PrettyPrinter", Any, IO[str], int, int, Set[int], int], None],
] = {}
def _pprint_dict(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
write = stream.write
write("{")
if self._sort_dicts:
items = sorted(object.items(), key=_safe_tuple)
else:
items = object.items()
self._format_dict_items(items, stream, indent, allowance, context, level)
write("}")
_dispatch[dict.__repr__] = _pprint_dict
def _pprint_ordered_dict(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
if not len(object):
stream.write(repr(object))
return
cls = object.__class__
stream.write(cls.__name__ + "(")
self._pprint_dict(object, stream, indent, allowance, context, level)
stream.write(")")
_dispatch[_collections.OrderedDict.__repr__] = _pprint_ordered_dict
def _pprint_list(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
stream.write("[")
self._format_items(object, stream, indent, allowance, context, level)
stream.write("]")
_dispatch[list.__repr__] = _pprint_list
def _pprint_tuple(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
stream.write("(")
self._format_items(object, stream, indent, allowance, context, level)
stream.write(")")
_dispatch[tuple.__repr__] = _pprint_tuple
def _pprint_set(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
if not len(object):
stream.write(repr(object))
return
typ = object.__class__
if typ is set:
stream.write("{")
endchar = "}"
else:
stream.write(typ.__name__ + "({")
endchar = "})"
object = sorted(object, key=_safe_key)
self._format_items(object, stream, indent, allowance, context, level)
stream.write(endchar)
_dispatch[set.__repr__] = _pprint_set
_dispatch[frozenset.__repr__] = _pprint_set
def _pprint_str(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
write = stream.write
if not len(object):
write(repr(object))
return
chunks = []
lines = object.splitlines(True)
if level == 1:
indent += 1
allowance += 1
max_width1 = max_width = self._width - indent
for i, line in enumerate(lines):
rep = repr(line)
if i == len(lines) - 1:
max_width1 -= allowance
if len(rep) <= max_width1:
chunks.append(rep)
else:
# A list of alternating (non-space, space) strings
parts = re.findall(r"\S*\s*", line)
assert parts
assert not parts[-1]
parts.pop() # drop empty last part
max_width2 = max_width
current = ""
for j, part in enumerate(parts):
candidate = current + part
if j == len(parts) - 1 and i == len(lines) - 1:
max_width2 -= allowance
if len(repr(candidate)) > max_width2:
if current:
chunks.append(repr(current))
current = part
else:
current = candidate
if current:
chunks.append(repr(current))
if len(chunks) == 1:
write(rep)
return
if level == 1:
write("(")
for i, rep in enumerate(chunks):
if i > 0:
write("\n" + " " * indent)
write(rep)
if level == 1:
write(")")
_dispatch[str.__repr__] = _pprint_str
def _pprint_bytes(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
write = stream.write
if len(object) <= 4:
write(repr(object))
return
parens = level == 1
if parens:
indent += 1
allowance += 1
write("(")
delim = ""
for rep in _wrap_bytes_repr(object, self._width - indent, allowance):
write(delim)
write(rep)
if not delim:
delim = "\n" + " " * indent
if parens:
write(")")
_dispatch[bytes.__repr__] = _pprint_bytes
def _pprint_bytearray(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
write = stream.write
write("bytearray(")
self._pprint_bytes(
bytes(object), stream, indent + 10, allowance + 1, context, level + 1
)
write(")")
_dispatch[bytearray.__repr__] = _pprint_bytearray
def _pprint_mappingproxy(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
stream.write("mappingproxy(")
self._format(object.copy(), stream, indent, allowance, context, level)
stream.write(")")
_dispatch[_types.MappingProxyType.__repr__] = _pprint_mappingproxy
def _pprint_simplenamespace(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
if type(object) is _types.SimpleNamespace:
# The SimpleNamespace repr is "namespace" instead of the class
# name, so we do the same here. For subclasses; use the class name.
cls_name = "namespace"
else:
cls_name = object.__class__.__name__
items = object.__dict__.items()
stream.write(cls_name + "(")
self._format_namespace_items(items, stream, indent, allowance, context, level)
stream.write(")")
_dispatch[_types.SimpleNamespace.__repr__] = _pprint_simplenamespace
def _format_dict_items(
self,
items: List[Tuple[Any, Any]],
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
if not items:
return
write = stream.write
item_indent = indent + self._indent_per_level
delimnl = "\n" + " " * item_indent
for key, ent in items:
write(delimnl)
write(self._repr(key, context, level))
write(": ")
self._format(ent, stream, item_indent, 1, context, level)
write(",")
write("\n" + " " * indent)
def _format_namespace_items(
self,
items: List[Tuple[Any, Any]],
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
if not items:
return
write = stream.write
item_indent = indent + self._indent_per_level
delimnl = "\n" + " " * item_indent
for key, ent in items:
write(delimnl)
write(key)
write("=")
if id(ent) in context:
# Special-case representation of recursion to match standard
# recursive dataclass repr.
write("...")
else:
self._format(
ent,
stream,
item_indent + len(key) + 1,
1,
context,
level,
)
write(",")
write("\n" + " " * indent)
def _format_items(
self,
items: List[Any],
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
if not items:
return
write = stream.write
item_indent = indent + self._indent_per_level
delimnl = "\n" + " " * item_indent
for item in items:
write(delimnl)
self._format(item, stream, item_indent, 1, context, level)
write(",")
write("\n" + " " * indent)
def _repr(self, object: Any, context: Set[int], level: int) -> str:
return self.format(object, context.copy(), self._depth, level)
def format(
self, object: Any, context: Set[int], maxlevels: Optional[int], level: int
) -> str:
return self._safe_repr(object, context, maxlevels, level)
def _pprint_default_dict(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
rdf = self._repr(object.default_factory, context, level)
stream.write(f"{object.__class__.__name__}({rdf}, ")
self._pprint_dict(object, stream, indent, allowance, context, level)
stream.write(")")
_dispatch[_collections.defaultdict.__repr__] = _pprint_default_dict
def _pprint_counter(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
stream.write(object.__class__.__name__ + "(")
if object:
stream.write("{")
items = object.most_common()
self._format_dict_items(items, stream, indent, allowance, context, level)
stream.write("}")
stream.write(")")
_dispatch[_collections.Counter.__repr__] = _pprint_counter
def _pprint_chain_map(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])):
stream.write(repr(object))
return
stream.write(object.__class__.__name__ + "(")
self._format_items(object.maps, stream, indent, allowance, context, level)
stream.write(")")
_dispatch[_collections.ChainMap.__repr__] = _pprint_chain_map
def _pprint_deque(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
stream.write(object.__class__.__name__ + "(")
if object.maxlen is not None:
stream.write("maxlen=%d, " % object.maxlen)
stream.write("[")
self._format_items(object, stream, indent, allowance + 1, context, level)
stream.write("])")
_dispatch[_collections.deque.__repr__] = _pprint_deque
def _pprint_user_dict(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
self._format(object.data, stream, indent, allowance, context, level - 1)
_dispatch[_collections.UserDict.__repr__] = _pprint_user_dict
def _pprint_user_list(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
self._format(object.data, stream, indent, allowance, context, level - 1)
_dispatch[_collections.UserList.__repr__] = _pprint_user_list
def _pprint_user_string(
self,
object: Any,
stream: IO[str],
indent: int,
allowance: int,
context: Set[int],
level: int,
) -> None:
self._format(object.data, stream, indent, allowance, context, level - 1)
_dispatch[_collections.UserString.__repr__] = _pprint_user_string
def _safe_repr(
self, object: Any, context: Set[int], maxlevels: Optional[int], level: int
) -> str:
typ = type(object)
if typ in _builtin_scalars:
return repr(object)
r = getattr(typ, "__repr__", None)
if issubclass(typ, int) and r is int.__repr__:
if self._underscore_numbers:
return f"{object:_d}"
else:
return repr(object)
if issubclass(typ, dict) and r is dict.__repr__:
if not object:
return "{}"
objid = id(object)
if maxlevels and level >= maxlevels:
return "{...}"
if objid in context:
return _recursion(object)
context.add(objid)
components: List[str] = []
append = components.append
level += 1
if self._sort_dicts:
items = sorted(object.items(), key=_safe_tuple)
else:
items = object.items()
for k, v in items:
krepr = self.format(k, context, maxlevels, level)
vrepr = self.format(v, context, maxlevels, level)
append(f"{krepr}: {vrepr}")
context.remove(objid)
return "{%s}" % ", ".join(components)
if (issubclass(typ, list) and r is list.__repr__) or (
issubclass(typ, tuple) and r is tuple.__repr__
):
if issubclass(typ, list):
if not object:
return "[]"
format = "[%s]"
elif len(object) == 1:
format = "(%s,)"
else:
if not object:
return "()"
format = "(%s)"
objid = id(object)
if maxlevels and level >= maxlevels:
return format % "..."
if objid in context:
return _recursion(object)
context.add(objid)
components = []
append = components.append
level += 1
for o in object:
orepr = self.format(o, context, maxlevels, level)
append(orepr)
context.remove(objid)
return format % ", ".join(components)
return repr(object)
_builtin_scalars = frozenset({str, bytes, bytearray, float, complex, bool, type(None)})
def _recursion(object: Any) -> str:
return f"<Recursion on {type(object).__name__} with id={id(object)}>"
def _wrap_bytes_repr(object: Any, width: int, allowance: int) -> Iterator[str]:
current = b""
last = len(object) // 4 * 4
for i in range(0, len(object), 4):
part = object[i : i + 4]
candidate = current + part
if i == last:
width -= allowance
if len(repr(candidate)) > width:
if current:
yield repr(current)
current = part
else:
current = candidate
if current:
yield repr(current)

View File

@ -1,8 +1,5 @@
import pprint import pprint
import reprlib import reprlib
from typing import Any
from typing import Dict
from typing import IO
from typing import Optional from typing import Optional
@ -132,49 +129,3 @@ def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
return repr(obj) return repr(obj)
except Exception as exc: except Exception as exc:
return _format_repr_exception(exc, obj) return _format_repr_exception(exc, obj)
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
"""PrettyPrinter that always dispatches (regardless of width)."""
def _format(
self,
object: object,
stream: IO[str],
indent: int,
allowance: int,
context: Dict[int, Any],
level: int,
) -> None:
# Type ignored because _dispatch is private.
p = self._dispatch.get(type(object).__repr__, None) # type: ignore[attr-defined]
objid = id(object)
if objid in context or p is None:
# Type ignored because _format is private.
super()._format( # type: ignore[misc]
object,
stream,
indent,
allowance,
context,
level,
)
return
context[objid] = 1
p(self, object, stream, indent, allowance, context, level + 1)
del context[objid]
def _pformat_dispatch(
object: object,
indent: int = 1,
width: int = 80,
depth: Optional[int] = None,
*,
compact: bool = False,
) -> str:
return AlwaysDispatchingPrettyPrinter(
indent=indent, width=width, depth=depth, compact=compact
).pformat(object)

View File

@ -3,6 +3,7 @@ import os
import shutil import shutil
import sys import sys
from typing import final from typing import final
from typing import Literal
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
from typing import TextIO from typing import TextIO
@ -193,15 +194,21 @@ class TerminalWriter:
for indent, new_line in zip(indents, new_lines): for indent, new_line in zip(indents, new_lines):
self.line(indent + new_line) self.line(indent + new_line)
def _highlight(self, source: str) -> str: def _highlight(
"""Highlight the given source code if we have markup support.""" self, source: str, lexer: Literal["diff", "python"] = "python"
) -> str:
"""Highlight the given source if we have markup support."""
from _pytest.config.exceptions import UsageError from _pytest.config.exceptions import UsageError
if not self.hasmarkup or not self.code_highlight: if not self.hasmarkup or not self.code_highlight:
return source return source
try: try:
from pygments.formatters.terminal import TerminalFormatter from pygments.formatters.terminal import TerminalFormatter
from pygments.lexers.python import PythonLexer
if lexer == "python":
from pygments.lexers.python import PythonLexer as Lexer
elif lexer == "diff":
from pygments.lexers.diff import DiffLexer as Lexer
from pygments import highlight from pygments import highlight
import pygments.util import pygments.util
except ImportError: except ImportError:
@ -210,7 +217,7 @@ class TerminalWriter:
try: try:
highlighted: str = highlight( highlighted: str = highlight(
source, source,
PythonLexer(), Lexer(),
TerminalFormatter( TerminalFormatter(
bg=os.getenv("PYTEST_THEME_MODE", "dark"), bg=os.getenv("PYTEST_THEME_MODE", "dark"),
style=os.getenv("PYTEST_THEME"), style=os.getenv("PYTEST_THEME"),

View File

@ -755,7 +755,13 @@ class LocalPath:
if ensure: if ensure:
self.dirpath().ensure(dir=1) self.dirpath().ensure(dir=1)
if encoding: if encoding:
return error.checked_call(io.open, self.strpath, mode, encoding=encoding) # Using type ignore here because of this error:
# error: Argument 1 has incompatible type overloaded function;
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
# Which seems incorrect, given io.open supports the given argument types.
return error.checked_call(
io.open, self.strpath, mode, encoding=encoding # type:ignore[arg-type]
)
return error.checked_call(open, self.strpath, mode) return error.checked_call(open, self.strpath, mode)
def _fastjoin(self, name): def _fastjoin(self, name):
@ -1261,13 +1267,19 @@ class LocalPath:
@classmethod @classmethod
def mkdtemp(cls, rootdir=None): def mkdtemp(cls, rootdir=None):
"""Return a Path object pointing to a fresh new temporary directory """Return a Path object pointing to a fresh new temporary directory
(which we created ourself). (which we created ourselves).
""" """
import tempfile import tempfile
if rootdir is None: if rootdir is None:
rootdir = cls.get_temproot() rootdir = cls.get_temproot()
return cls(error.checked_call(tempfile.mkdtemp, dir=str(rootdir))) # Using type ignore here because of this error:
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
path = error.checked_call(
tempfile.mkdtemp, dir=str(rootdir) # type:ignore[arg-type]
)
return cls(path)
@classmethod @classmethod
def make_numbered_dir( def make_numbered_dir(

View File

@ -42,6 +42,14 @@ def pytest_addoption(parser: Parser) -> None:
help="Enables the pytest_assertion_pass hook. " help="Enables the pytest_assertion_pass hook. "
"Make sure to delete any previously generated pyc cache files.", "Make sure to delete any previously generated pyc cache files.",
) )
Config._add_verbosity_ini(
parser,
Config.VERBOSITY_ASSERTIONS,
help=(
"Specify a verbosity level for assertions, overriding the main level. "
"Higher levels will provide more detailed explanation when an assertion fails."
),
)
def register_assert_rewrite(*names: str) -> None: def register_assert_rewrite(*names: str) -> None:

View File

@ -13,9 +13,11 @@ 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 Path
from pathlib import PurePath from pathlib import PurePath
from typing import Callable from typing import Callable
from typing import DefaultDict
from typing import Dict from typing import Dict
from typing import IO from typing import IO
from typing import Iterable from typing import Iterable
@ -45,6 +47,10 @@ if TYPE_CHECKING:
from _pytest.assertion import AssertionState from _pytest.assertion import AssertionState
class Sentinel:
pass
assertstate_key = StashKey["AssertionState"]() assertstate_key = StashKey["AssertionState"]()
# pytest caches rewritten pycs in pycache dirs # pytest caches rewritten pycs in pycache dirs
@ -52,6 +58,9 @@ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_EXT = ".py" + (__debug__ and "c" or "o")
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
# Special marker that denotes we have just left a scope definition
_SCOPE_END_MARKER = Sentinel()
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader): class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
"""PEP302/PEP451 import hook which rewrites asserts.""" """PEP302/PEP451 import hook which rewrites asserts."""
@ -418,7 +427,10 @@ def _saferepr(obj: object) -> str:
def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]: def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]:
"""Get `maxsize` configuration for saferepr based on the given config object.""" """Get `maxsize` configuration for saferepr based on the given config object."""
verbosity = config.getoption("verbose") if config is not None else 0 if config is None:
verbosity = 0
else:
verbosity = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
if verbosity >= 2: if verbosity >= 2:
return None return None
if verbosity >= 1: if verbosity >= 1:
@ -634,6 +646,8 @@ class AssertionRewriter(ast.NodeVisitor):
.push_format_context() and .pop_format_context() which allows .push_format_context() and .pop_format_context() which allows
to build another %-formatted string while already building one. to build another %-formatted string while already building one.
:scope: A tuple containing the current scope used for variables_overwrite.
:variables_overwrite: A dict filled with references to variables :variables_overwrite: A dict filled with references to variables
that change value within an assert. This happens when a variable is that change value within an assert. This happens when a variable is
reassigned with the walrus operator reassigned with the walrus operator
@ -655,7 +669,10 @@ class AssertionRewriter(ast.NodeVisitor):
else: else:
self.enable_assertion_pass_hook = False self.enable_assertion_pass_hook = False
self.source = source self.source = source
self.variables_overwrite: Dict[str, str] = {} self.scope: Tuple[ast.AST, ...] = ()
self.variables_overwrite: DefaultDict[
Tuple[ast.AST, ...], Dict[str, str]
] = defaultdict(dict)
def run(self, mod: ast.Module) -> None: def run(self, mod: ast.Module) -> None:
"""Find all assert statements in *mod* and rewrite them.""" """Find all assert statements in *mod* and rewrite them."""
@ -719,9 +736,17 @@ class AssertionRewriter(ast.NodeVisitor):
mod.body[pos:pos] = imports mod.body[pos:pos] = imports
# Collect asserts. # Collect asserts.
nodes: List[ast.AST] = [mod] self.scope = (mod,)
nodes: List[Union[ast.AST, Sentinel]] = [mod]
while nodes: while nodes:
node = nodes.pop() node = nodes.pop()
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
self.scope = tuple((*self.scope, node))
nodes.append(_SCOPE_END_MARKER)
if node == _SCOPE_END_MARKER:
self.scope = self.scope[:-1]
continue
assert isinstance(node, ast.AST)
for name, field in ast.iter_fields(node): for name, field in ast.iter_fields(node):
if isinstance(field, list): if isinstance(field, list):
new: List[ast.AST] = [] new: List[ast.AST] = []
@ -992,7 +1017,7 @@ class AssertionRewriter(ast.NodeVisitor):
] ]
): ):
pytest_temp = self.variable() pytest_temp = self.variable()
self.variables_overwrite[ self.variables_overwrite[self.scope][
v.left.target.id v.left.target.id
] = v.left # type:ignore[assignment] ] = v.left # type:ignore[assignment]
v.left.target.id = pytest_temp v.left.target.id = pytest_temp
@ -1035,17 +1060,20 @@ class AssertionRewriter(ast.NodeVisitor):
new_args = [] new_args = []
new_kwargs = [] new_kwargs = []
for arg in call.args: for arg in call.args:
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite: if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
arg = self.variables_overwrite[arg.id] # type:ignore[assignment] self.scope, {}
):
arg = self.variables_overwrite[self.scope][
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)
for keyword in call.keywords: for keyword in call.keywords:
if ( if isinstance(
isinstance(keyword.value, ast.Name) keyword.value, ast.Name
and keyword.value.id in self.variables_overwrite ) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
): keyword.value = self.variables_overwrite[self.scope][
keyword.value = self.variables_overwrite[
keyword.value.id keyword.value.id
] # type:ignore[assignment] ] # type:ignore[assignment]
res, expl = self.visit(keyword.value) res, expl = self.visit(keyword.value)
@ -1081,12 +1109,14 @@ class AssertionRewriter(ast.NodeVisitor):
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
self.push_format_context() self.push_format_context()
# We first check if we have overwritten a variable in the previous assert # We first check if we have overwritten a variable in the previous assert
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite: if isinstance(
comp.left = self.variables_overwrite[ comp.left, ast.Name
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
comp.left = self.variables_overwrite[self.scope][
comp.left.id comp.left.id
] # type:ignore[assignment] ] # type:ignore[assignment]
if isinstance(comp.left, ast.NamedExpr): if isinstance(comp.left, ast.NamedExpr):
self.variables_overwrite[ self.variables_overwrite[self.scope][
comp.left.target.id comp.left.target.id
] = comp.left # type:ignore[assignment] ] = comp.left # type:ignore[assignment]
left_res, left_expl = self.visit(comp.left) left_res, left_expl = self.visit(comp.left)
@ -1106,7 +1136,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.variables_overwrite[self.scope][
left_res.id left_res.id
] = next_operand # type:ignore[assignment] ] = next_operand # type:ignore[assignment]
next_res, next_expl = self.visit(next_operand) next_res, next_expl = self.visit(next_operand)

View File

@ -1,12 +1,13 @@
"""Utilities for truncating assertion output. """Utilities for truncating assertion output.
Current default behaviour is to truncate assertion explanations at Current default behaviour is to truncate assertion explanations at
~8 terminal lines, unless running in "-vv" mode 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
from _pytest.assertion import util from _pytest.assertion import util
from _pytest.config import Config
from _pytest.nodes import Item from _pytest.nodes import Item
@ -26,7 +27,7 @@ def truncate_if_required(
def _should_truncate_item(item: Item) -> bool: def _should_truncate_item(item: Item) -> bool:
"""Whether or not this test item is eligible for truncation.""" """Whether or not this test item is eligible for truncation."""
verbose = item.config.option.verbose verbose = item.config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
return verbose < 2 and not util.running_on_ci() return verbose < 2 and not util.running_on_ci()

View File

@ -7,14 +7,16 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import Iterable from typing import Iterable
from typing import List from typing import List
from typing import Literal
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
from typing import Protocol
from typing import Sequence from typing import Sequence
from unicodedata import normalize from unicodedata import normalize
import _pytest._code import _pytest._code
from _pytest import outcomes from _pytest import outcomes
from _pytest._io.saferepr import _pformat_dispatch 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
@ -33,6 +35,11 @@ _assertion_pass: Optional[Callable[[int, str, str], None]] = None
_config: Optional[Config] = None _config: Optional[Config] = None
class _HighlightFunc(Protocol):
def __call__(self, source: str, lexer: Literal["diff", "python"] = "python") -> str:
"""Apply highlighting to the given source."""
def format_explanation(explanation: str) -> str: def format_explanation(explanation: str) -> str:
r"""Format an explanation. r"""Format an explanation.
@ -132,7 +139,7 @@ def isiterable(obj: Any) -> bool:
try: try:
iter(obj) iter(obj)
return not istext(obj) return not istext(obj)
except TypeError: except Exception:
return False return False
@ -161,7 +168,7 @@ def assertrepr_compare(
config, op: str, left: Any, right: Any, use_ascii: bool = False config, op: str, left: Any, right: Any, use_ascii: bool = False
) -> Optional[List[str]]: ) -> Optional[List[str]]:
"""Return specialised explanations for some operators/operands.""" """Return specialised explanations for some operators/operands."""
verbose = config.getoption("verbose") verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS)
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier. # Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
# See issue #3246. # See issue #3246.
@ -189,10 +196,27 @@ def assertrepr_compare(
explanation = None explanation = None
try: try:
if op == "==": if op == "==":
explanation = _compare_eq_any(left, right, verbose) writer = config.get_terminal_writer()
explanation = _compare_eq_any(left, right, writer._highlight, verbose)
elif op == "not in": elif op == "not in":
if istext(left) and istext(right): if istext(left) and istext(right):
explanation = _notin_text(left, right, verbose) explanation = _notin_text(left, right, verbose)
elif op == "!=":
if isset(left) and isset(right):
explanation = ["Both sets are equal"]
elif op == ">=":
if isset(left) and isset(right):
explanation = _compare_gte_set(left, right, verbose)
elif op == "<=":
if isset(left) and isset(right):
explanation = _compare_lte_set(left, right, verbose)
elif op == ">":
if isset(left) and isset(right):
explanation = _compare_gt_set(left, right, verbose)
elif op == "<":
if isset(left) and isset(right):
explanation = _compare_lt_set(left, right, verbose)
except outcomes.Exit: except outcomes.Exit:
raise raise
except Exception: except Exception:
@ -206,10 +230,14 @@ def assertrepr_compare(
if not explanation: if not explanation:
return None return None
if explanation[0] != "":
explanation = [""] + explanation
return [summary] + explanation return [summary] + explanation
def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]: def _compare_eq_any(
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0
) -> List[str]:
explanation = [] explanation = []
if istext(left) and istext(right): if istext(left) and istext(right):
explanation = _diff_text(left, right, verbose) explanation = _diff_text(left, right, verbose)
@ -229,7 +257,7 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
# field values, not the type or field names. But this branch # field values, not the type or field names. But this branch
# intentionally only handles the same-type case, which was often # intentionally only handles the same-type case, which was often
# used in older code bases before dataclasses/attrs were available. # used in older code bases before dataclasses/attrs were available.
explanation = _compare_eq_cls(left, right, verbose) explanation = _compare_eq_cls(left, right, highlighter, verbose)
elif issequence(left) and issequence(right): elif issequence(left) and issequence(right):
explanation = _compare_eq_sequence(left, right, verbose) explanation = _compare_eq_sequence(left, right, verbose)
elif isset(left) and isset(right): elif isset(left) and isset(right):
@ -238,7 +266,7 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
explanation = _compare_eq_dict(left, right, verbose) explanation = _compare_eq_dict(left, right, verbose)
if isiterable(left) and isiterable(right): if isiterable(left) and isiterable(right):
expl = _compare_eq_iterable(left, right, verbose) expl = _compare_eq_iterable(left, right, highlighter, verbose)
explanation.extend(expl) explanation.extend(expl)
return explanation return explanation
@ -292,45 +320,31 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
return explanation return explanation
def _surrounding_parens_on_own_lines(lines: List[str]) -> None:
"""Move opening/closing parenthesis/bracket to own lines."""
opening = lines[0][:1]
if opening in ["(", "[", "{"]:
lines[0] = " " + lines[0][1:]
lines[:] = [opening] + lines
closing = lines[-1][-1:]
if closing in [")", "]", "}"]:
lines[-1] = lines[-1][:-1] + ","
lines[:] = lines + [closing]
def _compare_eq_iterable( def _compare_eq_iterable(
left: Iterable[Any], right: Iterable[Any], verbose: int = 0 left: Iterable[Any],
right: Iterable[Any],
highligher: _HighlightFunc,
verbose: int = 0,
) -> List[str]: ) -> List[str]:
if verbose <= 0 and not running_on_ci(): if verbose <= 0 and not running_on_ci():
return ["Use -v to get more diff"] return ["Use -v to get more diff"]
# dynamic import to speedup pytest # dynamic import to speedup pytest
import difflib import difflib
left_formatting = pprint.pformat(left).splitlines() left_formatting = PrettyPrinter().pformat(left).splitlines()
right_formatting = pprint.pformat(right).splitlines() right_formatting = PrettyPrinter().pformat(right).splitlines()
# Re-format for different output lengths. explanation = ["", "Full diff:"]
lines_left = len(left_formatting)
lines_right = len(right_formatting)
if lines_left != lines_right:
left_formatting = _pformat_dispatch(left).splitlines()
right_formatting = _pformat_dispatch(right).splitlines()
if lines_left > 1 or lines_right > 1:
_surrounding_parens_on_own_lines(left_formatting)
_surrounding_parens_on_own_lines(right_formatting)
explanation = ["Full diff:"]
# "right" is the expected base against which we compare "left", # "right" is the expected base against which we compare "left",
# see https://github.com/pytest-dev/pytest/issues/3333 # see https://github.com/pytest-dev/pytest/issues/3333
explanation.extend( explanation.extend(
line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting) highligher(
"\n".join(
line.rstrip()
for line in difflib.ndiff(right_formatting, left_formatting)
),
lexer="diff",
).splitlines()
) )
return explanation return explanation
@ -392,15 +406,49 @@ def _compare_eq_set(
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
) -> List[str]: ) -> List[str]:
explanation = [] explanation = []
diff_left = left - right explanation.extend(_set_one_sided_diff("left", left, right))
diff_right = right - left explanation.extend(_set_one_sided_diff("right", right, left))
if diff_left: return explanation
explanation.append("Extra items in the left set:")
for item in diff_left:
explanation.append(saferepr(item)) def _compare_gt_set(
if diff_right: left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
explanation.append("Extra items in the right set:") ) -> List[str]:
for item in diff_right: explanation = _compare_gte_set(left, right, verbose)
if not explanation:
return ["Both sets are equal"]
return explanation
def _compare_lt_set(
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
) -> List[str]:
explanation = _compare_lte_set(left, right, verbose)
if not explanation:
return ["Both sets are equal"]
return explanation
def _compare_gte_set(
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
) -> List[str]:
return _set_one_sided_diff("right", right, left)
def _compare_lte_set(
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
) -> List[str]:
return _set_one_sided_diff("left", left, right)
def _set_one_sided_diff(
posn: str, set1: AbstractSet[Any], set2: AbstractSet[Any]
) -> List[str]:
explanation = []
diff = set1 - set2
if diff:
explanation.append(f"Extra items in the {posn} set:")
for item in diff:
explanation.append(saferepr(item)) explanation.append(saferepr(item))
return explanation return explanation
@ -446,7 +494,9 @@ def _compare_eq_dict(
return explanation return explanation
def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]: def _compare_eq_cls(
left: Any, right: Any, highlighter: _HighlightFunc, verbose: int
) -> List[str]:
if not has_default_eq(left): if not has_default_eq(left):
return [] return []
if isdatacls(left): if isdatacls(left):
@ -492,7 +542,9 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
] ]
explanation += [ explanation += [
indent + line indent + line
for line in _compare_eq_any(field_left, field_right, verbose) for line in _compare_eq_any(
field_left, field_right, highlighter, verbose
)
] ]
return explanation return explanation

View File

@ -499,7 +499,11 @@ def pytest_addoption(parser: Parser) -> None:
dest="last_failed_no_failures", dest="last_failed_no_failures",
choices=("all", "none"), choices=("all", "none"),
default="all", default="all",
help="Which tests to run with no previously (known) failures", help="With ``--lf``, determines whether to execute tests when there "
"are no previously (known) failures or when no "
"cached ``lastfailed`` data was found. "
"``all`` (the default) runs the full test suite again. "
"``none`` just emits a message about no known failures and exits successfully.",
) )

View File

@ -588,7 +588,7 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
@final @final
class CaptureResult(NamedTuple, Generic[AnyStr]): class CaptureResult(NamedTuple, Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`.""" """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
out: AnyStr out: AnyStr
err: AnyStr err: AnyStr
@ -598,7 +598,7 @@ else:
class CaptureResult( class CaptureResult(
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr] collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
): ):
"""The result of :method:`CaptureFixture.readouterr`.""" """The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
__slots__ = () __slots__ = ()

View File

@ -314,15 +314,24 @@ def safe_isclass(obj: object) -> bool:
def get_user_id() -> int | None: def get_user_id() -> int | None:
"""Return the current user id, or None if we cannot get it reliably on the current platform.""" """Return the current process's real user id or None if it could not be
# win32 does not have a getuid() function. determined.
# On Emscripten, getuid() is a stub that always returns 0.
if sys.platform in ("win32", "emscripten"): :return: The user id or None if it could not be determined.
"""
# mypy follows the version and platform checking expectation of PEP 484:
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
# Containment checks are too complex for mypy v1.5.0 and cause failure.
if sys.platform == "win32" or sys.platform == "emscripten":
# win32 does not have a getuid() function.
# Emscripten has a return 0 stub.
return None return None
# getuid shouldn't fail, but cpython defines such a case. else:
# Let's hope for the best. # On other platforms, a return value of -1 is assumed to indicate that
uid = os.getuid() # the current process's real user id could not be determined.
return uid if uid != -1 else None ERROR = -1
uid = os.getuid()
return uid if uid != ERROR else None
# Perform exhaustiveness checking. # Perform exhaustiveness checking.

View File

@ -22,6 +22,7 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Dict from typing import Dict
from typing import Final
from typing import final from typing import final
from typing import Generator from typing import Generator
from typing import IO from typing import IO
@ -37,13 +38,17 @@ from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import pluggy
from pluggy import HookimplMarker from pluggy import HookimplMarker
from pluggy import HookimplOpts
from pluggy import HookspecMarker from pluggy import HookspecMarker
from pluggy import HookspecOpts
from pluggy import PluginManager from pluggy import PluginManager
import _pytest._code import _pytest._code
import _pytest.deprecated import _pytest.deprecated
import _pytest.hookspec import _pytest.hookspec
from .compat import PathAwareHookProxy
from .exceptions import PrintHelp as PrintHelp from .exceptions import PrintHelp as PrintHelp
from .exceptions import UsageError as UsageError from .exceptions import UsageError as UsageError
from .findpaths import determine_setup from .findpaths import determine_setup
@ -57,6 +62,7 @@ from _pytest.pathlib import bestrelpath
from _pytest.pathlib import import_path from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode from _pytest.pathlib import ImportMode
from _pytest.pathlib import resolve_package_path from _pytest.pathlib import resolve_package_path
from _pytest.pathlib import safe_exists
from _pytest.stash import Stash 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
@ -64,7 +70,7 @@ from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING: if TYPE_CHECKING:
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 from .argparsing import Argument, Parser
_PluggyPlugin = object _PluggyPlugin = object
@ -440,15 +446,18 @@ class PytestPluginManager(PluginManager):
# Used to know when we are importing conftests after the pytest_configure stage. # Used to know when we are importing conftests after the pytest_configure stage.
self._configured = False self._configured = False
def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str): def parse_hookimpl_opts(
self, plugin: _PluggyPlugin, name: str
) -> Optional[HookimplOpts]:
""":meta private:"""
# pytest hooks are always prefixed with "pytest_", # pytest hooks are always prefixed with "pytest_",
# so we avoid accessing possibly non-readable attributes # so we avoid accessing possibly non-readable attributes
# (see issue #1073). # (see issue #1073).
if not name.startswith("pytest_"): if not name.startswith("pytest_"):
return return None
# Ignore names which can not be hooks. # Ignore names which can not be hooks.
if name == "pytest_plugins": if name == "pytest_plugins":
return return None
opts = super().parse_hookimpl_opts(plugin, name) opts = super().parse_hookimpl_opts(plugin, name)
if opts is not None: if opts is not None:
@ -457,18 +466,19 @@ class PytestPluginManager(PluginManager):
method = getattr(plugin, name) method = getattr(plugin, name)
# Consider only actual functions for hooks (#3775). # Consider only actual functions for hooks (#3775).
if not inspect.isroutine(method): if not inspect.isroutine(method):
return return None
# Collect unmarked hooks as long as they have the `pytest_' prefix. # Collect unmarked hooks as long as they have the `pytest_' prefix.
return _get_legacy_hook_marks( return _get_legacy_hook_marks( # type: ignore[return-value]
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper") method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
) )
def parse_hookspec_opts(self, module_or_class, name: str): def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]:
""":meta private:"""
opts = super().parse_hookspec_opts(module_or_class, name) opts = super().parse_hookspec_opts(module_or_class, name)
if opts is None: if opts is None:
method = getattr(module_or_class, name) method = getattr(module_or_class, name)
if name.startswith("pytest_"): if name.startswith("pytest_"):
opts = _get_legacy_hook_marks( opts = _get_legacy_hook_marks( # type: ignore[assignment]
method, method,
"spec", "spec",
("firstresult", "historic"), ("firstresult", "historic"),
@ -558,12 +568,8 @@ class PytestPluginManager(PluginManager):
anchor = absolutepath(current / path) anchor = absolutepath(current / 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). # is in fact a very long option (#10169, #11394).
try: if safe_exists(anchor):
anchor_exists = anchor.exists()
except OSError: # pragma: no cover
anchor_exists = False
if anchor_exists:
self._try_load_conftest(anchor, importmode, rootpath) self._try_load_conftest(anchor, importmode, rootpath)
foundanchor = True foundanchor = True
if not foundanchor: if not foundanchor:
@ -953,7 +959,8 @@ class Config:
#: Command line arguments. #: Command line arguments.
ARGS = enum.auto() ARGS = enum.auto()
#: Invocation directory. #: Invocation directory.
INCOVATION_DIR = enum.auto() INVOCATION_DIR = enum.auto()
INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
#: 'testpaths' configuration value. #: 'testpaths' configuration value.
TESTPATHS = enum.auto() TESTPATHS = enum.auto()
@ -1003,10 +1010,8 @@ class Config:
# Deprecated alias. Was never public. Can be removed in a few releases. # Deprecated alias. Was never public. Can be removed in a few releases.
self._store = self.stash self._store = self.stash
from .compat import PathAwareHookProxy
self.trace = self.pluginmanager.trace.root.get("config") self.trace = self.pluginmanager.trace.root.get("config")
self.hook = PathAwareHookProxy(self.pluginmanager.hook) self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
self._inicache: Dict[str, Any] = {} self._inicache: Dict[str, Any] = {}
self._override_ini: Sequence[str] = () self._override_ini: Sequence[str] = ()
self._opt2dest: Dict[str, str] = {} self._opt2dest: Dict[str, str] = {}
@ -1066,9 +1071,10 @@ class Config:
fin() fin()
def get_terminal_writer(self) -> TerminalWriter: def get_terminal_writer(self) -> TerminalWriter:
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin( terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
"terminalreporter" "terminalreporter"
) )
assert terminalreporter is not None
return terminalreporter._tw return terminalreporter._tw
def pytest_cmdline_parse( def pytest_cmdline_parse(
@ -1278,7 +1284,7 @@ class Config:
else: else:
result = [] result = []
if not result: if not result:
source = Config.ArgsSource.INCOVATION_DIR source = Config.ArgsSource.INVOCATION_DIR
result = [str(invocation_dir)] result = [str(invocation_dir)]
return result, source return result, source
@ -1492,6 +1498,27 @@ class Config:
def getini(self, name: str): def getini(self, name: str):
"""Return configuration value from an :ref:`ini file <configfiles>`. """Return configuration value from an :ref:`ini file <configfiles>`.
If a configuration value is not defined in an
:ref:`ini file <configfiles>`, then the ``default`` value provided while
registering the configuration through
:func:`parser.addini <pytest.Parser.addini>` will be returned.
Please note that you can even provide ``None`` as a valid
default value.
If ``default`` is not provided while registering using
:func:`parser.addini <pytest.Parser.addini>`, then a default value
based on the ``type`` parameter passed to
:func:`parser.addini <pytest.Parser.addini>` will be returned.
The default values based on ``type`` are:
``paths``, ``pathlist``, ``args`` and ``linelist`` : empty list ``[]``
``bool`` : ``False``
``string`` : empty string ``""``
If neither the ``default`` nor the ``type`` parameter is passed
while registering the configuration through
:func:`parser.addini <pytest.Parser.addini>`, then the configuration
is treated as a string and a default empty string '' is returned.
If the specified name hasn't been registered through a prior If the specified name hasn't been registered through a prior
:func:`parser.addini <pytest.Parser.addini>` call (usually from a :func:`parser.addini <pytest.Parser.addini>` call (usually from a
plugin), a ValueError is raised. plugin), a ValueError is raised.
@ -1518,11 +1545,7 @@ class Config:
try: try:
value = self.inicfg[name] value = self.inicfg[name]
except KeyError: except KeyError:
if default is not None: return default
return default
if type is None:
return ""
return []
else: else:
value = override_value value = override_value
# Coerce the values based on types. # Coerce the values based on types.
@ -1630,6 +1653,78 @@ class Config:
"""Deprecated, use getoption(skip=True) instead.""" """Deprecated, use getoption(skip=True) instead."""
return self.getoption(name, skip=True) return self.getoption(name, skip=True)
#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
VERBOSITY_ASSERTIONS: Final = "assertions"
_VERBOSITY_INI_DEFAULT: Final = "auto"
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
r"""Retrieve the verbosity level for a fine-grained verbosity type.
:param verbosity_type: Verbosity type to get level for. If a level is
configured for the given type, that value will be returned. If the
given type is not a known verbosity type, the global verbosity
level will be returned. If the given type is None (default), the
global verbosity level will be returned.
To configure a level for a fine-grained verbosity type, the
configuration file should have a setting for the configuration name
and a numeric value for the verbosity level. A special value of "auto"
can be used to explicitly use the global verbosity level.
Example:
.. code-block:: ini
# content of pytest.ini
[pytest]
verbosity_assertions = 2
.. code-block:: console
pytest -v
.. code-block:: python
print(config.get_verbosity()) # 1
print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2
"""
global_level = self.option.verbose
assert isinstance(global_level, int)
if verbosity_type is None:
return global_level
ini_name = Config._verbosity_ini_name(verbosity_type)
if ini_name not in self._parser._inidict:
return global_level
level = self.getini(ini_name)
if level == Config._VERBOSITY_INI_DEFAULT:
return global_level
return int(level)
@staticmethod
def _verbosity_ini_name(verbosity_type: str) -> str:
return f"verbosity_{verbosity_type}"
@staticmethod
def _add_verbosity_ini(parser: "Parser", verbosity_type: str, help: str) -> None:
"""Add a output verbosity configuration option for the given output type.
:param parser: Parser for command line arguments and ini-file values.
:param verbosity_type: Fine-grained verbosity category.
:param help: Description of the output this type controls.
The value should be retrieved via a call to
:py:func:`config.get_verbosity(type) <pytest.Config.get_verbosity>`.
"""
parser.addini(
Config._verbosity_ini_name(verbosity_type),
help=help,
type="string",
default=Config._VERBOSITY_INI_DEFAULT,
)
def _warn_about_missing_assertion(self, mode: str) -> None: def _warn_about_missing_assertion(self, mode: str) -> None:
if not _assertion_supported(): if not _assertion_supported():
if mode == "plain": if mode == "plain":

View File

@ -27,6 +27,14 @@ from _pytest.deprecated import check_ispytest
FILE_OR_DIR = "file_or_dir" FILE_OR_DIR = "file_or_dir"
class NotSet:
def __repr__(self) -> str:
return "<notset>"
NOT_SET = NotSet()
@final @final
class Parser: class Parser:
"""Parser for command line arguments and ini-file values. """Parser for command line arguments and ini-file values.
@ -90,7 +98,7 @@ class Parser:
:param opts: :param opts:
Option names, can be short or long options. Option names, can be short or long options.
:param attrs: :param attrs:
Same attributes as the argparse library's :py:func:`add_argument() Same attributes as the argparse library's :meth:`add_argument()
<argparse.ArgumentParser.add_argument>` function accepts. <argparse.ArgumentParser.add_argument>` function accepts.
After command line parsing, options are available on the pytest config After command line parsing, options are available on the pytest config
@ -176,7 +184,7 @@ class Parser:
type: Optional[ type: Optional[
Literal["string", "paths", "pathlist", "args", "linelist", "bool"] Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
] = None, ] = None,
default: Any = None, default: Any = NOT_SET,
) -> None: ) -> None:
"""Register an ini-file option. """Register an ini-file option.
@ -203,10 +211,30 @@ class Parser:
:py:func:`config.getini(name) <pytest.Config.getini>`. :py:func:`config.getini(name) <pytest.Config.getini>`.
""" """
assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool") assert type in (None, "string", "paths", "pathlist", "args", "linelist", "bool")
if default is NOT_SET:
default = get_ini_default_for_type(type)
self._inidict[name] = (help, type, default) self._inidict[name] = (help, type, default)
self._ininames.append(name) self._ininames.append(name)
def get_ini_default_for_type(
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]]
) -> Any:
"""
Used by addini to get the default value for a given ini-option type, when
default is not supplied.
"""
if type is None:
return ""
elif type in ("paths", "pathlist", "args", "linelist"):
return []
elif type == "bool":
return False
else:
return ""
class ArgumentError(Exception): class ArgumentError(Exception):
"""Raised if an Argument instance is created with invalid or """Raised if an Argument instance is created with invalid or
inconsistent arguments.""" inconsistent arguments."""
@ -372,7 +400,7 @@ class OptionGroup:
:param opts: :param opts:
Option names, can be short or long options. Option names, can be short or long options.
:param attrs: :param attrs:
Same attributes as the argparse library's :py:func:`add_argument() Same attributes as the argparse library's :meth:`add_argument()
<argparse.ArgumentParser.add_argument>` function accepts. <argparse.ArgumentParser.add_argument>` function accepts.
""" """
conflict = set(opts).intersection( conflict = set(opts).intersection(

View File

@ -1,15 +1,18 @@
from __future__ import annotations
import functools import functools
import warnings import warnings
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Mapping
import pluggy
from ..compat import LEGACY_PATH from ..compat import LEGACY_PATH
from ..compat import legacy_path from ..compat import legacy_path
from ..deprecated import HOOK_LEGACY_PATH_ARG from ..deprecated import HOOK_LEGACY_PATH_ARG
from _pytest.nodes import _check_path
# hookname: (Path, LEGACY_PATH) # hookname: (Path, LEGACY_PATH)
imply_paths_hooks = { imply_paths_hooks: Mapping[str, tuple[str, str]] = {
"pytest_ignore_collect": ("collection_path", "path"), "pytest_ignore_collect": ("collection_path", "path"),
"pytest_collect_file": ("file_path", "path"), "pytest_collect_file": ("file_path", "path"),
"pytest_pycollect_makemodule": ("module_path", "path"), "pytest_pycollect_makemodule": ("module_path", "path"),
@ -18,6 +21,14 @@ imply_paths_hooks = {
} }
def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
if Path(fspath) != path:
raise ValueError(
f"Path({fspath!r}) != {path!r}\n"
"if both path and fspath are given they need to be equal"
)
class PathAwareHookProxy: class PathAwareHookProxy:
""" """
this helper wraps around hook callers this helper wraps around hook callers
@ -27,24 +38,24 @@ class PathAwareHookProxy:
this may have to be changed later depending on bugs this may have to be changed later depending on bugs
""" """
def __init__(self, hook_caller): def __init__(self, hook_relay: pluggy.HookRelay) -> None:
self.__hook_caller = hook_caller self._hook_relay = hook_relay
def __dir__(self): def __dir__(self) -> list[str]:
return dir(self.__hook_caller) return dir(self._hook_relay)
def __getattr__(self, key, _wraps=functools.wraps): def __getattr__(self, key: str) -> pluggy.HookCaller:
hook = getattr(self.__hook_caller, key) hook: pluggy.HookCaller = getattr(self._hook_relay, key)
if key not in imply_paths_hooks: if key not in imply_paths_hooks:
self.__dict__[key] = hook self.__dict__[key] = hook
return hook return hook
else: else:
path_var, fspath_var = imply_paths_hooks[key] path_var, fspath_var = imply_paths_hooks[key]
@_wraps(hook) @functools.wraps(hook)
def fixed_hook(**kw): def fixed_hook(**kw):
path_value: Optional[Path] = kw.pop(path_var, None) path_value: Path | None = kw.pop(path_var, None)
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None) fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
if fspath_value is not None: if fspath_value is not None:
warnings.warn( warnings.warn(
HOOK_LEGACY_PATH_ARG.format( HOOK_LEGACY_PATH_ARG.format(
@ -65,6 +76,8 @@ class PathAwareHookProxy:
kw[fspath_var] = fspath_value kw[fspath_var] = fspath_value
return hook(**kw) return hook(**kw)
fixed_hook.name = hook.name # type: ignore[attr-defined]
fixed_hook.spec = hook.spec # type: ignore[attr-defined]
fixed_hook.__name__ = key fixed_hook.__name__ = key
self.__dict__[key] = fixed_hook self.__dict__[key] = fixed_hook
return fixed_hook return fixed_hook # type: ignore[return-value]

View File

@ -15,6 +15,7 @@ from .exceptions import UsageError
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import commonpath from _pytest.pathlib import commonpath
from _pytest.pathlib import safe_exists
def _parse_ini_config(path: Path) -> iniconfig.IniConfig: def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
@ -147,14 +148,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
return path return path
return path.parent return path.parent
def safe_exists(path: Path) -> bool:
# This can throw on paths that contain characters unrepresentable at the OS level,
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
try:
return path.exists()
except OSError:
return False
# These look like paths but may not exist # These look like paths but may not exist
possible_paths = ( possible_paths = (
absolutepath(get_file_part_from_node_id(arg)) absolutepath(get_file_part_from_node_id(arg))

View File

@ -1,5 +1,6 @@
"""Discover and run doctests in modules and test files.""" """Discover and run doctests in modules and test files."""
import bdb import bdb
import functools
import inspect import inspect
import os import os
import platform import platform
@ -32,7 +33,7 @@ from _pytest.compat import safe_getattr
from _pytest.config import Config from _pytest.config import Config
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import TopRequest
from _pytest.nodes import Collector from _pytest.nodes import Collector
from _pytest.nodes import Item from _pytest.nodes import Item
from _pytest.outcomes import OutcomeException from _pytest.outcomes import OutcomeException
@ -254,14 +255,20 @@ class DoctestItem(Item):
self, self,
name: str, name: str,
parent: "Union[DoctestTextfile, DoctestModule]", parent: "Union[DoctestTextfile, DoctestModule]",
runner: Optional["doctest.DocTestRunner"] = None, runner: "doctest.DocTestRunner",
dtest: Optional["doctest.DocTest"] = None, dtest: "doctest.DocTest",
) -> None: ) -> None:
super().__init__(name, parent) super().__init__(name, parent)
self.runner = runner self.runner = runner
self.dtest = dtest self.dtest = dtest
# Stuff needed for fixture support.
self.obj = None self.obj = None
self.fixture_request: Optional[FixtureRequest] = None fm = self.session._fixturemanager
fixtureinfo = fm.getfixtureinfo(node=self, func=None, cls=None)
self._fixtureinfo = fixtureinfo
self.fixturenames = fixtureinfo.names_closure
self._initrequest()
@classmethod @classmethod
def from_parent( # type: ignore def from_parent( # type: ignore
@ -276,19 +283,18 @@ class DoctestItem(Item):
"""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)
def _initrequest(self) -> None:
self.funcargs: Dict[str, object] = {}
self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type]
def setup(self) -> None: def setup(self) -> None:
if self.dtest is not None: self._request._fillfixtures()
self.fixture_request = _setup_fixtures(self) globs = dict(getfixture=self._request.getfixturevalue)
globs = dict(getfixture=self.fixture_request.getfixturevalue) for name, value in self._request.getfixturevalue("doctest_namespace").items():
for name, value in self.fixture_request.getfixturevalue( globs[name] = value
"doctest_namespace" self.dtest.globs.update(globs)
).items():
globs[name] = value
self.dtest.globs.update(globs)
def runtest(self) -> None: def runtest(self) -> None:
assert self.dtest is not None
assert self.runner is not None
_check_all_skipped(self.dtest) _check_all_skipped(self.dtest)
self._disable_output_capturing_for_darwin() self._disable_output_capturing_for_darwin()
failures: List["doctest.DocTestFailure"] = [] failures: List["doctest.DocTestFailure"] = []
@ -375,7 +381,6 @@ class DoctestItem(Item):
return ReprFailDoctest(reprlocation_lines) return ReprFailDoctest(reprlocation_lines)
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
assert self.dtest is not None
return self.path, self.dtest.lineno, "[doctest] %s" % self.name return self.path, self.dtest.lineno, "[doctest] %s" % self.name
@ -395,8 +400,8 @@ def _get_flag_lookup() -> Dict[str, int]:
) )
def get_optionflags(parent): def get_optionflags(config: Config) -> int:
optionflags_str = parent.config.getini("doctest_optionflags") optionflags_str = config.getini("doctest_optionflags")
flag_lookup_table = _get_flag_lookup() flag_lookup_table = _get_flag_lookup()
flag_acc = 0 flag_acc = 0
for flag in optionflags_str: for flag in optionflags_str:
@ -404,8 +409,8 @@ def get_optionflags(parent):
return flag_acc return flag_acc
def _get_continue_on_failure(config): def _get_continue_on_failure(config: Config) -> bool:
continue_on_failure = config.getvalue("doctest_continue_on_failure") continue_on_failure: bool = config.getvalue("doctest_continue_on_failure")
if continue_on_failure: if continue_on_failure:
# We need to turn off this if we use pdb since we should stop at # We need to turn off this if we use pdb since we should stop at
# the first failure. # the first failure.
@ -428,7 +433,7 @@ class DoctestTextfile(Module):
name = self.path.name name = self.path.name
globs = {"__name__": "__main__"} globs = {"__name__": "__main__"}
optionflags = get_optionflags(self) optionflags = get_optionflags(self.config)
runner = _get_runner( runner = _get_runner(
verbose=False, verbose=False,
@ -536,6 +541,23 @@ class DoctestModule(Module):
tests, obj, name, module, source_lines, globs, seen tests, obj, name, module, source_lines, globs, seen
) )
if sys.version_info < (3, 13):
def _from_module(self, module, object):
"""`cached_property` objects are never considered a part
of the 'current module'. As such they are skipped by doctest.
Here we override `_from_module` to check the underlying
function instead. https://github.com/python/cpython/issues/107995
"""
if isinstance(object, functools.cached_property):
object = object.func
# Type ignored because this is a private function.
return super()._from_module(module, object) # type: ignore[misc]
else: # pragma: no cover
pass
if self.path.name == "conftest.py": if self.path.name == "conftest.py":
module = self.config.pluginmanager._importconftest( module = self.config.pluginmanager._importconftest(
self.path, self.path,
@ -556,7 +578,7 @@ class DoctestModule(Module):
raise raise
# Uses internal doctest module parsing mechanism. # Uses internal doctest module parsing mechanism.
finder = MockAwareDocTestFinder() finder = MockAwareDocTestFinder()
optionflags = get_optionflags(self) optionflags = get_optionflags(self.config)
runner = _get_runner( runner = _get_runner(
verbose=False, verbose=False,
optionflags=optionflags, optionflags=optionflags,
@ -571,22 +593,6 @@ class DoctestModule(Module):
) )
def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest:
"""Used by DoctestTextfile and DoctestItem to setup fixture information."""
def func() -> None:
pass
doctest_item.funcargs = {} # type: ignore[attr-defined]
fm = doctest_item.session._fixturemanager
doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined]
node=doctest_item, func=func, cls=None, funcargs=False
)
fixture_request = FixtureRequest(doctest_item, _ispytest=True) # type: ignore[arg-type]
fixture_request._fillfixtures()
return fixture_request
def _init_checker_class() -> Type["doctest.OutputChecker"]: def _init_checker_class() -> Type["doctest.OutputChecker"]:
import doctest import doctest
import re import re

View File

@ -1,4 +1,3 @@
import io
import os import os
import sys import sys
from typing import Generator from typing import Generator
@ -10,8 +9,8 @@ from _pytest.nodes import Item
from _pytest.stash import StashKey from _pytest.stash import StashKey
fault_handler_original_stderr_fd_key = StashKey[int]()
fault_handler_stderr_fd_key = StashKey[int]() fault_handler_stderr_fd_key = StashKey[int]()
fault_handler_originally_enabled_key = StashKey[bool]()
def pytest_addoption(parser: Parser) -> None: def pytest_addoption(parser: Parser) -> None:
@ -25,8 +24,15 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_configure(config: Config) -> None: def pytest_configure(config: Config) -> None:
import faulthandler import faulthandler
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno()) # at teardown we want to restore the original faulthandler fileno
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled() # but faulthandler has no api to return the original fileno
# so here we stash the stderr fileno to be used at teardown
# sys.stderr and sys.__stderr__ may be closed or patched during the session
# so we can't rely on their values being good at that point (#11572).
stderr_fileno = get_stderr_fileno()
if faulthandler.is_enabled():
config.stash[fault_handler_original_stderr_fd_key] = stderr_fileno
config.stash[fault_handler_stderr_fd_key] = os.dup(stderr_fileno)
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key]) faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
@ -38,9 +44,10 @@ def pytest_unconfigure(config: Config) -> None:
if fault_handler_stderr_fd_key in config.stash: if fault_handler_stderr_fd_key in config.stash:
os.close(config.stash[fault_handler_stderr_fd_key]) os.close(config.stash[fault_handler_stderr_fd_key])
del config.stash[fault_handler_stderr_fd_key] del config.stash[fault_handler_stderr_fd_key]
if config.stash.get(fault_handler_originally_enabled_key, False): # Re-enable the faulthandler if it was originally enabled.
# Re-enable the faulthandler if it was originally enabled. if fault_handler_original_stderr_fd_key in config.stash:
faulthandler.enable(file=get_stderr_fileno()) faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
del config.stash[fault_handler_original_stderr_fd_key]
def get_stderr_fileno() -> int: def get_stderr_fileno() -> int:
@ -51,7 +58,7 @@ def get_stderr_fileno() -> int:
if fileno == -1: if fileno == -1:
raise AttributeError() raise AttributeError()
return fileno return fileno
except (AttributeError, io.UnsupportedOperation): except (AttributeError, ValueError):
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
# This is potentially dangerous, but the best we can do. # This is potentially dangerous, but the best we can do.

View File

@ -1,3 +1,4 @@
import abc
import dataclasses import dataclasses
import functools import functools
import inspect import inspect
@ -7,6 +8,7 @@ from collections import defaultdict
from collections import deque from collections import deque
from contextlib import suppress from contextlib import suppress
from pathlib import Path from pathlib import Path
from typing import AbstractSet
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
@ -133,7 +135,9 @@ def get_scope_node(
import _pytest.python import _pytest.python
if scope is Scope.Function: if scope is Scope.Function:
return node.getparent(nodes.Item) # Type ignored because this is actually safe, see:
# https://github.com/python/mypy/issues/4717
return node.getparent(nodes.Item) # type: ignore[type-abstract]
elif scope is Scope.Class: elif scope is Scope.Class:
return node.getparent(_pytest.python.Class) return node.getparent(_pytest.python.Class)
elif scope is Scope.Module: elif scope is Scope.Module:
@ -209,16 +213,14 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {} argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {} items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
for scope in HIGH_SCOPES: for scope in HIGH_SCOPES:
d: Dict[nodes.Item, Dict[FixtureArgKey, None]] = {} scoped_argkeys_cache = argkeys_cache[scope] = {}
argkeys_cache[scope] = d scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
item_d: Dict[FixtureArgKey, Deque[nodes.Item]] = defaultdict(deque)
items_by_argkey[scope] = item_d
for item in items: for item in items:
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None) keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
if keys: if keys:
d[item] = keys scoped_argkeys_cache[item] = keys
for key in keys: for key in keys:
item_d[key].append(item) scoped_items_by_argkey[key].append(item)
items_dict = dict.fromkeys(items, None) items_dict = dict.fromkeys(items, None)
return list( return list(
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session) reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
@ -340,26 +342,32 @@ class FuncFixtureInfo:
self.names_closure[:] = sorted(closure, key=self.names_closure.index) self.names_closure[:] = sorted(closure, key=self.names_closure.index)
class FixtureRequest: class FixtureRequest(abc.ABC):
"""A request for a fixture from a test or fixture function. """The type of the ``request`` fixture.
A request object gives access to the requesting test context and has A request object gives access to the requesting test context and has a
an optional ``param`` attribute in case the fixture is parametrized ``param`` attribute in case the fixture is parametrized.
indirectly.
""" """
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None: def __init__(
self,
pyfuncitem: "Function",
fixturename: Optional[str],
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
arg2index: Dict[str, int],
fixture_defs: Dict[str, "FixtureDef[Any]"],
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest) check_ispytest(_ispytest)
#: Fixture for which this request is being performed. #: Fixture for which this request is being performed.
self.fixturename: Optional[str] = None self.fixturename: Final = fixturename
self._pyfuncitem = pyfuncitem self._pyfuncitem: Final = pyfuncitem
self._fixturemanager = pyfuncitem.session._fixturemanager
self._scope = Scope.Function
# The FixtureDefs for each fixture name requested by this item. # The FixtureDefs for each fixture name requested by this item.
# Starts from the statically-known fixturedefs resolved during # Starts from the statically-known fixturedefs resolved during
# collection. Dynamically requested fixtures (using # collection. Dynamically requested fixtures (using
# `request.getfixturevalue("foo")`) are added dynamically. # `request.getfixturevalue("foo")`) are added dynamically.
self._arg2fixturedefs = pyfuncitem._fixtureinfo.name2fixturedefs.copy() self._arg2fixturedefs: Final = arg2fixturedefs
# A fixture may override another fixture with the same name, e.g. a fixture # A fixture may override another fixture with the same name, e.g. a fixture
# in a module can override a fixture in a conftest, a fixture in a class can # in a module can override a fixture in a conftest, a fixture in a class can
# override a fixture in the module, and so on. # override a fixture in the module, and so on.
@ -369,10 +377,10 @@ class FixtureRequest:
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from # The fixturedefs list in _arg2fixturedefs for a given name is ordered from
# furthest to closest, so we use negative indexing -1, -2, ... to go from # furthest to closest, so we use negative indexing -1, -2, ... to go from
# last to first. # last to first.
self._arg2index: Dict[str, int] = {} self._arg2index: Final = arg2index
# The evaluated argnames so far, mapping to the FixtureDef they resolved # The evaluated argnames so far, mapping to the FixtureDef they resolved
# to. # to.
self._fixture_defs: Dict[str, FixtureDef[Any]] = {} self._fixture_defs: Final = fixture_defs
# Notes on the type of `param`: # Notes on the type of `param`:
# -`request.param` is only defined in parametrized fixtures, and will raise # -`request.param` is only defined in parametrized fixtures, and will raise
# AttributeError otherwise. Python typing has no notion of "undefined", so # AttributeError otherwise. Python typing has no notion of "undefined", so
@ -383,6 +391,15 @@ class FixtureRequest:
# for now just using Any. # for now just using Any.
self.param: Any self.param: Any
@property
def _fixturemanager(self) -> "FixtureManager":
return self._pyfuncitem.session._fixturemanager
@property
@abc.abstractmethod
def _scope(self) -> Scope:
raise NotImplementedError()
@property @property
def scope(self) -> _ScopeName: def scope(self) -> _ScopeName:
"""Scope string, one of "function", "class", "module", "package", "session".""" """Scope string, one of "function", "class", "module", "package", "session"."""
@ -391,30 +408,15 @@ class FixtureRequest:
@property @property
def fixturenames(self) -> List[str]: def fixturenames(self) -> List[str]:
"""Names of all active fixtures in this request.""" """Names of all active fixtures in this request."""
result = list(self._pyfuncitem._fixtureinfo.names_closure) result = list(self._pyfuncitem.fixturenames)
result.extend(set(self._fixture_defs).difference(result)) result.extend(set(self._fixture_defs).difference(result))
return result return result
@property @property
@abc.abstractmethod
def node(self): def node(self):
"""Underlying collection node (depends on current request scope).""" """Underlying collection node (depends on current request scope)."""
scope = self._scope raise NotImplementedError()
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on SubRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]": def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None) fixturedefs = self._arg2fixturedefs.get(argname, None)
@ -500,11 +502,11 @@ class FixtureRequest:
"""Pytest session object.""" """Pytest session object."""
return self._pyfuncitem.session # type: ignore[no-any-return] return self._pyfuncitem.session # type: ignore[no-any-return]
@abc.abstractmethod
def addfinalizer(self, finalizer: Callable[[], object]) -> None: def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called without arguments after """Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution.""" the last test within the requesting test context finished execution."""
# XXX usually this method is shadowed by fixturedef specific ones. raise NotImplementedError()
self.node.addfinalizer(finalizer)
def applymarker(self, marker: Union[str, MarkDecorator]) -> None: def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
"""Apply a marker to a single test function invocation. """Apply a marker to a single test function invocation.
@ -525,13 +527,6 @@ class FixtureRequest:
""" """
raise self._fixturemanager.FixtureLookupError(None, self, msg) raise self._fixturemanager.FixtureLookupError(None, self, msg)
def _fillfixtures(self) -> None:
item = self._pyfuncitem
fixturenames = getattr(item, "fixturenames", self.fixturenames)
for argname in fixturenames:
if argname not in item.funcargs:
item.funcargs[argname] = self.getfixturevalue(argname)
def getfixturevalue(self, argname: str) -> Any: def getfixturevalue(self, argname: str) -> Any:
"""Dynamically run a named fixture function. """Dynamically run a named fixture function.
@ -665,6 +660,97 @@ class FixtureRequest:
finalizer = functools.partial(fixturedef.finish, request=subrequest) finalizer = functools.partial(fixturedef.finish, request=subrequest)
subrequest.node.addfinalizer(finalizer) subrequest.node.addfinalizer(finalizer)
@final
class TopRequest(FixtureRequest):
"""The type of the ``request`` fixture in a test function."""
def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None:
super().__init__(
fixturename=None,
pyfuncitem=pyfuncitem,
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
arg2index={},
fixture_defs={},
_ispytest=_ispytest,
)
@property
def _scope(self) -> Scope:
return Scope.Function
@property
def node(self):
return self._pyfuncitem
def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node)
def _fillfixtures(self) -> None:
item = self._pyfuncitem
for argname in item.fixturenames:
if argname not in item.funcargs:
item.funcargs[argname] = self.getfixturevalue(argname)
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
self.node.addfinalizer(finalizer)
@final
class SubRequest(FixtureRequest):
"""The type of the ``request`` fixture in a fixture function requested
(transitively) by a test function."""
def __init__(
self,
request: FixtureRequest,
scope: Scope,
param: Any,
param_index: int,
fixturedef: "FixtureDef[object]",
*,
_ispytest: bool = False,
) -> None:
super().__init__(
pyfuncitem=request._pyfuncitem,
fixturename=fixturedef.argname,
fixture_defs=request._fixture_defs,
arg2fixturedefs=request._arg2fixturedefs,
arg2index=request._arg2index,
_ispytest=_ispytest,
)
self._parent_request: Final[FixtureRequest] = request
self._scope_field: Final = scope
self._fixturedef: Final = fixturedef
if param is not NOTSET:
self.param = param
self.param_index: Final = param_index
def __repr__(self) -> str:
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
@property
def _scope(self) -> Scope:
return self._scope_field
@property
def node(self):
scope = self._scope
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
node = get_scope_package(self._pyfuncitem, self._fixturedef)
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def _check_scope( def _check_scope(
self, self,
argname: str, argname: str,
@ -699,44 +785,7 @@ class FixtureRequest:
) )
return lines return lines
def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node)
@final
class SubRequest(FixtureRequest):
"""A sub request for handling getting a fixture from a test function/fixture."""
def __init__(
self,
request: "FixtureRequest",
scope: Scope,
param: Any,
param_index: int,
fixturedef: "FixtureDef[object]",
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self._parent_request = request
self.fixturename = fixturedef.argname
if param is not NOTSET:
self.param = param
self.param_index = param_index
self._scope = scope
self._fixturedef = fixturedef
self._pyfuncitem = request._pyfuncitem
self._fixture_defs = request._fixture_defs
self._arg2fixturedefs = request._arg2fixturedefs
self._arg2index = request._arg2index
self._fixturemanager = request._fixturemanager
def __repr__(self) -> str:
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
def addfinalizer(self, finalizer: Callable[[], object]) -> None: def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution."""
self._fixturedef.addfinalizer(finalizer) self._fixturedef.addfinalizer(finalizer)
def _schedule_finalizers( def _schedule_finalizers(
@ -745,7 +794,10 @@ class SubRequest(FixtureRequest):
# If the executing fixturedef was not explicitly requested in the argument list (via # If the executing fixturedef was not explicitly requested in the argument list (via
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished # getfixturevalue inside the fixture call) then ensure this fixture def will be finished
# first. # first.
if fixturedef.argname not in self.fixturenames: if (
fixturedef.argname not in self._fixture_defs
and fixturedef.argname not in self._pyfuncitem.fixturenames
):
fixturedef.addfinalizer( fixturedef.addfinalizer(
functools.partial(self._fixturedef.finish, request=self) functools.partial(self._fixturedef.finish, request=self)
) )
@ -1333,7 +1385,7 @@ def pytest_addoption(parser: Parser) -> None:
) )
def _get_direct_parametrize_args(node: nodes.Node) -> List[str]: def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
"""Return all direct parametrization arguments of a node, so we don't """Return all direct parametrization arguments of a node, so we don't
mistake them for fixtures. mistake them for fixtures.
@ -1342,17 +1394,22 @@ def _get_direct_parametrize_args(node: nodes.Node) -> List[str]:
These things are done later as well when dealing with parametrization These things are done later as well when dealing with parametrization
so this could be improved. so this could be improved.
""" """
parametrize_argnames: List[str] = [] parametrize_argnames: Set[str] = set()
for marker in node.iter_markers(name="parametrize"): for marker in node.iter_markers(name="parametrize"):
if not marker.kwargs.get("indirect", False): if not marker.kwargs.get("indirect", False):
p_argnames, _ = ParameterSet._parse_parametrize_args( p_argnames, _ = ParameterSet._parse_parametrize_args(
*marker.args, **marker.kwargs *marker.args, **marker.kwargs
) )
parametrize_argnames.extend(p_argnames) parametrize_argnames.update(p_argnames)
return parametrize_argnames return parametrize_argnames
def deduplicate_names(*seqs: Iterable[str]) -> Tuple[str, ...]:
"""De-duplicate the sequence of names while keeping the original order."""
# Ideally we would use a set, but it does not preserve insertion order.
return tuple(dict.fromkeys(name for seq in seqs for name in seq))
class FixtureManager: class FixtureManager:
"""pytest fixture definitions and information is stored and managed """pytest fixture definitions and information is stored and managed
from this class. from this class.
@ -1405,13 +1462,12 @@ class FixtureManager:
def getfixtureinfo( def getfixtureinfo(
self, self,
node: nodes.Item, node: nodes.Item,
func: Callable[..., object], func: Optional[Callable[..., object]],
cls: Optional[type], cls: Optional[type],
funcargs: bool = True,
) -> FuncFixtureInfo: ) -> FuncFixtureInfo:
"""Calculate the :class:`FuncFixtureInfo` for an item. """Calculate the :class:`FuncFixtureInfo` for an item.
If ``funcargs`` is false, or if the item sets an attribute If ``func`` is None, or if the item sets an attribute
``nofuncargs = True``, then ``func`` is not examined at all. ``nofuncargs = True``, then ``func`` is not examined at all.
:param node: :param node:
@ -1420,21 +1476,23 @@ class FixtureManager:
The item's function. The item's function.
:param cls: :param cls:
If the function is a method, the method's class. If the function is a method, the method's class.
:param funcargs:
Whether to look into func's parameters as fixture requests.
""" """
if funcargs and not getattr(node, "nofuncargs", False): if func is not None and not getattr(node, "nofuncargs", False):
argnames = getfuncargnames(func, name=node.name, cls=cls) argnames = getfuncargnames(func, name=node.name, cls=cls)
else: else:
argnames = () argnames = ()
usefixturesnames = self._getusefixturesnames(node)
autousenames = self._getautousenames(node.nodeid)
initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
usefixtures = tuple( direct_parametrize_args = _get_direct_parametrize_args(node)
arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args
) names_closure, arg2fixturedefs = self.getfixtureclosure(
initialnames = usefixtures + argnames parentnode=node,
initialnames, names_closure, arg2fixturedefs = self.getfixtureclosure( initialnames=initialnames,
initialnames, node, ignore_args=_get_direct_parametrize_args(node) ignore_args=direct_parametrize_args,
) )
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) -> None:
@ -1466,12 +1524,17 @@ class FixtureManager:
if basenames: if basenames:
yield from basenames yield from basenames
def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]:
"""Return the names of usefixtures fixtures applicable to node."""
for mark in node.iter_markers(name="usefixtures"):
yield from mark.args
def getfixtureclosure( def getfixtureclosure(
self, self,
fixturenames: Tuple[str, ...],
parentnode: nodes.Node, parentnode: nodes.Node,
ignore_args: Sequence[str] = (), initialnames: Tuple[str, ...],
) -> Tuple[Tuple[str, ...], List[str], Dict[str, Sequence[FixtureDef[Any]]]]: ignore_args: AbstractSet[str],
) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]:
# Collect the closure of all fixtures, starting with the given # Collect the closure of all fixtures, starting with the given
# fixturenames as the initial set. As we have to visit all # fixturenames as the initial set. As we have to visit all
# factory definitions anyway, we also return an arg2fixturedefs # factory definitions anyway, we also return an arg2fixturedefs
@ -1480,19 +1543,7 @@ class FixtureManager:
# (discovering matching fixtures for a given name/node is expensive). # (discovering matching fixtures for a given name/node is expensive).
parentid = parentnode.nodeid parentid = parentnode.nodeid
fixturenames_closure = list(self._getautousenames(parentid)) fixturenames_closure = list(initialnames)
def merge(otherlist: Iterable[str]) -> None:
for arg in otherlist:
if arg not in fixturenames_closure:
fixturenames_closure.append(arg)
merge(fixturenames)
# At this point, fixturenames_closure contains what we call "initialnames",
# which is a set of fixturenames the function immediately requests. We
# need to return it as well, so save this.
initialnames = tuple(fixturenames_closure)
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {} arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
lastlen = -1 lastlen = -1
@ -1506,7 +1557,9 @@ class FixtureManager:
fixturedefs = self.getfixturedefs(argname, parentid) fixturedefs = self.getfixturedefs(argname, parentid)
if fixturedefs: if fixturedefs:
arg2fixturedefs[argname] = fixturedefs arg2fixturedefs[argname] = fixturedefs
merge(fixturedefs[-1].argnames) for arg in fixturedefs[-1].argnames:
if arg not in fixturenames_closure:
fixturenames_closure.append(arg)
def sort_by_scope(arg_name: str) -> Scope: def sort_by_scope(arg_name: str) -> Scope:
try: try:
@ -1517,7 +1570,7 @@ class FixtureManager:
return fixturedefs[-1]._scope return fixturedefs[-1]._scope
fixturenames_closure.sort(key=sort_by_scope, reverse=True) fixturenames_closure.sort(key=sort_by_scope, reverse=True)
return initialnames, fixturenames_closure, arg2fixturedefs return fixturenames_closure, arg2fixturedefs
def pytest_generate_tests(self, metafunc: "Metafunc") -> None: def pytest_generate_tests(self, metafunc: "Metafunc") -> None:
"""Generate new tests based on parametrized fixtures used by the given metafunc""" """Generate new tests based on parametrized fixtures used by the given metafunc"""

View File

@ -12,6 +12,7 @@ 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
class HelpAction(Action): class HelpAction(Action):
@ -161,7 +162,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def showhelp(config: Config) -> None: def showhelp(config: Config) -> None:
import textwrap import textwrap
reporter = config.pluginmanager.get_plugin("terminalreporter") reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
"terminalreporter"
)
assert reporter is not None
tw = reporter._tw tw = reporter._tw
tw.write(config._parser.optparser.format_help()) tw.write(config._parser.optparser.format_help())
tw.line() tw.line()

View File

@ -55,7 +55,7 @@ hookspec = HookspecMarker("pytest")
@hookspec(historic=True) @hookspec(historic=True)
def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: 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
``pluginmanager.add_hookspecs(module_or_class, prefix)``. :func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`.
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager. :param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
@ -96,8 +96,8 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
<pytest.Parser.addini>`. <pytest.Parser.addini>`.
:param pytest.PytestPluginManager pluginmanager: :param pytest.PytestPluginManager pluginmanager:
The pytest plugin manager, which can be used to install :py:func:`hookspec`'s The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
or :py:func:`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.
Options can later be accessed through the Options can later be accessed through the
@ -858,8 +858,8 @@ def pytest_warning_recorded(
"""Process a warning captured by the internal pytest warnings plugin. """Process a warning captured by the internal pytest warnings plugin.
:param warning_message: :param warning_message:
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains The captured warning. This is the same object produced by :class:`warnings.catch_warnings`,
the same attributes as the parameters of :py:func:`warnings.showwarning`. and contains the same attributes as the parameters of :py:func:`warnings.showwarning`.
:param when: :param when:
Indicates when the warning was captured. Possible values: Indicates when the warning was captured. Possible values:
@ -940,10 +940,10 @@ def pytest_exception_interact(
interactively handled. interactively handled.
May be called during collection (see :hook:`pytest_make_collect_report`), May be called during collection (see :hook:`pytest_make_collect_report`),
in which case ``report`` is a :class:`CollectReport`. in which case ``report`` is a :class:`~pytest.CollectReport`.
May be called during runtest of an item (see :hook:`pytest_runtest_protocol`), May be called during runtest of an item (see :hook:`pytest_runtest_protocol`),
in which case ``report`` is a :class:`TestReport`. in which case ``report`` is a :class:`~pytest.TestReport`.
This hook is not called if the exception that was raised is an internal This hook is not called if the exception that was raised is an internal
exception like ``skip.Exception``. exception like ``skip.Exception``.

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