Compare commits

...

66 Commits
main ... 7.4.x

Author SHA1 Message Date
James Frost 40d58e0ce7
Add html_baseurl to sphinx conf.py (#12364) (#12391)
This is used to set the <link rel="canonical" href="X"> tag that points to the canonical version of the webpage. Including this indicates to search engines which version to include in their indexes, and should prevent older versions showing up.

Fixes #12363
2024-05-30 08:08:19 -03:00
github-actions[bot] 549190651e
[7.4.x] doc: Remove sold out training (#11824)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-01-16 14:07:26 +00:00
github-actions[bot] cf72d1a40e
[7.4.x] doc: Update training dates and add pytest sprint (#11820)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-01-15 20:18:31 +00:00
Ran Benita 18dcd9d38d
Merge pull request #11752 from pytest-dev/release-7.4.4
Prepare release 7.4.4
2023-12-31 14:10:58 +02:00
pytest bot 33f694f4b3 Prepare release version 7.4.4 2023-12-31 10:17:27 +00:00
Ran Benita 76c107c463
Merge pull request #11751 from bluetech/backport-11143-to-7.4.x
(cherry picked from commit 084d756ae6)

[ran: adapted to 7.4.x, fixed changelog issue number]
2023-12-31 12:12:34 +02:00
github-actions[bot] 531d76daa4
[7.4.x] Improve reporting from __iter__ exceptions (#11749) 2023-12-31 12:11:02 +02:00
Zac Hatfield-Dodds a0f58fa9e7 Merge pull request #11143 from tushar-deepsource/patch-1
(cherry picked from commit 084d756ae6)

[ran: adapted to 7.4.x, fixed changelog issue number]
2023-12-31 11:53:19 +02:00
github-actions[bot] b1f3387d42
[7.4.x] #11091: documentation should use hypthonated properties (#11750) 2023-12-31 11:50:38 +02:00
Ran Benita 2cdd619bf4
Merge pull request #11747 from pytest-dev/backport-11711-to-7.4.x
[7.4.x] nodes: fix tracebacks from collection errors are not getting pruned
2023-12-31 11:15:21 +02:00
Ran Benita d06c05bd23 [7.4.x] nodes: fix tracebacks from collection errors are not getting pruned 2023-12-31 08:14:55 +00:00
github-actions[bot] 5582bfcddf
[7.4.x] Improves clarity in Sphinx documentation for function signature. (#11702)
Co-authored-by: Arthur Richard <arthur.richard2299@gmail.com>
2023-12-14 08:19:40 -03:00
github-actions[bot] 13024efd7a
[7.4.x] Fix for operation on closed file in faulthandler teardown (#11631)
Co-authored-by: Simon Blanchard <bnomis@gmail.com>
2023-11-22 20:25:29 +00:00
github-actions[bot] a40dacf657
[7.4.x] XFAIL TestLocalPath.test_make_numbered_dir_multiprocess_safe (#11616)
Co-authored-by: Miro Hrončok <miro@hroncok.cz>
2023-11-14 15:08:28 +00:00
Ran Benita 3550906ead
Merge pull request #11570 from pytest-dev/backport-11567-to-7.4.x
[7.4.x] doc/reference: fix sidebar TOC depth
2023-10-30 09:26:36 +02:00
Ran Benita bd068705b1 [7.4.x] doc/reference: fix sidebar TOC depth 2023-10-30 07:02:22 +00:00
Bruno Oliveira ec5bd27cc7
Update build-and-inspect-python-package action (#11561) (#11562)
This should fix the action for Python 3.12.

Ref: hynek/build-and-inspect-python-package#72
(cherry picked from commit 247436819a)
2023-10-27 10:36:51 -03:00
Bruno Oliveira a32feda04c
Fix tag name generated by deploy workflow (#11550) (#11554)
Traditionally pytest publishes tags in the form `X.Y.Z`, however the deploy workflow (copied from somewhere else) published tags in the form `vX.Y.Z`.

This is the root cause of #11548, because it tried to publish the release notes for tag `X.Y.Z` (which did not exist).

Fix #11548

(cherry picked from commit c1728948ac)
2023-10-25 13:28:22 -03:00
github-actions[bot] f8070ffb9b
[7.4.x] Add deploy instructions using the command-line (#11553)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-10-25 13:37:56 +00:00
Bruno Oliveira 53df6164b4
Merge pull request #11546 from pytest-dev/release-7.4.3
Prepare release 7.4.3
2023-10-24 16:38:09 -03:00
Bruno Oliveira 2390610696
Tweak changelog.rst 2023-10-24 15:45:08 -03:00
pytest bot a0714aa007 Prepare release version 7.4.3 2023-10-24 18:43:16 +00:00
github-actions[bot] 44ad1c9811
[7.4.x] fix #10447 - consider marks in reverse mro order to give base classes priority (#11545)
Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
2023-10-24 15:04:13 +00:00
github-actions[bot] 5dc77253d4
[7.4.x] Ensure logging tests always cleanup after themselves (#11541)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-10-23 14:28:04 +00:00
github-actions[bot] a517827318
[7.4.x] Configure ReadTheDocs to fail on warnings (#11540)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-10-23 13:23:18 +00:00
github-actions[bot] 21fe071d79
[7.4.x] fix for ValueError raised in faulthandler teardown code (#11455)
Co-authored-by: Simon Blanchard <bnomis@gmail.com>
2023-09-20 12:41:01 +00:00
Bruno Oliveira f8bb8572fe
Force terminal width when running tests (#11425) (#11432)
Related to #11423

(cherry picked from commit 241f2a890e)
2023-09-11 09:48:22 -03:00
github-actions[bot] 1944dc06d3
[7.4.x] Fix --import-mode=importlib when root contains `__init__.py` file (#11426)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-09-10 13:27:53 +00:00
Bruno Oliveira 946634c84c
Merge pull request #11419 from nicoddemus/backport-11414-to-7.4.x
[7.4.x] Fix assert rewriting with assignment expressions (#11414)
2023-09-09 10:08:41 -03:00
github-actions[bot] d849a3ed64
[7.4.x] fix: closes #11343's [attr-defined] type errors (#11421)
Co-authored-by: Warren Markham <rabbitsinwarrens@gmail.com>
2023-09-09 13:02:31 +00:00
Bruno Oliveira 721a0881fb Skip test_assertion_walrus_different_test_cases on Python 3.7 2023-09-09 09:43:15 -03:00
Marc Mueller 5341b9cd67 Fix assert rewriting with assignment expressions (#11414)
Fixes #11239

(cherry picked from commit 7259e8db98)
2023-09-09 09:13:10 -03:00
Bruno Oliveira c39bdf6190
Adjustments to the release process (#11410) (#11415)
As discussed in #11408:

* Improve documentation for the release process.
* Fix the description for the PRs created by the `prepare release pr` workflow.
* Fix pushing tag in the `deploy` workflow.

(cherry picked from commit e5c81fa41a)
2023-09-08 08:42:55 -03:00
Bruno Oliveira b0c4775a28
Merge pull request #11408 from pytest-dev/release-7.4.2
Prepare release 7.4.2
2023-09-07 15:47:56 -03:00
pytest bot 45f34dfb8d Prepare release version 7.4.2 2023-09-07 17:21:49 +00:00
Bruno Oliveira e4f022f0d8
Merge pull request #11406 from nicoddemus/backport-11404-to-7.4.x
[7.4.x] Fix crash when passing a very long cmdline argument (#11404)
2023-09-07 14:14:40 -03:00
Bruno Oliveira 63b0c6f75f Use _pytest.pathlib.safe_exists in get_dirs_from_args
Related to #11394
2023-09-07 13:50:02 -03:00
Bruno Oliveira 884b911a9c Fix crash when passing a very long cmdline argument (#11404)
Fixes #11394

(cherry picked from commit 28ccf476b9)
2023-09-07 12:54:41 -03:00
github-actions[bot] 6e49a74089
[7.4.x] Fix doctest collection of `functools.cached_property` objects. (#11403)
Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
2023-09-07 13:33:12 +00:00
github-actions[bot] 79c2012d40
[7.4.x] doc: Remove done training (#11400)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2023-09-06 13:50:00 +00:00
github-actions[bot] de69883e3a
[7.4.x] improve plugin list disclaimer (#11398)
Co-authored-by: Stefaan Lippens <soxofaan@users.noreply.github.com>
2023-09-06 11:02:29 +00:00
github-actions[bot] 1de00e9830
[7.4.x] Fix import_path for packages (#11395)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-09-05 23:07:48 +00:00
Bruno Oliveira 7f5d9b9df4
Fix user_properties not saved to XML if fixture errors during teardown (#11382)
Move handling of user_properties to `finalize()`.

Previously if a fixture failed during teardown, `pytest_runtest_logreport` would not be called with "teardown", resulting in the user properties not being saved on the JUnit XML file.

Fixes: #11367
(cherry picked from commit 917ce9aa01)

Co-authored-by: Israel Fruchter <israel.fruchter@gmail.com>
2023-09-03 15:01:56 -03:00
Bruno Oliveira 82eb86f707
Merge pull request #11377 from pytest-dev/release-7.4.1
Prepare release 7.4.1
2023-09-02 12:41:32 -03:00
Bruno Oliveira 0319a0d4fd Checkout source code during deploy
We need the checked out repository in order to push the tag.
2023-09-02 12:38:39 -03:00
Bruno Oliveira 7855a72d2c Improve CI workflow
* Build the package only once, and test on all platforms.
* Deploy is now triggered manually via an Action, which is then responsible for tagging the repository after the package has been uploaded successfully.
* Drop 'docs': we nowadays rely on readthedocs preview PR builds.
2023-09-02 08:46:22 -03:00
pytest bot 7a0a0e8b08 Prepare release version 7.4.1 2023-09-02 11:03:06 +00:00
github-actions[bot] fbcfd3a52e
[7.4.x] Update CONTRIBUTING.rst (#11371)
Co-authored-by: Sourabh Beniwal <sourabhbeniwal@outlook.com>
2023-08-30 08:57:49 -03:00
github-actions[bot] b170081788
[7.4.x] Issue 11354 fixing docs for lfnf (#11364)
Co-authored-by: Sean Patrick Malloy <spmalloy@ucdavis.edu>
2023-08-29 00:40:49 +00:00
Ran Benita 7a5f2feefb
[7.4.x] Fixes for typed pluggy (#11355)
Since version 1.3 pluggy added typing, which requires some fixes to
please mypy.
2023-08-26 22:15:32 +00:00
github-actions[bot] 69140717d4
[7.4.x] Improve duplicate values documentation (#11296)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-08-08 14:44:03 +03:00
Bruno Oliveira 5c7c3f6329
Merge pull request #11294 from The-Compiler/pluggy-py38
ci: Use Python 3.8 to test latest pluggy
2023-08-07 08:13:03 -03:00
Florian Bruhin ba40975bb7 ci: Use Python 3.8 to test latest pluggy
Pluggy dropped Python 3.7 support.
Also see 165fbbd12a

Fixes #11293
2023-08-07 12:08:56 +02:00
github-actions[bot] e3fe7286f8
[7.4.x] doc: Link pytest.main to how-to guide (#11290)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2023-08-07 10:47:37 +02:00
github-actions[bot] 34c73944e1
[7.4.x] doc: update information about assertion messages (#11286)
Co-authored-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-08-07 10:47:12 +02:00
github-actions[bot] 350122abb2
[7.4.x] Remove ep2023 training (#11242)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2023-07-22 18:40:56 +00:00
github-actions[bot] 06ff7ca13b
[7.4.x] Clarify docs for pytest.main default behavior (#11188)
Co-authored-by: antosikv <79337398+antosikv@users.noreply.github.com>
2023-07-09 15:54:59 +00:00
github-actions[bot] 6dfe498c77
[7.4.x] doc: fix EncodingWarnings in examples (#11182)
Co-authored-by: Ran Benita <ran@unusedvar.com>
2023-07-08 19:17:21 +00:00
github-actions[bot] a566b78730
[7.4.x] reference: improve the node types docs a bit (#11181)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-07-08 19:07:55 +00:00
github-actions[bot] 511adf85be
[7.4.x] Fix error assertion handling in approx when None in dict comparison (#11180)
Co-authored-by: Zac Hatfield-Dodds <zac.hatfield.dodds@gmail.com>
2023-07-08 18:37:35 +00:00
github-actions[bot] c71b5df734
[7.4.x] Add child modules as attributes of parent modules. (#11163)
* [7.4.x] Add child modules as attributes of parent modules.

* Update 10337.bugfix.rst

---------

Co-authored-by: akhilramkee <31619526+akhilramkee@users.noreply.github.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-07-08 15:11:26 -03:00
github-actions[bot] d53951836d
[7.4.x] Update open trainings (#11172)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2023-07-04 21:49:24 +00:00
github-actions[bot] a4d7254d18
[7.4.x] Fix duplicated imports with importlib mode and doctest-modules (#11164)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-07-03 16:33:47 +00:00
Bruno Oliveira b6c55787fe
Switch to deploy environment and configure for pypi oidc (#10925) (#11162)
Closes #10871
Closes #10870

Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
2023-07-03 16:17:02 +00:00
Ran Benita fb03d1388b
Merge pull request #11131 from pytest-dev/release-7.4.0
Prepare release 7.4.0
2023-06-23 14:18:43 +03:00
pytest bot d9bf9dbec1 Prepare release version 7.4.0
[ran: made some fixups]
2023-06-23 14:03:58 +03:00
86 changed files with 1240 additions and 337 deletions

View File

@ -1,26 +1,23 @@
name: deploy
on:
push:
tags:
# These tags are protected, see:
# https://github.com/pytest-dev/pytest/settings/tag_protection
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
workflow_dispatch:
inputs:
version:
description: 'Release version'
required: true
default: '1.2.3'
# Set permissions at the job level.
permissions: {}
jobs:
deploy:
if: github.repository == 'pytest-dev/pytest'
package:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
@ -29,7 +26,19 @@ jobs:
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5
uses: hynek/build-and-inspect-python-package@v1.5.3
deploy:
if: github.repository == 'pytest-dev/pytest'
needs: [package]
runs-on: ubuntu-latest
environment: deploy
timeout-minutes: 30
permissions:
id-token: write
contents: write
steps:
- uses: actions/checkout@v3
- name: Download Package
uses: actions/download-artifact@v3
@ -38,14 +47,35 @@ jobs:
path: dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@v1.8.5
- 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:
# todo: generate the content in the build job
# the goal being of using a github action script to push the release data
# after success instead of creating a complete python/tox env
needs: [deploy]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
steps:
- uses: actions/checkout@v3
with:
password: ${{ secrets.pypi_token }}
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.7"
python-version: "3.10"
- name: Install tox
run: |

View File

@ -27,7 +27,19 @@ concurrency:
permissions: {}
jobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5.3
build:
needs: [package]
runs-on: ${{ matrix.os }}
timeout-minutes: 45
permissions:
@ -38,17 +50,17 @@ jobs:
matrix:
name: [
"windows-py37",
"windows-py37-pluggy",
"windows-py38",
"windows-py38-pluggy",
"windows-py39",
"windows-py310",
"windows-py311",
"windows-py312",
"ubuntu-py37",
"ubuntu-py37-pluggy",
"ubuntu-py37-freeze",
"ubuntu-py38",
"ubuntu-py38-pluggy",
"ubuntu-py39",
"ubuntu-py310",
"ubuntu-py311",
@ -60,7 +72,6 @@ jobs:
"macos-py310",
"macos-py312",
"docs",
"doctesting",
"plugins",
]
@ -70,15 +81,15 @@ jobs:
python: "3.7"
os: windows-latest
tox_env: "py37-numpy"
- name: "windows-py37-pluggy"
python: "3.7"
os: windows-latest
tox_env: "py37-pluggymain-pylib-xdist"
- name: "windows-py38"
python: "3.8"
os: windows-latest
tox_env: "py38-unittestextras"
use_coverage: true
- name: "windows-py38-pluggy"
python: "3.8"
os: windows-latest
tox_env: "py38-pluggymain-pylib-xdist"
- name: "windows-py39"
python: "3.9"
os: windows-latest
@ -101,10 +112,6 @@ jobs:
os: ubuntu-latest
tox_env: "py37-lsof-numpy-pexpect"
use_coverage: true
- name: "ubuntu-py37-pluggy"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-pluggymain-pylib-xdist"
- name: "ubuntu-py37-freeze"
python: "3.7"
os: ubuntu-latest
@ -113,6 +120,10 @@ jobs:
python: "3.8"
os: ubuntu-latest
tox_env: "py38-xdist"
- name: "ubuntu-py38-pluggy"
python: "3.8"
os: ubuntu-latest
tox_env: "py38-pluggymain-pylib-xdist"
- name: "ubuntu-py39"
python: "3.9"
os: ubuntu-latest
@ -159,10 +170,6 @@ jobs:
os: ubuntu-latest
tox_env: "plugins"
- name: "docs"
python: "3.7"
os: ubuntu-latest
tox_env: "docs"
- name: "doctesting"
python: "3.7"
os: ubuntu-latest
@ -175,6 +182,12 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Download Package
uses: actions/download-artifact@v3
with:
name: Packages
path: dist
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4
with:
@ -188,11 +201,13 @@ jobs:
- name: Test without 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
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
if: "matrix.use_coverage"
@ -206,10 +221,3 @@ jobs:
fail_ci_if_error: true
files: ./coverage.xml
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

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

10
AUTHORS
View File

@ -47,6 +47,7 @@ Ariel Pillemer
Armin Rigo
Aron Coyle
Aron Curzon
Arthur Richard
Ashish Kurmi
Aviral Verma
Aviv Palivoda
@ -166,6 +167,8 @@ Ian Bicking
Ian Lesperance
Ilya Konstantinov
Ionuț Turturică
Isaac Virshup
Israel Fruchter
Itxaso Aizpurua
Iwan Briquemont
Jaap Broekhuizen
@ -173,6 +176,7 @@ Jake VanderPlas
Jakob van Santen
Jakub Mitoraj
James Bourbeau
James Frost
Jan Balster
Janne Vanhala
Jason R. Coombs
@ -229,6 +233,7 @@ Maho
Maik Figura
Mandeep Bhutani
Manuel Krebber
Marc Mueller
Marc Schlaich
Marcelo Duarte Trevisani
Marcin Bachry
@ -320,6 +325,7 @@ Ronny Pfannschmidt
Ross Lawley
Ruaridh Williamson
Russel Winder
Ryan Puddephatt
Ryan Wooden
Saiprasad Kale
Samuel Colvin
@ -334,11 +340,13 @@ Serhii Mozghovyi
Seth Junot
Shantanu Jain
Shubham Adep
Simon Blanchard
Simon Gomizelj
Simon Holesch
Simon Kerr
Skylar Downes
Srinivas Reddy Thatiparthy
Stefaan Lippens
Stefan Farmbauer
Stefan Scherfke
Stefan Zimmermann
@ -370,7 +378,9 @@ Tomer Keren
Tony Narlock
Tor Colvin
Trevor Bekolay
Tushar Sadhwani
Tyler Goodlet
Tyler Smart
Tzu-ping Chung
Vasily Kuznetsov
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>`_.
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.
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going

View File

@ -133,14 +133,12 @@ Releasing
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
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
as source.
git fetch upstream
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
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>`_.
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
and tag the repository.
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.

View File

@ -1 +0,0 @@
Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.

View File

@ -1 +0,0 @@
Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.

View File

@ -1,2 +0,0 @@
Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.

View File

@ -1,5 +0,0 @@
When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
Previously, the last frame of the traceback was shown, even though it was hidden.

View File

@ -1,3 +0,0 @@
Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>` as a helper for that.

View File

@ -1 +0,0 @@
Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.

View File

@ -1 +0,0 @@
Added underlying exception to cache provider path creation and write warning messages.

View File

@ -1 +0,0 @@
Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.

View File

@ -1 +0,0 @@
Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.

View File

@ -1,3 +0,0 @@
When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir`.
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.

View File

@ -1 +0,0 @@
Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.

View File

@ -1,7 +0,0 @@
The :confval:`norecursedir` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
If after updating to this version you see that your `norecursedir` setting is not being respected,
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
Most likely, your hook returns `False` for paths it does not want to ignore,
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.

View File

@ -1,3 +0,0 @@
Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
even when it was not utilized (e.g. when explicit paths were given on the command line).
Now the ``testpaths`` are only considered when they are in use.

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

@ -0,0 +1 @@
The documentation webpages now links to a canonical version to reduce outdated documentation in search engine results.

View File

@ -1 +0,0 @@
Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).

View File

@ -1 +0,0 @@
Fix writing non-encodable text to log file when using ``--debug``.

View File

@ -1,3 +0,0 @@
:func:`_pytest.logging.LogCaptureFixture.set_level` and :func:`_pytest.logging.LogCaptureFixture.at_level`
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
``logging.disable(LEVEL)``.

View File

@ -1 +0,0 @@
Improve Documentation for `caplog.set_level`.

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:
* ``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.
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
* ``deprecation``: feature deprecation.

View File

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

View File

@ -0,0 +1,49 @@
pytest-7.4.0
=======================================
The pytest team is proud to announce the 7.4.0 release!
This release contains new features, improvements, and bug fixes,
the full list of changes is available in the changelog:
https://docs.pytest.org/en/stable/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/stable/
As usual, you can upgrade from PyPI via:
pip install -U pytest
Thanks to all of the contributors to this release:
* Adam J. Stewart
* Alessio Izzo
* Alex
* Alex Lambson
* Brian Larsen
* Bruno Oliveira
* Bryan Ricker
* Chris Mahoney
* Facundo Batista
* Florian Bruhin
* Jarrett Keifer
* Kenny Y
* Miro Hrončok
* Ran Benita
* Roberto Aldera
* Ronny Pfannschmidt
* Sergey Kim
* Stefanie Molin
* Vijay Arora
* Ville Skyttä
* Zac Hatfield-Dodds
* bzoracler
* leeyueh
* nondescryptid
* theirix
Happy testing,
The pytest Development Team

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

@ -0,0 +1,20 @@
pytest-7.4.4
=======================================
pytest 7.4.4 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
* Ran Benita
* Zac Hatfield-Dodds
Happy testing,
The pytest Development Team

View File

@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 0 items
cache -- .../_pytest/cacheprovider.py:510
cache -- .../_pytest/cacheprovider.py:532
Return a cache object that can persist state between testing sessions.
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()
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
namespace of doctests.
@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
pytestconfig [session scope] -- .../_pytest/fixtures.py:1353
Session-scoped fixture that returns the session's :class:`pytest.Config`
object.
@ -196,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:498
caplog -- .../_pytest/logging.py:570
Access and control log capturing.
Captured logs are available through the following properties/methods::

View File

@ -28,6 +28,180 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 7.4.4 (2023-12-31)
=========================
Bug Fixes
---------
- `#11140 <https://github.com/pytest-dev/pytest/issues/11140>`_: Fix non-string constants at the top of file being detected as docstrings on Python>=3.8.
- `#11572 <https://github.com/pytest-dev/pytest/issues/11572>`_: Handle an edge case where :data:`sys.stderr` and :data:`sys.__stderr__` might already be closed when :ref:`faulthandler` is tearing down.
- `#11710 <https://github.com/pytest-dev/pytest/issues/11710>`_: Fixed tracebacks from collection errors not getting pruned.
- `#7966 <https://github.com/pytest-dev/pytest/issues/7966>`_: Removed unhelpful error message from assertion rewrite mechanism when exceptions are raised in ``__iter__`` methods. Now they are treated un-iterable instead.
Improved Documentation
----------------------
- `#11091 <https://github.com/pytest-dev/pytest/issues/11091>`_: Updated documentation to refer to hyphenated options: replaced ``--junitxml`` with ``--junit-xml`` and ``--collectonly`` with ``--collect-only``.
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)
=========================
Features
--------
- `#10901 <https://github.com/pytest-dev/pytest/issues/10901>`_: Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
Improvements
------------
- `#10872 <https://github.com/pytest-dev/pytest/issues/10872>`_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
- `#10907 <https://github.com/pytest-dev/pytest/issues/10907>`_: When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
Previously, the last frame of the traceback was shown, even though it was hidden.
- `#10940 <https://github.com/pytest-dev/pytest/issues/10940>`_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
Added ``TerminalReporter.wrap_write()`` as a helper for that.
- `#10991 <https://github.com/pytest-dev/pytest/issues/10991>`_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
- `#11005 <https://github.com/pytest-dev/pytest/issues/11005>`_: Added the underlying exception to the cache provider's path creation and write warning messages.
- `#11013 <https://github.com/pytest-dev/pytest/issues/11013>`_: Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
- `#11043 <https://github.com/pytest-dev/pytest/issues/11043>`_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir <rootdir>`.
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
- `#11081 <https://github.com/pytest-dev/pytest/issues/11081>`_: The :confval:`norecursedirs` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
If after updating to this version you see that your `norecursedirs` setting is not being respected,
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
Most likely, your hook returns `False` for paths it does not want to ignore,
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
- `#8711 <https://github.com/pytest-dev/pytest/issues/8711>`_: :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :func:`caplog.at_level() <pytest.LogCaptureFixture.at_level>`
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
``logging.disable(LEVEL)``.
Bug Fixes
---------
- `#10831 <https://github.com/pytest-dev/pytest/issues/10831>`_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
- `#11068 <https://github.com/pytest-dev/pytest/issues/11068>`_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
- `#11104 <https://github.com/pytest-dev/pytest/issues/11104>`_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
even when it was not utilized (e.g. when explicit paths were given on the command line).
Now the ``testpaths`` are only considered when they are in use.
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
- `#7781 <https://github.com/pytest-dev/pytest/issues/7781>`_: Fix writing non-encodable text to log file when using ``--debug``.
Improved Documentation
----------------------
- `#9146 <https://github.com/pytest-dev/pytest/issues/9146>`_: Improved documentation for :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>`.
Trivial/Internal Changes
------------------------
- `#11031 <https://github.com/pytest-dev/pytest/issues/11031>`_: Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
pytest 7.3.2 (2023-06-10)
=========================

View File

@ -273,6 +273,9 @@ html_show_sourcelink = False
# Output file base name for HTML help builder.
htmlhelp_basename = "pytestdoc"
# The base URL which points to the root of the HTML documentation. It is used
# to indicate the location of document using the canonical link relation (#12363).
html_baseurl = "https://docs.pytest.org/en/stable/"
# -- Options for LaTeX output --------------------------------------------------

View File

@ -596,7 +596,7 @@ By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading
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
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``.
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
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
-------------------------------------------------------

View File

@ -12,7 +12,7 @@ class YamlFile(pytest.File):
# We need a yaml parser, e.g. PyYAML.
import yaml
raw = yaml.safe_load(self.path.open())
raw = yaml.safe_load(self.path.open(encoding="utf-8"))
for name, spec in sorted(raw.items()):
yield YamlItem.from_parent(self, name=name, spec=spec)

View File

@ -817,7 +817,7 @@ case we just write some information out to a ``failures`` file:
# we only look at actual failing test calls, not setup/teardown
if rep.when == "call" and rep.failed:
mode = "a" if os.path.exists("failures") else "w"
with open("failures", mode) as f:
with open("failures", mode, encoding="utf-8") as f:
# let's also access a fixture for the fun of it
if "tmp_path" in item.fixturenames:
extra = " ({})".format(item.funcargs["tmp_path"])
@ -1088,4 +1088,4 @@ application with standard ``pytest`` command-line options:
.. 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

@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 7.3.2
pytest 7.4.4
.. _`simpletest`:

View File

@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the
idiomatic python constructs without boilerplate code while not losing
introspection information.
However, if you specify a message with the assertion like this:
If a message is specified with the assertion like this:
.. code-block:: python
assert a % 2 == 0, "value was odd, should be even"
then no assertion introspection takes places at all and the message
will be simply shown in the traceback.
it is printed alongside the assertion introspection in the traceback.
See :ref:`assert-details` for more information on assertion introspection.

View File

@ -176,14 +176,21 @@ with more recent files coming first.
Behavior when no tests failed in the last run
---------------------------------------------
When no tests failed in the last run, or when no cached ``lastfailed`` data was
found, ``pytest`` can be configured either to run all of the tests or no tests,
using the ``--last-failed-no-failures`` option, which takes one of the following values:
The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
Determines whether to execute tests when there are no previously (known)
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
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior)
pytest --last-failed --last-failed-no-failures none # run no tests and exit
pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
The new config.cache object
--------------------------------

View File

@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker:
class TestDirectoryInit:
def test_cwd_starts_empty(self):
assert os.listdir(os.getcwd()) == []
with open("myfile", "w") as f:
with open("myfile", "w", encoding="utf-8") as f:
f.write("hello")
def test_cwd_again_starts_empty(self):

View File

@ -478,7 +478,7 @@ integration servers, use this invocation:
.. code-block:: bash
pytest --junitxml=path
pytest --junit-xml=path
to create an XML file at ``path``.

View File

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

View File

@ -207,10 +207,10 @@ creation of a per-test temporary directory:
@pytest.fixture(autouse=True)
def initdir(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory
tmp_path.joinpath("samplefile.ini").write_text("# testdata")
tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8")
def test_method(self):
with open("samplefile.ini") as f:
with open("samplefile.ini", encoding="utf-8") as f:
s = f.read()
assert "testdata" in s

View File

@ -173,7 +173,8 @@ You can invoke ``pytest`` from Python code directly:
this acts as if you would call "pytest" from the command line.
It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead.
You can pass in options and arguments:
If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable.
You can pass in options and arguments explicitly:
.. code-block:: python

View File

@ -1,9 +1,11 @@
:orphan:
.. sidebar:: Next Open Trainings
.. sidebar:: Next Open Trainings and Events
- `pytest tips and tricks for a better testsuite <https://ep2023.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2023 <https://ep2023.europython.eu/>`_, July 18th (3h), Prague/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/Remote
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
* **June 11th to 13th 2024**, Remote
* **March 4th to 6th 2025**, Leipzig, Germany / Remote
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, June 2024 (`date poll <https://nuudel.digitalcourage.de/2tEsEpRcwMNcAXVO>`_)
Also see :doc:`previous talks and blogposts <talks>`.

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`` 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.
.. code-block:: ini

View File

@ -1,14 +1,26 @@
.. _plugin-list:
Plugin List
===========
Pytest Plugin List
==================
PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically together with a manually-maintained list in `the source
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
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
creating a PDF, because otherwise the table gets far too wide for the
page.

View File

@ -1,3 +1,5 @@
:tocdepth: 3
.. _`api-reference`:
API Reference
@ -77,11 +79,13 @@ pytest.xfail
pytest.exit
~~~~~~~~~~~
.. autofunction:: pytest.exit(reason, [returncode=False, msg=None])
.. autofunction:: pytest.exit(reason, [returncode=None, msg=None])
pytest.main
~~~~~~~~~~~
**Tutorial**: :ref:`pytest.main-usage`
.. autofunction:: pytest.main
pytest.param
@ -783,18 +787,66 @@ reporting or interaction with exceptions:
.. autofunction:: pytest_leave_pdb
Objects
-------
Collection tree objects
-----------------------
Full reference to objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`.
These are the collector and item classes (collectively called "nodes") which
make up the collection tree.
Node
~~~~
CallInfo
~~~~~~~~
.. autoclass:: pytest.CallInfo()
.. autoclass:: _pytest.nodes.Node()
:members:
Collector
~~~~~~~~~
.. autoclass:: pytest.Collector()
:members:
:show-inheritance:
Item
~~~~
.. autoclass:: pytest.Item()
:members:
:show-inheritance:
File
~~~~
.. autoclass:: pytest.File()
:members:
:show-inheritance:
FSCollector
~~~~~~~~~~~
.. autoclass:: _pytest.nodes.FSCollector()
:members:
:show-inheritance:
Session
~~~~~~~
.. autoclass:: pytest.Session()
:members:
:show-inheritance:
Package
~~~~~~~
.. autoclass:: pytest.Package()
:members:
:show-inheritance:
Module
~~~~~~
.. autoclass:: pytest.Module()
:members:
:show-inheritance:
Class
~~~~~
@ -803,13 +855,34 @@ Class
:members:
:show-inheritance:
Collector
~~~~~~~~~
Function
~~~~~~~~
.. autoclass:: pytest.Collector()
.. autoclass:: pytest.Function()
:members:
:show-inheritance:
FunctionDefinition
~~~~~~~~~~~~~~~~~~
.. autoclass:: _pytest.python.FunctionDefinition()
:members:
:show-inheritance:
Objects
-------
Objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`
or importable from ``pytest``.
CallInfo
~~~~~~~~
.. autoclass:: pytest.CallInfo()
:members:
CollectReport
~~~~~~~~~~~~~
@ -837,13 +910,6 @@ ExitCode
.. autoclass:: pytest.ExitCode
:members:
File
~~~~
.. autoclass:: pytest.File()
:members:
:show-inheritance:
FixtureDef
~~~~~~~~~~
@ -852,34 +918,6 @@ FixtureDef
:members:
:show-inheritance:
FSCollector
~~~~~~~~~~~
.. autoclass:: _pytest.nodes.FSCollector()
:members:
:show-inheritance:
Function
~~~~~~~~
.. autoclass:: pytest.Function()
:members:
:show-inheritance:
FunctionDefinition
~~~~~~~~~~~~~~~~~~
.. autoclass:: _pytest.python.FunctionDefinition()
:members:
:show-inheritance:
Item
~~~~
.. autoclass:: pytest.Item()
:members:
:show-inheritance:
MarkDecorator
~~~~~~~~~~~~~
@ -907,19 +945,6 @@ Metafunc
.. autoclass:: pytest.Metafunc()
:members:
Module
~~~~~~
.. autoclass:: pytest.Module()
:members:
:show-inheritance:
Node
~~~~
.. autoclass:: _pytest.nodes.Node()
:members:
Parser
~~~~~~
@ -941,13 +966,6 @@ PytestPluginManager
:inherited-members:
:show-inheritance:
Session
~~~~~~~
.. autoclass:: pytest.Session()
:members:
:show-inheritance:
TestReport
~~~~~~~~~~
@ -962,10 +980,10 @@ TestShortLogReport
.. autoclass:: pytest.TestShortLogReport()
: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
~~~~~
@ -1871,8 +1889,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
tests. Optional argument: glob (default: '*').
--cache-clear Remove all cache contents at start of test run
--lfnf={all,none}, --last-failed-no-failures={all,none}
Which tests to run with no previously (known)
failures
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.
--sw, --stepwise Exit on test failure and continue from last failing
test next time
--sw-skip, --stepwise-skip
@ -1923,9 +1945,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
--strict-markers Markers not registered in the `markers` section of
the configuration file raise errors
--strict (Deprecated) alias to --strict-markers
-c, --config-file FILE
-c FILE, --config-file=FILE
Load configuration from `FILE` instead of trying to
locate one of the implicit configuration files
locate one of the implicit configuration files.
--continue-on-collection-errors
Force test execution even if collection errors occur
--rootdir=ROOTDIR Define root directory for tests. Can be relative

View File

@ -31,10 +31,22 @@ class InvalidFeatureRelease(Exception):
SLUG = "pytest-dev/pytest"
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
can be released by pushing a tag `{version}` to this repository.
Once all builds pass and it has been **approved** by one or more maintainers, start the \
[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

@ -13,14 +13,26 @@ from tqdm import tqdm
FILE_HEAD = r"""
.. _plugin-list:
Plugin List
===========
Pytest Plugin List
==================
PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically together with a manually-maintained list in `the source
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
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
creating a PDF, because otherwise the table gets far too wide for the
page.

View File

@ -13,6 +13,7 @@ import struct
import sys
import tokenize
import types
from collections import defaultdict
from pathlib import Path
from pathlib import PurePath
from typing import Callable
@ -56,6 +57,10 @@ else:
astNum = ast.Num
class Sentinel:
pass
assertstate_key = StashKey["AssertionState"]()
# pytest caches rewritten pycs in pycache dirs
@ -63,6 +68,9 @@ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
PYC_EXT = ".py" + (__debug__ and "c" or "o")
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):
"""PEP302/PEP451 import hook which rewrites asserts."""
@ -596,6 +604,13 @@ def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
return ret
def _get_ast_constant_value(value: astStr) -> object:
if sys.version_info >= (3, 8):
return value.value
else:
return value.s
class AssertionRewriter(ast.NodeVisitor):
"""Assertion rewriting implementation.
@ -645,6 +660,8 @@ class AssertionRewriter(ast.NodeVisitor):
.push_format_context() and .pop_format_context() which allows
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
that change value within an assert. This happens when a variable is
reassigned with the walrus operator
@ -666,7 +683,10 @@ class AssertionRewriter(ast.NodeVisitor):
else:
self.enable_assertion_pass_hook = False
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:
"""Find all assert statements in *mod* and rewrite them."""
@ -687,11 +707,10 @@ class AssertionRewriter(ast.NodeVisitor):
expect_docstring
and isinstance(item, ast.Expr)
and isinstance(item.value, astStr)
and isinstance(_get_ast_constant_value(item.value), str)
):
if sys.version_info >= (3, 8):
doc = item.value.value
else:
doc = item.value.s
doc = _get_ast_constant_value(item.value)
assert isinstance(doc, str)
if self.is_rewrite_disabled(doc):
return
expect_docstring = False
@ -732,9 +751,17 @@ class AssertionRewriter(ast.NodeVisitor):
mod.body[pos:pos] = imports
# Collect asserts.
nodes: List[ast.AST] = [mod]
self.scope = (mod,)
nodes: List[Union[ast.AST, Sentinel]] = [mod]
while nodes:
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):
if isinstance(field, list):
new: List[ast.AST] = []
@ -1005,7 +1032,7 @@ class AssertionRewriter(ast.NodeVisitor):
]
):
pytest_temp = self.variable()
self.variables_overwrite[
self.variables_overwrite[self.scope][
v.left.target.id
] = v.left # type:ignore[assignment]
v.left.target.id = pytest_temp
@ -1048,17 +1075,20 @@ class AssertionRewriter(ast.NodeVisitor):
new_args = []
new_kwargs = []
for arg in call.args:
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
self.scope, {}
):
arg = self.variables_overwrite[self.scope][
arg.id
] # type:ignore[assignment]
res, expl = self.visit(arg)
arg_expls.append(expl)
new_args.append(res)
for keyword in call.keywords:
if (
isinstance(keyword.value, ast.Name)
and keyword.value.id in self.variables_overwrite
):
keyword.value = self.variables_overwrite[
if isinstance(
keyword.value, ast.Name
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
keyword.value = self.variables_overwrite[self.scope][
keyword.value.id
] # type:ignore[assignment]
res, expl = self.visit(keyword.value)
@ -1094,12 +1124,14 @@ class AssertionRewriter(ast.NodeVisitor):
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
self.push_format_context()
# 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:
comp.left = self.variables_overwrite[
if isinstance(
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
] # type:ignore[assignment]
if isinstance(comp.left, namedExpr):
self.variables_overwrite[
self.variables_overwrite[self.scope][
comp.left.target.id
] = comp.left # type:ignore[assignment]
left_res, left_expl = self.visit(comp.left)
@ -1119,7 +1151,7 @@ class AssertionRewriter(ast.NodeVisitor):
and next_operand.target.id == left_res.id
):
next_operand.target.id = self.variable()
self.variables_overwrite[
self.variables_overwrite[self.scope][
left_res.id
] = next_operand # type:ignore[assignment]
next_res, next_expl = self.visit(next_operand)

View File

@ -132,7 +132,7 @@ def isiterable(obj: Any) -> bool:
try:
iter(obj)
return not istext(obj)
except TypeError:
except Exception:
return False

View File

@ -505,7 +505,11 @@ def pytest_addoption(parser: Parser) -> None:
dest="last_failed_no_failures",
choices=("all", "none"),
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

@ -380,15 +380,24 @@ else:
def get_user_id() -> int | None:
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
# win32 does not have a getuid() function.
# On Emscripten, getuid() is a stub that always returns 0.
if sys.platform in ("win32", "emscripten"):
"""Return the current process's real user id or None if it could not be
determined.
: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
# getuid shouldn't fail, but cpython defines such a case.
# Let's hope for the best.
uid = os.getuid()
return uid if uid != -1 else None
else:
# On other platforms, a return value of -1 is assumed to indicate that
# the current process's real user id could not be determined.
ERROR = -1
uid = os.getuid()
return uid if uid != ERROR else None
# Perform exhaustiveness checking.

View File

@ -57,6 +57,7 @@ from _pytest.pathlib import bestrelpath
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode
from _pytest.pathlib import resolve_package_path
from _pytest.pathlib import safe_exists
from _pytest.stash import Stash
from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for
@ -137,7 +138,9 @@ def main(
) -> Union[int, ExitCode]:
"""Perform an in-process test run.
:param args: List of command line arguments.
:param args:
List of command line arguments. If `None` or not given, defaults to reading
arguments directly from the process command line (:data:`sys.argv`).
:param plugins: List of plugin objects to be auto-registered during initialization.
:returns: An exit code.
@ -442,10 +445,10 @@ class PytestPluginManager(PluginManager):
# so we avoid accessing possibly non-readable attributes
# (see issue #1073).
if not name.startswith("pytest_"):
return
return None
# Ignore names which can not be hooks.
if name == "pytest_plugins":
return
return None
opts = super().parse_hookimpl_opts(plugin, name)
if opts is not None:
@ -454,9 +457,9 @@ class PytestPluginManager(PluginManager):
method = getattr(plugin, name)
# Consider only actual functions for hooks (#3775).
if not inspect.isroutine(method):
return
return None
# 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")
)
@ -465,7 +468,7 @@ class PytestPluginManager(PluginManager):
if opts is None:
method = getattr(module_or_class, name)
if name.startswith("pytest_"):
opts = _get_legacy_hook_marks(
opts = _get_legacy_hook_marks( # type: ignore[assignment]
method,
"spec",
("firstresult", "historic"),
@ -555,12 +558,8 @@ class PytestPluginManager(PluginManager):
anchor = absolutepath(current / path)
# Ensure we do not break if what appears to be an anchor
# is in fact a very long option (#10169).
try:
anchor_exists = anchor.exists()
except OSError: # pragma: no cover
anchor_exists = False
if anchor_exists:
# is in fact a very long option (#10169, #11394).
if safe_exists(anchor):
self._try_load_conftest(anchor, importmode, rootpath)
foundanchor = True
if not foundanchor:
@ -1063,9 +1062,10 @@ class Config:
fin()
def get_terminal_writer(self) -> TerminalWriter:
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
"terminalreporter"
)
assert terminalreporter is not None
return terminalreporter._tw
def pytest_cmdline_parse(

View File

@ -16,6 +16,7 @@ from .exceptions import UsageError
from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath
from _pytest.pathlib import commonpath
from _pytest.pathlib import safe_exists
if TYPE_CHECKING:
from . import Config
@ -151,14 +152,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
return path
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
possible_paths = (
absolutepath(get_file_part_from_node_id(arg))

View File

@ -1,5 +1,6 @@
"""Discover and run doctests in modules and test files."""
import bdb
import functools
import inspect
import os
import platform
@ -536,6 +537,25 @@ class DoctestModule(Module):
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 hasattr(functools, "cached_property") and 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":
module = self.config.pluginmanager._importconftest(
self.path,

View File

@ -1,4 +1,3 @@
import io
import os
import sys
from typing import Generator
@ -10,8 +9,8 @@ from _pytest.nodes import Item
from _pytest.stash import StashKey
fault_handler_original_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:
@ -25,8 +24,15 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_configure(config: Config) -> None:
import faulthandler
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
# at teardown we want to restore the original faulthandler fileno
# 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])
@ -38,9 +44,10 @@ def pytest_unconfigure(config: Config) -> None:
if fault_handler_stderr_fd_key in config.stash:
os.close(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.
faulthandler.enable(file=get_stderr_fileno())
# Re-enable the faulthandler if it was originally enabled.
if fault_handler_original_stderr_fd_key in config.stash:
faulthandler.enable(config.stash[fault_handler_original_stderr_fd_key])
del config.stash[fault_handler_original_stderr_fd_key]
def get_stderr_fileno() -> int:
@ -51,7 +58,7 @@ def get_stderr_fileno() -> int:
if fileno == -1:
raise AttributeError()
return fileno
except (AttributeError, io.UnsupportedOperation):
except (AttributeError, ValueError):
# 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
# This is potentially dangerous, but the best we can do.

View File

@ -11,6 +11,7 @@ from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PrintHelp
from _pytest.config.argparsing import Parser
from _pytest.terminal import TerminalReporter
class HelpAction(Action):
@ -159,7 +160,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def showhelp(config: Config) -> None:
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.write(config._parser.optparser.format_help())
tw.line()

View File

@ -369,7 +369,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
__tracebackhide__ = True
def record_func(name: str, value: object) -> None:
"""No-op function in case --junitxml was not passed in the command-line."""
"""No-op function in case --junit-xml was not passed in the command-line."""
__tracebackhide__ = True
_check_record_param_type("name", name)
@ -502,6 +502,10 @@ class LogXML:
# Local hack to handle xdist report order.
workernode = getattr(report, "node", None)
reporter = self.node_reporters.pop((nodeid, workernode))
for propname, propvalue in report.user_properties:
reporter.add_property(propname, str(propvalue))
if reporter is not None:
reporter.finalize()
@ -599,9 +603,6 @@ class LogXML:
reporter = self._opentestcase(report)
reporter.write_captured_output(report)
for propname, propvalue in report.user_properties:
reporter.add_property(propname, str(propvalue))
self.finalize(report)
report_wid = getattr(report, "worker_id", None)
report_ii = getattr(report, "item_index", None)

View File

@ -660,6 +660,8 @@ class LoggingPlugin:
)
if self._log_cli_enabled():
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
# Guaranteed by `_log_cli_enabled()`.
assert terminal_reporter is not None
capture_manager = config.pluginmanager.get_plugin("capturemanager")
# if capturemanager plugin is disabled, live logging still works.
self.log_cli_handler: Union[

View File

@ -36,6 +36,7 @@ from _pytest.outcomes import exit
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import safe_exists
from _pytest.pathlib import visit
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
@ -462,6 +463,11 @@ class _bestrelpath_cache(Dict[Path, str]):
@final
class Session(nodes.FSCollector):
"""The root of the collection tree.
``Session`` collects the initial paths given as arguments to pytest.
"""
Interrupted = Interrupted
Failed = Failed
# Set on the session by runner.pytest_sessionstart.
@ -890,7 +896,7 @@ def resolve_collection_argument(
strpath = search_pypath(strpath)
fspath = invocation_path / strpath
fspath = absolutepath(fspath)
if not fspath.exists():
if not safe_exists(fspath):
msg = (
"module or package not found: {arg} (missing __init__.py?)"
if as_pypath

View File

@ -373,7 +373,9 @@ def get_unpacked_marks(
if not consider_mro:
mark_lists = [obj.__dict__.get("pytestmark", [])]
else:
mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]
mark_lists = [
x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
]
mark_list = []
for item in mark_lists:
if isinstance(item, list):

View File

@ -157,10 +157,11 @@ class NodeMeta(type):
class Node(metaclass=NodeMeta):
"""Base class for Collector and Item, the components of the test
collection tree.
r"""Base class of :class:`Collector` and :class:`Item`, the components of
the test collection tree.
Collector subclasses have children; Items are leaf nodes.
``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the
leaf nodes.
"""
# Implemented in the legacypath plugin.
@ -525,15 +526,17 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
class Collector(Node):
"""Collector instances create children through collect() and thus
iteratively build a tree."""
"""Base class of all collectors.
Collector create children through `collect()` and thus iteratively build
the collection tree.
"""
class CollectError(Exception):
"""An error during collection, contains a custom message."""
def collect(self) -> Iterable[Union["Item", "Collector"]]:
"""Return a list of children (items and collectors) for this
collection node."""
"""Collect children (items and collectors) for this collector."""
raise NotImplementedError("abstract")
# TODO: This omits the style= parameter which breaks Liskov Substitution.
@ -564,7 +567,7 @@ class Collector(Node):
ntraceback = traceback.cut(path=self.path)
if ntraceback == traceback:
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
return excinfo.traceback.filter(excinfo)
return ntraceback.filter(excinfo)
return excinfo.traceback
@ -577,6 +580,8 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[
class FSCollector(Collector):
"""Base class for filesystem collectors."""
def __init__(
self,
fspath: Optional[LEGACY_PATH] = None,
@ -660,7 +665,7 @@ class File(FSCollector):
class Item(Node):
"""A basic test invocation item.
"""Base class of all test invocation items.
Note that for a single function there might be multiple test invocation items.
"""

View File

@ -123,7 +123,7 @@ def exit(
only because `msg` is deprecated.
:param returncode:
Return code to be used when exiting pytest.
Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.

View File

@ -523,6 +523,8 @@ def import_path(
if mode is ImportMode.importlib:
module_name = module_name_from_path(path, root)
with contextlib.suppress(KeyError):
return sys.modules[module_name]
for meta_importer in sys.meta_path:
spec = meta_importer.find_spec(module_name, [str(path.parent)])
@ -621,6 +623,11 @@ def module_name_from_path(path: Path, root: Path) -> str:
# Use the parts for the relative path to the root path.
path_parts = relative_path.parts
# Module name for packages do not contain the __init__ file, unless
# the `__init__.py` file is at the root.
if len(path_parts) >= 2 and path_parts[-1] == "__init__":
path_parts = path_parts[:-1]
return ".".join(path_parts)
@ -633,6 +640,9 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
otherwise "src.tests.test_foo" is not importable by ``__import__``.
"""
module_parts = module_name.split(".")
child_module: Union[ModuleType, None] = None
module: Union[ModuleType, None] = None
child_name: str = ""
while module_name:
if module_name not in modules:
try:
@ -642,13 +652,22 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
# ourselves to fall back to creating a dummy module.
if not sys.meta_path:
raise ModuleNotFoundError
importlib.import_module(module_name)
module = importlib.import_module(module_name)
except ModuleNotFoundError:
module = ModuleType(
module_name,
doc="Empty module created by pytest's importmode=importlib.",
)
else:
module = modules[module_name]
if child_module:
# Add child attribute to the parent that can reference the child
# modules.
if not hasattr(module, child_name):
setattr(module, child_name, child_module)
modules[module_name] = module
# Keep track of the child module while moving up the tree.
child_module, child_name = module, module_name.rpartition(".")[-1]
module_parts.pop(-1)
module_name = ".".join(module_parts)
@ -773,3 +792,13 @@ def copytree(source: Path, target: Path) -> None:
shutil.copyfile(x, newx)
elif x.is_dir():
newx.mkdir(exist_ok=True)
def safe_exists(p: Path) -> bool:
"""Like Path.exists(), but account for input arguments that might be too long (#11394)."""
try:
return p.exists()
except (ValueError, OSError):
# ValueError: stat: path too long for Windows
# OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
return False

View File

@ -752,7 +752,7 @@ class Pytester:
def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
"""Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True)
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined]
self._request.addfinalizer(reprec.finish_recording)
return reprec
@ -1074,7 +1074,7 @@ class Pytester:
return self.inline_run(*values)
def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
"""Run ``pytest.main(['--collectonly'])`` in-process.
"""Run ``pytest.main(['--collect-only'])`` in-process.
Runs the :py:func:`pytest.main` function to run all of pytest inside
the test process itself like :py:meth:`inline_run`, but returns a

View File

@ -522,7 +522,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
class Module(nodes.File, PyCollector):
"""Collector for test classes and functions."""
"""Collector for test classes and functions in a Python module."""
def _getobj(self):
return self._importtestmodule()
@ -659,6 +659,9 @@ class Module(nodes.File, PyCollector):
class Package(Module):
"""Collector for files and directories in a Python packages -- directories
with an `__init__.py` file."""
def __init__(
self,
fspath: Optional[LEGACY_PATH],
@ -788,7 +791,7 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[o
class Class(PyCollector):
"""Collector for test methods."""
"""Collector for test methods (and nested classes) in a Python class."""
@classmethod
def from_parent(cls, parent, *, name, obj=None, **kw):
@ -1149,7 +1152,7 @@ class CallSpec2:
arg2scope = self._arg2scope.copy()
for arg, val in zip(argnames, valset):
if arg in params or arg in funcargs:
raise ValueError(f"duplicate {arg!r}")
raise ValueError(f"duplicate parametrization of {arg!r}")
valtype_for_arg = valtypes[arg]
if valtype_for_arg == "params":
params[arg] = val
@ -1240,8 +1243,9 @@ class Metafunc:
during the collection phase. If you need to setup expensive resources
see about setting indirect to do it rather than at test setup time.
Can be called multiple times, in which case each call parametrizes all
previous parametrizations, e.g.
Can be called multiple times per test function (but only on different
argument names), in which case each call parametrizes all previous
parametrizations, e.g.
::
@ -1673,7 +1677,7 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
class Function(PyobjMixin, nodes.Item):
"""An Item responsible for setting up and executing a Python test function.
"""Item responsible for setting up and executing a Python test function.
:param name:
The full function name, including any decorations like those
@ -1830,10 +1834,8 @@ class Function(PyobjMixin, nodes.Item):
class FunctionDefinition(Function):
"""
This class is a step gap solution until we evolve to have actual function definition nodes
and manage to get rid of ``metafunc``.
"""
"""This class is a stop gap solution until we evolve to have actual function
definition nodes and manage to get rid of ``metafunc``."""
def runtest(self) -> None:
raise RuntimeError("function definitions are not supposed to be run as tests")

View File

@ -266,19 +266,20 @@ class ApproxMapping(ApproxBase):
approx_side_as_map.items(), other_side.values()
):
if approx_value != other_value:
max_abs_diff = max(
max_abs_diff, abs(approx_value.expected - other_value)
)
if approx_value.expected == 0.0:
max_rel_diff = math.inf
else:
max_rel_diff = max(
max_rel_diff,
abs(
(approx_value.expected - other_value)
/ approx_value.expected
),
if approx_value.expected is not None and other_value is not None:
max_abs_diff = max(
max_abs_diff, abs(approx_value.expected - other_value)
)
if approx_value.expected == 0.0:
max_rel_diff = math.inf
else:
max_rel_diff = max(
max_rel_diff,
abs(
(approx_value.expected - other_value)
/ approx_value.expected
),
)
different_ids.append(approx_key)
message_data = [

View File

@ -868,6 +868,9 @@ class TestLocalPath(CommonFSTests):
py_path.strpath, str_path
)
@pytest.mark.xfail(
reason="#11603", raises=(error.EEXIST, error.ENOENT), strict=False
)
def test_make_numbered_dir_multiprocess_safe(self, tmpdir):
# https://github.com/pytest-dev/py/issues/30
with multiprocessing.Pool() as pool:

View File

@ -1317,3 +1317,38 @@ def test_function_return_non_none_warning(pytester: Pytester) -> None:
)
res = pytester.runpytest()
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None:
"""
Regression test for #10811: previously import_path with ImportMode.importlib would
not return a module if already in sys.modules, resulting in modules being imported
multiple times, which causes problems with modules that have import side effects.
"""
# Uses the exact reproducer form #10811, given it is very minimal
# and illustrates the problem well.
pytester.makepyfile(
**{
"pmxbot/commands.py": "from . import logging",
"pmxbot/logging.py": "",
"tests/__init__.py": "",
"tests/test_commands.py": """
import importlib
from pmxbot import logging
class TestCommands:
def test_boo(self):
assert importlib.import_module('pmxbot.logging') is logging
""",
}
)
pytester.makeini(
"""
[pytest]
addopts=
--doctest-modules
--import-mode importlib
"""
)
result = pytester.runpytest_subprocess()
result.stdout.fnmatch_lines("*1 passed*")

View File

@ -21,6 +21,15 @@ if sys.gettrace():
sys.settrace(orig_trace)
@pytest.fixture(autouse=True)
def set_column_width(monkeypatch: pytest.MonkeyPatch) -> None:
"""
Force terminal width to 80: some tests check the formatting of --help, which is sensible
to terminal width.
"""
monkeypatch.setenv("COLUMNS", "80")
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_collection_modifyitems(items):
"""Prefer faster tests.

View File

@ -1,5 +1,7 @@
# mypy: disable-error-code="attr-defined"
# mypy: disallow-untyped-defs
import logging
from typing import Iterator
import pytest
from _pytest.logging import caplog_records_key
@ -9,8 +11,8 @@ logger = logging.getLogger(__name__)
sublogger = logging.getLogger(__name__ + ".baz")
@pytest.fixture
def cleanup_disabled_logging():
@pytest.fixture(autouse=True)
def cleanup_disabled_logging() -> Iterator[None]:
"""Simple fixture that ensures that a test doesn't disable logging.
This is necessary because ``logging.disable()`` is global, so a test disabling logging
@ -27,7 +29,7 @@ def test_fixture_help(pytester: Pytester) -> None:
result.stdout.fnmatch_lines(["*caplog*"])
def test_change_level(caplog):
def test_change_level(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.debug("handler DEBUG level")
logger.info("handler INFO level")
@ -42,7 +44,7 @@ def test_change_level(caplog):
assert "CRITICAL" in caplog.text
def test_change_level_logging_disabled(caplog, cleanup_disabled_logging):
def test_change_level_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
logging.disable(logging.CRITICAL)
assert logging.root.manager.disable == logging.CRITICAL
caplog.set_level(logging.WARNING)
@ -85,9 +87,7 @@ def test_change_level_undo(pytester: Pytester) -> None:
result.stdout.no_fnmatch_line("*log from test2*")
def test_change_disabled_level_undo(
pytester: Pytester, cleanup_disabled_logging
) -> None:
def test_change_disabled_level_undo(pytester: Pytester) -> None:
"""Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
Tests the logging output themselves (affected by disabled logging level).
@ -144,7 +144,7 @@ def test_change_level_undos_handler_level(pytester: Pytester) -> None:
result.assert_outcomes(passed=3)
def test_with_statement(caplog):
def test_with_statement(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.INFO):
logger.debug("handler DEBUG level")
logger.info("handler INFO level")
@ -159,7 +159,7 @@ def test_with_statement(caplog):
assert "CRITICAL" in caplog.text
def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
def test_with_statement_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
logging.disable(logging.CRITICAL)
assert logging.root.manager.disable == logging.CRITICAL
with caplog.at_level(logging.WARNING):
@ -198,8 +198,8 @@ def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
],
)
def test_force_enable_logging_level_string(
caplog, cleanup_disabled_logging, level_str, expected_disable_level
):
caplog: pytest.LogCaptureFixture, level_str: str, expected_disable_level: int
) -> None:
"""Test _force_enable_logging using a level string.
``expected_disable_level`` is one level below ``level_str`` because the disabled log level
@ -218,7 +218,7 @@ def test_force_enable_logging_level_string(
assert test_logger.manager.disable == expected_disable_level
def test_log_access(caplog):
def test_log_access(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.info("boo %s", "arg")
assert caplog.records[0].levelname == "INFO"
@ -226,7 +226,7 @@ def test_log_access(caplog):
assert "boo arg" in caplog.text
def test_messages(caplog):
def test_messages(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.info("boo %s", "arg")
logger.info("bar %s\nbaz %s", "arg1", "arg2")
@ -247,14 +247,14 @@ def test_messages(caplog):
assert "Exception" not in caplog.messages[-1]
def test_record_tuples(caplog):
def test_record_tuples(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.info("boo %s", "arg")
assert caplog.record_tuples == [(__name__, logging.INFO, "boo arg")]
def test_unicode(caplog):
def test_unicode(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.info("")
assert caplog.records[0].levelname == "INFO"
@ -262,7 +262,7 @@ def test_unicode(caplog):
assert "" in caplog.text
def test_clear(caplog):
def test_clear(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.info("")
assert len(caplog.records)
@ -273,7 +273,9 @@ def test_clear(caplog):
@pytest.fixture
def logging_during_setup_and_teardown(caplog):
def logging_during_setup_and_teardown(
caplog: pytest.LogCaptureFixture,
) -> Iterator[None]:
caplog.set_level("INFO")
logger.info("a_setup_log")
yield
@ -281,7 +283,9 @@ def logging_during_setup_and_teardown(caplog):
assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"]
def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardown):
def test_caplog_captures_for_all_stages(
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
) -> None:
assert not caplog.records
assert not caplog.get_records("call")
logger.info("a_call_log")
@ -290,25 +294,31 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
# This reaches into private API, don't use this type of thing in real tests!
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
caplog_records = caplog._item.stash[caplog_records_key]
assert set(caplog_records) == {"setup", "call"}
def test_clear_for_call_stage(caplog, logging_during_setup_and_teardown):
def test_clear_for_call_stage(
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
) -> None:
logger.info("a_call_log")
assert [x.message for x in caplog.get_records("call")] == ["a_call_log"]
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
caplog_records = caplog._item.stash[caplog_records_key]
assert set(caplog_records) == {"setup", "call"}
caplog.clear()
assert caplog.get_records("call") == []
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
caplog_records = caplog._item.stash[caplog_records_key]
assert set(caplog_records) == {"setup", "call"}
logging.info("a_call_log_after_clear")
assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"]
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
caplog_records = caplog._item.stash[caplog_records_key]
assert set(caplog_records) == {"setup", "call"}
def test_ini_controls_global_log_level(pytester: Pytester) -> None:

View File

@ -122,6 +122,23 @@ class TestApprox:
],
)
assert_approx_raises_regex(
{"a": 1.0, "b": None, "c": None},
{
"a": None,
"b": 1000.0,
"c": None,
},
[
r" comparison failed. Mismatched elements: 2 / 3:",
r" Max absolute difference: -inf",
r" Max relative difference: -inf",
r" Index \| Obtained\s+\| Expected\s+",
rf" a \| {SOME_FLOAT} \| None",
rf" b \| None\s+\| {SOME_FLOAT} ± {SOME_FLOAT}",
],
)
assert_approx_raises_regex(
[1.0, 2.0, 3.0, 4.0],
[1.0, 3.0, 3.0, 5.0],

View File

@ -1493,7 +1493,7 @@ class TestMetafuncFunctional:
pass
"""
)
result = pytester.runpytest("--collectonly")
result = pytester.runpytest("--collect-only")
result.stdout.fnmatch_lines(
[
"collected 0 items / 1 error",

View File

@ -686,6 +686,25 @@ class TestAssertionRewrite:
assert msg is not None
assert "<MY42 object> < 0" in msg
def test_assert_handling_raise_in__iter__(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""\
class A:
def __iter__(self):
raise ValueError()
def __eq__(self, o: object) -> bool:
return self is o
def __repr__(self):
return "<A object>"
assert A() == A()
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*E*assert <A object> == <A object>"])
def test_formatchar(self) -> None:
def f() -> None:
assert "%test" == "test" # type: ignore[comparison-overlap]
@ -1531,6 +1550,28 @@ class TestIssue11028:
result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
class TestIssue11239:
@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason="Only Python 3.8+")
def test_assertion_walrus_different_test_cases(self, pytester: Pytester) -> None:
"""Regression for (#11239)
Walrus operator rewriting would leak to separate test cases if they used the same variables.
"""
pytester.makepyfile(
"""
def test_1():
state = {"x": 2}.get("x")
assert state is not None
def test_2():
db = {"x": 2}
assert (state := db.get("x")) is not None
"""
)
result = pytester.runpytest()
assert result.ret == 0
@pytest.mark.skipif(
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
)
@ -2055,3 +2096,17 @@ class TestReprSizeVerbosity:
self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE * 10)
result = pytester.runpytest("-vv")
result.stdout.no_fnmatch_line("*xxx...xxx*")
class TestIssue11140:
def test_constant_not_picked_as_module_docstring(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""\
0
def test_foo():
pass
"""
)
result = pytester.runpytest()
assert result.ret == 0

View File

@ -345,6 +345,29 @@ class TestPrunetraceback:
result = pytester.runpytest(p)
result.stdout.fnmatch_lines(["*ERROR collecting*", "*header1*"])
def test_collection_error_traceback_is_clean(self, pytester: Pytester) -> None:
"""When a collection error occurs, the report traceback doesn't contain
internal pytest stack entries.
Issue #11710.
"""
pytester.makepyfile(
"""
raise Exception("LOUSY")
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(
[
"*ERROR collecting*",
"test_*.py:1: in <module>",
' raise Exception("LOUSY")',
"E Exception: LOUSY",
"*= short test summary info =*",
],
consecutive=True,
)
class TestCustomConftests:
def test_ignore_collect_path(self, pytester: Pytester) -> None:

View File

@ -482,6 +482,27 @@ class TestDoctests:
reprec = pytester.inline_run(p, "--doctest-modules")
reprec.assertoutcome(failed=1)
@pytest.mark.skipif(
sys.version_info[:2] <= (3, 7), reason="Only Python 3.7 or less"
)
def test_doctest_cached_property(self, pytester: Pytester):
p = pytester.makepyfile(
"""
import functools
class Foo:
@functools.cached_property
def foo(self):
'''
>>> assert False, "Tacos!"
'''
...
"""
)
result = pytester.runpytest(p, "--doctest-modules")
result.assert_outcomes(failed=1)
assert "Tacos!" in result.stdout.str()
def test_doctestmodule_external_and_issue116(self, pytester: Pytester):
p = pytester.mkpydir("hello")
p.joinpath("__init__.py").write_text(

View File

@ -1228,6 +1228,36 @@ def test_record_property(pytester: Pytester, run_and_parse: RunAndParse) -> None
result.stdout.fnmatch_lines(["*= 1 passed in *"])
def test_record_property_on_test_and_teardown_failure(
pytester: Pytester, run_and_parse: RunAndParse
) -> None:
pytester.makepyfile(
"""
import pytest
@pytest.fixture
def other(record_property):
record_property("bar", 1)
yield
assert 0
def test_record(record_property, other):
record_property("foo", "<1")
assert 0
"""
)
result, dom = run_and_parse()
node = dom.find_first_by_tag("testsuite")
tnodes = node.find_by_tag("testcase")
for tnode in tnodes:
psnode = tnode.find_first_by_tag("properties")
assert psnode, f"testcase didn't had expected properties:\n{tnode}"
pnodes = psnode.find_by_tag("property")
pnodes[0].assert_attr(name="bar", value="1")
pnodes[1].assert_attr(name="foo", value="<1")
result.stdout.fnmatch_lines(["*= 1 failed, 1 error *"])
def test_record_property_same_name(
pytester: Pytester, run_and_parse: RunAndParse
) -> None:

View File

@ -262,3 +262,34 @@ def test_module_full_path_without_drive(pytester: Pytester) -> None:
"* 1 passed in *",
]
)
def test_very_long_cmdline_arg(pytester: Pytester) -> None:
"""
Regression test for #11394.
Note: we could not manage to actually reproduce the error with this code, we suspect
GitHub runners are configured to support very long paths, however decided to leave
the test in place in case this ever regresses in the future.
"""
pytester.makeconftest(
"""
import pytest
def pytest_addoption(parser):
parser.addoption("--long-list", dest="long_list", action="store", default="all", help="List of things")
@pytest.fixture(scope="module")
def specified_feeds(request):
list_string = request.config.getoption("--long-list")
return list_string.split(',')
"""
)
pytester.makepyfile(
"""
def test_foo(specified_feeds):
assert len(specified_feeds) == 100_000
"""
)
result = pytester.runpytest("--long-list", ",".join(["helloworld"] * 100_000))
result.stdout.fnmatch_lines("* 1 passed *")

View File

@ -1130,6 +1130,41 @@ def test_mark_mro() -> None:
all_marks = get_unpacked_marks(C)
assert all_marks == [xfail("c").mark, xfail("a").mark, xfail("b").mark]
assert all_marks == [xfail("b").mark, xfail("a").mark, xfail("c").mark]
assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark]
# @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/10447")
def test_mark_fixture_order_mro(pytester: Pytester):
"""This ensures we walk marks of the mro starting with the base classes
the action at a distance fixtures are taken as minimal example from a real project
"""
foo = pytester.makepyfile(
"""
import pytest
@pytest.fixture
def add_attr1(request):
request.instance.attr1 = object()
@pytest.fixture
def add_attr2(request):
request.instance.attr2 = request.instance.attr1
@pytest.mark.usefixtures('add_attr1')
class Parent:
pass
@pytest.mark.usefixtures('add_attr2')
class TestThings(Parent):
def test_attrs(self):
assert self.attr1 == self.attr2
"""
)
result = pytester.runpytest(foo)
result.assert_outcomes(passed=1)

View File

@ -291,7 +291,8 @@ class TestParser:
def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None:
try:
encoding = locale.getencoding() # New in Python 3.11, ignores utf-8 mode
# New in Python 3.11, ignores utf-8 mode
encoding = locale.getencoding() # type: ignore[attr-defined]
except AttributeError:
encoding = locale.getpreferredencoding(False)
try:

View File

@ -1,3 +1,4 @@
import errno
import os.path
import pickle
import sys
@ -7,6 +8,7 @@ from textwrap import dedent
from types import ModuleType
from typing import Any
from typing import Generator
from typing import Iterator
import pytest
from _pytest.monkeypatch import MonkeyPatch
@ -17,13 +19,16 @@ from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import get_extended_length_path_str
from _pytest.pathlib import get_lock_path
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode
from _pytest.pathlib import ImportPathMismatchError
from _pytest.pathlib import insert_missing_modules
from _pytest.pathlib import maybe_delete_a_numbered_dir
from _pytest.pathlib import module_name_from_path
from _pytest.pathlib import resolve_package_path
from _pytest.pathlib import safe_exists
from _pytest.pathlib import symlink_or_skip
from _pytest.pathlib import visit
from _pytest.pytester import Pytester
from _pytest.tmpdir import TempPathFactory
@ -282,29 +287,36 @@ class TestImportPath:
import_path(tmp_path / "invalid.py", root=tmp_path)
@pytest.fixture
def simple_module(self, tmp_path: Path) -> Path:
fn = tmp_path / "_src/tests/mymod.py"
def simple_module(
self, tmp_path: Path, request: pytest.FixtureRequest
) -> Iterator[Path]:
name = f"mymod_{request.node.name}"
fn = tmp_path / f"_src/tests/{name}.py"
fn.parent.mkdir(parents=True)
fn.write_text("def foo(x): return 40 + x", encoding="utf-8")
return fn
module_name = module_name_from_path(fn, root=tmp_path)
yield fn
sys.modules.pop(module_name, None)
def test_importmode_importlib(self, simple_module: Path, tmp_path: Path) -> None:
def test_importmode_importlib(
self, simple_module: Path, tmp_path: Path, request: pytest.FixtureRequest
) -> None:
"""`importlib` mode does not change sys.path."""
module = import_path(simple_module, mode="importlib", root=tmp_path)
assert module.foo(2) == 42 # type: ignore[attr-defined]
assert str(simple_module.parent) not in sys.path
assert module.__name__ in sys.modules
assert module.__name__ == "_src.tests.mymod"
assert module.__name__ == f"_src.tests.mymod_{request.node.name}"
assert "_src" in sys.modules
assert "_src.tests" in sys.modules
def test_importmode_twice_is_different_module(
def test_remembers_previous_imports(
self, simple_module: Path, tmp_path: Path
) -> None:
"""`importlib` mode always returns a new module."""
"""`importlib` mode called remembers previous module (#10341, #10811)."""
module1 = import_path(simple_module, mode="importlib", root=tmp_path)
module2 = import_path(simple_module, mode="importlib", root=tmp_path)
assert module1 is not module2
assert module1 is module2
def test_no_meta_path_found(
self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path
@ -317,6 +329,9 @@ class TestImportPath:
# mode='importlib' fails if no spec is found to load the module
import importlib.util
# Force module to be re-imported.
del sys.modules[module.__name__]
monkeypatch.setattr(
importlib.util, "spec_from_file_location", lambda *args: None
)
@ -574,6 +589,14 @@ class TestImportLibMode:
result = module_name_from_path(Path("/home/foo/test_foo.py"), Path("/bar"))
assert result == "home.foo.test_foo"
# Importing __init__.py files should return the package as module name.
result = module_name_from_path(tmp_path / "src/app/__init__.py", tmp_path)
assert result == "src.app"
# Unless __init__.py file is at the root, in which case we cannot have an empty module name.
result = module_name_from_path(tmp_path / "__init__.py", tmp_path)
assert result == "__init__"
def test_insert_missing_modules(
self, monkeypatch: MonkeyPatch, tmp_path: Path
) -> None:
@ -592,3 +615,100 @@ class TestImportLibMode:
modules = {}
insert_missing_modules(modules, "")
assert modules == {}
def test_parent_contains_child_module_attribute(
self, monkeypatch: MonkeyPatch, tmp_path: Path
):
monkeypatch.chdir(tmp_path)
# Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and
# don't end up being imported.
modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")}
insert_missing_modules(modules, "xxx.tests.foo")
assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"]
assert modules["xxx"].tests is modules["xxx.tests"]
assert modules["xxx.tests"].foo is modules["xxx.tests.foo"]
def test_importlib_package(self, monkeypatch: MonkeyPatch, tmp_path: Path):
"""
Importing a package using --importmode=importlib should not import the
package's __init__.py file more than once (#11306).
"""
monkeypatch.chdir(tmp_path)
monkeypatch.syspath_prepend(tmp_path)
package_name = "importlib_import_package"
tmp_path.joinpath(package_name).mkdir()
init = tmp_path.joinpath(f"{package_name}/__init__.py")
init.write_text(
dedent(
"""
from .singleton import Singleton
instance = Singleton()
"""
),
encoding="ascii",
)
singleton = tmp_path.joinpath(f"{package_name}/singleton.py")
singleton.write_text(
dedent(
"""
class Singleton:
INSTANCES = []
def __init__(self) -> None:
self.INSTANCES.append(self)
if len(self.INSTANCES) > 1:
raise RuntimeError("Already initialized")
"""
),
encoding="ascii",
)
mod = import_path(init, root=tmp_path, mode=ImportMode.importlib)
assert len(mod.instance.INSTANCES) == 1
def test_importlib_root_is_package(self, pytester: Pytester) -> None:
"""
Regression for importing a `__init__`.py file that is at the root
(#11417).
"""
pytester.makepyfile(__init__="")
pytester.makepyfile(
"""
def test_my_test():
assert True
"""
)
result = pytester.runpytest("--import-mode=importlib")
result.stdout.fnmatch_lines("* 1 passed *")
def test_safe_exists(tmp_path: Path) -> None:
d = tmp_path.joinpath("some_dir")
d.mkdir()
assert safe_exists(d) is True
f = tmp_path.joinpath("some_file")
f.touch()
assert safe_exists(f) is True
# Use unittest.mock() as a context manager to have a very narrow
# patch lifetime.
p = tmp_path.joinpath("some long filename" * 100)
with unittest.mock.patch.object(
Path,
"exists",
autospec=True,
side_effect=OSError(errno.ENAMETOOLONG, "name too long"),
):
assert safe_exists(p) is False
with unittest.mock.patch.object(
Path,
"exists",
autospec=True,
side_effect=ValueError("name too long"),
):
assert safe_exists(p) is False

View File

@ -242,8 +242,12 @@ class TestPytestPluginManager:
mod = types.ModuleType("temp")
mod.__dict__["pytest_plugins"] = ["pytest_p1", "pytest_p2"]
pytestpm.consider_module(mod)
assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
assert pytestpm.get_plugin("pytest_p2").__name__ == "pytest_p2"
p1 = pytestpm.get_plugin("pytest_p1")
assert p1 is not None
assert p1.__name__ == "pytest_p1"
p2 = pytestpm.get_plugin("pytest_p2")
assert p2 is not None
assert p2.__name__ == "pytest_p2"
def test_consider_module_import_module(
self, pytester: Pytester, _config_for_test: Config
@ -336,6 +340,7 @@ class TestPytestPluginManager:
len2 = len(pytestpm.get_plugins())
assert len1 == len2
plugin1 = pytestpm.get_plugin("pytest_hello")
assert plugin1 is not None
assert plugin1.__name__.endswith("pytest_hello")
plugin2 = pytestpm.get_plugin("pytest_hello")
assert plugin2 is plugin1
@ -351,6 +356,7 @@ class TestPytestPluginManager:
pluginname = "pkg.plug"
pytestpm.import_plugin(pluginname)
mod = pytestpm.get_plugin("pkg.plug")
assert mod is not None
assert mod.x == 3
def test_consider_conftest_deps(