Merge branch 'main' into warn-when-a-mark-is-applied-to-a-fixture

This commit is contained in:
Thomas Grainger 2023-06-25 16:08:11 +01:00 committed by GitHub
commit 4cd95eeabf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
174 changed files with 10616 additions and 2941 deletions

View File

@ -9,3 +9,9 @@ updates:
allow: allow:
- dependency-type: direct - dependency-type: direct
- dependency-type: indirect - dependency-type: indirect
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
time: "03:00"
open-pull-requests-limit: 10

View File

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

View File

@ -23,30 +23,34 @@ jobs:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5
- name: Download Package
uses: actions/download-artifact@v3
with:
name: Packages
path: dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.pypi_token }}
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: "3.7" python-version: "3.7"
- name: Install dependencies - name: Install tox
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install --upgrade build tox pip install --upgrade tox
- name: Build package
run: |
python -m build
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.pypi_token }}
- name: Publish GitHub release notes - name: Publish GitHub release notes
env: env:

View File

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

23
.github/workflows/stale.yml vendored Normal file
View File

@ -0,0 +1,23 @@
name: close needs-information issues
on:
schedule:
- cron: "30 1 * * *"
workflow_dispatch:
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v8
with:
debug-only: false
days-before-issue-stale: 14
days-before-issue-close: 7
only-labels: "status: needs information"
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1

View File

@ -18,6 +18,11 @@ on:
env: env:
PYTEST_ADDOPTS: "--color=yes" PYTEST_ADDOPTS: "--color=yes"
# Cancel running jobs for the same workflow and branch.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Set permissions at the job level. # Set permissions at the job level.
permissions: {} permissions: {}
@ -38,6 +43,7 @@ jobs:
"windows-py39", "windows-py39",
"windows-py310", "windows-py310",
"windows-py311", "windows-py311",
"windows-py312",
"ubuntu-py37", "ubuntu-py37",
"ubuntu-py37-pluggy", "ubuntu-py37-pluggy",
@ -46,12 +52,13 @@ jobs:
"ubuntu-py39", "ubuntu-py39",
"ubuntu-py310", "ubuntu-py310",
"ubuntu-py311", "ubuntu-py311",
"ubuntu-py312",
"ubuntu-pypy3", "ubuntu-pypy3",
"macos-py37", "macos-py37",
"macos-py38",
"macos-py39", "macos-py39",
"macos-py310", "macos-py310",
"macos-py312",
"docs", "docs",
"doctesting", "doctesting",
@ -66,7 +73,7 @@ jobs:
- name: "windows-py37-pluggy" - name: "windows-py37-pluggy"
python: "3.7" python: "3.7"
os: windows-latest os: windows-latest
tox_env: "py37-pluggymain-xdist" tox_env: "py37-pluggymain-pylib-xdist"
- name: "windows-py38" - name: "windows-py38"
python: "3.8" python: "3.8"
os: windows-latest os: windows-latest
@ -81,9 +88,13 @@ jobs:
os: windows-latest os: windows-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "windows-py311" - name: "windows-py311"
python: "3.11-dev" python: "3.11"
os: windows-latest os: windows-latest
tox_env: "py311" tox_env: "py311"
- name: "windows-py312"
python: "3.12-dev"
os: windows-latest
tox_env: "py312"
- name: "ubuntu-py37" - name: "ubuntu-py37"
python: "3.7" python: "3.7"
@ -93,7 +104,7 @@ jobs:
- name: "ubuntu-py37-pluggy" - name: "ubuntu-py37-pluggy"
python: "3.7" python: "3.7"
os: ubuntu-latest os: ubuntu-latest
tox_env: "py37-pluggymain-xdist" tox_env: "py37-pluggymain-pylib-xdist"
- name: "ubuntu-py37-freeze" - name: "ubuntu-py37-freeze"
python: "3.7" python: "3.7"
os: ubuntu-latest os: ubuntu-latest
@ -111,10 +122,15 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "ubuntu-py311" - name: "ubuntu-py311"
python: "3.11-dev" python: "3.11"
os: ubuntu-latest os: ubuntu-latest
tox_env: "py311" tox_env: "py311"
use_coverage: true use_coverage: true
- name: "ubuntu-py312"
python: "3.12-dev"
os: ubuntu-latest
tox_env: "py312"
use_coverage: true
- name: "ubuntu-pypy3" - name: "ubuntu-pypy3"
python: "pypy-3.7" python: "pypy-3.7"
os: ubuntu-latest os: ubuntu-latest
@ -124,19 +140,19 @@ jobs:
python: "3.7" python: "3.7"
os: macos-latest os: macos-latest
tox_env: "py37-xdist" tox_env: "py37-xdist"
- name: "macos-py38"
python: "3.8"
os: macos-latest
tox_env: "py38-xdist"
use_coverage: true
- name: "macos-py39" - name: "macos-py39"
python: "3.9" python: "3.9"
os: macos-latest os: macos-latest
tox_env: "py39-xdist" tox_env: "py39-xdist"
use_coverage: true
- name: "macos-py310" - name: "macos-py310"
python: "3.10" python: "3.10"
os: macos-latest os: macos-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "macos-py312"
python: "3.12-dev"
os: macos-latest
tox_env: "py312-xdist"
- name: "plugins" - name: "plugins"
python: "3.9" python: "3.9"
@ -163,6 +179,7 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
check-latest: ${{ endsWith(matrix.python, '-dev') }}
- name: Install dependencies - name: Install dependencies
run: | run: |
@ -184,7 +201,15 @@ jobs:
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: "matrix.use_coverage" if: "matrix.use_coverage"
uses: codecov/codecov-action@v3 uses: codecov/codecov-action@v3
continue-on-error: true
with: with:
fail_ci_if_error: true fail_ci_if_error: true
files: ./coverage.xml files: ./coverage.xml
verbose: true verbose: true
check-package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5

View File

@ -11,7 +11,7 @@ on:
permissions: {} permissions: {}
jobs: jobs:
createPullRequest: update-plugin-list:
if: github.repository_owner == 'pytest-dev' if: github.repository_owner == 'pytest-dev'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@ -20,12 +20,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: 3.8 python-version: 3.8
@ -38,7 +38,7 @@ jobs:
run: python scripts/update-plugin-list.py run: python scripts/update-plugin-list.py
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6 uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
with: with:
commit-message: '[automated] Update plugin list' commit-message: '[automated] Update plugin list'
author: 'pytest bot <pytestbot@users.noreply.github.com>' author: 'pytest bot <pytestbot@users.noreply.github.com>'

View File

@ -1,18 +1,16 @@
default_language_version:
python: "3.10"
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.8.0 rev: 23.3.0
hooks: hooks:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs - repo: https://github.com/asottile/blacken-docs
rev: v1.12.1 rev: 1.14.0
hooks: hooks:
- id: blacken-docs - id: blacken-docs
additional_dependencies: [black==20.8b1] additional_dependencies: [black==23.1.0]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0 rev: v4.4.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@ -23,7 +21,7 @@ repos:
exclude: _pytest/(debugging|hookspec).py exclude: _pytest/(debugging|hookspec).py
language_version: python3 language_version: python3
- repo: https://github.com/PyCQA/autoflake - repo: https://github.com/PyCQA/autoflake
rev: v1.6.1 rev: v2.1.1
hooks: hooks:
- id: autoflake - id: autoflake
name: autoflake name: autoflake
@ -31,41 +29,40 @@ repos:
language: python language: python
files: \.py$ files: \.py$
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 5.0.4 rev: 6.0.0
hooks: hooks:
- id: flake8 - id: flake8
language_version: python3 language_version: python3
additional_dependencies: additional_dependencies:
- flake8-typing-imports==1.12.0 - flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0 - flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder_python_imports - repo: https://github.com/asottile/reorder-python-imports
rev: v3.8.3 rev: v3.10.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: ['--application-directories=.:src', --py37-plus] args: ['--application-directories=.:src', --py37-plus]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v2.38.2 rev: v3.7.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.0.0 rev: v2.3.0
hooks: hooks:
- id: setup-cfg-fmt - id: setup-cfg-fmt
args: ["--max-py-version=3.10", "--include-version-classifiers"] args: ["--max-py-version=3.12", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0 rev: v1.10.0
hooks: hooks:
- id: python-use-type-annotations - id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.981 rev: v1.3.0
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/)
args: [] args: []
additional_dependencies: additional_dependencies:
- iniconfig>=1.1.0 - iniconfig>=1.1.0
- py>=1.8.2
- attrs>=19.2.0 - attrs>=19.2.0
- packaging - packaging
- tomli - tomli

View File

@ -2,9 +2,12 @@ version: 2
python: python:
install: install:
- requirements: doc/en/requirements.txt # Install pytest first, then doc/en/requirements.txt.
- method: pip # This order is important to honor any pins in doc/en/requirements.txt
path: . # when the pinned library is also a dependency of pytest.
- method: pip
path: .
- requirements: doc/en/requirements.txt
build: build:
os: ubuntu-20.04 os: ubuntu-20.04

34
AUTHORS
View File

@ -8,10 +8,14 @@ Abdeali JK
Abdelrahman Elbehery Abdelrahman Elbehery
Abhijeet Kasurde Abhijeet Kasurde
Adam Johnson Adam Johnson
Adam Stewart
Adam Uhlir Adam Uhlir
Ahn Ki-Wook Ahn Ki-Wook
Akiomi Kamakura Akiomi Kamakura
Alan Velasco Alan Velasco
Alessio Izzo
Alex Jones
Alex Lambson
Alexander Johnson Alexander Johnson
Alexander King Alexander King
Alexei Kozlenok Alexei Kozlenok
@ -43,6 +47,7 @@ Ariel Pillemer
Armin Rigo Armin Rigo
Aron Coyle Aron Coyle
Aron Curzon Aron Curzon
Ashish Kurmi
Aviral Verma Aviral Verma
Aviv Palivoda Aviv Palivoda
Babak Keyvani Babak Keyvani
@ -53,10 +58,12 @@ Benjamin Peterson
Bernard Pratz Bernard Pratz
Bob Ippolito Bob Ippolito
Brian Dorsey Brian Dorsey
Brian Larsen
Brian Maissy Brian Maissy
Brian Okken Brian Okken
Brianna Laugher Brianna Laugher
Bruno Oliveira Bruno Oliveira
Cal Jacobson
Cal Leeming Cal Leeming
Carl Friedrich Bolz Carl Friedrich Bolz
Carlos Jenkins Carlos Jenkins
@ -65,6 +72,7 @@ Charles Cloud
Charles Machalow Charles Machalow
Charnjit SiNGH (CCSJ) Charnjit SiNGH (CCSJ)
Cheuk Ting Ho Cheuk Ting Ho
Chris Mahoney
Chris Lamb Chris Lamb
Chris NeJame Chris NeJame
Chris Rose Chris Rose
@ -88,6 +96,7 @@ Daniel Grana
Daniel Hahler Daniel Hahler
Daniel Nuri Daniel Nuri
Daniel Sánchez Castelló Daniel Sánchez Castelló
Daniel Valenzuela Zenteno
Daniel Wandschneider Daniel Wandschneider
Daniele Procida Daniele Procida
Danielle Jenkins Danielle Jenkins
@ -122,13 +131,16 @@ Eric Siegerman
Erik Aronesty Erik Aronesty
Erik M. Bray Erik M. Bray
Evan Kepner Evan Kepner
Evgeny Seliverstov
Fabien Zarifian Fabien Zarifian
Fabio Zadrozny Fabio Zadrozny
Felix Hofstätter
Felix Nieuwenhuizen Felix Nieuwenhuizen
Feng Ma Feng Ma
Florian Bruhin Florian Bruhin
Florian Dahlitz Florian Dahlitz
Floris Bruynooghe Floris Bruynooghe
Gabriel Landau
Gabriel Reis Gabriel Reis
Garvit Shubham Garvit Shubham
Gene Wood Gene Wood
@ -157,6 +169,7 @@ Ionuț Turturică
Itxaso Aizpurua Itxaso Aizpurua
Iwan Briquemont Iwan Briquemont
Jaap Broekhuizen Jaap Broekhuizen
Jake VanderPlas
Jakob van Santen Jakob van Santen
Jakub Mitoraj Jakub Mitoraj
James Bourbeau James Bourbeau
@ -181,10 +194,11 @@ Joseph Hunkeler
Josh Karpel Josh Karpel
Joshua Bronson Joshua Bronson
Jurko Gospodnetić Jurko Gospodnetić
Justyna Janczyszyn
Justice Ndou Justice Ndou
Justyna Janczyszyn
Kale Kundert Kale Kundert
Kamran Ahmad Kamran Ahmad
Kenny Y
Karl O. Pinc Karl O. Pinc
Karthikeyan Singaravelan Karthikeyan Singaravelan
Katarzyna Jachim Katarzyna Jachim
@ -221,6 +235,7 @@ Marcin Bachry
Marco Gorelli Marco Gorelli
Mark Abramowitz Mark Abramowitz
Mark Dickinson Mark Dickinson
Marko Pacak
Markus Unterwaditzer Markus Unterwaditzer
Martijn Faassen Martijn Faassen
Martin Altmayer Martin Altmayer
@ -234,7 +249,6 @@ Matthias Hafner
Maxim Filipenko Maxim Filipenko
Maximilian Cosmo Sitter Maximilian Cosmo Sitter
mbyt mbyt
Mickey Pashov
Michael Aquilina Michael Aquilina
Michael Birtwell Michael Birtwell
Michael Droettboom Michael Droettboom
@ -243,6 +257,7 @@ Michael Krebs
Michael Seifert Michael Seifert
Michal Wajszczuk Michal Wajszczuk
Michał Zięba Michał Zięba
Mickey Pashov
Mihai Capotă Mihai Capotă
Mike Hoyle (hoylemd) Mike Hoyle (hoylemd)
Mike Lundy Mike Lundy
@ -257,9 +272,9 @@ Niclas Olofsson
Nicolas Delaby Nicolas Delaby
Nikolay Kondratyev Nikolay Kondratyev
Nipunn Koorapati Nipunn Koorapati
Olga Matoula
Oleg Pidsadnyi Oleg Pidsadnyi
Oleg Sushchenko Oleg Sushchenko
Olga Matoula
Oliver Bestwalter Oliver Bestwalter
Omar Kohl Omar Kohl
Omer Hadari Omer Hadari
@ -275,6 +290,7 @@ Paweł Adamczak
Pedro Algarvio Pedro Algarvio
Petter Strandmark Petter Strandmark
Philipp Loose Philipp Loose
Pierre Sassoulas
Pieter Mulder Pieter Mulder
Piotr Banaszkiewicz Piotr Banaszkiewicz
Piotr Helm Piotr Helm
@ -284,15 +300,18 @@ Prashant Sharma
Pulkit Goyal Pulkit Goyal
Punyashloka Biswal Punyashloka Biswal
Quentin Pradet Quentin Pradet
q0w
Ralf Schmitt Ralf Schmitt
Ram Rachum
Ralph Giles Ralph Giles
Ram Rachum
Ran Benita Ran Benita
Raphael Castaneda Raphael Castaneda
Raphael Pierzina Raphael Pierzina
Rafal Semik
Raquel Alegre Raquel Alegre
Ravi Chandra Ravi Chandra
Robert Holt Robert Holt
Roberto Aldera
Roberto Polli Roberto Polli
Roland Puntaier Roland Puntaier
Romain Dorgueil Romain Dorgueil
@ -309,6 +328,7 @@ Samuel Searles-Bryant
Samuele Pedroni Samuele Pedroni
Sanket Duthade Sanket Duthade
Sankt Petersbug Sankt Petersbug
Saravanan Padmanaban
Segev Finer Segev Finer
Serhii Mozghovyi Serhii Mozghovyi
Seth Junot Seth Junot
@ -322,6 +342,7 @@ Srinivas Reddy Thatiparthy
Stefan Farmbauer Stefan Farmbauer
Stefan Scherfke Stefan Scherfke
Stefan Zimmermann Stefan Zimmermann
Stefanie Molin
Stefano Taschini Stefano Taschini
Steffen Allner Steffen Allner
Stephan Obermann Stephan Obermann
@ -340,6 +361,7 @@ Thomas Grainger
Thomas Hisch Thomas Hisch
Tim Hoffmann Tim Hoffmann
Tim Strazny Tim Strazny
TJ Bruno
Tobias Diez Tobias Diez
Tom Dalton Tom Dalton
Tom Viner Tom Viner
@ -355,6 +377,7 @@ Victor Maryama
Victor Rodriguez Victor Rodriguez
Victor Uriarte Victor Uriarte
Vidar T. Fauske Vidar T. Fauske
Vijay Arora
Virgil Dupras Virgil Dupras
Vitaly Lashmanov Vitaly Lashmanov
Vivaan Verma Vivaan Verma
@ -370,7 +393,10 @@ Wouter van Ackooy
Xixi Zhao Xixi Zhao
Xuan Luong Xuan Luong
Xuecong Liao Xuecong Liao
Yannick Péroux
Yoav Caspi Yoav Caspi
Yuliang Shao
Yusuke Kadowaki
Yuval Shimon Yuval Shimon
Zac Hatfield-Dodds Zac Hatfield-Dodds
Zachary Kneupper Zachary Kneupper

View File

@ -244,6 +244,11 @@ Here is a simple overview, with pytest-specific bits:
be released in micro releases whereas features will be released in be released in micro releases whereas features will be released in
minor releases and incompatible changes in major releases. minor releases and incompatible changes in major releases.
You will need the tags to test locally, so be sure you have the tags from the main repository. If you suspect you don't, set the main repository as upstream and fetch the tags::
$ git remote add upstream https://github.com/pytest-dev/pytest
$ git fetch upstream --tags
If you need some help with Git, follow this quick start If you need some help with Git, follow this quick start
guide: https://git.wiki.kernel.org/index.php/QuickStart guide: https://git.wiki.kernel.org/index.php/QuickStart
@ -380,7 +385,7 @@ them.
Backporting bug fixes for the next patch release Backporting bug fixes for the next patch release
------------------------------------------------ ------------------------------------------------
Pytest makes feature release every few weeks or months. In between, patch releases Pytest makes a feature release every few weeks or months. In between, patch releases
are made to the previous feature release, containing bug fixes only. The bug fixes are made to the previous feature release, containing bug fixes only. The bug fixes
usually fix regressions, but may be any change that should reach users before the usually fix regressions, but may be any change that should reach users before the
next feature release. next feature release.
@ -389,7 +394,7 @@ Suppose for example that the latest release was 1.2.3, and you want to include
a bug fix in 1.2.4 (check https://github.com/pytest-dev/pytest/releases for the a bug fix in 1.2.4 (check https://github.com/pytest-dev/pytest/releases for the
actual latest release). The procedure for this is: actual latest release). The procedure for this is:
#. First, make sure the bug is fixed the ``main`` branch, with a regular pull #. First, make sure the bug is fixed in the ``main`` branch, with a regular pull
request, as described above. An exception to this is if the bug fix is not request, as described above. An exception to this is if the bug fix is not
applicable to ``main`` anymore. applicable to ``main`` anymore.

View File

@ -1 +0,0 @@
Update :class:`pytest.PytestUnhandledCoroutineWarning` to a deprecation; it will raise an error in pytest 8.

View File

@ -1 +0,0 @@
:data:`sys.stdin` now contains all expected methods of a file-like object when capture is enabled.

View File

@ -1 +0,0 @@
:class:`~pytest.PytestReturnNotNoneWarning` is now a subclass of :class:`~pytest.PytestRemovedIn8Warning`: the plan is to make returning non-``None`` from tests an error in the future.

View File

@ -1,5 +0,0 @@
``@pytest.mark.parametrize()`` (and similar functions) now accepts any ``Sequence[str]`` for the argument names,
instead of just ``list[str]`` and ``tuple[str, ...]``.
(Note that ``str``, which is itself a ``Sequence[str]``, is still treated as a
comma-delimited name list, as before).

View File

@ -1,3 +0,0 @@
Made ``_pytest.doctest.DoctestItem`` export ``pytest.DoctestItem`` for
type check and runtime purposes. Made `_pytest.doctest` use internal APIs
to avoid circular imports.

View File

@ -1 +0,0 @@
Update information on writing plugins to use ``pyproject.toml`` instead of ``setup.py``.

View File

@ -1 +0,0 @@
Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expresed instead.

View File

@ -1,4 +0,0 @@
Deprecate configuring hook specs/impls using attributes/marks.
Instead use :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec`.
For more details, see the :ref:`docs <legacy-path-hooks-deprecated>`.

View File

@ -1 +0,0 @@
A warning is now emitted if a test function returns something other than `None`. This prevents a common mistake among beginners that expect that returning a `bool` (for example `return foo(a, b) == result`) would cause a test to pass or fail, instead of using `assert`.

View File

@ -0,0 +1,22 @@
**PytestRemovedIn8Warning deprecation warnings are now errors by default.**
Following our plan to remove deprecated features with as little disruption as
possible, all warnings of type ``PytestRemovedIn8Warning`` now generate errors
instead of warning messages by default.
**The affected features will be effectively removed in pytest 8.1**, so please consult the
:ref:`deprecations` section in the docs for directions on how to update existing code.
In the pytest ``8.0.X`` series, it is possible to change the errors back into warnings as a
stopgap measure by adding this to your ``pytest.ini`` file:
.. code-block:: ini
[pytest]
filterwarnings =
ignore::pytest.PytestRemovedIn8Warning
But this will stop working when pytest ``8.1`` is released.
**If you have concerns** about the removal of a specific feature, please add a
comment to :issue:`7363`.

View File

@ -1,2 +0,0 @@
Introduce multiline display for warning matching via :py:func:`pytest.warns` and
enhance match comparison for :py:func:`_pytest._code.ExceptionInfo.match` as returned by :py:func:`pytest.raises`.

View File

@ -1,2 +0,0 @@
Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing
error. We now raise immediately with a more helpful message.

View File

@ -0,0 +1,5 @@
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
(unless :confval:`python_files` was changed to allow `__init__.py` file).
To collect the entire package, specify just the directory: `pytest pkg`.

View File

@ -1 +0,0 @@
Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``.

View File

@ -1 +0,0 @@
The documentation is now built using Sphinx 5.x (up from 3.x previously).

View File

@ -1 +0,0 @@
Update documentation on how :func:`pytest.warns` affects :class:`DeprecationWarning`.

View File

@ -1,3 +0,0 @@
On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
:mod:`tomli`` is no longer a dependency on Python 3.11.

View File

@ -1 +0,0 @@
Display assertion message without escaped newline characters with ``-vv``.

View File

@ -1 +0,0 @@
Improved error message that is shown when no collector is found for a given file.

View File

@ -1 +0,0 @@
Some coloring has been added to the short test summary.

View File

@ -1 +0,0 @@
Ensure ``caplog.get_records(when)`` returns current/correct data after invoking ``caplog.clear()``.

View File

@ -1 +0,0 @@
Normalize the help description of all command-line options.

View File

@ -1,10 +0,0 @@
The functionality for running tests written for ``nose`` has been officially deprecated.
This includes:
* Plain ``setup`` and ``teardown`` functions and methods: this might catch users by surprise, as ``setup()`` and ``teardown()`` are not pytest idioms, but part of the ``nose`` support.
* Setup/teardown using the `@with_setup <with-setup-nose>`_ decorator.
For more details, consult the :ref:`deprecation docs <nose-deprecation>`.
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup

View File

@ -1 +0,0 @@
Added shell-style wildcard support to ``testpaths``.

View File

@ -1 +0,0 @@
Made ``_pytest.compat`` re-export ``importlib_metadata`` in the eyes of type checkers.

View File

@ -1 +0,0 @@
Fix default encoding warning (``EncodingWarning``) in ``cacheprovider``

View File

@ -1 +0,0 @@
Display full crash messages in ``short test summary info``, when runng in a CI environment.

View File

@ -1,4 +0,0 @@
Improve the error message when we attempt to access a fixture that has been
torn down.
Add an additional sentence to the docstring explaining when it's not a good
idea to call getfixturevalue.

View File

@ -1 +0,0 @@
Added support for hidden configuration file by allowing ``.pytest.ini`` as an alternative to ``pytest.ini``.

View File

@ -6,6 +6,13 @@ Release announcements
:maxdepth: 2 :maxdepth: 2
release-7.4.0
release-7.3.2
release-7.3.1
release-7.3.0
release-7.2.2
release-7.2.1
release-7.2.0
release-7.1.3 release-7.1.3
release-7.1.2 release-7.1.2
release-7.1.1 release-7.1.1

View File

@ -0,0 +1,93 @@
pytest-7.2.0
=======================================
The pytest team is proud to announce the 7.2.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:
* Aaron Berdy
* Adam Turner
* Albert Villanova del Moral
* Alice Purcell
* Anthony Sottile
* Anton Yakutovich
* Babak Keyvani
* Brandon Chinn
* Bruno Oliveira
* Chanvin Xiao
* Cheuk Ting Ho
* Chris Wheeler
* EmptyRabbit
* Ezio Melotti
* Florian Best
* Florian Bruhin
* Fredrik Berndtsson
* Gabriel Landau
* Gergely Kalmár
* Hugo van Kemenade
* James Gerity
* John Litborn
* Jon Parise
* Kevin C
* Kian Eliasi
* MatthewFlamm
* Miro Hrončok
* Nate Meyvis
* Neil Girdhar
* Nhieuvu1802
* Nipunn Koorapati
* Ofek Lev
* Paul Müller
* Paul Reece
* Pax
* Pete Baughman
* Peyman Salehi
* Philipp A
* Ran Benita
* Robert O'Shea
* Ronny Pfannschmidt
* Rowin
* Ruth Comer
* Samuel Colvin
* Samuel Gaist
* Sandro Tosi
* Shantanu
* Simon K
* Stephen Rosen
* Sviatoslav Sydorenko
* Tatiana Ovary
* Thierry Moisan
* Thomas Grainger
* Tim Hoffmann
* Tobias Diez
* Tony Narlock
* Vivaan Verma
* Wolfremium
* Zac Hatfield-Dodds
* Zach OBrien
* aizpurua23a
* gresm
* holesch
* itxasos23
* johnkangw
* skhomuti
* sommersoft
* wodny
* zx.qiu
Happy testing,
The pytest Development Team

View File

@ -0,0 +1,25 @@
pytest-7.2.1
=======================================
pytest 7.2.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:
* Anthony Sottile
* Bruno Oliveira
* Daniel Valenzuela
* Kadino
* Prerak Patel
* Ronny Pfannschmidt
* Santiago Castro
* s-padmanaban
Happy testing,
The pytest Development Team

View File

@ -0,0 +1,25 @@
pytest-7.2.2
=======================================
pytest 7.2.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
* Garvit Shubham
* Mahesh Vashishtha
* Ramsey
* Ronny Pfannschmidt
* Teejay
* q0w
* vin01
Happy testing,
The pytest Development Team

View File

@ -0,0 +1,130 @@
pytest-7.3.0
=======================================
The pytest team is proud to announce the 7.3.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:
* Aaron Berdy
* Adam Turner
* Albert Villanova del Moral
* Alessio Izzo
* Alex Hadley
* Alice Purcell
* Anthony Sottile
* Anton Yakutovich
* Ashish Kurmi
* Babak Keyvani
* Billy
* Brandon Chinn
* Bruno Oliveira
* Cal Jacobson
* Chanvin Xiao
* Cheuk Ting Ho
* Chris Wheeler
* Daniel Garcia Moreno
* Daniel Scheffler
* Daniel Valenzuela
* EmptyRabbit
* Ezio Melotti
* Felix Hofstätter
* Florian Best
* Florian Bruhin
* Fredrik Berndtsson
* Gabriel Landau
* Garvit Shubham
* Gergely Kalmár
* HTRafal
* Hugo van Kemenade
* Ilya Konstantinov
* Itxaso Aizpurua
* James Gerity
* Jay
* John Litborn
* Jon Parise
* Jouke Witteveen
* Kadino
* Kevin C
* Kian Eliasi
* Klaus Rettinghaus
* Kodi Arfer
* Mahesh Vashishtha
* Manuel Jacob
* Marko Pacak
* MatthewFlamm
* Miro Hrončok
* Nate Meyvis
* Neil Girdhar
* Nhieuvu1802
* Nipunn Koorapati
* Ofek Lev
* Paul Kehrer
* Paul Müller
* Paul Reece
* Pax
* Pete Baughman
* Peyman Salehi
* Philipp A
* Pierre Sassoulas
* Prerak Patel
* Ramsey
* Ran Benita
* Robert O'Shea
* Ronny Pfannschmidt
* Rowin
* Ruth Comer
* Samuel Colvin
* Samuel Gaist
* Sandro Tosi
* Santiago Castro
* Shantanu
* Simon K
* Stefanie Molin
* Stephen Rosen
* Sviatoslav Sydorenko
* Tatiana Ovary
* Teejay
* Thierry Moisan
* Thomas Grainger
* Tim Hoffmann
* Tobias Diez
* Tony Narlock
* Vivaan Verma
* Wolfremium
* Yannick PÉROUX
* Yusuke Kadowaki
* Zac Hatfield-Dodds
* Zach OBrien
* aizpurua23a
* bitzge
* bluthej
* gresm
* holesch
* itxasos23
* johnkangw
* q0w
* rdb
* s-padmanaban
* skhomuti
* sommersoft
* vin01
* wim glenn
* wodny
* zx.qiu
Happy testing,
The pytest Development Team

View File

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

View File

@ -0,0 +1,21 @@
pytest-7.3.2
=======================================
pytest 7.3.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:
* Adam J. Stewart
* Alessio Izzo
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team

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

@ -92,3 +92,5 @@ pytest version min. Python version
5.0 - 6.1 3.5+ 5.0 - 6.1 3.5+
3.3 - 4.6 2.7, 3.4+ 3.3 - 4.6 2.7, 3.4+
============== =================== ============== ===================
`Status of Python Versions <https://devguide.python.org/versions/>`__.

View File

@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
cachedir: .pytest_cache cachedir: .pytest_cache
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 0 items collected 0 items
cache -- .../_pytest/cacheprovider.py:510 cache -- .../_pytest/cacheprovider.py:528
Return a cache object that can persist state between testing sessions. Return a cache object that can persist state between testing sessions.
cache.get(key, default) cache.get(key, default)
@ -33,25 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Values can be any object handled by the json stdlib module. Values can be any object handled by the json stdlib module.
capsys -- .../_pytest/capture.py:878 capsysbinary -- .../_pytest/capture.py:1001
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsys.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``text`` objects.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
print("hello")
captured = capsys.readouterr()
assert captured.out == "hello\n"
capsysbinary -- .../_pytest/capture.py:906
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsysbinary.readouterr()`` The captured output is made available via ``capsysbinary.readouterr()``
@ -69,7 +51,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsysbinary.readouterr() captured = capsysbinary.readouterr()
assert captured.out == b"hello\n" assert captured.out == b"hello\n"
capfd -- .../_pytest/capture.py:934 capfd -- .../_pytest/capture.py:1029
Enable text capturing of writes to file descriptors ``1`` and ``2``. Enable text capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method The captured output is made available via ``capfd.readouterr()`` method
@ -87,7 +69,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capfd.readouterr() captured = capfd.readouterr()
assert captured.out == "hello\n" assert captured.out == "hello\n"
capfdbinary -- .../_pytest/capture.py:962 capfdbinary -- .../_pytest/capture.py:1057
Enable bytes capturing of writes to file descriptors ``1`` and ``2``. Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method The captured output is made available via ``capfd.readouterr()`` method
@ -105,7 +87,25 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capfdbinary.readouterr() captured = capfdbinary.readouterr()
assert captured.out == b"hello\n" assert captured.out == b"hello\n"
doctest_namespace [session scope] -- .../_pytest/doctest.py:735 capsys -- .../_pytest/capture.py:973
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsys.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``text`` objects.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
print("hello")
captured = capsys.readouterr()
assert captured.out == "hello\n"
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
Fixture that returns a :py:class:`dict` that will be injected into the Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests. namespace of doctests.
@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`. For more details: :ref:`doctest_namespace`.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1344 pytestconfig [session scope] -- .../_pytest/fixtures.py:1353
Session-scoped fixture that returns the session's :class:`pytest.Config` Session-scoped fixture that returns the session's :class:`pytest.Config`
object. object.
@ -163,7 +163,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
record_testsuite_property("ARCH", "PPC") record_testsuite_property("ARCH", "PPC")
record_testsuite_property("STORAGE_TYPE", "CEPH") record_testsuite_property("STORAGE_TYPE", "CEPH")
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped. :param name:
The property name.
:param value:
The property value. Will be converted to a string.
.. warning:: .. warning::
@ -193,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 .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:487 caplog -- .../_pytest/logging.py:570
Access and control log capturing. Access and control log capturing.
Captured logs are available through the following properties/methods:: Captured logs are available through the following properties/methods::
@ -204,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
* caplog.record_tuples -> list of (logger_name, level, message) tuples * caplog.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string * caplog.clear() -> clear captured records and formatted log output string
monkeypatch -- .../_pytest/monkeypatch.py:29 monkeypatch -- .../_pytest/monkeypatch.py:30
A convenient fixture for monkey-patching. A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or The fixture provides these methods to modify objects, dictionaries, or
@ -228,23 +231,25 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
To undo modifications done by the fixture in a contained scope, To undo modifications done by the fixture in a contained scope,
use :meth:`context() <pytest.MonkeyPatch.context>`. use :meth:`context() <pytest.MonkeyPatch.context>`.
recwarn -- .../_pytest/recwarn.py:29 recwarn -- .../_pytest/recwarn.py:30
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
See https://docs.python.org/library/how-to/capture-warnings.html for information See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
on warning categories. on warning categories.
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:184 tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:245
Return a :class:`pytest.TempPathFactory` instance for the test session. Return a :class:`pytest.TempPathFactory` instance for the test session.
tmp_path -- .../_pytest/tmpdir.py:199 tmp_path -- .../_pytest/tmpdir.py:260
Return a temporary directory path object which is unique to each test Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary function invocation, created as a sub directory of the base temporary
directory. directory.
By default, a new base temporary directory is created each test session, By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If and old bases are removed after 3 sessions, to aid in debugging.
``--basetemp`` is used then it is cleared each session. See :ref:`base This behavior can be configured with :confval:`tmp_path_retention_count` and
:confval:`tmp_path_retention_policy`.
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`. temporary directory`.
The returned object is a :class:`pathlib.Path` object. The returned object is a :class:`pathlib.Path` object.

View File

@ -28,6 +28,421 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start .. towncrier release notes start
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)
=========================
Bug Fixes
---------
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
pytest 7.3.1 (2023-04-14)
=========================
Improvements
------------
- `#10875 <https://github.com/pytest-dev/pytest/issues/10875>`_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests.
- `#10890 <https://github.com/pytest-dev/pytest/issues/10890>`_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.
Bug Fixes
---------
- `#10896 <https://github.com/pytest-dev/pytest/issues/10896>`_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option.
- `#10903 <https://github.com/pytest-dev/pytest/issues/10903>`_: Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden.
This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0.
pytest 7.3.0 (2023-04-08)
=========================
Features
--------
- `#10525 <https://github.com/pytest-dev/pytest/issues/10525>`_: Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods.
- `#10755 <https://github.com/pytest-dev/pytest/issues/10755>`_: :confval:`console_output_style` now supports ``progress-even-when-capture-no`` to force the use of the progress output even when capture is disabled. This is useful in large test suites where capture may have significant performance impact.
- `#7431 <https://github.com/pytest-dev/pytest/issues/7431>`_: ``--log-disable`` CLI option added to disable individual loggers.
- `#8141 <https://github.com/pytest-dev/pytest/issues/8141>`_: Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
Improvements
------------
- `#10226 <https://github.com/pytest-dev/pytest/issues/10226>`_: If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
- `#10658 <https://github.com/pytest-dev/pytest/issues/10658>`_: Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of
``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration
file.
- `#10710 <https://github.com/pytest-dev/pytest/issues/10710>`_: Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.
- `#10727 <https://github.com/pytest-dev/pytest/issues/10727>`_: Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.
- `#10840 <https://github.com/pytest-dev/pytest/issues/10840>`_: pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`.
- `#6267 <https://github.com/pytest-dev/pytest/issues/6267>`_: The full output of a test is no longer truncated if the truncation message would be longer than
the hidden text. The line number shown has also been fixed.
Bug Fixes
---------
- `#10743 <https://github.com/pytest-dev/pytest/issues/10743>`_: The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.
- `#10765 <https://github.com/pytest-dev/pytest/issues/10765>`_: Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions.
NOTE: This change was reverted in version 7.3.1.
Improved Documentation
----------------------
- `#10782 <https://github.com/pytest-dev/pytest/issues/10782>`_: Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.
Trivial/Internal Changes
------------------------
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer directly depends on the `attrs <https://www.attrs.org/en/stable/>`__ package. While
we at pytest all love the package dearly and would like to thank the ``attrs`` team for many years of cooperation and support,
it makes sense for ``pytest`` to have as little external dependencies as possible, as this helps downstream projects.
With that in mind, we have replaced the pytest's limited internal usage to use the standard library's ``dataclasses`` instead.
Nice diffs for ``attrs`` classes are still supported though.
pytest 7.2.2 (2023-03-03)
=========================
Bug Fixes
---------
- `#10533 <https://github.com/pytest-dev/pytest/issues/10533>`_: Fixed :func:`pytest.approx` handling of dictionaries containing one or more values of `0.0`.
- `#10592 <https://github.com/pytest-dev/pytest/issues/10592>`_: Fixed crash if `--cache-show` and `--help` are passed at the same time.
- `#10597 <https://github.com/pytest-dev/pytest/issues/10597>`_: Fixed bug where a fixture method named ``teardown`` would be called as part of ``nose`` teardown stage.
- `#10626 <https://github.com/pytest-dev/pytest/issues/10626>`_: Fixed crash if ``--fixtures`` and ``--help`` are passed at the same time.
- `#10660 <https://github.com/pytest-dev/pytest/issues/10660>`_: Fixed :py:func:`pytest.raises` to return a 'ContextManager' so that type-checkers could narrow
:code:`pytest.raises(...) if ... else nullcontext()` down to 'ContextManager' rather than 'object'.
Improved Documentation
----------------------
- `#10690 <https://github.com/pytest-dev/pytest/issues/10690>`_: Added `CI` and `BUILD_NUMBER` environment variables to the documentation.
- `#10721 <https://github.com/pytest-dev/pytest/issues/10721>`_: Fixed entry-points declaration in the documentation example using Hatch.
- `#10753 <https://github.com/pytest-dev/pytest/issues/10753>`_: Changed wording of the module level skip to be very explicit
about not collecting tests and not executing the rest of the module.
pytest 7.2.1 (2023-01-13)
=========================
Bug Fixes
---------
- `#10452 <https://github.com/pytest-dev/pytest/issues/10452>`_: Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12.
- `#10457 <https://github.com/pytest-dev/pytest/issues/10457>`_: If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location.
- `#10506 <https://github.com/pytest-dev/pytest/issues/10506>`_: Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows.
- `#10607 <https://github.com/pytest-dev/pytest/issues/10607>`_: Fix a race condition when creating junitxml reports, which could occur when multiple instances of pytest execute in parallel.
- `#10641 <https://github.com/pytest-dev/pytest/issues/10641>`_: Fix a race condition when creating or updating the stepwise plugin's cache, which could occur when multiple xdist worker nodes try to simultaneously update the stepwise plugin's cache.
pytest 7.2.0 (2022-10-23)
=========================
Deprecations
------------
- `#10012 <https://github.com/pytest-dev/pytest/issues/10012>`_: Update :class:`pytest.PytestUnhandledCoroutineWarning` to a deprecation; it will raise an error in pytest 8.
- `#10396 <https://github.com/pytest-dev/pytest/issues/10396>`_: pytest no longer depends on the ``py`` library. ``pytest`` provides a vendored copy of ``py.error`` and ``py.path`` modules but will use the ``py`` library if it is installed. If you need other ``py.*`` modules, continue to install the deprecated ``py`` library separately, otherwise it can usually be removed as a dependency.
- `#4562 <https://github.com/pytest-dev/pytest/issues/4562>`_: Deprecate configuring hook specs/impls using attributes/marks.
Instead use :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec`.
For more details, see the :ref:`docs <legacy-path-hooks-deprecated>`.
- `#9886 <https://github.com/pytest-dev/pytest/issues/9886>`_: The functionality for running tests written for ``nose`` has been officially deprecated.
This includes:
* Plain ``setup`` and ``teardown`` functions and methods: this might catch users by surprise, as ``setup()`` and ``teardown()`` are not pytest idioms, but part of the ``nose`` support.
* Setup/teardown using the `@with_setup <with-setup-nose>`_ decorator.
For more details, consult the :ref:`deprecation docs <nose-deprecation>`.
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
- `#7337 <https://github.com/pytest-dev/pytest/issues/7337>`_: A deprecation warning is now emitted if a test function returns something other than `None`. This prevents a common mistake among beginners that expect that returning a `bool` (for example `return foo(a, b) == result`) would cause a test to pass or fail, instead of using `assert`. The plan is to make returning non-`None` from tests an error in the future.
Features
--------
- `#9897 <https://github.com/pytest-dev/pytest/issues/9897>`_: Added shell-style wildcard support to ``testpaths``.
Improvements
------------
- `#10218 <https://github.com/pytest-dev/pytest/issues/10218>`_: ``@pytest.mark.parametrize()`` (and similar functions) now accepts any ``Sequence[str]`` for the argument names,
instead of just ``list[str]`` and ``tuple[str, ...]``.
(Note that ``str``, which is itself a ``Sequence[str]``, is still treated as a
comma-delimited name list, as before).
- `#10381 <https://github.com/pytest-dev/pytest/issues/10381>`_: The ``--no-showlocals`` flag has been added. This can be passed directly to tests to override ``--showlocals`` declared through ``addopts``.
- `#3426 <https://github.com/pytest-dev/pytest/issues/3426>`_: Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expressed instead.
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
enhance match comparison for :py:func:`_pytest._code.ExceptionInfo.match` as returned by :py:func:`pytest.raises`.
- `#8646 <https://github.com/pytest-dev/pytest/issues/8646>`_: Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing
error. We now raise immediately with a more helpful message.
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
:mod:`tomli` is no longer a dependency on Python 3.11.
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
- `#9823 <https://github.com/pytest-dev/pytest/issues/9823>`_: Improved error message that is shown when no collector is found for a given file.
- `#9873 <https://github.com/pytest-dev/pytest/issues/9873>`_: Some coloring has been added to the short test summary.
- `#9883 <https://github.com/pytest-dev/pytest/issues/9883>`_: Normalize the help description of all command-line options.
- `#9920 <https://github.com/pytest-dev/pytest/issues/9920>`_: Display full crash messages in ``short test summary info``, when running in a CI environment.
- `#9987 <https://github.com/pytest-dev/pytest/issues/9987>`_: Added support for hidden configuration file by allowing ``.pytest.ini`` as an alternative to ``pytest.ini``.
Bug Fixes
---------
- `#10150 <https://github.com/pytest-dev/pytest/issues/10150>`_: :data:`sys.stdin` now contains all expected methods of a file-like object when capture is enabled.
- `#10382 <https://github.com/pytest-dev/pytest/issues/10382>`_: Do not break into pdb when ``raise unittest.SkipTest()`` appears top-level in a file.
- `#7792 <https://github.com/pytest-dev/pytest/issues/7792>`_: Marks are now inherited according to the full MRO in test classes. Previously, if a test class inherited from two or more classes, only marks from the first super-class would apply.
When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse.
When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`pytest.Node.iter_markers` instead.
- `#9159 <https://github.com/pytest-dev/pytest/issues/9159>`_: Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``.
- `#9877 <https://github.com/pytest-dev/pytest/issues/9877>`_: Ensure ``caplog.get_records(when)`` returns current/correct data after invoking ``caplog.clear()``.
Improved Documentation
----------------------
- `#10344 <https://github.com/pytest-dev/pytest/issues/10344>`_: Update information on writing plugins to use ``pyproject.toml`` instead of ``setup.py``.
- `#9248 <https://github.com/pytest-dev/pytest/issues/9248>`_: The documentation is now built using Sphinx 5.x (up from 3.x previously).
- `#9291 <https://github.com/pytest-dev/pytest/issues/9291>`_: Update documentation on how :func:`pytest.warns` affects :class:`DeprecationWarning`.
Trivial/Internal Changes
------------------------
- `#10313 <https://github.com/pytest-dev/pytest/issues/10313>`_: Made ``_pytest.doctest.DoctestItem`` export ``pytest.DoctestItem`` for
type check and runtime purposes. Made `_pytest.doctest` use internal APIs
to avoid circular imports.
- `#9906 <https://github.com/pytest-dev/pytest/issues/9906>`_: Made ``_pytest.compat`` re-export ``importlib_metadata`` in the eyes of type checkers.
- `#9910 <https://github.com/pytest-dev/pytest/issues/9910>`_: Fix default encoding warning (``EncodingWarning``) in ``cacheprovider``
- `#9984 <https://github.com/pytest-dev/pytest/issues/9984>`_: Improve the error message when we attempt to access a fixture that has been
torn down.
Add an additional sentence to the docstring explaining when it's not a good
idea to call ``getfixturevalue``.
pytest 7.1.3 (2022-08-31) pytest 7.1.3 (2022-08-31)
========================= =========================
@ -268,7 +683,7 @@ Breaking Changes
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`. - `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation. Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted. Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`. Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead. Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
@ -3768,7 +4183,7 @@ Removals
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code. See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check. - :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
Use ``Node.get_closest_marker(name)`` as a replacement. Use ``Node.get_closest_marker(name)`` as a replacement.

View File

@ -341,7 +341,7 @@ epub_copyright = "2013, holger krekel et alii"
# The scheme of the identifier. Typical schemes are ISBN or URL. # The scheme of the identifier. Typical schemes are ISBN or URL.
# epub_scheme = '' # epub_scheme = ''
# The unique identifier of the text. This can be a ISBN number # The unique identifier of the text. This can be an ISBN number
# or the project homepage. # or the project homepage.
# epub_identifier = '' # epub_identifier = ''

View File

@ -486,12 +486,26 @@ The ``yield_fixture`` function/decorator
It has been so for a very long time, so can be search/replaced safely. It has been so for a very long time, so can be search/replaced safely.
Removed Features Removed Features and Breaking Changes
---------------- -------------------------------------
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
an appropriate period of deprecation has passed. an appropriate period of deprecation has passed.
Some breaking changes which could not be deprecated are also listed.
Collecting ``__init__.py`` files no longer collects package
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 8.0
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
(unless :confval:`python_files` was changed to allow `__init__.py` file).
To collect the entire package, specify just the directory: `pytest pkg`.
The ``pytest.collect`` module The ``pytest.collect`` module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -1071,7 +1085,7 @@ that are then turned into proper test methods. Example:
.. code-block:: python .. code-block:: python
def check(x, y): def check(x, y):
assert x ** x == y assert x**x == y
def test_squared(): def test_squared():
@ -1086,7 +1100,7 @@ This form of test function doesn't support fixtures properly, and users should s
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)]) @pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
def test_squared(x, y): def test_squared(x, y):
assert x ** x == y assert x**x == y
.. _internal classes accessed through node deprecated: .. _internal classes accessed through node deprecated:

View File

@ -17,7 +17,7 @@ def b(a, order):
@pytest.fixture @pytest.fixture
def c(a, b, order): def c(b, order):
order.append("c") order.append("c")

View File

@ -246,9 +246,9 @@ You can ask which markers exist for your test suite - the list includes our just
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead.
For an example on how to add and work with markers from a plugin, see For an example on how to add and work with markers from a plugin, see
@ -438,9 +438,9 @@ The ``--markers`` option always gives you a list of available markers:
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures @pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. @pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. @pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead.
.. _`passing callables to custom markers`: .. _`passing callables to custom markers`:
@ -611,7 +611,7 @@ then you will see two tests skipped and two executed tests as expected:
test_plat.py s.s. [100%] test_plat.py s.s. [100%]
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIPPED [2] conftest.py:12: cannot run on platform linux SKIPPED [2] conftest.py:13: cannot run on platform linux
======================= 2 passed, 2 skipped in 0.12s ======================= ======================= 2 passed, 2 skipped in 0.12s =======================
Note that if you specify a platform via the marker-command line option like this: Note that if you specify a platform via the marker-command line option like this:

View File

@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
" no further details known at this point.", " no further details known at this point.",
] ]
) )
return super().repr_failure(excinfo)
def reportinfo(self): def reportinfo(self):
return self.path, 0, f"usecase: {self.name}" return self.path, 0, f"usecase: {self.name}"

View File

@ -504,9 +504,9 @@ Running it results in some skips if we don't have all the python interpreters in
. $ pytest -rs -q multipython.py . $ pytest -rs -q multipython.py
sssssssssssssssssssssssssss [100%] sssssssssssssssssssssssssss [100%]
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIPPED [9] multipython.py:29: 'python3.5' not found SKIPPED [9] multipython.py:69: 'python3.5' not found
SKIPPED [9] multipython.py:29: 'python3.6' not found SKIPPED [9] multipython.py:69: 'python3.6' not found
SKIPPED [9] multipython.py:29: 'python3.7' not found SKIPPED [9] multipython.py:69: 'python3.7' not found
27 skipped in 0.12s 27 skipped in 0.12s
Indirect parametrization of optional implementations/imports Indirect parametrization of optional implementations/imports
@ -574,7 +574,7 @@ If you run this with reporting for skips enabled:
test_module.py .s [100%] test_module.py .s [100%]
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIPPED [1] conftest.py:12: could not import 'opt2': No module named 'opt2' SKIPPED [1] test_module.py:3: could not import 'opt2': No module named 'opt2'
======================= 1 passed, 1 skipped in 0.12s ======================= ======================= 1 passed, 1 skipped in 0.12s =======================
You'll see that we don't have an ``opt2`` module and thus the second test run You'll see that we don't have an ``opt2`` module and thus the second test run

View File

@ -148,7 +148,8 @@ The test collection would look like this:
$ pytest --collect-only $ pytest --collect-only
=========================== test session starts ============================ =========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini rootdir: /home/sweet/project
configfile: pytest.ini
collected 2 items collected 2 items
<Module check_myapp.py> <Module check_myapp.py>
@ -209,7 +210,8 @@ You can always peek at the collection tree without running tests like this:
. $ pytest --collect-only pythoncollection.py . $ pytest --collect-only pythoncollection.py
=========================== test session starts ============================ =========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini rootdir: /home/sweet/project
configfile: pytest.ini
collected 3 items collected 3 items
<Module CWD/pythoncollection.py> <Module CWD/pythoncollection.py>
@ -290,7 +292,8 @@ file will be left out:
$ pytest --collect-only $ pytest --collect-only
=========================== test session starts ============================ =========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini rootdir: /home/sweet/project
configfile: pytest.ini
collected 0 items collected 0 items
======================= no tests collected in 0.12s ======================== ======================= no tests collected in 0.12s ========================

View File

@ -144,7 +144,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E 1 E 1
E 1... E 1...
E E
E ...Full output truncated (7 lines hidden), use '-vv' to show E ...Full output truncated (6 lines hidden), use '-vv' to show
failure_demo.py:60: AssertionError failure_demo.py:60: AssertionError
_________________ TestSpecialisedExplanations.test_eq_list _________________ _________________ TestSpecialisedExplanations.test_eq_list _________________
@ -184,9 +184,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E Left contains 1 more item: E Left contains 1 more item:
E {'c': 0} E {'c': 0}
E Right contains 1 more item: E Right contains 1 more item:
E {'d': 0}... E {'d': 0}
E E Use -v to get more diff
E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:71: AssertionError failure_demo.py:71: AssertionError
_________________ TestSpecialisedExplanations.test_eq_set __________________ _________________ TestSpecialisedExplanations.test_eq_set __________________
@ -195,16 +194,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
def test_eq_set(self): def test_eq_set(self):
> assert {0, 10, 11, 12} == {0, 20, 21} > assert {0, 10, 11, 12} == {0, 20, 21}
E AssertionError: assert {0, 10, 11, 12} == {0, 20, 21} E assert {0, 10, 11, 12} == {0, 20, 21}
E Extra items in the left set: E Extra items in the left set:
E 10 E 10
E 11 E 11
E 12 E 12
E Extra items in the right set: E Extra items in the right set:
E 20 E 20
E 21... E 21
E E Use -v to get more diff
E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:74: AssertionError failure_demo.py:74: AssertionError
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________ _____________ TestSpecialisedExplanations.test_eq_longer_list ______________
@ -241,9 +239,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E which E which
E includes foo E includes foo
E ? +++ E ? +++
E and a... E and a
E E tail
E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:84: AssertionError failure_demo.py:84: AssertionError
___________ TestSpecialisedExplanations.test_not_in_text_single ____________ ___________ TestSpecialisedExplanations.test_not_in_text_single ____________
@ -307,9 +304,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E ['b'] E ['b']
E E
E Drill down into differing attribute b: E Drill down into differing attribute b:
E b: 'b' != 'c'... E b: 'b' != 'c'
E E - c
E ...Full output truncated (3 lines hidden), use '-vv' to show E + b
failure_demo.py:108: AssertionError failure_demo.py:108: AssertionError
________________ TestSpecialisedExplanations.test_eq_attrs _________________ ________________ TestSpecialisedExplanations.test_eq_attrs _________________
@ -334,9 +331,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E ['b'] E ['b']
E E
E Drill down into differing attribute b: E Drill down into differing attribute b:
E b: 'b' != 'c'... E b: 'b' != 'c'
E E - c
E ...Full output truncated (3 lines hidden), use '-vv' to show E + b
failure_demo.py:120: AssertionError failure_demo.py:120: AssertionError
______________________________ test_attribute ______________________________ ______________________________ test_attribute ______________________________
@ -673,7 +670,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list - asser... FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list - asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_long - ... FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_long - ...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dict - Asser... FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dict - Asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - Assert... FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - assert...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_longer_list FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_longer_list
FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser... FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline

View File

@ -661,8 +661,7 @@ If we run this:
test_step.py:11: AssertionError test_step.py:11: AssertionError
========================= short test summary info ========================== ========================= short test summary info ==========================
XFAIL test_step.py::TestUserHandling::test_deletion XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
reason: previous test failed (test_modification)
================== 1 failed, 2 passed, 1 xfailed in 0.12s ================== ================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================
We'll see that ``test_deletion`` was not executed because ``test_modification`` We'll see that ``test_deletion`` was not executed because ``test_modification``
@ -692,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory:
pass pass
@pytest.fixture(scope="session") @pytest.fixture(scope="package")
def db(): def db():
return DB() return DB()
@ -893,8 +892,11 @@ here is a little example implemented via a local plugin:
.. code-block:: python .. code-block:: python
# content of conftest.py # content of conftest.py
from typing import Dict
import pytest import pytest
from pytest import StashKey, CollectReport
phase_report_key = StashKey[Dict[str, CollectReport]]()
@pytest.hookimpl(tryfirst=True, hookwrapper=True) @pytest.hookimpl(tryfirst=True, hookwrapper=True)
@ -903,10 +905,9 @@ here is a little example implemented via a local plugin:
outcome = yield outcome = yield
rep = outcome.get_result() rep = outcome.get_result()
# set a report attribute for each phase of a call, which can # store test results for each phase of a call, which can
# be "setup", "call", "teardown" # be "setup", "call", "teardown"
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
setattr(item, "rep_" + rep.when, rep)
@pytest.fixture @pytest.fixture
@ -914,11 +915,11 @@ here is a little example implemented via a local plugin:
yield yield
# request.node is an "item" because we use the default # request.node is an "item" because we use the default
# "function" scope # "function" scope
if request.node.rep_setup.failed: report = request.node.stash[phase_report_key]
print("setting up a test failed!", request.node.nodeid) if report["setup"].failed:
elif request.node.rep_setup.passed: print("setting up a test failed or skipped", request.node.nodeid)
if request.node.rep_call.failed: elif ("call" not in report) or report["call"].failed:
print("executing test failed", request.node.nodeid) print("executing test failed or skipped", request.node.nodeid)
if you then have failing tests: if you then have failing tests:
@ -956,8 +957,8 @@ and run it:
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 3 items collected 3 items
test_module.py Esetting up a test failed! test_module.py::test_setup_fails test_module.py Esetting up a test failed or skipped test_module.py::test_setup_fails
Fexecuting test failed test_module.py::test_call_fails Fexecuting test failed or skipped test_module.py::test_call_fails
F F
================================== ERRORS ================================== ================================== ERRORS ==================================

View File

@ -22,10 +22,11 @@ The first few lines should look like this:
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[metadata] [project]
name = "PACKAGENAME" name = "PACKAGENAME"
version = "PACKAGEVERSION"
where ``PACKAGENAME`` is the name of your package. where ``PACKAGENAME`` and ``PACKAGEVERSION`` are the name and version of your package respectively.
You can then install your package in "editable" mode by running from the same directory: You can then install your package in "editable" mode by running from the same directory:
@ -50,8 +51,8 @@ Conventions for Python test discovery
* In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_. * In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_.
* From those files, collect test items: * From those files, collect test items:
* ``test`` prefixed test functions or methods outside of class * ``test`` prefixed test functions or methods outside of class.
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method) * ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method). Methods decorated with ``@staticmethod`` and ``@classmethods`` are also considered.
For examples of how to customize your test discovery :doc:`/example/pythoncollection`. For examples of how to customize your test discovery :doc:`/example/pythoncollection`.
@ -270,8 +271,8 @@ tox
Once you are done with your work and want to make sure that your actual Once you are done with your work and want to make sure that your actual
package passes all tests you may want to look into :doc:`tox <tox:index>`, the package passes all tests you may want to look into :doc:`tox <tox:index>`, the
virtualenv test automation tool and its :doc:`pytest support <tox:example/pytest>`. virtualenv test automation tool.
tox helps you to setup virtualenv environments with pre-defined ``tox`` helps you to setup virtualenv environments with pre-defined
dependencies and then executing a pre-configured test command with dependencies and then executing a pre-configured test command with
options. It will run tests against the installed package and not options. It will run tests against the installed package and not
against your source code checkout, helping to detect packaging against your source code checkout, helping to detect packaging
@ -293,3 +294,20 @@ See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`
setuptools intends to setuptools intends to
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_. `remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
Checking with flake8-pytest-style
---------------------------------
In order to ensure that pytest is being used correctly in your project,
it can be helpful to use the `flake8-pytest-style <https://github.com/m-burst/flake8-pytest-style>`_ flake8 plugin.
flake8-pytest-style checks for common mistakes and coding style violations in pytest code,
such as incorrect use of fixtures, test function names, and markers.
By using this plugin, you can catch these errors early in the development process
and ensure that your pytest code is consistent and easy to maintain.
A list of the lints detected by flake8-pytest-style can be found on its `PyPI page <https://pypi.org/project/flake8-pytest-style/>`_.
.. note::
flake8-pytest-style is not an official pytest project. Some of the rules enforce certain style choices, such as using `@pytest.fixture()` over `@pytest.fixture`, but you can configure the plugin to fit your preferred style.

View File

@ -16,7 +16,7 @@ import process can be controlled through the ``--import-mode`` command-line flag
these values: these values:
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning* * ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
of :py:data:`sys.path` if not already there, and then imported with the :func:`__import__ <__import__>` builtin. of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
This requires test module names to be unique when the test directory tree is not arranged in This requires test module names to be unique when the test directory tree is not arranged in
packages, because the modules will put in :py:data:`sys.modules` after importing. packages, because the modules will put in :py:data:`sys.modules` after importing.
@ -24,7 +24,7 @@ these values:
This is the classic mechanism, dating back from the time Python 2 was still supported. This is the classic mechanism, dating back from the time Python 2 was still supported.
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already * ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
there, and imported with ``__import__``. there, and imported with :func:`importlib.import_module <importlib.import_module>`.
This better allows to run test modules against installed versions of a package even if the This better allows to run test modules against installed versions of a package even if the
package under test has the same import root. For example: package under test has the same import root. For example:
@ -43,7 +43,7 @@ these values:
Same as ``prepend``, requires test module names to be unique when the test directory tree is Same as ``prepend``, requires test module names to be unique when the test directory tree is
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing. not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
* ``importlib``: new in pytest-6.0, this mode uses :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`. * ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
For this reason this doesn't require test module names to be unique. For this reason this doesn't require test module names to be unique.

View File

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

View File

@ -233,7 +233,7 @@ If you run this command for the first time, you can see the print statement:
> assert mydata == 23 > assert mydata == 23
E assert 42 == 23 E assert 42 == 23
test_caching.py:20: AssertionError test_caching.py:19: AssertionError
-------------------------- Captured stdout setup --------------------------- -------------------------- Captured stdout setup ---------------------------
running expensive computation... running expensive computation...
========================= short test summary info ========================== ========================= short test summary info ==========================
@ -256,7 +256,7 @@ the cache and nothing will be printed:
> assert mydata == 23 > assert mydata == 23
E assert 42 == 23 E assert 42 == 23
test_caching.py:20: AssertionError test_caching.py:19: AssertionError
========================= short test summary info ========================== ========================= short test summary info ==========================
FAILED test_caching.py::test_function - assert 42 == 23 FAILED test_caching.py::test_function - assert 42 == 23
1 failed in 0.12s 1 failed in 0.12s

View File

@ -109,6 +109,18 @@ When a warning matches more than one option in the list, the action for the last
is performed. is performed.
.. note::
The ``-W`` flag and the ``filterwarnings`` ini option use warning filters that are
similar in structure, but each configuration option interprets its filter
differently. For example, *message* in ``filterwarnings`` is a string containing a
regular expression that the start of the warning message must match,
case-insensitively, while *message* in ``-W`` is a literal string that the start of
the warning message must contain (case-insensitively), ignoring any whitespace at
the start or end of message. Consult the `warning filter`_ documentation for more
details.
.. _`filterwarnings`: .. _`filterwarnings`:
``@pytest.mark.filterwarnings`` ``@pytest.mark.filterwarnings``
@ -270,20 +282,34 @@ which works in a similar manner to :ref:`raises <assertraises>` (except that
warnings.warn("my warning", UserWarning) warnings.warn("my warning", UserWarning)
The test will fail if the warning in question is not raised. Use the keyword The test will fail if the warning in question is not raised. Use the keyword
argument ``match`` to assert that the warning matches a text or regex:: argument ``match`` to assert that the warning matches a text or regex.
To match a literal string that may contain regular expression metacharacters like ``(`` or ``.``, the pattern can
first be escaped with ``re.escape``.
>>> with warns(UserWarning, match='must be 0 or None'): Some examples:
.. code-block:: pycon
>>> with warns(UserWarning, match="must be 0 or None"):
... warnings.warn("value must be 0 or None", UserWarning) ... warnings.warn("value must be 0 or None", UserWarning)
...
>>> with warns(UserWarning, match=r'must be \d+$'): >>> with warns(UserWarning, match=r"must be \d+$"):
... warnings.warn("value must be 42", UserWarning) ... warnings.warn("value must be 42", UserWarning)
...
>>> with warns(UserWarning, match=r'must be \d+$'): >>> with warns(UserWarning, match=r"must be \d+$"):
... warnings.warn("this is not here", UserWarning) ... warnings.warn("this is not here", UserWarning)
...
Traceback (most recent call last): Traceback (most recent call last):
... ...
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
>>> with warns(UserWarning, match=re.escape("issue with foo() func")):
... warnings.warn("issue with foo() func")
...
You can also call :func:`pytest.warns` on a function or code string: You can also call :func:`pytest.warns` on a function or code string:
.. code-block:: python .. code-block:: python

View File

@ -1237,7 +1237,6 @@ If the data created by the factory requires managing, the fixture can take care
@pytest.fixture @pytest.fixture
def make_customer_record(): def make_customer_record():
created_records = [] created_records = []
def _make_customer_record(name): def _make_customer_record(name):

View File

@ -55,6 +55,13 @@ These options can also be customized through ``pytest.ini`` file:
log_format = %(asctime)s %(levelname)s %(message)s log_format = %(asctime)s %(levelname)s %(message)s
log_date_format = %Y-%m-%d %H:%M:%S log_date_format = %Y-%m-%d %H:%M:%S
Specific loggers can be disabled via ``--log-disable={logger_name}``.
This argument can be passed multiple times:
.. code-block:: bash
pytest --log-disable=main --log-disable=testing
Further it is possible to disable reporting of captured content (stdout, Further it is possible to disable reporting of captured content (stdout,
stderr and logs) on failed tests completely with: stderr and logs) on failed tests completely with:

View File

@ -135,10 +135,10 @@ This can be done in our test file by defining a class to represent ``r``.
# this is the previous code block example # this is the previous code block example
import app import app
# custom class to be the mock return value # custom class to be the mock return value
# will override the requests.Response returned from requests.get # will override the requests.Response returned from requests.get
class MockResponse: class MockResponse:
# mock json() method always returns a specific testing dictionary # mock json() method always returns a specific testing dictionary
@staticmethod @staticmethod
def json(): def json():
@ -146,7 +146,6 @@ This can be done in our test file by defining a class to represent ``r``.
def test_get_json(monkeypatch): def test_get_json(monkeypatch):
# Any arguments may be passed and mock_get() will always return our # Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method. # mocked object, which only has the .json() method.
def mock_get(*args, **kwargs): def mock_get(*args, **kwargs):
@ -181,6 +180,7 @@ This mock can be shared across tests using a ``fixture``:
# app.py that includes the get_json() function # app.py that includes the get_json() function
import app import app
# custom class to be the mock return value of requests.get() # custom class to be the mock return value of requests.get()
class MockResponse: class MockResponse:
@staticmethod @staticmethod
@ -358,7 +358,6 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific
def test_connection(monkeypatch): def test_connection(monkeypatch):
# Patch the values of DEFAULT_CONFIG to specific # Patch the values of DEFAULT_CONFIG to specific
# testing values only for this test. # testing values only for this test.
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user") monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
@ -383,7 +382,6 @@ You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove v
def test_missing_user(monkeypatch): def test_missing_user(monkeypatch):
# patch the DEFAULT_CONFIG t be missing the 'user' key # patch the DEFAULT_CONFIG t be missing the 'user' key
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False) monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
@ -404,6 +402,7 @@ separate fixtures for each potential mock and reference them in the needed tests
# app.py with the connection string function # app.py with the connection string function
import app import app
# all of the mocks are moved into separated fixtures # all of the mocks are moved into separated fixtures
@pytest.fixture @pytest.fixture
def mock_test_user(monkeypatch): def mock_test_user(monkeypatch):
@ -425,7 +424,6 @@ separate fixtures for each potential mock and reference them in the needed tests
# tests reference only the fixture mocks that are needed # tests reference only the fixture mocks that are needed
def test_connection(mock_test_user, mock_test_database): def test_connection(mock_test_user, mock_test_database):
expected = "User Id=test_user; Location=test_db;" expected = "User Id=test_user; Location=test_db;"
result = app.create_connection_string() result = app.create_connection_string()
@ -433,7 +431,6 @@ separate fixtures for each potential mock and reference them in the needed tests
def test_missing_user(mock_missing_default_user): def test_missing_user(mock_missing_default_user):
with pytest.raises(KeyError): with pytest.raises(KeyError):
_ = app.create_connection_string() _ = app.create_connection_string()

View File

@ -12,8 +12,9 @@ Examples for modifying traceback printing:
.. code-block:: bash .. code-block:: bash
pytest --showlocals # show local variables in tracebacks pytest --showlocals # show local variables in tracebacks
pytest -l # show local variables (shortcut) pytest -l # show local variables (shortcut)
pytest --no-showlocals # hide local variables (if addopts enables them)
pytest --tb=auto # (default) 'long' tracebacks for the first and last pytest --tb=auto # (default) 'long' tracebacks for the first and last
# entry, but 'short' style for the other entries # entry, but 'short' style for the other entries
@ -166,9 +167,9 @@ Now we can increase pytest's verbosity:
E Right contains 4 more items: E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40} E {'10': 10, '20': 20, '30': 30, '40': 40}
E Full diff: E Full diff:
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}... E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
E E ? - - - - - - - -
E ...Full output truncated (3 lines hidden), use '-vv' to show E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
test_verbosity_example.py:14: AssertionError test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________ ___________________________ test_long_text_fail ____________________________
@ -348,8 +349,7 @@ Example:
test_example.py:14: AssertionError test_example.py:14: AssertionError
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIPPED [1] test_example.py:22: skipping this test SKIPPED [1] test_example.py:22: skipping this test
XFAIL test_example.py::test_xfail XFAIL test_example.py::test_xfail - reason: xfailing this test
reason: xfailing this test
XPASS test_example.py::test_xpass always xfail XPASS test_example.py::test_xpass always xfail
ERROR test_example.py::test_error - assert 0 ERROR test_example.py::test_error - assert 0
FAILED test_example.py::test_fail - assert 0 FAILED test_example.py::test_fail - assert 0

View File

@ -131,10 +131,12 @@ The default base temporary directory
Temporary directories are by default created as sub-directories of Temporary directories are by default created as sub-directories of
the system temporary directory. The base name will be ``pytest-NUM`` where the system temporary directory. The base name will be ``pytest-NUM`` where
``NUM`` will be incremented with each test run. Moreover, entries older ``NUM`` will be incremented with each test run.
than 3 temporary directories will be removed. By default, entries older than 3 temporary directories will be removed.
This behavior can be configured with :confval:`tmp_path_retention_count` and
:confval:`tmp_path_retention_policy`.
The number of entries currently cannot be changed, but using the ``--basetemp`` Using the ``--basetemp``
option will remove the directory before every run, effectively meaning the temporary directories option will remove the directory before every run, effectively meaning the temporary directories
of only the most recent run will be kept. of only the most recent run will be kept.

View File

@ -157,7 +157,7 @@ the ``self.db`` values in the traceback:
E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001> E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
E assert 0 E assert 0
test_unittest_db.py:10: AssertionError test_unittest_db.py:11: AssertionError
___________________________ MyTest.test_method2 ____________________________ ___________________________ MyTest.test_method2 ____________________________
self = <test_unittest_db.MyTest testMethod=test_method2> self = <test_unittest_db.MyTest testMethod=test_method2>
@ -167,7 +167,7 @@ the ``self.db`` values in the traceback:
E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001> E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
E assert 0 E assert 0
test_unittest_db.py:13: AssertionError test_unittest_db.py:14: AssertionError
========================= short test summary info ========================== ========================= short test summary info ==========================
FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: <conft... FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: <conft...
FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: <conft... FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: <conft...

View File

@ -35,11 +35,12 @@ Pytest supports several ways to run and select tests from the command-line.
.. code-block:: bash .. code-block:: bash
pytest -k "MyClass and not method" pytest -k 'MyClass and not method'
This will run tests which contain names that match the given *string expression* (case-insensitive), This will run tests which contain names that match the given *string expression* (case-insensitive),
which can include Python operators that use filenames, class names and function names as variables. which can include Python operators that use filenames, class names and function names as variables.
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``. The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
Use ``""`` instead of ``''`` in expression when running this on Windows
.. _nodeids: .. _nodeids:

View File

@ -249,6 +249,7 @@ and use pytest_addoption as follows:
# contents of hooks.py # contents of hooks.py
# Use firstresult=True because we only want one plugin to define this # Use firstresult=True because we only want one plugin to define this
# default value # default value
@hookspec(firstresult=True) @hookspec(firstresult=True)

View File

@ -167,13 +167,8 @@ it in your ``pyproject.toml`` file.
"Framework :: Pytest", "Framework :: Pytest",
] ]
[tool.setuptools] [project.entry-points.pytest11]
packages = ["myproject"] myproject = "myproject.pluginmodule"
[project.entry_points]
pytest11 = [
"myproject = myproject.pluginmodule",
]
If a package is installed this way, ``pytest`` will load If a package is installed this way, ``pytest`` will load
``myproject.pluginmodule`` as a plugin which can define ``myproject.pluginmodule`` as a plugin which can define
@ -454,7 +449,8 @@ in our ``pytest.ini`` to tell pytest where to look for example files.
$ pytest $ pytest
=========================== test session starts ============================ =========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini rootdir: /home/sweet/project
configfile: pytest.ini
collected 2 items collected 2 items
test_example.py .. [100%] test_example.py .. [100%]

View File

@ -2,8 +2,8 @@
.. sidebar:: Next Open Trainings .. sidebar:: Next Open Trainings
- Professionelles Testen für Python mit pytest, part of `enterPy <https://www.enterpy.de/>`__ (German), `October 28th <https://www.enterpy.de/veranstaltung-15409-se-0-professionelles-testen-fuer-python-mit-pytest.html>`__ (sold out) and `November 4th <https://www.enterpy.de/veranstaltung-15557-se-0-professionelles-testen-fuer-python-mit-pytest-zusatztermin.html>`__, online - `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 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany - `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
Also see :doc:`previous talks and blogposts <talks>`. Also see :doc:`previous talks and blogposts <talks>`.

View File

@ -335,7 +335,7 @@ For example:
.. literalinclude:: /example/fixtures/test_fixtures_order_dependencies.py .. literalinclude:: /example/fixtures/test_fixtures_order_dependencies.py
If we map out what depends on what, we get something that look like this: If we map out what depends on what, we get something that looks like this:
.. image:: /example/fixtures/test_fixtures_order_dependencies.* .. image:: /example/fixtures/test_fixtures_order_dependencies.*
:align: center :align: center

File diff suppressed because it is too large Load Diff

View File

@ -956,6 +956,12 @@ TestReport
:show-inheritance: :show-inheritance:
:inherited-members: :inherited-members:
TestShortLogReport
~~~~~~~~~~~~~~~~~~
.. autoclass:: pytest.TestShortLogReport()
:members:
_Result _Result
~~~~~~~ ~~~~~~~
@ -1047,6 +1053,14 @@ Environment Variables
Environment variables that can be used to change pytest's behavior. Environment variables that can be used to change pytest's behavior.
.. envvar:: CI
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
.. envvar:: BUILD_NUMBER
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
.. envvar:: PYTEST_ADDOPTS .. envvar:: PYTEST_ADDOPTS
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given
@ -1139,6 +1153,9 @@ Custom warnings generated in some situations such as improper usage or deprecate
.. autoclass:: pytest.PytestRemovedIn8Warning .. autoclass:: pytest.PytestRemovedIn8Warning
:show-inheritance: :show-inheritance:
.. autoclass:: pytest.PytestRemovedIn9Warning
:show-inheritance:
.. autoclass:: pytest.PytestUnhandledCoroutineWarning .. autoclass:: pytest.PytestUnhandledCoroutineWarning
:show-inheritance: :show-inheritance:
@ -1212,6 +1229,7 @@ passed multiple times. The expected format is ``name=value``. For example::
* ``classic``: classic pytest output. * ``classic``: classic pytest output.
* ``progress``: like classic pytest output, but with a progress indicator. * ``progress``: like classic pytest output, but with a progress indicator.
* ``progress-even-when-capture-no``: allows the use of the progress indicator even when ``capture=no``.
* ``count``: like progress, but shows progress as the number of tests completed instead of a percent. * ``count``: like progress, but shows progress as the number of tests completed instead of a percent.
The default is ``progress``, but you can fallback to ``classic`` if you prefer or The default is ``progress``, but you can fallback to ``classic`` if you prefer or
@ -1688,6 +1706,11 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest] [pytest]
pythonpath = src1 src2 pythonpath = src1 src2
.. note::
``pythonpath`` does not affect some imports that happen very early,
most notably plugins loaded using the ``-p`` command line option.
.. confval:: required_plugins .. confval:: required_plugins
@ -1704,13 +1727,12 @@ passed multiple times. The expected format is ``name=value``. For example::
.. confval:: testpaths .. confval:: testpaths
Sets list of directories that should be searched for tests when Sets list of directories that should be searched for tests when
no specific directories, files or test ids are given in the command line when no specific directories, files or test ids are given in the command line when
executing pytest from the :ref:`rootdir <rootdir>` directory. executing pytest from the :ref:`rootdir <rootdir>` directory.
File system paths may use shell-style wildcards, including the recursive File system paths may use shell-style wildcards, including the recursive
``**`` pattern. ``**`` pattern.
Useful when all project tests are in a known location to speed up Useful when all project tests are in a known location to speed up
test collection and to avoid picking up undesired tests by accident. test collection and to avoid picking up undesired tests by accident.
@ -1719,8 +1741,51 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest] [pytest]
testpaths = testing doc testpaths = testing doc
This tells pytest to only look for tests in ``testing`` and ``doc`` This configuration means that executing:
directories when executing from the root directory.
.. code-block:: console
pytest
has the same practical effects as executing:
.. code-block:: console
pytest testing doc
.. confval:: tmp_path_retention_count
How many sessions should we keep the `tmp_path` directories,
according to `tmp_path_retention_policy`.
.. code-block:: ini
[pytest]
tmp_path_retention_count = 3
Default: ``3``
.. confval:: tmp_path_retention_policy
Controls which directories created by the `tmp_path` fixture are kept around,
based on test outcome.
* `all`: retains directories for all tests, regardless of the outcome.
* `failed`: retains directories only for tests with outcome `error` or `failed`.
* `none`: directories are always removed after each test ends, regardless of the outcome.
.. code-block:: ini
[pytest]
tmp_path_retention_policy = "all"
Default: ``all``
.. confval:: usefixtures .. confval:: usefixtures
@ -1759,12 +1824,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
$ pytest --help $ pytest --help
usage: pytest [options] [file_or_dir] [file_or_dir] [...] usage: pytest [options] [file_or_dir] [file_or_dir] [...]
Positional arguments: positional arguments:
file_or_dir file_or_dir
General: general:
-k EXPRESSION Only run tests which match the given substring -k EXPRESSION Only run tests which match the given substring
expression. An expression is a python evaluatable expression. An expression is a Python evaluatable
expression where all names are substring-matched expression where all names are substring-matched
against test names and their parent classes. against test names and their parent classes.
Example: -k 'test_method or test_other' matches all Example: -k 'test_method or test_other' matches all
@ -1778,9 +1843,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
'extra_keyword_matches' set, as well as functions 'extra_keyword_matches' set, as well as functions
which have names assigned directly to them. The which have names assigned directly to them. The
matching is case-insensitive. matching is case-insensitive.
-m MARKEXPR Only run tests matching given mark expression. -m MARKEXPR Only run tests matching given mark expression. For
For example: -m 'mark1 and not mark2'. example: -m 'mark1 and not mark2'.
--markers Show markers (builtin, plugin and per-project ones) --markers show markers (builtin, plugin and per-project ones).
-x, --exitfirst Exit instantly on first error or failed test -x, --exitfirst Exit instantly on first error or failed test
--fixtures, --funcargs --fixtures, --funcargs
Show available fixtures, sorted by plugin appearance Show available fixtures, sorted by plugin appearance
@ -1790,18 +1855,18 @@ All the command-line flags can be obtained by running ``pytest --help``::
KeyboardInterrupt KeyboardInterrupt
--pdbcls=modulename:classname --pdbcls=modulename:classname
Specify a custom interactive Python debugger for use Specify a custom interactive Python debugger for use
with --pdb. For example: with --pdb.For example:
--pdbcls=IPython.terminal.debugger:TerminalPdb --pdbcls=IPython.terminal.debugger:TerminalPdb
--trace Immediately break when running each test --trace Immediately break when running each test
--capture=method Per-test capturing method: one of fd|sys|no|tee-sys. --capture=method Per-test capturing method: one of fd|sys|no|tee-sys
-s Shortcut for --capture=no. -s Shortcut for --capture=no
--runxfail Report the results of xfail tests as if they were --runxfail Report the results of xfail tests as if they were
not marked not marked
--lf, --last-failed Rerun only the tests that failed at the last run (or --lf, --last-failed Rerun only the tests that failed at the last run (or
all if none failed) all if none failed)
--ff, --failed-first Run all tests, but run the last failures first --ff, --failed-first Run all tests, but run the last failures first. This
This may re-order tests and thus lead to repeated may re-order tests and thus lead to repeated fixture
fixture setup/teardown setup/teardown.
--nf, --new-first Run tests from new files first, then the rest of the --nf, --new-first Run tests from new files first, then the rest of the
tests sorted by file mtime tests sorted by file mtime
--cache-show=[CACHESHOW] --cache-show=[CACHESHOW]
@ -1815,11 +1880,10 @@ All the command-line flags can be obtained by running ``pytest --help``::
test next time test next time
--sw-skip, --stepwise-skip --sw-skip, --stepwise-skip
Ignore the first failing test but stop on the next Ignore the first failing test but stop on the next
failing test. failing test. Implicitly enables --stepwise.
implicitly enables --stepwise.
Reporting: Reporting:
--durations=N show N slowest setup/test durations (N=0 for all) --durations=N Show N slowest setup/test durations (N=0 for all)
--durations-min=N Minimal duration in seconds for inclusion in slowest --durations-min=N Minimal duration in seconds for inclusion in slowest
list. Default: 0.005. list. Default: 0.005.
-v, --verbose Increase verbosity -v, --verbose Increase verbosity
@ -1836,8 +1900,10 @@ All the command-line flags can be obtained by running ``pytest --help``::
--disable-warnings, --disable-pytest-warnings --disable-warnings, --disable-pytest-warnings
Disable warnings summary Disable warnings summary
-l, --showlocals Show locals in tracebacks (disabled by default) -l, --showlocals Show locals in tracebacks (disabled by default)
--no-showlocals Hide locals in tracebacks (negate --showlocals
passed through addopts)
--tb=style Traceback print mode --tb=style Traceback print mode
(auto/long/short/line/native/no). (auto/long/short/line/native/no)
--show-capture={no,stdout,stderr,log,all} --show-capture={no,stdout,stderr,log,all}
Controls how captured stdout/stderr/log is shown on Controls how captured stdout/stderr/log is shown on
failed tests. Default: all. failed tests. Default: all.
@ -1860,18 +1926,18 @@ All the command-line flags can be obtained by running ``pytest --help``::
--strict-markers Markers not registered in the `markers` section of --strict-markers Markers not registered in the `markers` section of
the configuration file raise errors the configuration file raise errors
--strict (Deprecated) alias to --strict-markers --strict (Deprecated) alias to --strict-markers
-c file Load configuration from `file` instead of trying to -c FILE, --config-file=FILE
locate one of the implicit configuration files Load configuration from `FILE` instead of trying to
locate one of the implicit configuration files.
--continue-on-collection-errors --continue-on-collection-errors
Force test execution even if collection errors Force test execution even if collection errors occur
occur
--rootdir=ROOTDIR Define root directory for tests. Can be relative --rootdir=ROOTDIR Define root directory for tests. Can be relative
path: 'root_dir', './root_dir', path: 'root_dir', './root_dir',
'root_dir/another_dir/'; absolute path: 'root_dir/another_dir/'; absolute path:
'/home/user/root_dir'; path with variables: '/home/user/root_dir'; path with variables:
'$HOME/root_dir'. '$HOME/root_dir'.
Collection: collection:
--collect-only, --co Only collect tests, don't execute them --collect-only, --co Only collect tests, don't execute them
--pyargs Try to interpret all arguments as Python packages --pyargs Try to interpret all arguments as Python packages
--ignore=path Ignore path during collection (multi-allowed) --ignore=path Ignore path during collection (multi-allowed)
@ -1899,27 +1965,24 @@ All the command-line flags can be obtained by running ``pytest --help``::
For a given doctest, continue to run after the first For a given doctest, continue to run after the first
failure failure
Test session debugging and configuration: test session debugging and configuration:
--basetemp=dir Base temporary directory for this test run. (Warning: --basetemp=dir Base temporary directory for this test run.
this directory is removed if it exists.) (Warning: this directory is removed if it exists.)
-V, --version Display pytest version and information about -V, --version Display pytest version and information about
plugins. When given twice, also display information plugins. When given twice, also display information
about plugins. about plugins.
-h, --help Show help message and configuration info -h, --help Show help message and configuration info
-p name Early-load given plugin module name or entry point -p name Early-load given plugin module name or entry point
(multi-allowed) (multi-allowed). To avoid loading of plugins, use
To avoid loading of plugins, use the `no:` prefix, the `no:` prefix, e.g. `no:doctest`.
e.g. `no:doctest`
--trace-config Trace considerations of conftest.py files --trace-config Trace considerations of conftest.py files
--debug=[DEBUG_FILE_NAME] --debug=[DEBUG_FILE_NAME]
Store internal tracing debug information in this log Store internal tracing debug information in this log
file. file. This file is opened with 'w' and truncated as
This file is opened with 'w' and truncated as a a result, care advised. Default: pytestdebug.log.
result, care advised.
Default: pytestdebug.log.
-o OVERRIDE_INI, --override-ini=OVERRIDE_INI -o OVERRIDE_INI, --override-ini=OVERRIDE_INI
Override ini option with "option=value" style, e.g. Override ini option with "option=value" style, e.g.
`-o xfail_strict=True -o cache_dir=cache` `-o xfail_strict=True -o cache_dir=cache`.
--assert=MODE Control assertion debugging tools. --assert=MODE Control assertion debugging tools.
'plain' performs no assertion debugging. 'plain' performs no assertion debugging.
'rewrite' (the default) rewrites assert statements 'rewrite' (the default) rewrites assert statements
@ -1930,11 +1993,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
--setup-plan Show what fixtures and tests would be executed but --setup-plan Show what fixtures and tests would be executed but
don't execute anything don't execute anything
Logging: logging:
--log-level=LEVEL Level of messages to catch/display. --log-level=LEVEL Level of messages to catch/display. Not set by
Not set by default, so it depends on the root/parent default, so it depends on the root/parent log
log handler's effective level, where it is "WARNING" handler's effective level, where it is "WARNING" by
by default. default.
--log-format=LOG_FORMAT --log-format=LOG_FORMAT
Log format used by the logging module Log format used by the logging module
--log-date-format=LOG_DATE_FORMAT --log-date-format=LOG_DATE_FORMAT
@ -1955,15 +2018,18 @@ All the command-line flags can be obtained by running ``pytest --help``::
--log-auto-indent=LOG_AUTO_INDENT --log-auto-indent=LOG_AUTO_INDENT
Auto-indent multiline messages passed to the logging Auto-indent multiline messages passed to the logging
module. Accepts true|on, false|off or an integer. module. Accepts true|on, false|off or an integer.
--log-disable=LOGGER_DISABLE
Disable a logger by name. Can be passed multiple
times.
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found: [pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
markers (linelist): Markers for test functions markers (linelist): Markers for test functions
empty_parameter_set_mark (string): empty_parameter_set_mark (string):
Default marker for empty parametersets Default marker for empty parametersets
norecursedirs (args): Directory patterns to avoid for recursion norecursedirs (args): Directory patterns to avoid for recursion
testpaths (args): Directories to search for tests when no files or testpaths (args): Directories to search for tests when no files or
directories are given in the command line directories are given on the command line
filterwarnings (linelist): filterwarnings (linelist):
Each line specifies a pattern for Each line specifies a pattern for
warnings.filterwarnings. Processed after warnings.filterwarnings. Processed after
@ -1984,9 +2050,18 @@ All the command-line flags can be obtained by running ``pytest --help``::
console_output_style (string): console_output_style (string):
Console output: "classic", or with additional Console output: "classic", or with additional
progress information ("progress" (percentage) | progress information ("progress" (percentage) |
"count") "count" | "progress-even-when-capture-no" (forces
progress even when capture=no)
xfail_strict (bool): Default for the strict parameter of xfail markers xfail_strict (bool): Default for the strict parameter of xfail markers
when not given explicitly (default: False) when not given explicitly (default: False)
tmp_path_retention_count (string):
How many sessions should we keep the `tmp_path`
directories, according to
`tmp_path_retention_policy`.
tmp_path_retention_policy (string):
Controls which directories created by the `tmp_path`
fixture are kept around, based on test outcome.
(all/failed/none)
enable_assertion_pass_hook (bool): enable_assertion_pass_hook (bool):
Enables the pytest_assertion_pass hook. Make sure to Enables the pytest_assertion_pass hook. Make sure to
delete any previously generated pyc cache files. delete any previously generated pyc cache files.

View File

@ -1,7 +1,11 @@
pallets-sphinx-themes pallets-sphinx-themes
pluggy>=1.0 pluggy>=1.0
pygments-pytest>=2.2.0 pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0 sphinx-removed-in>=0.2.0
sphinx>=5,<6 sphinx>=5,<6
sphinxcontrib-trio sphinxcontrib-trio
sphinxcontrib-svg2pdfconverter sphinxcontrib-svg2pdfconverter
# Pin packaging because it no longer handles 'latest' version, which
# is the version that is assigned to the docs.
# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045.
packaging <22

View File

@ -114,3 +114,8 @@ template = "changelog/_template.rst"
[tool.black] [tool.black]
target-version = ['py37'] target-version = ['py37']
# check-wheel-contents is executed by the build-and-inspect-python-package action.
[tool.check-wheel-contents]
# W009: Wheel contains multiple toplevel library entries
ignore = "W009"

View File

@ -7,7 +7,9 @@ def main():
Platform agnostic wrapper script for towncrier. Platform agnostic wrapper script for towncrier.
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs. Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
""" """
with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file: with open(
"doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8"
) as draft_file:
return call(("towncrier", "--draft"), stdout=draft_file) return call(("towncrier", "--draft"), stdout=draft_file)

View File

@ -17,7 +17,9 @@ Plugin List
=========== ===========
PyPI projects that match "pytest-\*" are considered plugins and are listed PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically. Packages classified as inactive are excluded. automatically together with a manually-maintained list in `the source
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
Packages classified as inactive are excluded.
.. The following conditional uses a different format for this list when .. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the creating a PDF, because otherwise the table gets far too wide for the
@ -33,6 +35,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
"Development Status :: 6 - Mature", "Development Status :: 6 - Mature",
"Development Status :: 7 - Inactive", "Development Status :: 7 - Inactive",
) )
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert",
}
def escape_rst(text: str) -> str: def escape_rst(text: str) -> str:
@ -52,18 +57,18 @@ def iter_plugins():
regex = r">([\d\w-]*)</a>" regex = r">([\d\w-]*)</a>"
response = requests.get("https://pypi.org/simple") response = requests.get("https://pypi.org/simple")
matches = list( match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
match plugin_names = [
for match in re.finditer(regex, response.text) name
if match.groups()[0].startswith("pytest-") for name in match_names
) if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
]
for match in tqdm(matches, smoothing=0): for name in tqdm(plugin_names, smoothing=0):
name = match.groups()[0]
response = requests.get(f"https://pypi.org/pypi/{name}/json") response = requests.get(f"https://pypi.org/pypi/{name}/json")
if response.status_code == 404: if response.status_code == 404:
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
# return 404 on the JSON API. Skip. # but return 404 on the JSON API. Skip.
continue continue
response.raise_for_status() response.raise_for_status()
info = response.json()["info"] info = response.json()["info"]
@ -78,11 +83,23 @@ def iter_plugins():
requires = "N/A" requires = "N/A"
if info["requires_dist"]: if info["requires_dist"]:
for requirement in info["requires_dist"]: for requirement in info["requires_dist"]:
if requirement == "pytest" or "pytest " in requirement: if re.match(r"pytest(?![-.\w])", requirement):
requires = requirement requires = requirement
break break
def version_sort_key(version_string):
"""
Return the sort key for the given version string
returned by the API.
"""
try:
return packaging.version.parse(version_string)
except packaging.version.InvalidVersion:
# Use a hard-coded pre-release version.
return packaging.version.Version("0.0.0alpha")
releases = response.json()["releases"] releases = response.json()["releases"]
for release in sorted(releases, key=packaging.version.parse, reverse=True): for release in sorted(releases, key=version_sort_key, reverse=True):
if releases[release]: if releases[release]:
release_date = datetime.date.fromisoformat( release_date = datetime.date.fromisoformat(
releases[release][-1]["upload_time_iso_8601"].split("T")[0] releases[release][-1]["upload_time_iso_8601"].split("T")[0]
@ -90,7 +107,9 @@ def iter_plugins():
last_release = release_date.strftime("%b %d, %Y") last_release = release_date.strftime("%b %d, %Y")
break break
name = f':pypi:`{info["name"]}`' name = f':pypi:`{info["name"]}`'
summary = escape_rst(info["summary"].replace("\n", "")) summary = ""
if info["summary"]:
summary = escape_rst(info["summary"].replace("\n", ""))
yield { yield {
"name": name, "name": name,
"summary": summary.strip(), "summary": summary.strip(),
@ -122,7 +141,7 @@ def main():
reference_dir = pathlib.Path("doc", "en", "reference") reference_dir = pathlib.Path("doc", "en", "reference")
plugin_list = reference_dir / "plugin_list.rst" plugin_list = reference_dir / "plugin_list.rst"
with plugin_list.open("w") as f: with plugin_list.open("w", encoding="UTF-8") as f:
f.write(FILE_HEAD) f.write(FILE_HEAD)
f.write(f"This list contains {len(plugins)} plugins.\n\n") f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(".. only:: not latex\n\n") f.write(".. only:: not latex\n\n")

View File

@ -6,7 +6,7 @@ long_description_content_type = text/x-rst
url = https://docs.pytest.org/en/latest/ url = https://docs.pytest.org/en/latest/
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
license = MIT license = MIT
license_file = LICENSE license_files = LICENSE
platforms = unix, linux, osx, cygwin, win32 platforms = unix, linux, osx, cygwin, win32
classifiers = classifiers =
Development Status :: 6 - Mature Development Status :: 6 - Mature
@ -21,6 +21,8 @@ classifiers =
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Software Development :: Libraries Topic :: Software Development :: Libraries
Topic :: Software Development :: Testing Topic :: Software Development :: Testing
Topic :: Utilities Topic :: Utilities
@ -36,16 +38,16 @@ packages =
_pytest _pytest
_pytest._code _pytest._code
_pytest._io _pytest._io
_pytest._py
_pytest.assertion _pytest.assertion
_pytest.config _pytest.config
_pytest.mark _pytest.mark
pytest pytest
py_modules = py
install_requires = install_requires =
attrs>=19.2.0
iniconfig iniconfig
packaging packaging
pluggy>=0.12,<2.0 pluggy>=0.12,<2.0
py>=1.8.2
colorama;sys_platform=="win32" colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11" exceptiongroup>=1.0.0rc8;python_version<"3.11"
importlib-metadata>=0.12;python_version<"3.8" importlib-metadata>=0.12;python_version<"3.8"
@ -66,11 +68,13 @@ console_scripts =
[options.extras_require] [options.extras_require]
testing = testing =
argcomplete argcomplete
attrs>=19.2.0
hypothesis>=3.56 hypothesis>=3.56
mock mock
nose nose
pygments>=2.7.2 pygments>=2.7.2
requests requests
setuptools
xmlschema xmlschema
[options.package_data] [options.package_data]
@ -94,7 +98,6 @@ mypy_path = src
check_untyped_defs = True check_untyped_defs = True
disallow_any_generics = True disallow_any_generics = True
ignore_missing_imports = True ignore_missing_imports = True
no_implicit_optional = True
show_error_codes = True show_error_codes = True
strict_equality = True strict_equality = True
warn_redundant_casts = True warn_redundant_casts = True

View File

@ -78,15 +78,15 @@ class FastFilesCompleter:
def __call__(self, prefix: str, **kwargs: Any) -> List[str]: def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
# Only called on non option completions. # Only called on non option completions.
if os.path.sep in prefix[1:]: if os.sep in prefix[1:]:
prefix_dir = len(os.path.dirname(prefix) + os.path.sep) prefix_dir = len(os.path.dirname(prefix) + os.sep)
else: else:
prefix_dir = 0 prefix_dir = 0
completion = [] completion = []
globbed = [] globbed = []
if "*" not in prefix and "?" not in prefix: if "*" not in prefix and "?" not in prefix:
# We are on unix, otherwise no bash. # We are on unix, otherwise no bash.
if not prefix or prefix[-1] == os.path.sep: if not prefix or prefix[-1] == os.sep:
globbed.extend(glob(prefix + ".*")) globbed.extend(glob(prefix + ".*"))
prefix += "*" prefix += "*"
globbed.extend(glob(prefix)) globbed.extend(glob(prefix))

View File

@ -1,4 +1,5 @@
import ast import ast
import dataclasses
import inspect import inspect
import os import os
import re import re
@ -30,9 +31,7 @@ from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
from weakref import ref
import attr
import pluggy import pluggy
import _pytest import _pytest
@ -50,9 +49,9 @@ from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal from typing_extensions import Literal
from typing_extensions import SupportsIndex from typing_extensions import SupportsIndex
from weakref import ReferenceType
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
@ -194,25 +193,25 @@ class Frame:
class TracebackEntry: class TracebackEntry:
"""A single entry in a Traceback.""" """A single entry in a Traceback."""
__slots__ = ("_rawentry", "_excinfo", "_repr_style") __slots__ = ("_rawentry", "_repr_style")
def __init__( def __init__(
self, self,
rawentry: TracebackType, rawentry: TracebackType,
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, repr_style: Optional['Literal["short", "long"]'] = None,
) -> None: ) -> None:
self._rawentry = rawentry self._rawentry: "Final" = rawentry
self._excinfo = excinfo self._repr_style: "Final" = repr_style
self._repr_style: Optional['Literal["short", "long"]'] = None
def with_repr_style(
self, repr_style: Optional['Literal["short", "long"]']
) -> "TracebackEntry":
return TracebackEntry(self._rawentry, repr_style)
@property @property
def lineno(self) -> int: def lineno(self) -> int:
return self._rawentry.tb_lineno - 1 return self._rawentry.tb_lineno - 1
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
assert mode in ("short", "long")
self._repr_style = mode
@property @property
def frame(self) -> Frame: def frame(self) -> Frame:
return Frame(self._rawentry.tb_frame) return Frame(self._rawentry.tb_frame)
@ -272,7 +271,7 @@ class TracebackEntry:
source = property(getsource) source = property(getsource)
def ishidden(self) -> bool: def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
"""Return True if the current frame has a var __tracebackhide__ """Return True if the current frame has a var __tracebackhide__
resolving to True. resolving to True.
@ -296,7 +295,7 @@ class TracebackEntry:
else: else:
break break
if tbh and callable(tbh): if tbh and callable(tbh):
return tbh(None if self._excinfo is None else self._excinfo()) return tbh(excinfo)
return tbh return tbh
def __str__(self) -> str: def __str__(self) -> str:
@ -329,16 +328,14 @@ class Traceback(List[TracebackEntry]):
def __init__( def __init__(
self, self,
tb: Union[TracebackType, Iterable[TracebackEntry]], tb: Union[TracebackType, Iterable[TracebackEntry]],
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
) -> None: ) -> None:
"""Initialize from given python traceback object and ExceptionInfo.""" """Initialize from given python traceback object and ExceptionInfo."""
self._excinfo = excinfo
if isinstance(tb, TracebackType): if isinstance(tb, TracebackType):
def f(cur: TracebackType) -> Iterable[TracebackEntry]: def f(cur: TracebackType) -> Iterable[TracebackEntry]:
cur_: Optional[TracebackType] = cur cur_: Optional[TracebackType] = cur
while cur_ is not None: while cur_ is not None:
yield TracebackEntry(cur_, excinfo=excinfo) yield TracebackEntry(cur_)
cur_ = cur_.tb_next cur_ = cur_.tb_next
super().__init__(f(tb)) super().__init__(f(tb))
@ -378,7 +375,7 @@ class Traceback(List[TracebackEntry]):
continue continue
if firstlineno is not None and x.frame.code.firstlineno != firstlineno: if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
continue continue
return Traceback(x._rawentry, self._excinfo) return Traceback(x._rawentry)
return self return self
@overload @overload
@ -398,26 +395,27 @@ class Traceback(List[TracebackEntry]):
return super().__getitem__(key) return super().__getitem__(key)
def filter( def filter(
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden() self,
# TODO(py38): change to positional only.
_excinfo_or_fn: Union[
"ExceptionInfo[BaseException]",
Callable[[TracebackEntry], bool],
],
) -> "Traceback": ) -> "Traceback":
"""Return a Traceback instance with certain items removed """Return a Traceback instance with certain items removed.
fn is a function that gets a single argument, a TracebackEntry If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
instance, and should return True when the item should be added which are hidden (see ishidden() above).
to the Traceback, False when not.
By default this removes all the TracebackEntries which are hidden Otherwise, the filter is a function that gets a single argument, a
(see ishidden() above). ``TracebackEntry`` instance, and should return True when the item should
be added to the ``Traceback``, False when not.
""" """
return Traceback(filter(fn, self), self._excinfo) if isinstance(_excinfo_or_fn, ExceptionInfo):
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
def getcrashentry(self) -> TracebackEntry: else:
"""Return last non-hidden traceback entry that lead to the exception of a traceback.""" fn = _excinfo_or_fn
for i in range(-1, -len(self) - 1, -1): return Traceback(filter(fn, self))
entry = self[i]
if not entry.ishidden():
return entry
return self[-1]
def recursionindex(self) -> Optional[int]: def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if """Return the index of the frame/TracebackEntry where recursion originates if
@ -445,7 +443,7 @@ E = TypeVar("E", bound=BaseException, covariant=True)
@final @final
@attr.s(repr=False, init=False, auto_attribs=True) @dataclasses.dataclass
class ExceptionInfo(Generic[E]): class ExceptionInfo(Generic[E]):
"""Wraps sys.exc_info() objects and offers help for navigating the traceback.""" """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
@ -469,22 +467,41 @@ class ExceptionInfo(Generic[E]):
self._traceback = traceback self._traceback = traceback
@classmethod @classmethod
def from_exc_info( def from_exception(
cls, cls,
exc_info: Tuple[Type[E], E, TracebackType], # Ignoring error: "Cannot use a covariant type variable as a parameter".
# This is OK to ignore because this class is (conceptually) readonly.
# See https://github.com/python/mypy/issues/7049.
exception: E, # type: ignore[misc]
exprinfo: Optional[str] = None, exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]": ) -> "ExceptionInfo[E]":
"""Return an ExceptionInfo for an existing exc_info tuple. """Return an ExceptionInfo for an existing exception.
.. warning:: The exception must have a non-``None`` ``__traceback__`` attribute,
otherwise this function fails with an assertion error. This means that
Experimental API the exception must have been raised, or added a traceback with the
:py:meth:`~BaseException.with_traceback()` method.
:param exprinfo: :param exprinfo:
A text string helping to determine if we should strip A text string helping to determine if we should strip
``AssertionError`` from the output. Defaults to the exception ``AssertionError`` from the output. Defaults to the exception
message/``__str__()``. message/``__str__()``.
.. versionadded:: 7.4
""" """
assert (
exception.__traceback__
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
exc_info = (type(exception), exception, exception.__traceback__)
return cls.from_exc_info(exc_info, exprinfo)
@classmethod
def from_exc_info(
cls,
exc_info: Tuple[Type[E], E, TracebackType],
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]":
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
_striptext = "" _striptext = ""
if exprinfo is None and isinstance(exc_info[1], AssertionError): if exprinfo is None and isinstance(exc_info[1], AssertionError):
exprinfo = getattr(exc_info[1], "msg", None) exprinfo = getattr(exc_info[1], "msg", None)
@ -563,7 +580,7 @@ class ExceptionInfo(Generic[E]):
def traceback(self) -> Traceback: def traceback(self) -> Traceback:
"""The traceback.""" """The traceback."""
if self._traceback is None: if self._traceback is None:
self._traceback = Traceback(self.tb, excinfo=ref(self)) self._traceback = Traceback(self.tb)
return self._traceback return self._traceback
@traceback.setter @traceback.setter
@ -602,18 +619,25 @@ class ExceptionInfo(Generic[E]):
""" """
return isinstance(self.value, exc) return isinstance(self.value, exc)
def _getreprcrash(self) -> "ReprFileLocation": def _getreprcrash(self) -> Optional["ReprFileLocation"]:
exconly = self.exconly(tryshort=True) # Find last non-hidden traceback entry that led to the exception of the
entry = self.traceback.getcrashentry() # traceback, or None if all hidden.
path, lineno = entry.frame.code.raw.co_filename, entry.lineno for i in range(-1, -len(self.traceback) - 1, -1):
return ReprFileLocation(path, lineno + 1, exconly) entry = self.traceback[i]
if not entry.ishidden(self):
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
exconly = self.exconly(tryshort=True)
return ReprFileLocation(path, lineno + 1, exconly)
return None
def getrepr( def getrepr(
self, self,
showlocals: bool = False, showlocals: bool = False,
style: "_TracebackStyle" = "long", style: "_TracebackStyle" = "long",
abspath: bool = False, abspath: bool = False,
tbfilter: bool = True, tbfilter: Union[
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
] = True,
funcargs: bool = False, funcargs: bool = False,
truncate_locals: bool = True, truncate_locals: bool = True,
chain: bool = True, chain: bool = True,
@ -625,14 +649,20 @@ class ExceptionInfo(Generic[E]):
Ignored if ``style=="native"``. Ignored if ``style=="native"``.
:param str style: :param str style:
long|short|no|native|value traceback style. long|short|line|no|native|value traceback style.
:param bool abspath: :param bool abspath:
If paths should be changed to absolute or left unchanged. If paths should be changed to absolute or left unchanged.
:param bool tbfilter: :param tbfilter:
Hide entries that contain a local variable ``__tracebackhide__==True``. A filter for traceback entries.
Ignored if ``style=="native"``.
* If false, don't hide any entries.
* If true, hide internal entries and entries that contain a local
variable ``__tracebackhide__ = True``.
* If a callable, delegates the filtering to the callable.
Ignored if ``style`` is ``"native"``.
:param bool funcargs: :param bool funcargs:
Show fixtures ("funcargs" for legacy purposes) per traceback entry. Show fixtures ("funcargs" for legacy purposes) per traceback entry.
@ -649,12 +679,14 @@ class ExceptionInfo(Generic[E]):
""" """
if style == "native": if style == "native":
return ReprExceptionInfo( return ReprExceptionInfo(
ReprTracebackNative( reprtraceback=ReprTracebackNative(
traceback.format_exception( traceback.format_exception(
self.type, self.value, self.traceback[0]._rawentry self.type,
self.value,
self.traceback[0]._rawentry if self.traceback else None,
) )
), ),
self._getreprcrash(), reprcrash=self._getreprcrash(),
) )
fmt = FormattedExcinfo( fmt = FormattedExcinfo(
@ -684,7 +716,7 @@ class ExceptionInfo(Generic[E]):
return True return True
@attr.s(auto_attribs=True) @dataclasses.dataclass
class FormattedExcinfo: class FormattedExcinfo:
"""Presenting information about failing Functions and Generators.""" """Presenting information about failing Functions and Generators."""
@ -695,12 +727,12 @@ class FormattedExcinfo:
showlocals: bool = False showlocals: bool = False
style: "_TracebackStyle" = "long" style: "_TracebackStyle" = "long"
abspath: bool = True abspath: bool = True
tbfilter: bool = True tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
funcargs: bool = False funcargs: bool = False
truncate_locals: bool = True truncate_locals: bool = True
chain: bool = True chain: bool = True
astcache: Dict[Union[str, Path], ast.AST] = attr.ib( astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
factory=dict, init=False, repr=False default_factory=dict, init=False, repr=False
) )
def _getindent(self, source: "Source") -> int: def _getindent(self, source: "Source") -> int:
@ -741,11 +773,13 @@ class FormattedExcinfo:
) -> List[str]: ) -> List[str]:
"""Return formatted and marked up source lines.""" """Return formatted and marked up source lines."""
lines = [] lines = []
if source is None or line_index >= len(source.lines): if source is not None and line_index < 0:
line_index += len(source)
if source is None or line_index >= len(source.lines) or line_index < 0:
# `line_index` could still be outside `range(len(source.lines))` if
# we're processing AST with pathological position attributes.
source = Source("???") source = Source("???")
line_index = 0 line_index = 0
if line_index < 0:
line_index += len(source)
space_prefix = " " space_prefix = " "
if short: if short:
lines.append(space_prefix + source.lines[line_index].strip()) lines.append(space_prefix + source.lines[line_index].strip())
@ -805,12 +839,16 @@ class FormattedExcinfo:
def repr_traceback_entry( def repr_traceback_entry(
self, self,
entry: TracebackEntry, entry: Optional[TracebackEntry],
excinfo: Optional[ExceptionInfo[BaseException]] = None, excinfo: Optional[ExceptionInfo[BaseException]] = None,
) -> "ReprEntry": ) -> "ReprEntry":
lines: List[str] = [] lines: List[str] = []
style = entry._repr_style if entry._repr_style is not None else self.style style = (
if style in ("short", "long"): entry._repr_style
if entry is not None and entry._repr_style is not None
else self.style
)
if style in ("short", "long") and entry is not None:
source = self._getentrysource(entry) source = self._getentrysource(entry)
if source is None: if source is None:
source = Source("???") source = Source("???")
@ -851,25 +889,31 @@ class FormattedExcinfo:
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
traceback = excinfo.traceback traceback = excinfo.traceback
if self.tbfilter: if callable(self.tbfilter):
traceback = traceback.filter() traceback = self.tbfilter(excinfo)
elif self.tbfilter:
traceback = traceback.filter(excinfo)
if isinstance(excinfo.value, RecursionError): if isinstance(excinfo.value, RecursionError):
traceback, extraline = self._truncate_recursive_traceback(traceback) traceback, extraline = self._truncate_recursive_traceback(traceback)
else: else:
extraline = None extraline = None
if not traceback:
if extraline is None:
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
entries = [self.repr_traceback_entry(None, excinfo)]
return ReprTraceback(entries, extraline, style=self.style)
last = traceback[-1] last = traceback[-1]
entries = []
if self.style == "value": if self.style == "value":
reprentry = self.repr_traceback_entry(last, excinfo) entries = [self.repr_traceback_entry(last, excinfo)]
entries.append(reprentry)
return ReprTraceback(entries, None, style=self.style) return ReprTraceback(entries, None, style=self.style)
for index, entry in enumerate(traceback): entries = [
einfo = (last == entry) and excinfo or None self.repr_traceback_entry(entry, excinfo if last == entry else None)
reprentry = self.repr_traceback_entry(entry, einfo) for entry in traceback
entries.append(reprentry) ]
return ReprTraceback(entries, extraline, style=self.style) return ReprTraceback(entries, extraline, style=self.style)
def _truncate_recursive_traceback( def _truncate_recursive_traceback(
@ -926,6 +970,7 @@ class FormattedExcinfo:
seen: Set[int] = set() seen: Set[int] = set()
while e is not None and id(e) not in seen: while e is not None and id(e) not in seen:
seen.add(id(e)) seen.add(id(e))
if excinfo_: if excinfo_:
# Fall back to native traceback as a temporary workaround until # Fall back to native traceback as a temporary workaround until
# full support for exception groups added to ExceptionInfo. # full support for exception groups added to ExceptionInfo.
@ -942,9 +987,7 @@ class FormattedExcinfo:
) )
else: else:
reprtraceback = self.repr_traceback(excinfo_) reprtraceback = self.repr_traceback(excinfo_)
reprcrash: Optional[ReprFileLocation] = ( reprcrash = excinfo_._getreprcrash()
excinfo_._getreprcrash() if self.style != "value" else None
)
else: else:
# Fallback to native repr if the exception doesn't have a traceback: # Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work. # ExceptionInfo objects require a full traceback to work.
@ -952,25 +995,17 @@ class FormattedExcinfo:
traceback.format_exception(type(e), e, None) traceback.format_exception(type(e), e, None)
) )
reprcrash = None reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)] repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None and self.chain: if e.__cause__ is not None and self.chain:
e = e.__cause__ e = e.__cause__
excinfo_ = ( excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
descr = "The above exception was the direct cause of the following exception:" descr = "The above exception was the direct cause of the following exception:"
elif ( elif (
e.__context__ is not None and not e.__suppress_context__ and self.chain e.__context__ is not None and not e.__suppress_context__ and self.chain
): ):
e = e.__context__ e = e.__context__
excinfo_ = ( excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
descr = "During handling of the above exception, another exception occurred:" descr = "During handling of the above exception, another exception occurred:"
else: else:
e = None e = None
@ -978,7 +1013,7 @@ class FormattedExcinfo:
return ExceptionChainRepr(repr_chain) return ExceptionChainRepr(repr_chain)
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class TerminalRepr: class TerminalRepr:
def __str__(self) -> str: def __str__(self) -> str:
# FYI this is called from pytest-xdist's serialization of exception # FYI this is called from pytest-xdist's serialization of exception
@ -996,14 +1031,14 @@ class TerminalRepr:
# This class is abstract -- only subclasses are instantiated. # This class is abstract -- only subclasses are instantiated.
@attr.s(eq=False) @dataclasses.dataclass(eq=False)
class ExceptionRepr(TerminalRepr): class ExceptionRepr(TerminalRepr):
# Provided by subclasses. # Provided by subclasses.
reprcrash: Optional["ReprFileLocation"]
reprtraceback: "ReprTraceback" reprtraceback: "ReprTraceback"
reprcrash: Optional["ReprFileLocation"]
def __attrs_post_init__(self) -> None: sections: List[Tuple[str, str, str]] = dataclasses.field(
self.sections: List[Tuple[str, str, str]] = [] init=False, default_factory=list
)
def addsection(self, name: str, content: str, sep: str = "-") -> None: def addsection(self, name: str, content: str, sep: str = "-") -> None:
self.sections.append((name, content, sep)) self.sections.append((name, content, sep))
@ -1014,16 +1049,23 @@ class ExceptionRepr(TerminalRepr):
tw.line(content) tw.line(content)
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ExceptionChainRepr(ExceptionRepr): class ExceptionChainRepr(ExceptionRepr):
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]] chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
def __attrs_post_init__(self) -> None: def __init__(
super().__attrs_post_init__() self,
chain: Sequence[
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
],
) -> None:
# reprcrash and reprtraceback of the outermost (the newest) exception # reprcrash and reprtraceback of the outermost (the newest) exception
# in the chain. # in the chain.
self.reprtraceback = self.chain[-1][0] super().__init__(
self.reprcrash = self.chain[-1][1] reprtraceback=chain[-1][0],
reprcrash=chain[-1][1],
)
self.chain = chain
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
for element in self.chain: for element in self.chain:
@ -1034,17 +1076,17 @@ class ExceptionChainRepr(ExceptionRepr):
super().toterminal(tw) super().toterminal(tw)
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprExceptionInfo(ExceptionRepr): class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback" reprtraceback: "ReprTraceback"
reprcrash: "ReprFileLocation" reprcrash: Optional["ReprFileLocation"]
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw) self.reprtraceback.toterminal(tw)
super().toterminal(tw) super().toterminal(tw)
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprTraceback(TerminalRepr): class ReprTraceback(TerminalRepr):
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
extraline: Optional[str] extraline: Optional[str]
@ -1073,12 +1115,12 @@ class ReprTraceback(TerminalRepr):
class ReprTracebackNative(ReprTraceback): class ReprTracebackNative(ReprTraceback):
def __init__(self, tblines: Sequence[str]) -> None: def __init__(self, tblines: Sequence[str]) -> None:
self.style = "native"
self.reprentries = [ReprEntryNative(tblines)] self.reprentries = [ReprEntryNative(tblines)]
self.extraline = None self.extraline = None
self.style = "native"
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprEntryNative(TerminalRepr): class ReprEntryNative(TerminalRepr):
lines: Sequence[str] lines: Sequence[str]
@ -1088,7 +1130,7 @@ class ReprEntryNative(TerminalRepr):
tw.write("".join(self.lines)) tw.write("".join(self.lines))
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprEntry(TerminalRepr): class ReprEntry(TerminalRepr):
lines: Sequence[str] lines: Sequence[str]
reprfuncargs: Optional["ReprFuncArgs"] reprfuncargs: Optional["ReprFuncArgs"]
@ -1142,8 +1184,8 @@ class ReprEntry(TerminalRepr):
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
if self.style == "short": if self.style == "short":
assert self.reprfileloc is not None if self.reprfileloc:
self.reprfileloc.toterminal(tw) self.reprfileloc.toterminal(tw)
self._write_entry_lines(tw) self._write_entry_lines(tw)
if self.reprlocals: if self.reprlocals:
self.reprlocals.toterminal(tw, indent=" " * 8) self.reprlocals.toterminal(tw, indent=" " * 8)
@ -1168,12 +1210,15 @@ class ReprEntry(TerminalRepr):
) )
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprFileLocation(TerminalRepr): class ReprFileLocation(TerminalRepr):
path: str = attr.ib(converter=str) path: str
lineno: int lineno: int
message: str message: str
def __post_init__(self) -> None:
self.path = str(self.path)
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
# Filename and lineno output for each entry, using an output format # Filename and lineno output for each entry, using an output format
# that most editors understand. # that most editors understand.
@ -1185,7 +1230,7 @@ class ReprFileLocation(TerminalRepr):
tw.line(f":{self.lineno}: {msg}") tw.line(f":{self.lineno}: {msg}")
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprLocals(TerminalRepr): class ReprLocals(TerminalRepr):
lines: Sequence[str] lines: Sequence[str]
@ -1194,7 +1239,7 @@ class ReprLocals(TerminalRepr):
tw.line(indent + line) tw.line(indent + line)
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprFuncArgs(TerminalRepr): class ReprFuncArgs(TerminalRepr):
args: Sequence[Tuple[str, object]] args: Sequence[Tuple[str, object]]

View File

109
src/_pytest/_py/error.py Normal file
View File

@ -0,0 +1,109 @@
"""create errno-specific classes for IO or os calls."""
from __future__ import annotations
import errno
import os
import sys
from typing import Callable
from typing import TYPE_CHECKING
from typing import TypeVar
if TYPE_CHECKING:
from typing_extensions import ParamSpec
P = ParamSpec("P")
R = TypeVar("R")
class Error(EnvironmentError):
def __repr__(self) -> str:
return "{}.{} {!r}: {} ".format(
self.__class__.__module__,
self.__class__.__name__,
self.__class__.__doc__,
" ".join(map(str, self.args)),
# repr(self.args)
)
def __str__(self) -> str:
s = "[{}]: {}".format(
self.__class__.__doc__,
" ".join(map(str, self.args)),
)
return s
_winerrnomap = {
2: errno.ENOENT,
3: errno.ENOENT,
17: errno.EEXIST,
18: errno.EXDEV,
13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable
22: errno.ENOTDIR,
20: errno.ENOTDIR,
267: errno.ENOTDIR,
5: errno.EACCES, # anything better?
}
class ErrorMaker:
"""lazily provides Exception classes for each possible POSIX errno
(as defined per the 'errno' module). All such instances
subclass EnvironmentError.
"""
_errno2class: dict[int, type[Error]] = {}
def __getattr__(self, name: str) -> type[Error]:
if name[0] == "_":
raise AttributeError(name)
eno = getattr(errno, name)
cls = self._geterrnoclass(eno)
setattr(self, name, cls)
return cls
def _geterrnoclass(self, eno: int) -> type[Error]:
try:
return self._errno2class[eno]
except KeyError:
clsname = errno.errorcode.get(eno, "UnknownErrno%d" % (eno,))
errorcls = type(
clsname,
(Error,),
{"__module__": "py.error", "__doc__": os.strerror(eno)},
)
self._errno2class[eno] = errorcls
return errorcls
def checked_call(
self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
) -> R:
"""Call a function and raise an errno-exception if applicable."""
__tracebackhide__ = True
try:
return func(*args, **kwargs)
except Error:
raise
except OSError as value:
if not hasattr(value, "errno"):
raise
errno = value.errno
if sys.platform == "win32":
try:
cls = self._geterrnoclass(_winerrnomap[errno])
except KeyError:
raise value
else:
# we are not on Windows, or we got a proper OSError
cls = self._geterrnoclass(errno)
raise cls(f"{func.__name__}{args!r}")
_error_maker = ErrorMaker()
checked_call = _error_maker.checked_call
def __getattr__(attr: str) -> type[Error]:
return getattr(_error_maker, attr) # type: ignore[no-any-return]

1475
src/_pytest/_py/path.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -44,10 +44,20 @@ from _pytest.stash import StashKey
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.assertion import AssertionState from _pytest.assertion import AssertionState
if sys.version_info >= (3, 8):
namedExpr = ast.NamedExpr
astNameConstant = ast.Constant
astStr = ast.Constant
astNum = ast.Constant
else:
namedExpr = ast.Expr
astNameConstant = ast.NameConstant
astStr = ast.Str
astNum = ast.Num
assertstate_key = StashKey["AssertionState"]() assertstate_key = StashKey["AssertionState"]()
# pytest caches rewritten pycs in pycache dirs # pytest caches rewritten pycs in pycache dirs
PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}" PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_EXT = ".py" + (__debug__ and "c" or "o")
@ -180,7 +190,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
for initial_path in self.session._initialpaths: for initial_path in self.session._initialpaths:
# Make something as c:/projects/my_project/path.py -> # Make something as c:/projects/my_project/path.py ->
# ['c:', 'projects', 'my_project', 'path.py'] # ['c:', 'projects', 'my_project', 'path.py']
parts = str(initial_path).split(os.path.sep) parts = str(initial_path).split(os.sep)
# add 'path' to basenames to be checked. # add 'path' to basenames to be checked.
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0]) self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
@ -274,8 +284,12 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
return f.read() return f.read()
if sys.version_info >= (3, 10): if sys.version_info >= (3, 10):
if sys.version_info >= (3, 12):
from importlib.resources.abc import TraversableResources
else:
from importlib.abc import TraversableResources
def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore
if sys.version_info < (3, 11): if sys.version_info < (3, 11):
from importlib.readers import FileReader from importlib.readers import FileReader
else: else:
@ -631,8 +645,12 @@ class AssertionRewriter(ast.NodeVisitor):
.push_format_context() and .pop_format_context() which allows .push_format_context() and .pop_format_context() which allows
to build another %-formatted string while already building one. to build another %-formatted string while already building one.
This state is reset on every new assert statement visited and used :variables_overwrite: A dict filled with references to variables
by the other visitors. that change value within an assert. This happens when a variable is
reassigned with the walrus operator
This state, except the variables_overwrite, is reset on every new assert
statement visited and used by the other visitors.
""" """
def __init__( def __init__(
@ -648,6 +666,7 @@ class AssertionRewriter(ast.NodeVisitor):
else: else:
self.enable_assertion_pass_hook = False self.enable_assertion_pass_hook = False
self.source = source self.source = source
self.variables_overwrite: Dict[str, str] = {}
def run(self, mod: ast.Module) -> None: def run(self, mod: ast.Module) -> None:
"""Find all assert statements in *mod* and rewrite them.""" """Find all assert statements in *mod* and rewrite them."""
@ -662,14 +681,17 @@ class AssertionRewriter(ast.NodeVisitor):
if doc is not None and self.is_rewrite_disabled(doc): if doc is not None and self.is_rewrite_disabled(doc):
return return
pos = 0 pos = 0
lineno = 1 item = None
for item in mod.body: for item in mod.body:
if ( if (
expect_docstring expect_docstring
and isinstance(item, ast.Expr) and isinstance(item, ast.Expr)
and isinstance(item.value, ast.Str) and isinstance(item.value, astStr)
): ):
doc = item.value.s if sys.version_info >= (3, 8):
doc = item.value.value
else:
doc = item.value.s
if self.is_rewrite_disabled(doc): if self.is_rewrite_disabled(doc):
return return
expect_docstring = False expect_docstring = False
@ -801,7 +823,7 @@ class AssertionRewriter(ast.NodeVisitor):
current = self.stack.pop() current = self.stack.pop()
if self.stack: if self.stack:
self.explanation_specifiers = self.stack[-1] self.explanation_specifiers = self.stack[-1]
keys = [ast.Str(key) for key in current.keys()] keys = [astStr(key) for key in current.keys()]
format_dict = ast.Dict(keys, list(current.values())) format_dict = ast.Dict(keys, list(current.values()))
form = ast.BinOp(expl_expr, ast.Mod(), format_dict) form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
name = "@py_format" + str(next(self.variable_counter)) name = "@py_format" + str(next(self.variable_counter))
@ -855,16 +877,16 @@ class AssertionRewriter(ast.NodeVisitor):
negation = ast.UnaryOp(ast.Not(), top_condition) negation = ast.UnaryOp(ast.Not(), top_condition)
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
msg = self.pop_format_context(ast.Str(explanation)) msg = self.pop_format_context(astStr(explanation))
# Failed # Failed
if assert_.msg: if assert_.msg:
assertmsg = self.helper("_format_assertmsg", assert_.msg) assertmsg = self.helper("_format_assertmsg", assert_.msg)
gluestr = "\n>assert " gluestr = "\n>assert "
else: else:
assertmsg = ast.Str("") assertmsg = astStr("")
gluestr = "assert " gluestr = "assert "
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg) err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
err_name = ast.Name("AssertionError", ast.Load()) err_name = ast.Name("AssertionError", ast.Load())
fmt = self.helper("_format_explanation", err_msg) fmt = self.helper("_format_explanation", err_msg)
@ -880,8 +902,8 @@ class AssertionRewriter(ast.NodeVisitor):
hook_call_pass = ast.Expr( hook_call_pass = ast.Expr(
self.helper( self.helper(
"_call_assertion_pass", "_call_assertion_pass",
ast.Num(assert_.lineno), astNum(assert_.lineno),
ast.Str(orig), astStr(orig),
fmt_pass, fmt_pass,
) )
) )
@ -900,7 +922,7 @@ class AssertionRewriter(ast.NodeVisitor):
variables = [ variables = [
ast.Name(name, ast.Store()) for name in self.format_variables ast.Name(name, ast.Store()) for name in self.format_variables
] ]
clear_format = ast.Assign(variables, ast.NameConstant(None)) clear_format = ast.Assign(variables, astNameConstant(None))
self.statements.append(clear_format) self.statements.append(clear_format)
else: # Original assertion rewriting else: # Original assertion rewriting
@ -911,9 +933,9 @@ class AssertionRewriter(ast.NodeVisitor):
assertmsg = self.helper("_format_assertmsg", assert_.msg) assertmsg = self.helper("_format_assertmsg", assert_.msg)
explanation = "\n>assert " + explanation explanation = "\n>assert " + explanation
else: else:
assertmsg = ast.Str("") assertmsg = astStr("")
explanation = "assert " + explanation explanation = "assert " + explanation
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
msg = self.pop_format_context(template) msg = self.pop_format_context(template)
fmt = self.helper("_format_explanation", msg) fmt = self.helper("_format_explanation", msg)
err_name = ast.Name("AssertionError", ast.Load()) err_name = ast.Name("AssertionError", ast.Load())
@ -925,7 +947,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Clear temporary variables by setting them to None. # Clear temporary variables by setting them to None.
if self.variables: if self.variables:
variables = [ast.Name(name, ast.Store()) for name in self.variables] variables = [ast.Name(name, ast.Store()) for name in self.variables]
clear = ast.Assign(variables, ast.NameConstant(None)) clear = ast.Assign(variables, astNameConstant(None))
self.statements.append(clear) self.statements.append(clear)
# Fix locations (line numbers/column offsets). # Fix locations (line numbers/column offsets).
for stmt in self.statements: for stmt in self.statements:
@ -933,14 +955,26 @@ class AssertionRewriter(ast.NodeVisitor):
ast.copy_location(node, assert_) ast.copy_location(node, assert_)
return self.statements return self.statements
def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
# This method handles the 'walrus operator' repr of the target
# name if it's a local variable or _should_repr_global_name()
# thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
target_id = name.target.id # type: ignore[attr-defined]
inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), astStr(target_id))
return name, self.explanation_param(expr)
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]: def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
# Display the repr of the name if it's a local variable or # Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable. # _should_repr_global_name() thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], []) locs = ast.Call(self.builtin("locals"), [], [])
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs]) inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name) dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) expr = ast.IfExp(test, self.display(name), astStr(name.id))
return name, self.explanation_param(expr) return name, self.explanation_param(expr)
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
@ -959,10 +993,26 @@ class AssertionRewriter(ast.NodeVisitor):
# cond is set in a prior loop iteration below # cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
self.expl_stmts = fail_inner self.expl_stmts = fail_inner
# Check if the left operand is a namedExpr and the value has already been visited
if (
isinstance(v, ast.Compare)
and isinstance(v.left, namedExpr)
and v.left.target.id
in [
ast_expr.id
for ast_expr in boolop.values[:i]
if hasattr(ast_expr, "id")
]
):
pytest_temp = self.variable()
self.variables_overwrite[
v.left.target.id
] = v.left # type:ignore[assignment]
v.left.target.id = pytest_temp
self.push_format_context() self.push_format_context()
res, expl = self.visit(v) res, expl = self.visit(v)
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
expl_format = self.pop_format_context(ast.Str(expl)) expl_format = self.pop_format_context(astStr(expl))
call = ast.Call(app, [expl_format], []) call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call)) self.expl_stmts.append(ast.Expr(call))
if i < levels: if i < levels:
@ -974,7 +1024,7 @@ class AssertionRewriter(ast.NodeVisitor):
self.statements = body = inner self.statements = body = inner
self.statements = save self.statements = save
self.expl_stmts = fail_save self.expl_stmts = fail_save
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or)) expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
expl = self.pop_format_context(expl_template) expl = self.pop_format_context(expl_template)
return ast.Name(res_var, ast.Load()), self.explanation_param(expl) return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
@ -998,10 +1048,19 @@ class AssertionRewriter(ast.NodeVisitor):
new_args = [] new_args = []
new_kwargs = [] new_kwargs = []
for arg in call.args: for arg in call.args:
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
res, expl = self.visit(arg) res, expl = self.visit(arg)
arg_expls.append(expl) arg_expls.append(expl)
new_args.append(res) new_args.append(res)
for keyword in call.keywords: for keyword in call.keywords:
if (
isinstance(keyword.value, ast.Name)
and keyword.value.id in self.variables_overwrite
):
keyword.value = self.variables_overwrite[
keyword.value.id
] # type:ignore[assignment]
res, expl = self.visit(keyword.value) res, expl = self.visit(keyword.value)
new_kwargs.append(ast.keyword(keyword.arg, res)) new_kwargs.append(ast.keyword(keyword.arg, res))
if keyword.arg: if keyword.arg:
@ -1034,6 +1093,15 @@ class AssertionRewriter(ast.NodeVisitor):
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
self.push_format_context() self.push_format_context()
# We first check if we have overwritten a variable in the previous assert
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
comp.left = self.variables_overwrite[
comp.left.id
] # type:ignore[assignment]
if isinstance(comp.left, namedExpr):
self.variables_overwrite[
comp.left.target.id
] = comp.left # type:ignore[assignment]
left_res, left_expl = self.visit(comp.left) left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)): if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})" left_expl = f"({left_expl})"
@ -1045,14 +1113,23 @@ class AssertionRewriter(ast.NodeVisitor):
syms = [] syms = []
results = [left_res] results = [left_res]
for i, op, next_operand in it: for i, op, next_operand in it:
if (
isinstance(next_operand, namedExpr)
and isinstance(left_res, ast.Name)
and next_operand.target.id == left_res.id
):
next_operand.target.id = self.variable()
self.variables_overwrite[
left_res.id
] = next_operand # type:ignore[assignment]
next_res, next_expl = self.visit(next_operand) next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)): if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = f"({next_expl})" next_expl = f"({next_expl})"
results.append(next_res) results.append(next_res)
sym = BINOP_MAP[op.__class__] sym = BINOP_MAP[op.__class__]
syms.append(ast.Str(sym)) syms.append(astStr(sym))
expl = f"{left_expl} {sym} {next_expl}" expl = f"{left_expl} {sym} {next_expl}"
expls.append(ast.Str(expl)) expls.append(astStr(expl))
res_expr = ast.Compare(left_res, [op], [next_res]) res_expr = ast.Compare(left_res, [op], [next_res])
self.statements.append(ast.Assign([store_names[i]], res_expr)) self.statements.append(ast.Assign([store_names[i]], res_expr))
left_res, left_expl = next_res, next_expl left_res, left_expl = next_res, next_expl
@ -1068,6 +1145,7 @@ class AssertionRewriter(ast.NodeVisitor):
res: ast.expr = ast.BoolOp(ast.And(), load_names) res: ast.expr = ast.BoolOp(ast.And(), load_names)
else: else:
res = load_names[0] res = load_names[0]
return res, self.explanation_param(self.pop_format_context(expl_call)) return res, self.explanation_param(self.pop_format_context(expl_call))

View File

@ -38,9 +38,9 @@ def _truncate_explanation(
"""Truncate given list of strings that makes up the assertion explanation. """Truncate given list of strings that makes up the assertion explanation.
Truncates to either 8 lines, or 640 characters - whichever the input reaches Truncates to either 8 lines, or 640 characters - whichever the input reaches
first. The remaining lines will be replaced by a usage message. first, taking the truncation explanation into account. The remaining lines
will be replaced by a usage message.
""" """
if max_lines is None: if max_lines is None:
max_lines = DEFAULT_MAX_LINES max_lines = DEFAULT_MAX_LINES
if max_chars is None: if max_chars is None:
@ -48,35 +48,56 @@ def _truncate_explanation(
# Check if truncation required # Check if truncation required
input_char_count = len("".join(input_lines)) input_char_count = len("".join(input_lines))
if len(input_lines) <= max_lines and input_char_count <= max_chars: # The length of the truncation explanation depends on the number of lines
# removed but is at least 68 characters:
# The real value is
# 64 (for the base message:
# '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
# )
# + 1 (for plural)
# + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
# + 3 for the '...' added to the truncated line
# But if there's more than 100 lines it's very likely that we're going to
# truncate, so we don't need the exact value using log10.
tolerable_max_chars = (
max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
)
# The truncation explanation add two lines to the output
tolerable_max_lines = max_lines + 2
if (
len(input_lines) <= tolerable_max_lines
and input_char_count <= tolerable_max_chars
):
return input_lines return input_lines
# Truncate first to max_lines, and then truncate to max_chars if necessary
# Truncate first to max_lines, and then truncate to max_chars if max_chars
# is exceeded.
truncated_explanation = input_lines[:max_lines] truncated_explanation = input_lines[:max_lines]
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars) truncated_char = True
# We reevaluate the need to truncate chars following removal of some lines
# Add ellipsis to final line if len("".join(truncated_explanation)) > tolerable_max_chars:
truncated_explanation[-1] = truncated_explanation[-1] + "..." truncated_explanation = _truncate_by_char_count(
truncated_explanation, max_chars
# Append useful message to explanation )
truncated_line_count = len(input_lines) - len(truncated_explanation)
truncated_line_count += 1 # Account for the part-truncated final line
msg = "...Full output truncated"
if truncated_line_count == 1:
msg += f" ({truncated_line_count} line hidden)"
else: else:
msg += f" ({truncated_line_count} lines hidden)" truncated_char = False
msg += f", {USAGE_MSG}"
truncated_explanation.extend(["", str(msg)]) truncated_line_count = len(input_lines) - len(truncated_explanation)
return truncated_explanation if truncated_explanation[-1]:
# Add ellipsis and take into account part-truncated final line
truncated_explanation[-1] = truncated_explanation[-1] + "..."
if truncated_char:
# It's possible that we did not remove any char from this line
truncated_line_count += 1
else:
# Add proper ellipsis when we were able to fit a full line exactly
truncated_explanation[-1] = "..."
return truncated_explanation + [
"",
f"...Full output truncated ({truncated_line_count} line"
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
]
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]: def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
# Check if truncation required
if len("".join(input_lines)) <= max_chars:
return input_lines
# Find point at which input length exceeds total allowed length # Find point at which input length exceeds total allowed length
iterated_char_count = 0 iterated_char_count = 0
for iterated_index, input_line in enumerate(input_lines): for iterated_index, input_line in enumerate(input_lines):

View File

@ -1,6 +1,7 @@
"""Implementation of the cache provider.""" """Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external # This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version. # pytest-cache version.
import dataclasses
import json import json
import os import os
from pathlib import Path from pathlib import Path
@ -12,8 +13,6 @@ from typing import Optional
from typing import Set from typing import Set
from typing import Union from typing import Union
import attr
from .pathlib import resolve_from_str from .pathlib import resolve_from_str
from .pathlib import rm_rf from .pathlib import rm_rf
from .reports import CollectReport from .reports import CollectReport
@ -28,11 +27,10 @@ from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.main import Session from _pytest.main import Session
from _pytest.python import Module from _pytest.nodes import File
from _pytest.python import Package from _pytest.python import Package
from _pytest.reports import TestReport from _pytest.reports import TestReport
README_CONTENT = """\ README_CONTENT = """\
# pytest cache directory # # pytest cache directory #
@ -53,10 +51,12 @@ Signature: 8a477f597d28d172789f06886806bc55
@final @final
@attr.s(init=False, auto_attribs=True) @dataclasses.dataclass
class Cache: class Cache:
_cachedir: Path = attr.ib(repr=False) """Instance of the `cache` fixture."""
_config: Config = attr.ib(repr=False)
_cachedir: Path = dataclasses.field(repr=False)
_config: Config = dataclasses.field(repr=False)
# Sub-directory under cache-dir for directories created by `mkdir()`. # Sub-directory under cache-dir for directories created by `mkdir()`.
_CACHE_PREFIX_DIRS = "d" _CACHE_PREFIX_DIRS = "d"
@ -179,16 +179,22 @@ class Cache:
else: else:
cache_dir_exists_already = self._cachedir.exists() cache_dir_exists_already = self._cachedir.exists()
path.parent.mkdir(exist_ok=True, parents=True) path.parent.mkdir(exist_ok=True, parents=True)
except OSError: except OSError as exc:
self.warn("could not create cache path {path}", path=path, _ispytest=True) self.warn(
f"could not create cache path {path}: {exc}",
_ispytest=True,
)
return return
if not cache_dir_exists_already: if not cache_dir_exists_already:
self._ensure_supporting_files() self._ensure_supporting_files()
data = json.dumps(value, ensure_ascii=False, indent=2) data = json.dumps(value, ensure_ascii=False, indent=2)
try: try:
f = path.open("w", encoding="UTF-8") f = path.open("w", encoding="UTF-8")
except OSError: except OSError as exc:
self.warn("cache could not write path {path}", path=path, _ispytest=True) self.warn(
f"cache could not write path {path}: {exc}",
_ispytest=True,
)
else: else:
with f: with f:
f.write(data) f.write(data)
@ -213,22 +219,30 @@ class LFPluginCollWrapper:
@hookimpl(hookwrapper=True) @hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector): def pytest_make_collect_report(self, collector: nodes.Collector):
if isinstance(collector, Session): if isinstance(collector, (Session, Package)):
out = yield out = yield
res: CollectReport = out.get_result() res: CollectReport = out.get_result()
# Sort any lf-paths to the beginning. # Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths lf_paths = self.lfplugin._last_failed_paths
# Use stable sort to priorize last failed.
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
# Package.path is the __init__.py file, we need the directory.
if isinstance(node, Package):
path = node.path.parent
else:
path = node.path
return path in lf_paths
res.result = sorted( res.result = sorted(
res.result, res.result,
# use stable sort to priorize last failed key=sort_key,
key=lambda x: x.path in lf_paths,
reverse=True, reverse=True,
) )
return return
elif isinstance(collector, Module): elif isinstance(collector, File):
if collector.path in self.lfplugin._last_failed_paths: if collector.path in self.lfplugin._last_failed_paths:
out = yield out = yield
res = out.get_result() res = out.get_result()
@ -266,10 +280,9 @@ class LFPluginCollSkipfiles:
def pytest_make_collect_report( def pytest_make_collect_report(
self, collector: nodes.Collector self, collector: nodes.Collector
) -> Optional[CollectReport]: ) -> Optional[CollectReport]:
# Packages are Modules, but _last_failed_paths only contains # Packages are Files, but we only want to skip test-bearing Files,
# test-bearing paths and doesn't try to include the paths of their # so don't filter Packages.
# packages, so don't filter them. if isinstance(collector, File) and not isinstance(collector, Package):
if isinstance(collector, Module) and not isinstance(collector, Package):
if collector.path not in self.lfplugin._last_failed_paths: if collector.path not in self.lfplugin._last_failed_paths:
self.lfplugin._skipped_files += 1 self.lfplugin._skipped_files += 1
@ -299,9 +312,14 @@ class LFPlugin:
) )
def get_last_failed_paths(self) -> Set[Path]: def get_last_failed_paths(self) -> Set[Path]:
"""Return a set with all Paths()s of the previously failed nodeids.""" """Return a set with all Paths of the previously failed nodeids and
their parents."""
rootpath = self.config.rootpath rootpath = self.config.rootpath
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed} result = set()
for nodeid in self.lastfailed:
path = rootpath / nodeid.split("::")[0]
result.add(path)
result.update(path.parents)
return {x for x in result if x.exists()} return {x for x in result if x.exists()}
def pytest_report_collectionfinish(self) -> Optional[str]: def pytest_report_collectionfinish(self) -> Optional[str]:
@ -492,7 +510,7 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
if config.option.cacheshow: if config.option.cacheshow and not config.option.help:
from _pytest.main import wrap_session from _pytest.main import wrap_session
return wrap_session(config, cacheshow) return wrap_session(config, cacheshow)

View File

@ -1,19 +1,26 @@
"""Per-test stdout/stderr capturing mechanism.""" """Per-test stdout/stderr capturing mechanism."""
import abc
import collections
import contextlib import contextlib
import functools
import io import io
import os import os
import sys import sys
from io import UnsupportedOperation from io import UnsupportedOperation
from tempfile import TemporaryFile from tempfile import TemporaryFile
from types import TracebackType
from typing import Any from typing import Any
from typing import AnyStr from typing import AnyStr
from typing import BinaryIO
from typing import Generator from typing import Generator
from typing import Generic from typing import Generic
from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List
from typing import NamedTuple
from typing import Optional from typing import Optional
from typing import TextIO from typing import TextIO
from typing import Tuple from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
@ -29,6 +36,7 @@ from _pytest.nodes import File
from _pytest.nodes import Item from _pytest.nodes import Item
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal from typing_extensions import Literal
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
@ -185,19 +193,27 @@ class TeeCaptureIO(CaptureIO):
return self._other.write(s) return self._other.write(s)
class DontReadFromInput: class DontReadFromInput(TextIO):
encoding = None @property
def encoding(self) -> str:
return sys.__stdin__.encoding
def read(self, *args): def read(self, size: int = -1) -> str:
raise OSError( raise OSError(
"pytest: reading from stdin while output is captured! Consider using `-s`." "pytest: reading from stdin while output is captured! Consider using `-s`."
) )
readline = read readline = read
readlines = read
__next__ = read
def __iter__(self): def __next__(self) -> str:
return self.readline()
def readlines(self, hint: Optional[int] = -1) -> List[str]:
raise OSError(
"pytest: reading from stdin while output is captured! Consider using `-s`."
)
def __iter__(self) -> Iterator[str]:
return self return self
def fileno(self) -> int: def fileno(self) -> int:
@ -215,7 +231,7 @@ class DontReadFromInput:
def readable(self) -> bool: def readable(self) -> bool:
return False return False
def seek(self, offset: int) -> int: def seek(self, offset: int, whence: int = 0) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)") raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
def seekable(self) -> bool: def seekable(self) -> bool:
@ -224,41 +240,104 @@ class DontReadFromInput:
def tell(self) -> int: def tell(self) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()") raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
def truncate(self, size: int) -> None: def truncate(self, size: Optional[int] = None) -> int:
raise UnsupportedOperation("cannont truncate stdin") raise UnsupportedOperation("cannot truncate stdin")
def write(self, *args) -> None: def write(self, data: str) -> int:
raise UnsupportedOperation("cannot write to stdin") raise UnsupportedOperation("cannot write to stdin")
def writelines(self, *args) -> None: def writelines(self, lines: Iterable[str]) -> None:
raise UnsupportedOperation("Cannot write to stdin") raise UnsupportedOperation("Cannot write to stdin")
def writable(self) -> bool: def writable(self) -> bool:
return False return False
@property def __enter__(self) -> "DontReadFromInput":
def buffer(self):
return self return self
def __exit__(
self,
type: Optional[Type[BaseException]],
value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
pass
@property
def buffer(self) -> BinaryIO:
# The str/bytes doesn't actually matter in this type, so OK to fake.
return self # type: ignore[return-value]
# Capture classes. # Capture classes.
class CaptureBase(abc.ABC, Generic[AnyStr]):
EMPTY_BUFFER: AnyStr
@abc.abstractmethod
def __init__(self, fd: int) -> None:
raise NotImplementedError()
@abc.abstractmethod
def start(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def done(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def suspend(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def resume(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def writeorg(self, data: AnyStr) -> None:
raise NotImplementedError()
@abc.abstractmethod
def snap(self) -> AnyStr:
raise NotImplementedError()
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
class NoCapture: class NoCapture(CaptureBase[str]):
EMPTY_BUFFER = None EMPTY_BUFFER = ""
__init__ = start = done = suspend = resume = lambda *args: None
def __init__(self, fd: int) -> None:
pass
def start(self) -> None:
pass
def done(self) -> None:
pass
def suspend(self) -> None:
pass
def resume(self) -> None:
pass
def snap(self) -> str:
return ""
def writeorg(self, data: str) -> None:
pass
class SysCaptureBinary: class SysCaptureBase(CaptureBase[AnyStr]):
def __init__(
EMPTY_BUFFER = b"" self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
) -> None:
def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
name = patchsysdict[fd] name = patchsysdict[fd]
self._old = getattr(sys, name) self._old: TextIO = getattr(sys, name)
self.name = name self.name = name
if tmpfile is None: if tmpfile is None:
if name == "stdin": if name == "stdin":
@ -298,14 +377,6 @@ class SysCaptureBinary:
setattr(sys, self.name, self.tmpfile) setattr(sys, self.name, self.tmpfile)
self._state = "started" self._state = "started"
def snap(self):
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def done(self) -> None: def done(self) -> None:
self._assert_state("done", ("initialized", "started", "suspended", "done")) self._assert_state("done", ("initialized", "started", "suspended", "done"))
if self._state == "done": if self._state == "done":
@ -327,36 +398,43 @@ class SysCaptureBinary:
setattr(sys, self.name, self.tmpfile) setattr(sys, self.name, self.tmpfile)
self._state = "started" self._state = "started"
def writeorg(self, data) -> None:
class SysCaptureBinary(SysCaptureBase[bytes]):
EMPTY_BUFFER = b""
def snap(self) -> bytes:
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def writeorg(self, data: bytes) -> None:
self._assert_state("writeorg", ("started", "suspended")) self._assert_state("writeorg", ("started", "suspended"))
self._old.flush() self._old.flush()
self._old.buffer.write(data) self._old.buffer.write(data)
self._old.buffer.flush() self._old.buffer.flush()
class SysCapture(SysCaptureBinary): class SysCapture(SysCaptureBase[str]):
EMPTY_BUFFER = "" # type: ignore[assignment] EMPTY_BUFFER = ""
def snap(self): def snap(self) -> str:
self._assert_state("snap", ("started", "suspended"))
assert isinstance(self.tmpfile, CaptureIO)
res = self.tmpfile.getvalue() res = self.tmpfile.getvalue()
self.tmpfile.seek(0) self.tmpfile.seek(0)
self.tmpfile.truncate() self.tmpfile.truncate()
return res return res
def writeorg(self, data): def writeorg(self, data: str) -> None:
self._assert_state("writeorg", ("started", "suspended")) self._assert_state("writeorg", ("started", "suspended"))
self._old.write(data) self._old.write(data)
self._old.flush() self._old.flush()
class FDCaptureBinary: class FDCaptureBase(CaptureBase[AnyStr]):
"""Capture IO to/from a given OS-level file descriptor.
snap() produces `bytes`.
"""
EMPTY_BUFFER = b""
def __init__(self, targetfd: int) -> None: def __init__(self, targetfd: int) -> None:
self.targetfd = targetfd self.targetfd = targetfd
@ -382,7 +460,7 @@ class FDCaptureBinary:
if targetfd == 0: if targetfd == 0:
self.tmpfile = open(os.devnull, encoding="utf-8") self.tmpfile = open(os.devnull, encoding="utf-8")
self.syscapture = SysCapture(targetfd) self.syscapture: CaptureBase[str] = SysCapture(targetfd)
else: else:
self.tmpfile = EncodedFile( self.tmpfile = EncodedFile(
TemporaryFile(buffering=0), TemporaryFile(buffering=0),
@ -394,7 +472,7 @@ class FDCaptureBinary:
if targetfd in patchsysdict: if targetfd in patchsysdict:
self.syscapture = SysCapture(targetfd, self.tmpfile) self.syscapture = SysCapture(targetfd, self.tmpfile)
else: else:
self.syscapture = NoCapture() self.syscapture = NoCapture(targetfd)
self._state = "initialized" self._state = "initialized"
@ -421,14 +499,6 @@ class FDCaptureBinary:
self.syscapture.start() self.syscapture.start()
self._state = "started" self._state = "started"
def snap(self):
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def done(self) -> None: def done(self) -> None:
"""Stop capturing, restore streams, return original capture file, """Stop capturing, restore streams, return original capture file,
seeked to position zero.""" seeked to position zero."""
@ -461,22 +531,38 @@ class FDCaptureBinary:
os.dup2(self.tmpfile.fileno(), self.targetfd) os.dup2(self.tmpfile.fileno(), self.targetfd)
self._state = "started" self._state = "started"
def writeorg(self, data):
class FDCaptureBinary(FDCaptureBase[bytes]):
"""Capture IO to/from a given OS-level file descriptor.
snap() produces `bytes`.
"""
EMPTY_BUFFER = b""
def snap(self) -> bytes:
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def writeorg(self, data: bytes) -> None:
"""Write to original file descriptor.""" """Write to original file descriptor."""
self._assert_state("writeorg", ("started", "suspended")) self._assert_state("writeorg", ("started", "suspended"))
os.write(self.targetfd_save, data) os.write(self.targetfd_save, data)
class FDCapture(FDCaptureBinary): class FDCapture(FDCaptureBase[str]):
"""Capture IO to/from a given OS-level file descriptor. """Capture IO to/from a given OS-level file descriptor.
snap() produces text. snap() produces text.
""" """
# Ignore type because it doesn't match the type in the superclass (bytes). EMPTY_BUFFER = ""
EMPTY_BUFFER = "" # type: ignore
def snap(self): def snap(self) -> str:
self._assert_state("snap", ("started", "suspended")) self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0) self.tmpfile.seek(0)
res = self.tmpfile.read() res = self.tmpfile.read()
@ -484,77 +570,49 @@ class FDCapture(FDCaptureBinary):
self.tmpfile.truncate() self.tmpfile.truncate()
return res return res
def writeorg(self, data): def writeorg(self, data: str) -> None:
"""Write to original file descriptor.""" """Write to original file descriptor."""
super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream self._assert_state("writeorg", ("started", "suspended"))
# XXX use encoding of original stream
os.write(self.targetfd_save, data.encode("utf-8"))
# MultiCapture # MultiCapture
# This class was a namedtuple, but due to mypy limitation[0] it could not be # Generic NamedTuple only supported since Python 3.11.
# made generic, so was replaced by a regular class which tries to emulate the if sys.version_info >= (3, 11) or TYPE_CHECKING:
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
# make it a namedtuple again.
# [0]: https://github.com/python/mypy/issues/685
@final
@functools.total_ordering
class CaptureResult(Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`."""
__slots__ = ("out", "err") @final
class CaptureResult(NamedTuple, Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`."""
def __init__(self, out: AnyStr, err: AnyStr) -> None: out: AnyStr
self.out: AnyStr = out err: AnyStr
self.err: AnyStr = err
def __len__(self) -> int: else:
return 2
def __iter__(self) -> Iterator[AnyStr]: class CaptureResult(
return iter((self.out, self.err)) collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
):
"""The result of :method:`CaptureFixture.readouterr`."""
def __getitem__(self, item: int) -> AnyStr: __slots__ = ()
return tuple(self)[item]
def _replace(
self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None
) -> "CaptureResult[AnyStr]":
return CaptureResult(
out=self.out if out is None else out, err=self.err if err is None else err
)
def count(self, value: AnyStr) -> int:
return tuple(self).count(value)
def index(self, value) -> int:
return tuple(self).index(value)
def __eq__(self, other: object) -> bool:
if not isinstance(other, (CaptureResult, tuple)):
return NotImplemented
return tuple(self) == tuple(other)
def __hash__(self) -> int:
return hash(tuple(self))
def __lt__(self, other: object) -> bool:
if not isinstance(other, (CaptureResult, tuple)):
return NotImplemented
return tuple(self) < tuple(other)
def __repr__(self) -> str:
return f"CaptureResult(out={self.out!r}, err={self.err!r})"
class MultiCapture(Generic[AnyStr]): class MultiCapture(Generic[AnyStr]):
_state = None _state = None
_in_suspended = False _in_suspended = False
def __init__(self, in_, out, err) -> None: def __init__(
self.in_ = in_ self,
self.out = out in_: Optional[CaptureBase[AnyStr]],
self.err = err out: Optional[CaptureBase[AnyStr]],
err: Optional[CaptureBase[AnyStr]],
) -> None:
self.in_: Optional[CaptureBase[AnyStr]] = in_
self.out: Optional[CaptureBase[AnyStr]] = out
self.err: Optional[CaptureBase[AnyStr]] = err
def __repr__(self) -> str: def __repr__(self) -> str:
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format( return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
@ -578,8 +636,10 @@ class MultiCapture(Generic[AnyStr]):
"""Pop current snapshot out/err capture and flush to orig streams.""" """Pop current snapshot out/err capture and flush to orig streams."""
out, err = self.readouterr() out, err = self.readouterr()
if out: if out:
assert self.out is not None
self.out.writeorg(out) self.out.writeorg(out)
if err: if err:
assert self.err is not None
self.err.writeorg(err) self.err.writeorg(err)
return out, err return out, err
@ -600,6 +660,7 @@ class MultiCapture(Generic[AnyStr]):
if self.err: if self.err:
self.err.resume() self.err.resume()
if self._in_suspended: if self._in_suspended:
assert self.in_ is not None
self.in_.resume() self.in_.resume()
self._in_suspended = False self._in_suspended = False
@ -622,7 +683,8 @@ class MultiCapture(Generic[AnyStr]):
def readouterr(self) -> CaptureResult[AnyStr]: def readouterr(self) -> CaptureResult[AnyStr]:
out = self.out.snap() if self.out else "" out = self.out.snap() if self.out else ""
err = self.err.snap() if self.err else "" err = self.err.snap() if self.err else ""
return CaptureResult(out, err) # TODO: This type error is real, need to fix.
return CaptureResult(out, err) # type: ignore[arg-type]
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
@ -662,7 +724,7 @@ class CaptureManager:
""" """
def __init__(self, method: "_CaptureMethod") -> None: def __init__(self, method: "_CaptureMethod") -> None:
self._method = method self._method: Final = method
self._global_capturing: Optional[MultiCapture[str]] = None self._global_capturing: Optional[MultiCapture[str]] = None
self._capture_fixture: Optional[CaptureFixture[Any]] = None self._capture_fixture: Optional[CaptureFixture[Any]] = None
@ -831,14 +893,18 @@ class CaptureFixture(Generic[AnyStr]):
:fixture:`capfd` and :fixture:`capfdbinary` fixtures.""" :fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
def __init__( def __init__(
self, captureclass, request: SubRequest, *, _ispytest: bool = False self,
captureclass: Type[CaptureBase[AnyStr]],
request: SubRequest,
*,
_ispytest: bool = False,
) -> None: ) -> None:
check_ispytest(_ispytest) check_ispytest(_ispytest)
self.captureclass = captureclass self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
self.request = request self.request = request
self._capture: Optional[MultiCapture[AnyStr]] = None self._capture: Optional[MultiCapture[AnyStr]] = None
self._captured_out = self.captureclass.EMPTY_BUFFER self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
def _start(self) -> None: def _start(self) -> None:
if self._capture is None: if self._capture is None:
@ -893,7 +959,9 @@ class CaptureFixture(Generic[AnyStr]):
@contextlib.contextmanager @contextlib.contextmanager
def disabled(self) -> Generator[None, None, None]: def disabled(self) -> Generator[None, None, None]:
"""Temporarily disable capturing while inside the ``with`` block.""" """Temporarily disable capturing while inside the ``with`` block."""
capmanager = self.request.config.pluginmanager.getplugin("capturemanager") capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
"capturemanager"
)
with capmanager.global_and_fixture_disabled(): with capmanager.global_and_fixture_disabled():
yield yield
@ -920,8 +988,8 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
captured = capsys.readouterr() captured = capsys.readouterr()
assert captured.out == "hello\n" assert captured.out == "hello\n"
""" """
capman = request.config.pluginmanager.getplugin("capturemanager") capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True) capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture) capman.set_fixture(capture_fixture)
capture_fixture._start() capture_fixture._start()
yield capture_fixture yield capture_fixture
@ -948,8 +1016,8 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
captured = capsysbinary.readouterr() captured = capsysbinary.readouterr()
assert captured.out == b"hello\n" assert captured.out == b"hello\n"
""" """
capman = request.config.pluginmanager.getplugin("capturemanager") capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True) capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture) capman.set_fixture(capture_fixture)
capture_fixture._start() capture_fixture._start()
yield capture_fixture yield capture_fixture
@ -976,8 +1044,8 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
captured = capfd.readouterr() captured = capfd.readouterr()
assert captured.out == "hello\n" assert captured.out == "hello\n"
""" """
capman = request.config.pluginmanager.getplugin("capturemanager") capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True) capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture) capman.set_fixture(capture_fixture)
capture_fixture._start() capture_fixture._start()
yield capture_fixture yield capture_fixture
@ -1005,8 +1073,8 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
assert captured.out == b"hello\n" assert captured.out == b"hello\n"
""" """
capman = request.config.pluginmanager.getplugin("capturemanager") capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True) capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture) capman.set_fixture(capture_fixture)
capture_fixture._start() capture_fixture._start()
yield capture_fixture yield capture_fixture

View File

@ -1,4 +1,7 @@
"""Python version compatibility code.""" """Python version compatibility code."""
from __future__ import annotations
import dataclasses
import enum import enum
import functools import functools
import inspect import inspect
@ -11,13 +14,9 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import Generic from typing import Generic
from typing import NoReturn from typing import NoReturn
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union
import attr
import py import py
# fmt: off # fmt: off
@ -46,7 +45,7 @@ LEGACY_PATH = py.path. local
# fmt: on # fmt: on
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH: def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
"""Internal wrapper to prepare lazy proxies for legacy_path instances""" """Internal wrapper to prepare lazy proxies for legacy_path instances"""
return LEGACY_PATH(path) return LEGACY_PATH(path)
@ -56,7 +55,7 @@ def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum): class NotSetType(enum.Enum):
token = 0 token = 0
NOTSET: "Final" = NotSetType.token # noqa: E305 NOTSET: Final = NotSetType.token # noqa: E305
# fmt: on # fmt: on
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
@ -94,7 +93,7 @@ def is_async_function(func: object) -> bool:
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
def getlocation(function, curdir: Optional[str] = None) -> str: def getlocation(function, curdir: str | None = None) -> str:
function = get_real_func(function) function = get_real_func(function)
fn = Path(inspect.getfile(function)) fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno lineno = function.__code__.co_firstlineno
@ -132,8 +131,8 @@ def getfuncargnames(
*, *,
name: str = "", name: str = "",
is_method: bool = False, is_method: bool = False,
cls: Optional[type] = None, cls: type | None = None,
) -> Tuple[str, ...]: ) -> tuple[str, ...]:
"""Return the names of a function's mandatory arguments. """Return the names of a function's mandatory arguments.
Should return the names of all function arguments that: Should return the names of all function arguments that:
@ -197,7 +196,7 @@ def getfuncargnames(
return arg_names return arg_names
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]: def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
# Note: this code intentionally mirrors the code at the beginning of # Note: this code intentionally mirrors the code at the beginning of
# getfuncargnames, to get the arguments which were excluded from its result # getfuncargnames, to get the arguments which were excluded from its result
# because they had default values. # because they had default values.
@ -228,7 +227,7 @@ def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace") return val.decode("ascii", "backslashreplace")
def ascii_escaped(val: Union[bytes, str]) -> str: def ascii_escaped(val: bytes | str) -> str:
r"""If val is pure ASCII, return it as an str, otherwise, escape r"""If val is pure ASCII, return it as an str, otherwise, escape
bytes objects into a sequence of escaped bytes: bytes objects into a sequence of escaped bytes:
@ -252,7 +251,7 @@ def ascii_escaped(val: Union[bytes, str]) -> str:
return _translate_non_printable(ret) return _translate_non_printable(ret)
@attr.s @dataclasses.dataclass
class _PytestWrapper: class _PytestWrapper:
"""Dummy wrapper around a function object for internal use only. """Dummy wrapper around a function object for internal use only.
@ -261,7 +260,7 @@ class _PytestWrapper:
decorator to issue warnings when the fixture function is called directly. decorator to issue warnings when the fixture function is called directly.
""" """
obj = attr.ib() obj: Any
def get_real_func(obj): def get_real_func(obj):
@ -355,7 +354,6 @@ else:
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
from functools import cached_property as cached_property from functools import cached_property as cached_property
else: else:
from typing import Type
class cached_property(Generic[_S, _T]): class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__") __slots__ = ("func", "__doc__")
@ -366,12 +364,12 @@ else:
@overload @overload
def __get__( def __get__(
self, instance: None, owner: Optional[Type[_S]] = ... self, instance: None, owner: type[_S] | None = ...
) -> "cached_property[_S, _T]": ) -> cached_property[_S, _T]:
... ...
@overload @overload
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T: def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
... ...
def __get__(self, instance, owner=None): def __get__(self, instance, owner=None):
@ -381,6 +379,18 @@ else:
return value return value
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 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
# Perform exhaustiveness checking. # Perform exhaustiveness checking.
# #
# Consider this example: # Consider this example:

View File

@ -2,6 +2,7 @@
import argparse import argparse
import collections.abc import collections.abc
import copy import copy
import dataclasses
import enum import enum
import glob import glob
import inspect import inspect
@ -34,7 +35,6 @@ from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import attr
from pluggy import HookimplMarker from pluggy import HookimplMarker
from pluggy import HookspecMarker from pluggy import HookspecMarker
from pluggy import PluginManager from pluggy import PluginManager
@ -49,7 +49,7 @@ from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final from _pytest.compat import final
from _pytest.compat import importlib_metadata from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
@ -62,7 +62,6 @@ from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
from .argparsing import Argument from .argparsing import Argument
@ -527,7 +526,13 @@ class PytestPluginManager(PluginManager):
# Internal API for local conftest plugin handling. # Internal API for local conftest plugin handling.
# #
def _set_initial_conftests( def _set_initial_conftests(
self, namespace: argparse.Namespace, rootpath: Path self,
args: Sequence[Union[str, Path]],
pyargs: bool,
noconftest: bool,
rootpath: Path,
confcutdir: Optional[Path],
importmode: Union[ImportMode, str],
) -> None: ) -> None:
"""Load initial conftest files given a preparsed "namespace". """Load initial conftest files given a preparsed "namespace".
@ -537,27 +542,29 @@ class PytestPluginManager(PluginManager):
common options will not confuse our logic here. common options will not confuse our logic here.
""" """
current = Path.cwd() current = Path.cwd()
self._confcutdir = ( self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
absolutepath(current / namespace.confcutdir) self._noconftest = noconftest
if namespace.confcutdir self._using_pyargs = pyargs
else None
)
self._noconftest = namespace.noconftest
self._using_pyargs = namespace.pyargs
testpaths = namespace.file_or_dir
foundanchor = False foundanchor = False
for testpath in testpaths: for intitial_path in args:
path = str(testpath) path = str(intitial_path)
# remove node-id syntax # remove node-id syntax
i = path.find("::") i = path.find("::")
if i != -1: if i != -1:
path = path[:i] path = path[:i]
anchor = absolutepath(current / path) anchor = absolutepath(current / path)
if anchor.exists(): # we found some file object
self._try_load_conftest(anchor, namespace.importmode, rootpath) # 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:
self._try_load_conftest(anchor, importmode, rootpath)
foundanchor = True foundanchor = True
if not foundanchor: if not foundanchor:
self._try_load_conftest(current, namespace.importmode, rootpath) self._try_load_conftest(current, importmode, rootpath)
def _is_in_confcutdir(self, path: Path) -> bool: def _is_in_confcutdir(self, path: Path) -> bool:
"""Whether a path is within the confcutdir. """Whether a path is within the confcutdir.
@ -697,6 +704,7 @@ class PytestPluginManager(PluginManager):
parg = opt[2:] parg = opt[2:]
else: else:
continue continue
parg = parg.strip()
if exclude_only and not parg.startswith("no:"): if exclude_only and not parg.startswith("no:"):
continue continue
self.consider_pluginarg(parg) self.consider_pluginarg(parg)
@ -886,10 +894,6 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
yield from _iter_rewritable_modules(new_package_files) yield from _iter_rewritable_modules(new_package_files)
def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
return tuple(args)
@final @final
class Config: class Config:
"""Access to configuration values, pluginmanager and plugin hooks. """Access to configuration values, pluginmanager and plugin hooks.
@ -903,7 +907,7 @@ class Config:
""" """
@final @final
@attr.s(frozen=True, auto_attribs=True) @dataclasses.dataclass(frozen=True)
class InvocationParams: class InvocationParams:
"""Holds parameters passed during :func:`pytest.main`. """Holds parameters passed during :func:`pytest.main`.
@ -919,13 +923,24 @@ class Config:
Plugins accessing ``InvocationParams`` must be aware of that. Plugins accessing ``InvocationParams`` must be aware of that.
""" """
args: Tuple[str, ...] = attr.ib(converter=_args_converter) args: Tuple[str, ...]
"""The command-line arguments as passed to :func:`pytest.main`.""" """The command-line arguments as passed to :func:`pytest.main`."""
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
"""Extra plugins, might be `None`.""" """Extra plugins, might be `None`."""
dir: Path dir: Path
"""The directory from which :func:`pytest.main` was invoked.""" """The directory from which :func:`pytest.main` was invoked."""
def __init__(
self,
*,
args: Iterable[str],
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]],
dir: Path,
) -> None:
object.__setattr__(self, "args", tuple(args))
object.__setattr__(self, "plugins", plugins)
object.__setattr__(self, "dir", dir)
class ArgsSource(enum.Enum): class ArgsSource(enum.Enum):
"""Indicates the source of the test arguments. """Indicates the source of the test arguments.
@ -998,6 +1013,8 @@ class Config:
self.hook.pytest_addoption.call_historic( self.hook.pytest_addoption.call_historic(
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
) )
self.args_source = Config.ArgsSource.ARGS
self.args: List[str] = []
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.cacheprovider import Cache from _pytest.cacheprovider import Cache
@ -1057,7 +1074,6 @@ class Config:
try: try:
self.parse(args) self.parse(args)
except UsageError: except UsageError:
# Handle --version and --help here in a minimal fashion. # Handle --version and --help here in a minimal fashion.
# This gets done via helpconfig normally, but its # This gets done via helpconfig normally, but its
# pytest_cmdline_main is not called in case of errors. # pytest_cmdline_main is not called in case of errors.
@ -1122,8 +1138,25 @@ class Config:
@hookimpl(trylast=True) @hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config: "Config") -> None: def pytest_load_initial_conftests(self, early_config: "Config") -> None:
# We haven't fully parsed the command line arguments yet, so
# early_config.args it not set yet. But we need it for
# discovering the initial conftests. So "pre-run" the logic here.
# It will be done for real in `parse()`.
args, args_source = early_config._decide_args(
args=early_config.known_args_namespace.file_or_dir,
pyargs=early_config.known_args_namespace.pyargs,
testpaths=early_config.getini("testpaths"),
invocation_dir=early_config.invocation_params.dir,
rootpath=early_config.rootpath,
warn=False,
)
self.pluginmanager._set_initial_conftests( self.pluginmanager._set_initial_conftests(
early_config.known_args_namespace, rootpath=early_config.rootpath args=args,
pyargs=early_config.known_args_namespace.pyargs,
noconftest=early_config.known_args_namespace.noconftest,
rootpath=early_config.rootpath,
confcutdir=early_config.known_args_namespace.confcutdir,
importmode=early_config.known_args_namespace.importmode,
) )
def _initini(self, args: Sequence[str]) -> None: def _initini(self, args: Sequence[str]) -> None:
@ -1203,6 +1236,49 @@ class Config:
return args return args
def _decide_args(
self,
*,
args: List[str],
pyargs: List[str],
testpaths: List[str],
invocation_dir: Path,
rootpath: Path,
warn: bool,
) -> Tuple[List[str], ArgsSource]:
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
:param warn: Whether can issue warnings.
"""
if args:
source = Config.ArgsSource.ARGS
result = args
else:
if invocation_dir == rootpath:
source = Config.ArgsSource.TESTPATHS
if pyargs:
result = testpaths
else:
result = []
for path in testpaths:
result.extend(sorted(glob.iglob(path, recursive=True)))
if testpaths and not result:
if warn:
warning_text = (
"No files were found in testpaths; "
"consider removing or adjusting your testpaths configuration. "
"Searching recursively from the current directory instead."
)
self.issue_config_time_warning(
PytestConfigWarning(warning_text), stacklevel=3
)
else:
result = []
if not result:
source = Config.ArgsSource.INCOVATION_DIR
result = [str(invocation_dir)]
return result, source
def _preparse(self, args: List[str], addopts: bool = True) -> None: def _preparse(self, args: List[str], addopts: bool = True) -> None:
if addopts: if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "") env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
@ -1241,8 +1317,11 @@ class Config:
_pytest.deprecated.STRICT_OPTION, stacklevel=2 _pytest.deprecated.STRICT_OPTION, stacklevel=2
) )
if self.known_args_namespace.confcutdir is None and self.inipath is not None: if self.known_args_namespace.confcutdir is None:
confcutdir = str(self.inipath.parent) if self.inipath is not None:
confcutdir = str(self.inipath.parent)
else:
confcutdir = str(self.rootpath)
self.known_args_namespace.confcutdir = confcutdir self.known_args_namespace.confcutdir = confcutdir
try: try:
self.hook.pytest_load_initial_conftests( self.hook.pytest_load_initial_conftests(
@ -1337,8 +1416,8 @@ class Config:
def parse(self, args: List[str], addopts: bool = True) -> None: def parse(self, args: List[str], addopts: bool = True) -> None:
# Parse given cmdline arguments into this config object. # Parse given cmdline arguments into this config object.
assert not hasattr( assert (
self, "args" self.args == []
), "can only parse cmdline args at most once per Config object" ), "can only parse cmdline args at most once per Config object"
self.hook.pytest_addhooks.call_historic( self.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=self.pluginmanager) kwargs=dict(pluginmanager=self.pluginmanager)
@ -1348,25 +1427,17 @@ class Config:
self.hook.pytest_cmdline_preparse(config=self, args=args) self.hook.pytest_cmdline_preparse(config=self, args=args)
self._parser.after_preparse = True # type: ignore self._parser.after_preparse = True # type: ignore
try: try:
source = Config.ArgsSource.ARGS
args = self._parser.parse_setoption( args = self._parser.parse_setoption(
args, self.option, namespace=self.option args, self.option, namespace=self.option
) )
if not args: self.args, self.args_source = self._decide_args(
if self.invocation_params.dir == self.rootpath: args=args,
source = Config.ArgsSource.TESTPATHS pyargs=self.known_args_namespace.pyargs,
testpaths: List[str] = self.getini("testpaths") testpaths=self.getini("testpaths"),
if self.known_args_namespace.pyargs: invocation_dir=self.invocation_params.dir,
args = testpaths rootpath=self.rootpath,
else: warn=True,
args = [] )
for path in testpaths:
args.extend(sorted(glob.iglob(path, recursive=True)))
if not args:
source = Config.ArgsSource.INCOVATION_DIR
args = [str(self.invocation_params.dir)]
self.args = args
self.args_source = source
except PrintHelp: except PrintHelp:
pass pass

View File

@ -43,7 +43,6 @@ class PathAwareHookProxy:
@_wraps(hook) @_wraps(hook)
def fixed_hook(**kw): def fixed_hook(**kw):
path_value: Optional[Path] = kw.pop(path_var, None) path_value: Optional[Path] = kw.pop(path_var, None)
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None) fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
if fspath_value is not None: if fspath_value is not None:

View File

@ -203,8 +203,7 @@ def determine_setup(
else: else:
cwd = Path.cwd() cwd = Path.cwd()
rootdir = get_common_ancestor([cwd, ancestor]) rootdir = get_common_ancestor([cwd, ancestor])
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" if is_fs_root(rootdir):
if is_fs_root:
rootdir = ancestor rootdir = ancestor
if rootdir_cmd_arg: if rootdir_cmd_arg:
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
@ -216,3 +215,11 @@ def determine_setup(
) )
assert rootdir is not None assert rootdir is not None
return rootdir, inipath, inicfg or {} return rootdir, inipath, inicfg or {}
def is_fs_root(p: Path) -> bool:
r"""
Return True if the given path is pointing to the root of the
file system ("/" on Unix and "C:\\" on Windows for example).
"""
return os.path.splitdrive(str(p))[1] == os.sep

View File

@ -3,6 +3,7 @@ import argparse
import functools import functools
import sys import sys
import types import types
import unittest
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Generator from typing import Generator
@ -293,7 +294,9 @@ class PdbInvoke:
sys.stdout.write(out) sys.stdout.write(out)
sys.stdout.write(err) sys.stdout.write(err)
assert call.excinfo is not None assert call.excinfo is not None
_enter_pdb(node, call.excinfo, report)
if not isinstance(call.excinfo.value, unittest.SkipTest):
_enter_pdb(node, call.excinfo, report)
def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None: def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
tb = _postmortem_traceback(excinfo) tb = _postmortem_traceback(excinfo)

View File

@ -531,7 +531,6 @@ class DoctestModule(Module):
if _is_mocked(obj): if _is_mocked(obj):
return return
with _patch_unwrap_mock_aware(): with _patch_unwrap_mock_aware():
# Type ignored because this is a private function. # Type ignored because this is a private function.
super()._find( # type:ignore[misc] super()._find( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen tests, obj, name, module, source_lines, globs, seen

View File

@ -2,7 +2,6 @@ import io
import os import os
import sys import sys
from typing import Generator from typing import Generator
from typing import TextIO
import pytest import pytest
from _pytest.config import Config from _pytest.config import Config
@ -11,7 +10,7 @@ from _pytest.nodes import Item
from _pytest.stash import StashKey from _pytest.stash import StashKey
fault_handler_stderr_key = StashKey[TextIO]() fault_handler_stderr_fd_key = StashKey[int]()
fault_handler_originally_enabled_key = StashKey[bool]() fault_handler_originally_enabled_key = StashKey[bool]()
@ -26,10 +25,9 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_configure(config: Config) -> None: def pytest_configure(config: Config) -> None:
import faulthandler import faulthandler
stderr_fd_copy = os.dup(get_stderr_fileno()) config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled() config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
faulthandler.enable(file=config.stash[fault_handler_stderr_key]) faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
def pytest_unconfigure(config: Config) -> None: def pytest_unconfigure(config: Config) -> None:
@ -37,9 +35,9 @@ def pytest_unconfigure(config: Config) -> None:
faulthandler.disable() faulthandler.disable()
# Close the dup file installed during pytest_configure. # Close the dup file installed during pytest_configure.
if fault_handler_stderr_key in config.stash: if fault_handler_stderr_fd_key in config.stash:
config.stash[fault_handler_stderr_key].close() os.close(config.stash[fault_handler_stderr_fd_key])
del config.stash[fault_handler_stderr_key] del config.stash[fault_handler_stderr_fd_key]
if config.stash.get(fault_handler_originally_enabled_key, False): if config.stash.get(fault_handler_originally_enabled_key, False):
# Re-enable the faulthandler if it was originally enabled. # Re-enable the faulthandler if it was originally enabled.
faulthandler.enable(file=get_stderr_fileno()) faulthandler.enable(file=get_stderr_fileno())
@ -67,10 +65,10 @@ def get_timeout_config_value(config: Config) -> float:
@pytest.hookimpl(hookwrapper=True, trylast=True) @pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
timeout = get_timeout_config_value(item.config) timeout = get_timeout_config_value(item.config)
stderr = item.config.stash[fault_handler_stderr_key] if timeout > 0:
if timeout > 0 and stderr is not None:
import faulthandler import faulthandler
stderr = item.config.stash[fault_handler_stderr_fd_key]
faulthandler.dump_traceback_later(timeout, file=stderr) faulthandler.dump_traceback_later(timeout, file=stderr)
try: try:
yield yield

View File

@ -1,3 +1,4 @@
import dataclasses
import functools import functools
import inspect import inspect
import os import os
@ -28,8 +29,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import attr
import _pytest import _pytest
from _pytest import nodes from _pytest import nodes
from _pytest._code import getfslineno from _pytest._code import getfslineno
@ -47,6 +46,7 @@ from _pytest.compat import getimfunc
from _pytest.compat import getlocation from _pytest.compat import getlocation
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import NotSetType
from _pytest.compat import overload from _pytest.compat import overload
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@ -59,6 +59,7 @@ from _pytest.mark import Mark
from _pytest.mark import ParameterSet from _pytest.mark import ParameterSet
from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import MarkDecorator
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import skip
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
@ -103,7 +104,7 @@ _FixtureCachedResult = Union[
] ]
@attr.s(frozen=True, auto_attribs=True) @dataclasses.dataclass(frozen=True)
class PseudoFixtureDef(Generic[FixtureValue]): class PseudoFixtureDef(Generic[FixtureValue]):
cached_result: "_FixtureCachedResult[FixtureValue]" cached_result: "_FixtureCachedResult[FixtureValue]"
_scope: Scope _scope: Scope
@ -113,16 +114,18 @@ def pytest_sessionstart(session: "Session") -> None:
session._fixturemanager = FixtureManager(session) session._fixturemanager = FixtureManager(session)
def get_scope_package(node, fixturedef: "FixtureDef[object]"): def get_scope_package(
import pytest node: nodes.Item,
fixturedef: "FixtureDef[object]",
) -> Optional[Union[nodes.Item, nodes.Collector]]:
from _pytest.python import Package
cls = pytest.Package current: Optional[Union[nodes.Item, nodes.Collector]] = node
current = node
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py") fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
while current and ( while current and (
type(current) is not cls or fixture_package_name != current.nodeid not isinstance(current, Package) or fixture_package_name != current.nodeid
): ):
current = current.parent current = current.parent # type: ignore[assignment]
if current is None: if current is None:
return node.session return node.session
return current return current
@ -350,8 +353,10 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
return request.param return request.param
@attr.s(slots=True, auto_attribs=True) @dataclasses.dataclass
class FuncFixtureInfo: class FuncFixtureInfo:
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
# Original function argument names. # Original function argument names.
argnames: Tuple[str, ...] argnames: Tuple[str, ...]
# Argnames that function immediately requires. These include argnames + # Argnames that function immediately requires. These include argnames +
@ -433,7 +438,23 @@ class FixtureRequest:
@property @property
def node(self): def node(self):
"""Underlying collection node (depends on current request scope).""" """Underlying collection node (depends on current request scope)."""
return self._getscopeitem(self._scope) scope = self._scope
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on FixtureRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]": def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None) fixturedefs = self._arg2fixturedefs.get(argname, None)
@ -517,11 +538,7 @@ class FixtureRequest:
"""Add finalizer/teardown function to be called without arguments after """Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution.""" the last test within the requesting test context finished execution."""
# XXX usually this method is shadowed by fixturedef specific ones. # XXX usually this method is shadowed by fixturedef specific ones.
self._addfinalizer(finalizer, scope=self.scope) self.node.addfinalizer(finalizer)
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
node = self._getscopeitem(scope)
node.addfinalizer(finalizer)
def applymarker(self, marker: Union[str, MarkDecorator]) -> None: def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
"""Apply a marker to a single test function invocation. """Apply a marker to a single test function invocation.
@ -716,28 +733,6 @@ class FixtureRequest:
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
return lines return lines
def _getscopeitem(
self, scope: Union[Scope, "_ScopeName"]
) -> Union[nodes.Item, nodes.Collector]:
if isinstance(scope, str):
scope = Scope(scope)
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on FixtureRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def __repr__(self) -> str: def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node) return "<FixtureRequest for %r>" % (self.node)
@ -1130,6 +1125,10 @@ def pytest_fixture_setup(
except TEST_OUTCOME: except TEST_OUTCOME:
exc_info = sys.exc_info() exc_info = sys.exc_info()
assert exc_info[0] is not None assert exc_info[0] is not None
if isinstance(
exc_info[1], skip.Exception
) and not fixturefunc.__name__.startswith("xunit_setup"):
exc_info[1]._use_item_location = True # type: ignore[attr-defined]
fixturedef.cached_result = (None, my_cache_key, exc_info) fixturedef.cached_result = (None, my_cache_key, exc_info)
raise raise
fixturedef.cached_result = (result, my_cache_key, None) fixturedef.cached_result = (result, my_cache_key, None)
@ -1177,19 +1176,21 @@ def wrap_function_to_error_out_if_called_directly(
@final @final
@attr.s(frozen=True, auto_attribs=True) @dataclasses.dataclass(frozen=True)
class FixtureFunctionMarker: class FixtureFunctionMarker:
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter) params: Optional[Tuple[object, ...]]
autouse: bool = False autouse: bool = False
ids: Optional[ ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = attr.ib( ] = None
default=None,
converter=_ensure_immutable_ids,
)
name: Optional[str] = None name: Optional[str] = None
_ispytest: dataclasses.InitVar[bool] = False
def __post_init__(self, _ispytest: bool) -> None:
check_ispytest(_ispytest)
def __call__(self, function: FixtureFunction) -> FixtureFunction: def __call__(self, function: FixtureFunction) -> FixtureFunction:
if inspect.isclass(function): if inspect.isclass(function):
raise ValueError("class fixtures not supported (maybe in the future)") raise ValueError("class fixtures not supported (maybe in the future)")
@ -1312,10 +1313,11 @@ def fixture( # noqa: F811
""" """
fixture_marker = FixtureFunctionMarker( fixture_marker = FixtureFunctionMarker(
scope=scope, scope=scope,
params=params, params=tuple(params) if params is not None else None,
autouse=autouse, autouse=autouse,
ids=ids, ids=None if ids is None else ids if callable(ids) else tuple(ids),
name=name, name=name,
_ispytest=True,
) )
# Direct decoration. # Direct decoration.
@ -1588,13 +1590,52 @@ class FixtureManager:
# Separate parametrized setups. # Separate parametrized setups.
items[:] = reorder_items(items) items[:] = reorder_items(items)
@overload
def parsefactories( def parsefactories(
self, node_or_obj, nodeid=NOTSET, unittest: bool = False self,
node_or_obj: nodes.Node,
*,
unittest: bool = ...,
) -> None: ) -> None:
raise NotImplementedError()
@overload
def parsefactories( # noqa: F811
self,
node_or_obj: object,
nodeid: Optional[str],
*,
unittest: bool = ...,
) -> None:
raise NotImplementedError()
def parsefactories( # noqa: F811
self,
node_or_obj: Union[nodes.Node, object],
nodeid: Union[str, NotSetType, None] = NOTSET,
*,
unittest: bool = False,
) -> None:
"""Collect fixtures from a collection node or object.
Found fixtures are parsed into `FixtureDef`s and saved.
If `node_or_object` is a collection node (with an underlying Python
object), the node's object is traversed and the node's nodeid is used to
determine the fixtures' visibilty. `nodeid` must not be specified in
this case.
If `node_or_object` is an object (e.g. a plugin), the object is
traversed and the given `nodeid` is used to determine the fixtures'
visibility. `nodeid` must be specified in this case; None and "" mean
total visibility.
"""
if nodeid is not NOTSET: if nodeid is not NOTSET:
holderobj = node_or_obj holderobj = node_or_obj
else: else:
holderobj = node_or_obj.obj assert isinstance(node_or_obj, nodes.Node)
holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
assert isinstance(node_or_obj.nodeid, str)
nodeid = node_or_obj.nodeid nodeid = node_or_obj.nodeid
if holderobj in self._holderobjseen: if holderobj in self._holderobjseen:
return return

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