Compare commits

..

34 Commits

Author SHA1 Message Date
pytest bot
2f2f1a601e Prepare release version 7.1.2 2022-04-23 11:33:44 +00:00
github-actions[bot]
5c04f3a1a2 [7.1.x] Fix wrong log_file docs (#9879)
Co-authored-by: Zac Hatfield-Dodds <zac.hatfield.dodds@gmail.com>
2022-04-22 18:40:15 +00:00
Bruno Oliveira
078733c005 Merge pull request #9872 from pytest-dev/backport-9871-to-7.1.x 2022-04-21 00:07:25 -03:00
Zac Hatfield-Dodds
3a7ead6bcf [7.1.x] fix: move 'import getpass' statement to try-clause 2022-04-21 02:46:24 +00:00
Bruno Oliveira
6d75333780 [7.1.x] Increase stacklevel to point at user's code (#9870)
Co-authored-by: Hugo van Kemenade <hugovk@users.noreply.github.com>
2022-04-20 14:38:00 -03:00
Hugo van Kemenade
ddbb998aed [7.1.x] Increase stacklevel to point at user's code 2022-04-20 16:09:01 +00:00
Bruno Oliveira
0ec5886ad5 Merge pull request #9855 from pytest-dev/backport-9854-to-7.1.x 2022-04-12 14:05:44 -03:00
Bruno Oliveira
f2469fca37 [7.1.x] Docs: link to easy issues in contributing guide 2022-04-12 16:42:32 +00:00
Bruno Oliveira
94ec0f8ad8 Merge pull request #9846 from pytest-dev/backport-9842-to-7.1.x 2022-04-09 11:25:28 -03:00
Anthony Sottile
5ef96fdb53 [7.1.x] fix comparison of dataclasses with InitVar 2022-04-09 00:48:09 +00:00
Bruno Oliveira
7a501fb313 Merge pull request #9844 from pytest-dev/backport-9843-to-7.1.x 2022-04-08 21:46:44 -03:00
Anthony Sottile
1769c66def [7.1.x] update pre-commit hooks 2022-04-08 20:21:19 -04:00
Bruno Oliveira
840c418de6 [7.1.x] temporarily pin jinja2 version for docs build 2022-04-08 23:35:13 +00:00
Bruno Oliveira
6461e2e385 Merge pull request #9805 from pytest-dev/backport-9804-to-7.1.x 2022-03-21 13:39:05 -03:00
Bruno Oliveira
b55b7f1ad4 [7.1.x] Change directories during some tests in test_collection.py 2022-03-21 16:13:52 +00:00
Bruno Oliveira
d9794ed3cf Merge pull request #9801 from pytest-dev/backport-9800-to-7.1.x 2022-03-21 08:38:29 -03:00
Bruno Oliveira
8b33683cbf [7.1.x] Fix CI for Python 3.11 2022-03-21 11:20:19 +00:00
Bruno Oliveira
1d2e50faa6 Merge pull request #9799 from pytest-dev/backport-9798-to-7.1.x 2022-03-20 22:55:24 -03:00
Bruno Oliveira
6820ab2bd4 Merge pull request #9795 from pytest-dev/backport-9794-to-7.1.x
[7.1.x] Split test/deploy workflows
2022-03-20 22:35:44 -03:00
Kian Eliasi
78356dc353 [7.1.x] Remove unnecessary numpy import 2022-03-21 01:35:22 +00:00
Bruno Oliveira
f1c27608ec [7.1.x] Split test/deploy workflows 2022-03-19 12:45:41 +00:00
Bruno Oliveira
0ceaa57d9d Merge pull request #9790 from pytest-dev/backport-9789-to-7.1.x
[7.1.x] [style]: fix typo in docstring
2022-03-18 19:01:32 -03:00
Bruno Oliveira
93fad3286b [7.1.x] [style]: fix typo in docstring 2022-03-18 21:59:16 +00:00
Ran Benita
b9462ed7d0 Merge pull request #9785 from pytest-dev/release-7.1.1
Prepare release 7.1.1
2022-03-17 23:17:11 +02:00
pytest bot
0ffe9e0742 Prepare release version 7.1.1 2022-03-17 20:21:30 +00:00
Ran Benita
6f2c1ec035 Merge pull request #9784 from pytest-dev/backport-9768-to-7.1.x
[7.1.x] testing: fix tests when run under `-v` or `-vv`
2022-03-17 22:13:28 +02:00
Ran Benita
a65c47a1a4 Merge pull request #9783 from pytest-dev/backport-9780-to-7.1.x
[7.1.x] config: restore pre-pytest 7.1.0 confcutdir exclusion behavior
2022-03-17 18:34:49 +02:00
pre-commit-ci[bot]
30d995ed25 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-03-17 16:27:25 +00:00
Ran Benita
10a14d1318 [7.1.x] testing: fix tests when run under -v or -vv 2022-03-17 16:26:18 +00:00
pre-commit-ci[bot]
f4cfc596c6 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-03-17 16:16:12 +00:00
Ran Benita
f1df8074b3 [7.1.x] config: restore pre-pytest 7.1.0 confcutdir exclusion behavior 2022-03-17 16:15:03 +00:00
Ran Benita
7d4d1ecde6 Merge pull request #9758 from pytest-dev/release-7.1.0
Prepare release 7.1.0
2022-03-13 16:58:05 +02:00
pre-commit-ci[bot]
1dbffcc0b4 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2022-03-13 13:44:48 +00:00
pytest bot
d53a5fb371 Prepare release version 7.1.0 2022-03-13 13:43:44 +00:00
163 changed files with 3555 additions and 11766 deletions

View File

@@ -9,9 +9,3 @@ updates:
allow:
- dependency-type: direct
- 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
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: true

View File

@@ -23,34 +23,30 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 0
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
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: "3.7"
- name: Install tox
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade tox
pip install --upgrade build 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
env:

View File

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

View File

@@ -18,11 +18,6 @@ on:
env:
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.
permissions: {}
@@ -71,7 +66,7 @@ jobs:
- name: "windows-py37-pluggy"
python: "3.7"
os: windows-latest
tox_env: "py37-pluggymain-pylib-xdist"
tox_env: "py37-pluggymain-xdist"
- name: "windows-py38"
python: "3.8"
os: windows-latest
@@ -98,7 +93,7 @@ jobs:
- name: "ubuntu-py37-pluggy"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-pluggymain-pylib-xdist"
tox_env: "py37-pluggymain-xdist"
- name: "ubuntu-py37-freeze"
python: "3.7"
os: ubuntu-latest
@@ -119,7 +114,6 @@ jobs:
python: "3.11-dev"
os: ubuntu-latest
tox_env: "py311"
use_coverage: true
- name: "ubuntu-pypy3"
python: "pypy-3.7"
os: ubuntu-latest
@@ -159,13 +153,13 @@ jobs:
use_coverage: true
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: false
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python }}
@@ -188,16 +182,8 @@ jobs:
- name: Upload coverage to Codecov
if: "matrix.use_coverage"
uses: codecov/codecov-action@v3
continue-on-error: true
uses: codecov/codecov-action@v2
with:
fail_ci_if_error: true
files: ./coverage.xml
verbose: true
check-package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5

View File

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

1
.gitignore vendored
View File

@@ -50,7 +50,6 @@ coverage.xml
.project
.settings
.vscode
__pycache__/
# generated by pip
pip-wheel-metadata/

View File

@@ -1,18 +1,16 @@
default_language_version:
python: "3.10"
repos:
- repo: https://github.com/psf/black
rev: 23.3.0
rev: 22.3.0
hooks:
- id: black
args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs
rev: 1.13.0
rev: v1.12.1
hooks:
- id: blacken-docs
additional_dependencies: [black==23.1.0]
additional_dependencies: [black==20.8b1]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
rev: v4.1.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -22,8 +20,8 @@ repos:
- id: debug-statements
exclude: _pytest/(debugging|hookspec).py
language_version: python3
- repo: https://github.com/PyCQA/autoflake
rev: v2.0.2
- repo: https://github.com/myint/autoflake
rev: v1.4
hooks:
- id: autoflake
name: autoflake
@@ -31,7 +29,7 @@ repos:
language: python
files: \.py$
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
rev: 4.0.1
hooks:
- id: flake8
language_version: python3
@@ -39,39 +37,38 @@ repos:
- flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.9.0
rev: v3.0.1
hooks:
- id: reorder-python-imports
args: ['--application-directories=.:src', --py37-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v2.31.0
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.2.0
rev: v1.20.0
hooks:
- id: setup-cfg-fmt
args: ["--max-py-version=3.11", "--include-version-classifiers"]
args: [--max-py-version=3.10]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
rev: v1.9.0
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.1.1
rev: v0.931
hooks:
- id: mypy
files: ^(src/|testing/)
args: []
additional_dependencies:
- iniconfig>=1.1.0
- py>=1.8.2
- attrs>=19.2.0
- packaging
- tomli
- types-atomicwrites
- types-pkg_resources
# for mypy running on python>=3.11 since exceptiongroup is only a dependency
# on <3.11
- exceptiongroup>=1.0.0rc8
- repo: local
hooks:
- id: rst
@@ -104,7 +101,7 @@ repos:
types: [python]
- id: py-path-deprecated
name: py.path usage is deprecated
exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py|src/_pytest/legacypath.py
exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py
language: pygrep
entry: \bpy\.path\.local
types: [python]

View File

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

46
AUTHORS
View File

@@ -12,11 +12,9 @@ Adam Uhlir
Ahn Ki-Wook
Akiomi Kamakura
Alan Velasco
Alessio Izzo
Alexander Johnson
Alexander King
Alexei Kozlenok
Alice Purcell
Allan Feldman
Aly Sivji
Amir Elkess
@@ -44,10 +42,8 @@ Ariel Pillemer
Armin Rigo
Aron Coyle
Aron Curzon
Ashish Kurmi
Aviral Verma
Aviv Palivoda
Babak Keyvani
Barney Gale
Ben Gartner
Ben Webb
@@ -59,7 +55,6 @@ Brian Maissy
Brian Okken
Brianna Laugher
Bruno Oliveira
Cal Jacobson
Cal Leeming
Carl Friedrich Bolz
Carlos Jenkins
@@ -67,11 +62,9 @@ Ceridwen
Charles Cloud
Charles Machalow
Charnjit SiNGH (CCSJ)
Cheuk Ting Ho
Chris Lamb
Chris NeJame
Chris Rose
Chris Wheeler
Christian Boelsen
Christian Fetzer
Christian Neumüller
@@ -90,8 +83,6 @@ Damian Skrzypczak
Daniel Grana
Daniel Hahler
Daniel Nuri
Daniel Sánchez Castelló
Daniel Valenzuela Zenteno
Daniel Wandschneider
Daniele Procida
Danielle Jenkins
@@ -128,13 +119,11 @@ Erik M. Bray
Evan Kepner
Fabien Zarifian
Fabio Zadrozny
Felix Hofstätter
Felix Nieuwenhuizen
Feng Ma
Florian Bruhin
Florian Dahlitz
Floris Bruynooghe
Gabriel Landau
Gabriel Reis
Garvit Shubham
Gene Wood
@@ -160,10 +149,8 @@ Ian Bicking
Ian Lesperance
Ilya Konstantinov
Ionuț Turturică
Itxaso Aizpurua
Iwan Briquemont
Jaap Broekhuizen
Jake VanderPlas
Jakob van Santen
Jakub Mitoraj
James Bourbeau
@@ -176,9 +163,7 @@ Jeff Rackauckas
Jeff Widman
Jenni Rinker
John Eddie Ayson
John Litborn
John Towler
Jon Parise
Jon Sonesen
Jonas Obrist
Jordan Guymon
@@ -188,8 +173,8 @@ Joseph Hunkeler
Josh Karpel
Joshua Bronson
Jurko Gospodnetić
Justice Ndou
Justyna Janczyszyn
Justice Ndou
Kale Kundert
Kamran Ahmad
Karl O. Pinc
@@ -198,9 +183,7 @@ Katarzyna Jachim
Katarzyna Król
Katerina Koukiou
Keri Volans
Kevin C
Kevin Cox
Kevin Hierro Carrasco
Kevin J. Foley
Kian Eliasi
Kian-Meng Ang
@@ -228,7 +211,6 @@ Marcin Bachry
Marco Gorelli
Mark Abramowitz
Mark Dickinson
Marko Pacak
Markus Unterwaditzer
Martijn Faassen
Martin Altmayer
@@ -242,6 +224,7 @@ Matthias Hafner
Maxim Filipenko
Maximilian Cosmo Sitter
mbyt
Mickey Pashov
Michael Aquilina
Michael Birtwell
Michael Droettboom
@@ -250,7 +233,6 @@ Michael Krebs
Michael Seifert
Michal Wajszczuk
Michał Zięba
Mickey Pashov
Mihai Capotă
Mike Hoyle (hoylemd)
Mike Lundy
@@ -264,10 +246,9 @@ Nicholas Murphy
Niclas Olofsson
Nicolas Delaby
Nikolay Kondratyev
Nipunn Koorapati
Olga Matoula
Oleg Pidsadnyi
Oleg Sushchenko
Olga Matoula
Oliver Bestwalter
Omar Kohl
Omer Hadari
@@ -276,14 +257,12 @@ Oscar Benjamin
Parth Patel
Patrick Hayes
Paul Müller
Paul Reece
Pauli Virtanen
Pavel Karateev
Paweł Adamczak
Pedro Algarvio
Petter Strandmark
Philipp Loose
Pierre Sassoulas
Pieter Mulder
Piotr Banaszkiewicz
Piotr Helm
@@ -293,14 +272,12 @@ Prashant Sharma
Pulkit Goyal
Punyashloka Biswal
Quentin Pradet
q0w
Ralf Schmitt
Ralph Giles
Ram Rachum
Ralph Giles
Ran Benita
Raphael Castaneda
Raphael Pierzina
Rafal Semik
Raquel Alegre
Ravi Chandra
Robert Holt
@@ -314,27 +291,23 @@ Ruaridh Williamson
Russel Winder
Ryan Wooden
Saiprasad Kale
Samuel Colvin
Samuel Dion-Girardeau
Samuel Searles-Bryant
Samuele Pedroni
Sanket Duthade
Sankt Petersbug
Saravanan Padmanaban
Segev Finer
Serhii Mozghovyi
Seth Junot
Shantanu Jain
Shubham Adep
Simon Gomizelj
Simon Holesch
Simon Kerr
Skylar Downes
Srinivas Reddy Thatiparthy
Stefan Farmbauer
Stefan Scherfke
Stefan Zimmermann
Stefanie Molin
Stefano Taschini
Steffen Allner
Stephan Obermann
@@ -346,32 +319,26 @@ Taneli Hukkinen
Tanvi Mehta
Tarcisio Fischer
Tareq Alayan
Tatiana Ovary
Ted Xiao
Terje Runde
Thomas Grainger
Thomas Hisch
Tim Hoffmann
Tim Strazny
TJ Bruno
Tobias Diez
Tom Dalton
Tom Viner
Tomáš Gavenčiak
Tomer Keren
Tony Narlock
Tor Colvin
Trevor Bekolay
Tyler Goodlet
Tzu-ping Chung
Vasily Kuznetsov
Victor Maryama
Victor Rodriguez
Victor Uriarte
Vidar T. Fauske
Virgil Dupras
Vitaly Lashmanov
Vivaan Verma
Vlad Dragos
Vlad Radziuk
Vladyslav Rachek
@@ -384,14 +351,9 @@ Wouter van Ackooy
Xixi Zhao
Xuan Luong
Xuecong Liao
Yannick Péroux
Yoav Caspi
Yuliang Shao
Yusuke Kadowaki
Yuval Shimon
Zac Hatfield-Dodds
Zachary Kneupper
Zachary OBrien
Zhouxin Qiu
Zoltán Máté
Zsolt Cserna

View File

@@ -223,7 +223,7 @@ changes you want to review and merge. Pull requests are stored on
Once you send a pull request, we can discuss its potential modifications and
even add more commits to it later on. There's an excellent tutorial on how Pull
Requests work in the
`GitHub Help Center <https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests>`_.
`GitHub Help Center <https://help.github.com/articles/using-pull-requests/>`_.
Here is a simple overview, with pytest-specific bits:
@@ -244,11 +244,6 @@ Here is a simple overview, with pytest-specific bits:
be released in micro releases whereas features will be released in
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
guide: https://git.wiki.kernel.org/index.php/QuickStart
@@ -385,7 +380,7 @@ them.
Backporting bug fixes for the next patch release
------------------------------------------------
Pytest makes a feature release every few weeks or months. In between, patch releases
Pytest makes 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
usually fix regressions, but may be any change that should reach users before the
next feature release.
@@ -394,7 +389,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
actual latest release). The procedure for this is:
#. First, make sure the bug is fixed in the ``main`` branch, with a regular pull
#. First, make sure the bug is fixed the ``main`` branch, with a regular pull
request, as described above. An exception to this is if the bug fix is not
applicable to ``main`` anymore.

View File

@@ -17,6 +17,7 @@
<li><a href="{{ pathto('changelog') }}">Changelog</a></li>
<li><a href="{{ pathto('contributing') }}">Contributing</a></li>
<li><a href="{{ pathto('backwards-compatibility') }}">Backwards Compatibility</a></li>
<li><a href="{{ pathto('py27-py34-deprecation') }}">Python 2.7 and 3.4 Support</a></li>
<li><a href="{{ pathto('sponsor') }}">Sponsor</a></li>
<li><a href="{{ pathto('tidelift') }}">pytest for Enterprise</a></li>
<li><a href="{{ pathto('license') }}">License</a></li>
@@ -29,3 +30,5 @@
{%- endif %}
<hr>
<a href="{{ pathto('genindex') }}">Index</a>
<hr>

View File

@@ -6,11 +6,6 @@ Release announcements
:maxdepth: 2
release-7.3.0
release-7.2.2
release-7.2.1
release-7.2.0
release-7.1.3
release-7.1.2
release-7.1.1
release-7.1.0

View File

@@ -1,28 +0,0 @@
pytest-7.1.3
=======================================
pytest 7.1.3 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Anthony Sottile
* Bruno Oliveira
* Gergely Kalmár
* Nipunn Koorapati
* Pax
* Sviatoslav Sydorenko
* Tim Hoffmann
* Tony Narlock
* Wolfremium
* Zach OBrien
* aizpurua23a
Happy testing,
The pytest Development Team

View File

@@ -1,93 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,25 +0,0 @@
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

@@ -1,130 +0,0 @@
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

@@ -77,18 +77,3 @@ Deprecation Roadmap
Features currently deprecated and removed in previous releases can be found in :ref:`deprecations`.
We track future deprecation and removal of features using milestones and the `deprecation <https://github.com/pytest-dev/pytest/issues?q=label%3A%22type%3A+deprecation%22>`_ and `removal <https://github.com/pytest-dev/pytest/labels/type%3A%20removal>`_ labels on GitHub.
Python version support
======================
Released pytest versions support all Python versions that are actively maintained at the time of the release:
============== ===================
pytest version min. Python version
============== ===================
7.1+ 3.7+
6.2 - 7.0 3.6+
5.0 - 6.1 3.5+
3.3 - 4.6 2.7, 3.4+
============== ===================

View File

@@ -33,93 +33,39 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Values can be any object handled by the json stdlib module.
capsysbinary -- .../_pytest/capture.py:1001
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsysbinary.readouterr()``
method calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``bytes`` objects.
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsysbinary):
print("hello")
captured = capsysbinary.readouterr()
assert captured.out == b"hello\n"
capfd -- .../_pytest/capture.py:1029
Enable text capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.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_system_echo(capfd):
os.system('echo "hello"')
captured = capfd.readouterr()
assert captured.out == "hello\n"
capfdbinary -- .../_pytest/capture.py:1057
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``byte`` objects.
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfdbinary):
os.system('echo "hello"')
captured = capfdbinary.readouterr()
assert captured.out == b"hello\n"
capsys -- .../_pytest/capture.py:973
capsys -- .../_pytest/capture.py:878
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>`.
capsysbinary -- .../_pytest/capture.py:895
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
Example:
The captured output is made available via ``capsysbinary.readouterr()``
method calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``bytes`` objects.
.. code-block:: python
capfd -- .../_pytest/capture.py:912
Enable text capturing of writes to file descriptors ``1`` and ``2``.
def test_output(capsys):
print("hello")
captured = capsys.readouterr()
assert captured.out == "hello\n"
The captured output is made available via ``capfd.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``text`` objects.
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
capfdbinary -- .../_pytest/capture.py:929
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``byte`` objects.
doctest_namespace [session scope] -- .../_pytest/doctest.py:731
Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.
Usually this fixture is used in conjunction with another ``autouse`` fixture:
.. code-block:: python
@pytest.fixture(autouse=True)
def add_np(doctest_namespace):
doctest_namespace["np"] = numpy
For more details: :ref:`doctest_namespace`.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
pytestconfig [session scope] -- .../_pytest/fixtures.py:1334
Session-scoped fixture that returns the session's :class:`pytest.Config`
object.
@@ -163,10 +109,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
record_testsuite_property("ARCH", "PPC")
record_testsuite_property("STORAGE_TYPE", "CEPH")
:param name:
The property name.
:param value:
The property value. Will be converted to a string.
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
.. warning::
@@ -174,10 +117,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
:issue:`7767` for details.
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:295
Return a :class:`pytest.TempdirFactory` instance for the test session.
tmpdir -- .../_pytest/legacypath.py:309
tmpdir -- .../_pytest/legacypath.py:302
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
@@ -189,14 +132,9 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
The returned object is a `legacy_path`_ object.
.. note::
These days, it is preferred to use ``tmp_path``.
:ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`.
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:498
caplog -- .../_pytest/logging.py:487
Access and control log capturing.
Captured logs are available through the following properties/methods::
@@ -210,46 +148,39 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
monkeypatch -- .../_pytest/monkeypatch.py:29
A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or
:data:`os.environ`:
The fixture provides these methods to modify objects, dictionaries or
os.environ::
* :meth:`monkeypatch.setattr(obj, name, value, raising=True) <pytest.MonkeyPatch.setattr>`
* :meth:`monkeypatch.delattr(obj, name, raising=True) <pytest.MonkeyPatch.delattr>`
* :meth:`monkeypatch.setitem(mapping, name, value) <pytest.MonkeyPatch.setitem>`
* :meth:`monkeypatch.delitem(obj, name, raising=True) <pytest.MonkeyPatch.delitem>`
* :meth:`monkeypatch.setenv(name, value, prepend=None) <pytest.MonkeyPatch.setenv>`
* :meth:`monkeypatch.delenv(name, raising=True) <pytest.MonkeyPatch.delenv>`
* :meth:`monkeypatch.syspath_prepend(path) <pytest.MonkeyPatch.syspath_prepend>`
* :meth:`monkeypatch.chdir(path) <pytest.MonkeyPatch.chdir>`
* :meth:`monkeypatch.context() <pytest.MonkeyPatch.context>`
monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.setitem(mapping, name, value)
monkeypatch.delitem(obj, name, raising=True)
monkeypatch.setenv(name, value, prepend=None)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
All modifications will be undone after the requesting test function or
fixture has finished. The ``raising`` parameter determines if a :class:`KeyError`
or :class:`AttributeError` will be raised if the set/deletion operation does not have the
specified target.
fixture has finished. The ``raising`` parameter determines if a KeyError
or AttributeError will be raised if the set/deletion operation has no target.
To undo modifications done by the fixture in a contained scope,
use :meth:`context() <pytest.MonkeyPatch.context>`.
recwarn -- .../_pytest/recwarn.py:30
recwarn -- .../_pytest/recwarn.py:29
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
See https://docs.python.org/library/how-to/capture-warnings.html for information
on warning categories.
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:245
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:184
Return a :class:`pytest.TempPathFactory` instance for the test session.
tmp_path -- .../_pytest/tmpdir.py:260
tmp_path -- .../_pytest/tmpdir.py:199
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging.
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
and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
The returned object is a :class:`pathlib.Path` object.

View File

@@ -28,317 +28,6 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
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.
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 depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported).
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)
=========================
Bug Fixes
---------
- `#10060 <https://github.com/pytest-dev/pytest/issues/10060>`_: When running with ``--pdb``, ``TestCase.tearDown`` is no longer called for tests when the *class* has been skipped via ``unittest.skip`` or ``pytest.mark.skip``.
- `#10190 <https://github.com/pytest-dev/pytest/issues/10190>`_: Invalid XML characters in setup or teardown error messages are now properly escaped for JUnit XML reports.
- `#10230 <https://github.com/pytest-dev/pytest/issues/10230>`_: Ignore ``.py`` files created by ``pyproject.toml``-based editable builds introduced in `pip 21.3 <https://pip.pypa.io/en/stable/news/#v21-3>`__.
- `#3396 <https://github.com/pytest-dev/pytest/issues/3396>`_: Doctests now respect the ``--import-mode`` flag.
- `#9514 <https://github.com/pytest-dev/pytest/issues/9514>`_: Type-annotate ``FixtureRequest.param`` as ``Any`` as a stop gap measure until :issue:`8073` is fixed.
- `#9791 <https://github.com/pytest-dev/pytest/issues/9791>`_: Fixed a path handling code in ``rewrite.py`` that seems to work fine, but was incorrect and fails in some systems.
- `#9917 <https://github.com/pytest-dev/pytest/issues/9917>`_: Fixed string representation for :func:`pytest.approx` when used to compare tuples.
Improved Documentation
----------------------
- `#9937 <https://github.com/pytest-dev/pytest/issues/9937>`_: Explicit note that :fixture:`tmpdir` fixture is discouraged in favour of :fixture:`tmp_path`.
Trivial/Internal Changes
------------------------
- `#10114 <https://github.com/pytest-dev/pytest/issues/10114>`_: Replace `atomicwrites <https://github.com/untitaker/python-atomicwrites>`__ dependency on windows with `os.replace`.
pytest 7.1.2 (2022-04-23)
=========================
@@ -2929,8 +2618,7 @@ Important
This release is a Python3.5+ only release.
For more details, see our `Python 2.7 and 3.4 support plan
<https://docs.pytest.org/en/7.0.x/py27-py34-deprecation.html>`_.
For more details, see our :std:doc:`Python 2.7 and 3.4 support plan <py27-py34-deprecation>`.
Removals
--------
@@ -3154,11 +2842,7 @@ Features
- :issue:`6870`: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``.
Remark: while this is technically a new feature and according to our
`policy <https://docs.pytest.org/en/7.0.x/py27-py34-deprecation.html#what-goes-into-4-6-x-releases>`_
it should not have been backported, we have opened an exception in this
particular case because it fixes a serious interaction with ``pytest-xdist``,
so it can also be considered a bugfix.
Remark: while this is technically a new feature and according to our :ref:`policy <what goes into 4.6.x releases>` it should not have been backported, we have opened an exception in this particular case because it fixes a serious interaction with ``pytest-xdist``, so it can also be considered a bugfix.
Trivial/Internal Changes
------------------------
@@ -3330,8 +3014,7 @@ Important
The ``4.6.X`` series will be the last series to support **Python 2 and Python 3.4**.
For more details, see our `Python 2.7 and 3.4 support plan
<https://docs.pytest.org/en/7.0.x/py27-py34-deprecation.html>`_.
For more details, see our :std:doc:`Python 2.7 and 3.4 support plan <py27-py34-deprecation>`.
Features
@@ -6499,7 +6182,7 @@ Bug Fixes
Thanks :user:`adborden` for the report and :user:`nicoddemus` for the PR.
* Clean up unittest TestCase objects after tests are complete (:issue:`1649`).
Thanks :user:`d-b-w` for the report and PR.
Thanks :user:`d_b_w` for the report and PR.
3.0.3 (2016-09-28)
@@ -6514,7 +6197,7 @@ Bug Fixes
Thanks :user:`nicoddemus` for the PR.
* Fix pkg_resources import error in Jython projects (:issue:`1853`).
Thanks :user:`raquelalegre` for the PR.
Thanks :user:`raquel-ucl` for the PR.
* Got rid of ``AttributeError: 'Module' object has no attribute '_obj'`` exception
in Python 3 (:issue:`1944`).

View File

@@ -38,7 +38,6 @@ release = ".".join(version.split(".")[:2])
autodoc_member_order = "bysource"
autodoc_typehints = "description"
autodoc_typehints_description_target = "documented"
todo_include_todos = 1
latex_engine = "lualatex"
@@ -163,11 +162,11 @@ linkcheck_workers = 5
_repo = "https://github.com/pytest-dev/pytest"
extlinks = {
"bpo": ("https://bugs.python.org/issue%s", "bpo-%s"),
"pypi": ("https://pypi.org/project/%s/", "%s"),
"issue": (f"{_repo}/issues/%s", "issue #%s"),
"pull": (f"{_repo}/pull/%s", "pull request #%s"),
"user": ("https://github.com/%s", "@%s"),
"bpo": ("https://bugs.python.org/issue%s", "bpo-"),
"pypi": ("https://pypi.org/project/%s/", ""),
"issue": (f"{_repo}/issues/%s", "issue #"),
"pull": (f"{_repo}/pull/%s", "pull request #"),
"user": ("https://github.com/%s", "@"),
}
@@ -248,7 +247,7 @@ html_sidebars = {
html_domain_indices = True
# If false, no index is generated.
html_use_index = False
html_use_index = True
# If true, the index is split into individual pages for each letter.
# html_split_index = False
@@ -321,9 +320,7 @@ latex_domain_indices = False
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
("how-to/usage", "pytest", "pytest usage", ["holger krekel at merlinux eu"], 1)
]
man_pages = [("usage", "pytest", "pytest usage", ["holger krekel at merlinux eu"], 1)]
# -- Options for Epub output ---------------------------------------------------
@@ -393,7 +390,6 @@ intersphinx_mapping = {
"tox": ("https://tox.wiki/en/stable", None),
"virtualenv": ("https://virtualenv.pypa.io/en/stable", None),
"setuptools": ("https://setuptools.pypa.io/en/stable", None),
"packaging": ("https://packaging.python.org/en/latest", None),
}
@@ -421,6 +417,8 @@ def configure_logging(app: "sphinx.application.Sphinx") -> None:
def setup(app: "sphinx.application.Sphinx") -> None:
# from sphinx.ext.autodoc import cut_lines
# app.connect('autodoc-process-docstring', cut_lines(4, what=['module']))
app.add_crossref_type(
"fixture",
"fixture",

View File

@@ -85,6 +85,7 @@ Further topics
backwards-compatibility
deprecations
py27-py34-deprecation
contributing
development_guide

View File

@@ -18,113 +18,6 @@ Deprecated Features
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
.. _nose-deprecation:
Support for tests written for nose
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.2
Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated.
``nose`` has been in maintenance mode-only for years, and maintaining the plugin is not trivial as it spills
over the code base (see :issue:`9886` for more details).
setup/teardown
^^^^^^^^^^^^^^
One thing that might catch users by surprise is that plain ``setup`` and ``teardown`` methods are not pytest native,
they are in fact part of the ``nose`` support.
.. code-block:: python
class Test:
def setup(self):
self.resource = make_resource()
def teardown(self):
self.resource.close()
def test_foo(self):
...
def test_bar(self):
...
Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`xunit-method-setup`), so the above should be changed to:
.. code-block:: python
class Test:
def setup_method(self):
self.resource = make_resource()
def teardown_method(self):
self.resource.close()
def test_foo(self):
...
def test_bar(self):
...
This is easy to do in an entire code base by doing a simple find/replace.
@with_setup
^^^^^^^^^^^
Code using `@with_setup <with-setup-nose>`_ such as this:
.. code-block:: python
from nose.tools import with_setup
def setup_some_resource():
...
def teardown_some_resource():
...
@with_setup(setup_some_resource, teardown_some_resource)
def test_foo():
...
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
.. code-block:: python
import pytest
def setup_some_resource():
...
def teardown_some_resource():
...
@pytest.fixture
def some_resource():
setup_some_resource()
yield
teardown_some_resource()
def test_foo(some_resource):
...
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
.. _instance-collector-deprecation:
The ``pytest.Instance`` collector
@@ -185,50 +78,6 @@ no matter what argument was used in the constructor. We expect to deprecate the
.. _legacy-path-hooks-deprecated:
Configuring hook specs/impls using markers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before pluggy, pytest's plugin library, was its own package and had a clear API,
pytest just used ``pytest.mark`` to configure hooks.
The :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec` decorators
have been available since years and should be used instead.
.. code-block:: python
@pytest.mark.tryfirst
def pytest_runtest_call():
...
# or
def pytest_runtest_call():
...
pytest_runtest_call.tryfirst = True
should be changed to:
.. code-block:: python
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_call():
...
Changed ``hookimpl`` attributes:
* ``tryfirst``
* ``trylast``
* ``optionalhook``
* ``hookwrapper``
Changed ``hookwrapper`` attributes:
* ``firstresult``
* ``historic``
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -403,47 +252,6 @@ or ``pytest.warns(Warning)``.
See :ref:`warns use cases` for examples.
Returning non-None value in test functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.2
A :class:`pytest.PytestReturnNotNoneWarning` is now emitted if a test function returns something other than `None`.
This prevents a common mistake among beginners that expect that returning a `bool` would cause a test to pass or fail, for example:
.. code-block:: python
@pytest.mark.parametrize(
["a", "b", "result"],
[
[1, 2, 5],
[2, 3, 8],
[5, 3, 18],
],
)
def test_foo(a, b, result):
return foo(a, b) == result
Given that pytest ignores the return value, this might be surprising that it will never fail.
The proper fix is to change the `return` to an `assert`:
.. code-block:: python
@pytest.mark.parametrize(
["a", "b", "result"],
[
[1, 2, 5],
[2, 3, 8],
[5, 3, 18],
],
)
def test_foo(a, b, result):
assert foo(a, b) == result
The ``--strict`` command-line option
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1052,7 +860,7 @@ that are then turned into proper test methods. Example:
.. code-block:: python
def check(x, y):
assert x**x == y
assert x ** x == y
def test_squared():
@@ -1067,7 +875,7 @@ This form of test function doesn't support fixtures properly, and users should s
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
def test_squared(x, y):
assert x**x == y
assert x ** x == y
.. _internal classes accessed through node deprecated:

View File

@@ -25,7 +25,7 @@ example: specifying and selecting acceptance tests
self.tmpdir = request.config.mktemp(request.function.__name__, numbered=True)
def run(self, *cmd):
"""called by test code to execute an acceptance test."""
""" called by test code to execute an acceptance test. """
self.tmpdir.chdir()
return subprocess.check_output(cmd).decode()

View File

@@ -17,7 +17,7 @@ def b(a, order):
@pytest.fixture
def c(b, order):
def c(a, b, order):
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.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.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early 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.
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
For an example on how to add and work with markers from a plugin, see
@@ -346,7 +346,7 @@ Custom marker and command line option to control test runs
Plugins can provide custom markers and implement specific behaviour
based on it. This is a self-contained example which adds a command
line option and a parametrized test function marker to run tests
specified via named environments:
specifies via named environments:
.. code-block:: python
@@ -375,7 +375,7 @@ specified via named environments:
envnames = [mark.args[0] for mark in item.iter_markers(name="env")]
if envnames:
if item.config.getoption("-E") not in envnames:
pytest.skip(f"test requires env in {envnames!r}")
pytest.skip("test requires env in {!r}".format(envnames))
A test file using this local plugin:
@@ -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.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.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early 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.
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
.. _`passing callables to custom markers`:
@@ -528,7 +528,7 @@ test function. From a conftest file we can read it like this:
def pytest_runtest_setup(item):
for mark in item.iter_markers(name="glob"):
print(f"glob args={mark.args} kwargs={mark.kwargs}")
print("glob args={} kwargs={}".format(mark.args, mark.kwargs))
sys.stdout.flush()
Let's run this without capturing output and see what we get:
@@ -558,7 +558,6 @@ for your particular platform, you could use the following plugin:
# content of conftest.py
#
import sys
import pytest
ALL = set("darwin linux win32".split())
@@ -568,7 +567,7 @@ for your particular platform, you could use the following plugin:
supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers())
plat = sys.platform
if supported_platforms and plat not in supported_platforms:
pytest.skip(f"cannot run on platform {plat}")
pytest.skip("cannot run on platform {}".format(plat))
then tests will be skipped if they were specified for a different platform.
Let's do a little test file to show how this looks like:
@@ -611,7 +610,7 @@ then you will see two tests skipped and two executed tests as expected:
test_plat.py s.s. [100%]
========================= short test summary info ==========================
SKIPPED [2] conftest.py:13: cannot run on platform linux
SKIPPED [2] conftest.py:12: cannot run on platform linux
======================= 2 passed, 2 skipped in 0.12s =======================
Note that if you specify a platform via the marker-command line option like this:

View File

@@ -9,7 +9,7 @@ Working with non-python tests
A basic example for specifying tests in Yaml files
--------------------------------------------------------------
.. _`pytest-yamlwsgi`: https://pypi.org/project/pytest-yamlwsgi/
.. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py
Here is an example ``conftest.py`` (extracted from Ali Afshar's special purpose `pytest-yamlwsgi`_ plugin). This ``conftest.py`` will collect ``test*.yaml`` files and will execute the yaml-formatted content as custom tests:

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
sssssssssssssssssssssssssss [100%]
========================= short test summary info ==========================
SKIPPED [9] multipython.py:69: 'python3.5' not found
SKIPPED [9] multipython.py:69: 'python3.6' not found
SKIPPED [9] multipython.py:69: 'python3.7' not found
SKIPPED [9] multipython.py:29: 'python3.5' not found
SKIPPED [9] multipython.py:29: 'python3.6' not found
SKIPPED [9] multipython.py:29: 'python3.7' not found
27 skipped in 0.12s
Indirect parametrization of optional implementations/imports
@@ -574,7 +574,7 @@ If you run this with reporting for skips enabled:
test_module.py .s [100%]
========================= short test summary info ==========================
SKIPPED [1] test_module.py:3: could not import 'opt2': No module named 'opt2'
SKIPPED [1] conftest.py:12: could not import 'opt2': No module named 'opt2'
======================= 1 passed, 1 skipped in 0.12s =======================
You'll see that we don't have an ``opt2`` module and thus the second test run
@@ -657,17 +657,20 @@ Use :func:`pytest.raises` with the
:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests
in which some tests raise exceptions and others do not.
It may be helpful to use ``nullcontext`` as a complement to ``raises``.
For example:
It is helpful to define a no-op context manager ``does_not_raise`` to serve
as a complement to ``raises``. For example:
.. code-block:: python
from contextlib import nullcontext as does_not_raise
from contextlib import contextmanager
import pytest
@contextmanager
def does_not_raise():
yield
@pytest.mark.parametrize(
"example_input,expectation",
[
@@ -684,3 +687,22 @@ For example:
In the example above, the first three test cases should run unexceptionally,
while the fourth should raise ``ZeroDivisionError``.
If you're only supporting Python 3.7+, you can simply use ``nullcontext``
to define ``does_not_raise``:
.. code-block:: python
from contextlib import nullcontext as does_not_raise
Or, if you're supporting Python 3.3+ you can use:
.. code-block:: python
from contextlib import ExitStack as does_not_raise
Or, if desired, you can ``pip install contextlib2`` and use:
.. code-block:: python
from contextlib2 import nullcontext as does_not_raise

View File

@@ -148,8 +148,7 @@ The test collection would look like this:
$ pytest --collect-only
=========================== test session starts ============================
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
<Module check_myapp.py>
@@ -210,8 +209,7 @@ You can always peek at the collection tree without running tests like this:
. $ pytest --collect-only pythoncollection.py
=========================== test session starts ============================
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
<Module CWD/pythoncollection.py>
@@ -292,8 +290,7 @@ file will be left out:
$ pytest --collect-only
=========================== test session starts ============================
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
======================= 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
E ...Full output truncated (6 lines hidden), use '-vv' to show
E ...Full output truncated (7 lines hidden), use '-vv' to show
failure_demo.py:60: AssertionError
_________________ TestSpecialisedExplanations.test_eq_list _________________
@@ -184,8 +184,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E Left contains 1 more item:
E {'c': 0}
E Right contains 1 more item:
E {'d': 0}
E Use -v to get more diff
E {'d': 0}...
E
E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:71: AssertionError
_________________ TestSpecialisedExplanations.test_eq_set __________________
@@ -194,15 +195,16 @@ Here is a nice run of several failures and how ``pytest`` presents things:
def test_eq_set(self):
> assert {0, 10, 11, 12} == {0, 20, 21}
E assert {0, 10, 11, 12} == {0, 20, 21}
E AssertionError: assert {0, 10, 11, 12} == {0, 20, 21}
E Extra items in the left set:
E 10
E 11
E 12
E Extra items in the right set:
E 20
E 21
E Use -v to get more diff
E 21...
E
E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:74: AssertionError
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
@@ -239,8 +241,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E which
E includes foo
E ? +++
E and a
E tail
E and a...
E
E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:84: AssertionError
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
@@ -304,9 +307,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E ['b']
E
E Drill down into differing attribute b:
E b: 'b' != 'c'
E - c
E + b
E b: 'b' != 'c'...
E
E ...Full output truncated (3 lines hidden), use '-vv' to show
failure_demo.py:108: AssertionError
________________ TestSpecialisedExplanations.test_eq_attrs _________________
@@ -331,9 +334,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E ['b']
E
E Drill down into differing attribute b:
E b: 'b' != 'c'
E - c
E + b
E b: 'b' != 'c'...
E
E ...Full output truncated (3 lines hidden), use '-vv' to show
failure_demo.py:120: AssertionError
______________________________ test_attribute ______________________________
@@ -670,7 +673,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_long - ...
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_in_list - asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline

View File

@@ -342,7 +342,7 @@ Example:
def checkconfig(x):
__tracebackhide__ = True
if not hasattr(x, "config"):
pytest.fail(f"not configured: {x}")
pytest.fail("not configured: {}".format(x))
def test_something():
@@ -376,7 +376,6 @@ this to make sure unexpected exception types aren't hidden:
.. code-block:: python
import operator
import pytest
@@ -387,7 +386,7 @@ this to make sure unexpected exception types aren't hidden:
def checkconfig(x):
__tracebackhide__ = operator.methodcaller("errisinstance", ConfigException)
if not hasattr(x, "config"):
raise ConfigException(f"not configured: {x}")
raise ConfigException("not configured: {}".format(x))
def test_something():
@@ -566,7 +565,6 @@ an ``incremental`` marker which is to be used on classes:
# content of conftest.py
from typing import Dict, Tuple
import pytest
# store history of failures per test class name and per index in parametrize (if parametrize used)
@@ -610,7 +608,7 @@ an ``incremental`` marker which is to be used on classes:
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
# if name found, test has failed for the combination of class name & test name
if test_name is not None:
pytest.xfail(f"previous test failed ({test_name})")
pytest.xfail("previous test failed ({})".format(test_name))
These two hook implementations work together to abort incremental-marked
@@ -661,7 +659,8 @@ If we run this:
test_step.py:11: AssertionError
========================= short test summary info ==========================
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
XFAIL test_step.py::TestUserHandling::test_deletion
reason: previous test failed (test_modification)
================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================
We'll see that ``test_deletion`` was not executed because ``test_modification``
@@ -803,9 +802,8 @@ case we just write some information out to a ``failures`` file:
# content of conftest.py
import os.path
import pytest
import os.path
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
@@ -892,11 +890,8 @@ here is a little example implemented via a local plugin:
.. code-block:: python
# content of conftest.py
from typing import Dict
import pytest
from pytest import StashKey, CollectReport
phase_report_key = StashKey[Dict[str, CollectReport]]()
import pytest
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
@@ -905,9 +900,10 @@ here is a little example implemented via a local plugin:
outcome = yield
rep = outcome.get_result()
# store test results for each phase of a call, which can
# set a report attribute for each phase of a call, which can
# be "setup", "call", "teardown"
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
setattr(item, "rep_" + rep.when, rep)
@pytest.fixture
@@ -915,11 +911,11 @@ here is a little example implemented via a local plugin:
yield
# request.node is an "item" because we use the default
# "function" scope
report = request.node.stash[phase_report_key]
if report["setup"].failed:
print("setting up a test failed or skipped", request.node.nodeid)
elif ("call" not in report) or report["call"].failed:
print("executing test failed or skipped", request.node.nodeid)
if request.node.rep_setup.failed:
print("setting up a test failed!", request.node.nodeid)
elif request.node.rep_setup.passed:
if request.node.rep_call.failed:
print("executing test failed", request.node.nodeid)
if you then have failing tests:
@@ -957,8 +953,8 @@ and run it:
rootdir: /home/sweet/project
collected 3 items
test_module.py Esetting up a test failed or skipped test_module.py::test_setup_fails
Fexecuting test failed or skipped test_module.py::test_call_fails
test_module.py Esetting up a test failed! test_module.py::test_setup_fails
Fexecuting test failed test_module.py::test_call_fails
F
================================== ERRORS ==================================
@@ -1070,7 +1066,6 @@ like ``pytest-timeout`` they must be imported explicitly and passed on to pytest
# contents of app_main.py
import sys
import pytest_timeout # Third party plugin
if len(sys.argv) > 1 and sys.argv[1] == "--pytest":

View File

@@ -94,7 +94,7 @@ Mark Lapierre discusses the `Pros and Cons of Quarantined Tests <https://dev.to/
CI tools that rerun on failure
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Azure Pipelines (the Azure cloud CI/CD tool, formerly Visual Studio Team Services or VSTS) has a feature to `identify flaky tests <https://docs.microsoft.com/en-us/previous-versions/azure/devops/2017/dec-11-vsts?view=tfs-2017#identify-flaky-tests>`_ and rerun failed tests.
Azure Pipelines (the Azure cloud CI/CD tool, formerly Visual Studio Team Services or VSTS) has a feature to `identify flaky tests <https://docs.microsoft.com/en-us/azure/devops/release-notes/2017/dec-11-vsts#identify-flaky-tests>`_ and rerun failed tests.

View File

@@ -12,27 +12,41 @@ For development, we recommend you use :mod:`venv` for virtual environments and
as well as the ``pytest`` package itself.
This ensures your code and dependencies are isolated from your system Python installation.
Create a ``pyproject.toml`` file in the root of your repository as described in
:doc:`packaging:tutorials/packaging-projects`.
The first few lines should look like this:
Next, place a ``pyproject.toml`` file in the root of your package:
.. code-block:: toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "PACKAGENAME"
version = "PACKAGEVERSION"
and a ``setup.cfg`` file containing your package's metadata with the following minimum content:
where ``PACKAGENAME`` and ``PACKAGEVERSION`` are the name and version of your package respectively.
.. code-block:: ini
[metadata]
name = PACKAGENAME
[options]
packages = find:
where ``PACKAGENAME`` is the name of your package.
.. note::
If your pip version is older than ``21.3``, you'll also need a ``setup.py`` file:
.. code-block:: python
from setuptools import setup
setup()
You can then install your package in "editable" mode by running from the same directory:
.. code-block:: bash
pip install -e .
pip install -e .
which lets you change your source code (both tests and application) and rerun tests at will.
@@ -51,8 +65,8 @@ Conventions for Python test discovery
* In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_.
* From those files, collect test items:
* ``test`` prefixed test functions or methods outside of class.
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method). Methods decorated with ``@staticmethod`` and ``@classmethods`` are also considered.
* ``test`` prefixed test functions or methods outside of class
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method)
For examples of how to customize your test discovery :doc:`/example/pythoncollection`.
@@ -75,11 +89,11 @@ to keep tests separate from actual application code (often a good idea):
.. code-block:: text
pyproject.toml
src/
mypkg/
__init__.py
app.py
view.py
setup.cfg
mypkg/
__init__.py
app.py
view.py
tests/
test_app.py
test_view.py
@@ -89,57 +103,84 @@ This has the following benefits:
* Your tests can run against an installed version after executing ``pip install .``.
* Your tests can run against the local copy with an editable install after executing ``pip install --editable .``.
For new projects, we recommend to use ``importlib`` :ref:`import mode <import-modes>`
(see which-import-mode_ for a detailed explanation).
To this end, add the following to your ``pyproject.toml``:
.. code-block:: toml
[tool.pytest.ini_options]
addopts = [
"--import-mode=importlib",
]
.. _src-layout:
Generally, but especially if you use the default import mode ``prepend``,
it is **strongly** suggested to use a ``src`` layout.
Here, your application root package resides in a sub-directory of your root,
i.e. ``src/mypkg/`` instead of ``mypkg``.
This layout prevents a lot of common pitfalls and has many benefits,
which are better explained in this excellent `blog post`_ by Ionel Cristian Mărieș.
.. _blog post: https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure>
* If you don't use an editable install and are relying on the fact that Python by default puts the current
directory in ``sys.path`` to import your package, you can execute ``python -m pytest`` to execute the tests against the
local copy directly, without using ``pip``.
.. note::
If you do not use an editable install and use the ``src`` layout as above you need to extend the Python's
search path for module files to execute the tests against the local copy directly. You can do it in an
ad-hoc manner by setting the ``PYTHONPATH`` environment variable:
.. code-block:: bash
PYTHONPATH=src pytest
or in a permanent manner by using the :confval:`pythonpath` configuration variable and adding the
following to your ``pyproject.toml``:
.. code-block:: toml
[tool.pytest.ini_options]
pythonpath = "src"
.. note::
If you do not use an editable install and not use the ``src`` layout (``mypkg`` directly in the root
directory) you can rely on the fact that Python by default puts the current directory in ``sys.path`` to
import your package and run ``python -m pytest`` to execute the tests against the local copy directly.
See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and
``python -m pytest``.
Note that this scheme has a drawback if you are using ``prepend`` :ref:`import mode <import-modes>`
(which is the default): your test files must have **unique names**, because
``pytest`` will import them as *top-level* modules since there are no packages
to derive a full package name from. In other words, the test files in the example above will
be imported as ``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to
``sys.path``.
If you need to have test modules with the same name, you might add ``__init__.py`` files to your
``tests`` folder and subfolders, changing them to packages:
.. code-block:: text
pyproject.toml
setup.cfg
mypkg/
...
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test_view``, allowing
you to have modules with the same name. But now this introduces a subtle problem: in order to load
the test modules from the ``tests`` directory, pytest prepends the root of the repository to
``sys.path``, which adds the side-effect that now ``mypkg`` is also importable.
This is problematic if you are using a tool like `tox`_ to test your package in a virtual environment,
because you want to test the *installed* version of your package, not the local code from the repository.
.. _`src-layout`:
In this situation, it is **strongly** suggested to use a ``src`` layout where application root package resides in a
sub-directory of your root:
.. code-block:: text
pyproject.toml
setup.cfg
src/
mypkg/
__init__.py
app.py
view.py
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
This layout prevents a lot of common pitfalls and has many benefits, which are better explained in this excellent
`blog post by Ionel Cristian Mărieș <https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure>`_.
.. note::
The new ``--import-mode=importlib`` (see :ref:`import-modes`) doesn't have
any of the drawbacks above because ``sys.path`` is not changed when importing
test modules, so users that run
into this issue are strongly encouraged to try it and report if the new option works well for them.
The ``src`` directory layout is still strongly recommended however.
Tests as part of application code
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -150,11 +191,12 @@ want to distribute them along with your application:
.. code-block:: text
pyproject.toml
[src/]mypkg/
setup.cfg
mypkg/
__init__.py
app.py
view.py
tests/
test/
__init__.py
test_app.py
test_view.py
@@ -212,56 +254,6 @@ Note that this layout also works in conjunction with the ``src`` layout mentione
much less surprising.
.. _which-import-mode:
Choosing an import mode
^^^^^^^^^^^^^^^^^^^^^^^
For historical reasons, pytest defaults to the ``prepend`` :ref:`import mode <import-modes>`
instead of the ``importlib`` import mode we recommend for new projects.
The reason lies in the way the ``prepend`` mode works:
Since there are no packages to derive a full package name from,
``pytest`` will import your test files as *top-level* modules.
The test files in the first example (:ref:`src layout <src-layout>`) would be imported as
``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to ``sys.path``.
This results in a drawback compared to the import mode ``importlib``:
your test files must have **unique names**.
If you need to have test modules with the same name,
as a workaround you might add ``__init__.py`` files to your ``tests`` folder and subfolders,
changing them to packages:
.. code-block:: text
pyproject.toml
mypkg/
...
tests/
__init__.py
foo/
__init__.py
test_view.py
bar/
__init__.py
test_view.py
Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test_view``,
allowing you to have modules with the same name.
But now this introduces a subtle problem:
in order to load the test modules from the ``tests`` directory,
pytest prepends the root of the repository to ``sys.path``,
which adds the side-effect that now ``mypkg`` is also importable.
This is problematic if you are using a tool like tox_ to test your package in a virtual environment,
because you want to test the *installed* version of your package,
not the local code from the repository.
The ``importlib`` import mode does not have any of the drawbacks above,
because ``sys.path`` is not changed when importing test modules.
.. _`buildout`: http://www.buildout.org/en/latest/
.. _`use tox`:
@@ -271,8 +263,8 @@ tox
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
virtualenv test automation tool.
``tox`` helps you to setup virtualenv environments with pre-defined
virtualenv test automation tool and its :doc:`pytest support <tox:example/pytest>`.
tox helps you to setup virtualenv environments with pre-defined
dependencies and then executing a pre-configured test command with
options. It will run tests against the installed package and not
against your source code checkout, helping to detect packaging

View File

@@ -16,7 +16,7 @@ import process can be controlled through the ``--import-mode`` command-line flag
these values:
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
of :py:data:`sys.path` if not already there, and then imported with the :func:`__import__ <__import__>` builtin.
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.
@@ -24,7 +24,7 @@ these values:
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
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
there, and imported with ``__import__``.
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:
@@ -43,21 +43,12 @@ these values:
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.
* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
* ``importlib``: 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`.
For this reason this doesn't require test module names to be unique.
One drawback however is that test modules are non-importable by each other. Also, utility
modules in the tests directories are not automatically importable because the tests directory is no longer
added to :py:data:`sys.path`.
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
.. seealso::
The :confval:`pythonpath` configuration variable.
For this reason this doesn't require test module names to be unique, but also makes test
modules non-importable by each other.
We intend to make ``importlib`` the default in future releases, depending on feedback.
``prepend`` and ``append`` import modes scenarios
-------------------------------------------------

View File

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

View File

@@ -238,7 +238,7 @@ file which provides an alternative explanation for ``Foo`` objects:
if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
return [
"Comparing Foo instances:",
f" vals: {left.val} != {right.val}",
" vals: {} != {}".format(left.val, right.val),
]
now, given this test module:

View File

@@ -199,6 +199,7 @@ across pytest invocations:
# content of test_caching.py
import pytest
import time
def expensive_computation():
@@ -233,7 +234,7 @@ If you run this command for the first time, you can see the print statement:
> assert mydata == 23
E assert 42 == 23
test_caching.py:19: AssertionError
test_caching.py:20: AssertionError
-------------------------- Captured stdout setup ---------------------------
running expensive computation...
========================= short test summary info ==========================
@@ -256,7 +257,7 @@ the cache and nothing will be printed:
> assert mydata == 23
E assert 42 == 23
test_caching.py:19: AssertionError
test_caching.py:20: AssertionError
========================= short test summary info ==========================
FAILED test_caching.py::test_function - assert 42 == 23
1 failed in 0.12s

View File

@@ -42,8 +42,6 @@ Running pytest now produces this output:
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
======================= 1 passed, 1 warning in 0.12s =======================
.. _`controlling-warnings`:
Controlling warnings
--------------------
@@ -109,18 +107,6 @@ When a warning matches more than one option in the list, the action for the last
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`:
``@pytest.mark.filterwarnings``
@@ -190,14 +176,11 @@ using an external system.
DeprecationWarning and PendingDeprecationWarning
------------------------------------------------
By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings from
user code and third-party libraries, as recommended by :pep:`565`.
This helps users keep their code modern and avoid breakages when deprecated warnings are effectively removed.
However, in the specific case where users capture any type of warnings in their test, either with
:func:`pytest.warns`, :func:`pytest.deprecated_call` or using the :ref:`recwarn <recwarn>` fixture,
no warning will be displayed at all.
Sometimes it is useful to hide some specific deprecation warnings that happen in code that you have no control over
(such as third-party libraries), in which case you might use the warning filters options (ini or marks) to ignore
those warnings.
@@ -214,9 +197,6 @@ For example:
This will ignore all warnings of type ``DeprecationWarning`` where the start of the message matches
the regular expression ``".*U.*mode is deprecated"``.
See :ref:`@pytest.mark.filterwarnings <filterwarnings>` and
:ref:`Controlling warnings <controlling-warnings>` for more examples.
.. note::
If warnings are configured at the interpreter level, using
@@ -265,15 +245,14 @@ when called with a ``17`` argument.
Asserting warnings with the warns function
------------------------------------------
You can check that code raises a particular warning using :func:`pytest.warns`,
which works in a similar manner to :ref:`raises <assertraises>` (except that
:ref:`raises <assertraises>` does not capture all exceptions, only the
``expected_exception``):
which works in a similar manner to :ref:`raises <assertraises>`:
.. code-block:: python
import warnings
import pytest
@@ -281,35 +260,21 @@ which works in a similar manner to :ref:`raises <assertraises>` (except that
with pytest.warns(UserWarning):
warnings.warn("my warning", UserWarning)
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.
To match a literal string that may contain regular expression metacharacters like ``(`` or ``.``, the pattern can
first be escaped with ``re.escape``.
The test will fail if the warning in question is not raised. The keyword
argument ``match`` to assert that the exception matches a text or regex::
Some examples:
.. code-block:: pycon
>>> with warns(UserWarning, match="must be 0 or None"):
>>> with warns(UserWarning, match='must be 0 or None'):
... 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)
...
>>> with warns(UserWarning, match=r"must be \d+$"):
>>> with warns(UserWarning, match=r'must be \d+$'):
... warnings.warn("this is not here", UserWarning)
...
Traceback (most recent call last):
...
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:
.. code-block:: python
@@ -393,32 +358,20 @@ Additional use cases of warnings in tests
Here are some use cases involving warnings that often come up in tests, and suggestions on how to deal with them:
- To ensure that **at least one** of the indicated warnings is issued, use:
- To ensure that **at least one** warning is emitted, use:
.. code-block:: python
def test_warning():
with pytest.warns((RuntimeWarning, UserWarning)):
...
- To ensure that **only** certain warnings are issued, use:
.. code-block:: python
def test_warning(recwarn):
with pytest.warns():
...
assert len(recwarn) == 1
user_warning = recwarn.pop(UserWarning)
assert issubclass(user_warning.category, UserWarning)
- To ensure that **no** warnings are emitted, use:
.. code-block:: python
def test_warning():
with warnings.catch_warnings():
warnings.simplefilter("error")
...
with warnings.catch_warnings():
warnings.simplefilter("error")
...
- To suppress warnings, use:

View File

@@ -126,17 +126,14 @@ pytest also introduces new options:
in expected doctest output.
* ``NUMBER``: when enabled, floating-point numbers only need to match as far as
the precision you have written in the expected doctest output. The numbers are
compared using :func:`pytest.approx` with relative tolerance equal to the
precision. For example, the following output would only need to match to 2
decimal places when comparing ``3.14`` to
``pytest.approx(math.pi, rel=10**-2)``::
the precision you have written in the expected doctest output. For example,
the following output would only need to match to 2 decimal places::
>>> math.pi
3.14
If you wrote ``3.1416`` then the actual output would need to match to
approximately 4 decimal places; and so on.
If you wrote ``3.1416`` then the actual output would need to match to 4
decimal places; and so on.
This avoids false positives caused by limited floating-point precision, like
this::
@@ -242,6 +239,7 @@ which can then be used in your doctests directly:
>>> len(a)
10
"""
pass
Note that like the normal ``conftest.py``, the fixtures are discovered in the directory tree conftest is in.
Meaning that if you put your doctest with your source code, the relevant conftest.py needs to be in the same directory tree.

View File

@@ -398,9 +398,8 @@ access the fixture function:
.. code-block:: python
# content of conftest.py
import smtplib
import pytest
import smtplib
@pytest.fixture(scope="module")
@@ -610,10 +609,10 @@ Here's what that might look like:
.. code-block:: python
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
from emaillib import Email, MailAdminClient
@pytest.fixture
def mail_admin():
@@ -631,7 +630,6 @@ Here's what that might look like:
def receiving_user(mail_admin):
user = mail_admin.create_user()
yield user
user.clear_mailbox()
mail_admin.delete_user(user)
@@ -685,10 +683,10 @@ Here's how the previous example would look using the ``addfinalizer`` method:
.. code-block:: python
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
from emaillib import Email, MailAdminClient
@pytest.fixture
def mail_admin():
@@ -738,87 +736,6 @@ does offer some nuances for when you're in a pinch.
. [100%]
1 passed in 0.12s
Note on finalizer order
""""""""""""""""""""""""
Finalizers are executed in a first-in-last-out order.
For yield fixtures, the first teardown code to run is from the right-most fixture, i.e. the last test parameter.
.. code-block:: python
# content of test_finalizers.py
import pytest
def test_bar(fix_w_yield1, fix_w_yield2):
print("test_bar")
@pytest.fixture
def fix_w_yield1():
yield
print("after_yield_1")
@pytest.fixture
def fix_w_yield2():
yield
print("after_yield_2")
.. code-block:: pytest
$ pytest -s test_finalizers.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_finalizers.py test_bar
.after_yield_2
after_yield_1
============================ 1 passed in 0.12s =============================
For finalizers, the first fixture to run is last call to `request.addfinalizer`.
.. code-block:: python
# content of test_finalizers.py
from functools import partial
import pytest
@pytest.fixture
def fix_w_finalizers(request):
request.addfinalizer(partial(print, "finalizer_2"))
request.addfinalizer(partial(print, "finalizer_1"))
def test_bar(fix_w_finalizers):
print("test_bar")
.. code-block:: pytest
$ pytest -s test_finalizers.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 1 item
test_finalizers.py test_bar
.finalizer_1
finalizer_2
============================ 1 passed in 0.12s =============================
This is so because yield fixtures use `addfinalizer` behind the scenes: when the fixture executes, `addfinalizer` registers a function that resumes the generator, which in turn calls the teardown code.
.. _`safe teardowns`:
Safe teardowns
@@ -835,10 +752,10 @@ above):
.. code-block:: python
# content of test_emaillib.py
from emaillib import Email, MailAdminClient
import pytest
from emaillib import Email, MailAdminClient
@pytest.fixture
def setup():
@@ -1113,9 +1030,8 @@ read an optional server URL from the test module which uses our fixture:
.. code-block:: python
# content of conftest.py
import smtplib
import pytest
import smtplib
@pytest.fixture(scope="module")
@@ -1123,7 +1039,7 @@ read an optional server URL from the test module which uses our fixture:
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
smtp_connection = smtplib.SMTP(server, 587, timeout=5)
yield smtp_connection
print(f"finalizing {smtp_connection} ({server})")
print("finalizing {} ({})".format(smtp_connection, server))
smtp_connection.close()
We use the ``request.module`` attribute to optionally obtain an
@@ -1237,6 +1153,7 @@ If the data created by the factory requires managing, the fixture can take care
@pytest.fixture
def make_customer_record():
created_records = []
def _make_customer_record(name):
@@ -1276,16 +1193,15 @@ through the special :py:class:`request <FixtureRequest>` object:
.. code-block:: python
# content of conftest.py
import smtplib
import pytest
import smtplib
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
def smtp_connection(request):
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
yield smtp_connection
print(f"finalizing {smtp_connection}")
print("finalizing {}".format(smtp_connection))
smtp_connection.close()
The main change is the declaration of ``params`` with
@@ -1416,15 +1332,13 @@ Running the above tests results in the following test IDs being used:
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project
collected 12 items
collected 11 items
<Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]>
<Module test_emaillib.py>
<Function test_email_received>
<Module test_finalizers.py>
<Function test_bar>
<Module test_ids.py>
<Function test_a[spam]>
<Function test_a[ham]>
@@ -1436,7 +1350,7 @@ Running the above tests results in the following test IDs being used:
<Function test_ehlo[mail.python.org]>
<Function test_noop[mail.python.org]>
======================= 12 tests collected in 0.12s ========================
======================= 11 tests collected in 0.12s ========================
.. _`fixture-parametrize-marks`:
@@ -1589,7 +1503,7 @@ to show the setup/teardown flow:
def test_2(otherarg, modarg):
print(f" RUN test2 with otherarg {otherarg} and modarg {modarg}")
print(" RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))
Let's run the tests in verbose mode and with looking at the print-output:
@@ -1690,7 +1604,6 @@ and declare its use in a test module via a ``usefixtures`` marker:
# content of test_setenv.py
import os
import pytest
@@ -1771,6 +1684,8 @@ Given the tests file structure is:
::
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@@ -1785,6 +1700,8 @@ Given the tests file structure is:
assert username == 'username'
subfolder/
__init__.py
conftest.py
# content of tests/subfolder/conftest.py
import pytest
@@ -1793,8 +1710,8 @@ Given the tests file structure is:
def username(username):
return 'overridden-' + username
test_something_else.py
# content of tests/subfolder/test_something_else.py
test_something.py
# content of tests/subfolder/test_something.py
def test_username(username):
assert username == 'overridden-username'
@@ -1810,6 +1727,8 @@ Given the tests file structure is:
::
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@@ -1851,6 +1770,8 @@ Given the tests file structure is:
::
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest
@@ -1887,6 +1808,8 @@ Given the tests file structure is:
::
tests/
__init__.py
conftest.py
# content of tests/conftest.py
import pytest

View File

@@ -55,13 +55,6 @@ These options can also be customized through ``pytest.ini`` file:
log_format = %(asctime)s %(levelname)s %(message)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,
stderr and logs) on failed tests completely with:
@@ -80,6 +73,7 @@ messages. This is supported by the ``caplog`` fixture:
def test_foo(caplog):
caplog.set_level(logging.INFO)
pass
By default the level is set on the root logger,
however as a convenience it is also possible to set the log level of any
@@ -89,6 +83,7 @@ logger:
def test_foo(caplog):
caplog.set_level(logging.CRITICAL, logger="root.baz")
pass
The log levels set are restored automatically at the end of the test.
@@ -166,7 +161,9 @@ the records for the ``setup`` and ``call`` stages during teardown like so:
x.message for x in caplog.get_records(when) if x.levelno == logging.WARNING
]
if messages:
pytest.fail(f"warning messages encountered during testing: {messages}")
pytest.fail(
"warning messages encountered during testing: {}".format(messages)
)
@@ -183,8 +180,8 @@ logging records as they are emitted directly into the console.
You can specify the logging level for which log records with equal or higher
level are printed to the console by passing ``--log-cli-level``. This setting
accepts the logging level names or numeric values as seen in
:ref:`logging's documentation <python:levels>`.
accepts the logging level names as seen in python's documentation or an integer
as the logging level num.
Additionally, you can also specify ``--log-cli-format`` and
``--log-cli-date-format`` which mirror and default to ``--log-format`` and
@@ -205,8 +202,9 @@ Note that relative paths for the log-file location, whether passed on the CLI or
config file, are always resolved relative to the current working directory.
You can also specify the logging level for the log file by passing
``--log-file-level``. This setting accepts the logging level names or numeric
values as seen in :ref:`logging's documentation <python:levels>`.
``--log-file-level``. This setting accepts the logging level names as seen in
python's documentation(ie, uppercased level names) or an integer as the logging
level num.
Additionally, you can also specify ``--log-file-format`` and
``--log-file-date-format`` which are equal to ``--log-format`` and

View File

@@ -3,7 +3,7 @@
How to monkeypatch/mock modules and environments
================================================================
.. currentmodule:: pytest
.. currentmodule:: _pytest.monkeypatch
Sometimes tests need to invoke functionality which depends
on global settings or which invokes code which cannot be easily
@@ -14,16 +14,17 @@ environment variable, or to modify ``sys.path`` for importing.
The ``monkeypatch`` fixture provides these helper methods for safely patching and mocking
functionality in tests:
* :meth:`monkeypatch.setattr(obj, name, value, raising=True) <pytest.MonkeyPatch.setattr>`
* :meth:`monkeypatch.delattr(obj, name, raising=True) <pytest.MonkeyPatch.delattr>`
* :meth:`monkeypatch.setitem(mapping, name, value) <pytest.MonkeyPatch.setitem>`
* :meth:`monkeypatch.delitem(obj, name, raising=True) <pytest.MonkeyPatch.delitem>`
* :meth:`monkeypatch.setenv(name, value, prepend=None) <pytest.MonkeyPatch.setenv>`
* :meth:`monkeypatch.delenv(name, raising=True) <pytest.MonkeyPatch.delenv>`
* :meth:`monkeypatch.syspath_prepend(path) <pytest.MonkeyPatch.syspath_prepend>`
* :meth:`monkeypatch.chdir(path) <pytest.MonkeyPatch.chdir>`
* :meth:`monkeypatch.context() <pytest.MonkeyPatch.context>`
.. code-block:: python
monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.setattr("somemodule.obj.name", value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.setitem(mapping, name, value)
monkeypatch.delitem(obj, name, raising=True)
monkeypatch.setenv(name, value, prepend=None)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
All modifications will be undone after the requesting
test function or fixture has finished. The ``raising``
@@ -54,16 +55,13 @@ during a test.
5. Use :py:meth:`monkeypatch.syspath_prepend <MonkeyPatch.syspath_prepend>` to modify ``sys.path`` which will also
call ``pkg_resources.fixup_namespace_packages`` and :py:func:`importlib.invalidate_caches`.
6. Use :py:meth:`monkeypatch.context <MonkeyPatch.context>` to apply patches only in a specific scope, which can help
control teardown of complex fixtures or patches to the stdlib.
See the `monkeypatch blog post`_ for some introduction material
and a discussion of its motivation.
.. _`monkeypatch blog post`: https://tetamap.wordpress.com//2009/03/03/monkeypatching-in-unit-tests-done-right/
Monkeypatching functions
------------------------
Simple example: monkeypatching functions
----------------------------------------
Consider a scenario where you are working with user directories. In the context of
testing, you do not want your test to depend on the running user. ``monkeypatch``
@@ -135,10 +133,10 @@ This can be done in our test file by defining a class to represent ``r``.
# this is the previous code block example
import app
# custom class to be the mock return value
# will override the requests.Response returned from requests.get
class MockResponse:
# mock json() method always returns a specific testing dictionary
@staticmethod
def json():
@@ -146,6 +144,7 @@ This can be done in our test file by defining a class to represent ``r``.
def test_get_json(monkeypatch):
# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method.
def mock_get(*args, **kwargs):
@@ -180,7 +179,6 @@ This mock can be shared across tests using a ``fixture``:
# app.py that includes the get_json() function
import app
# custom class to be the mock return value of requests.get()
class MockResponse:
@staticmethod
@@ -358,6 +356,7 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific
def test_connection(monkeypatch):
# Patch the values of DEFAULT_CONFIG to specific
# testing values only for this test.
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
@@ -382,6 +381,7 @@ You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove v
def test_missing_user(monkeypatch):
# patch the DEFAULT_CONFIG t be missing the 'user' key
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
@@ -402,7 +402,6 @@ separate fixtures for each potential mock and reference them in the needed tests
# app.py with the connection string function
import app
# all of the mocks are moved into separated fixtures
@pytest.fixture
def mock_test_user(monkeypatch):
@@ -424,6 +423,7 @@ separate fixtures for each potential mock and reference them in the needed tests
# tests reference only the fixture mocks that are needed
def test_connection(mock_test_user, mock_test_database):
expected = "User Id=test_user; Location=test_db;"
result = app.create_connection_string()
@@ -431,11 +431,12 @@ separate fixtures for each potential mock and reference them in the needed tests
def test_missing_user(mock_missing_default_user):
with pytest.raises(KeyError):
_ = app.create_connection_string()
.. currentmodule:: pytest
.. currentmodule:: _pytest.monkeypatch
API Reference
-------------

View File

@@ -5,9 +5,6 @@ How to run tests written for nose
``pytest`` has basic support for running tests written for nose_.
.. warning::
This functionality has been deprecated and is likely to be removed in ``pytest 8.x``.
.. _nosestyle:
Usage
@@ -26,8 +23,8 @@ make use of pytest's capabilities.
Supported nose Idioms
----------------------
* ``setup()`` and ``teardown()`` at module/class/method level: any function or method called ``setup`` will be called during the setup phase for each test, same for ``teardown``.
* ``SkipTest`` exceptions and markers
* setup and teardown at module/class/method level
* SkipTest exceptions and markers
* setup/teardown decorators
* ``__test__`` attribute on modules/classes/functions
* general usage of nose utilities

View File

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

View File

@@ -51,9 +51,6 @@ Here is a little annotated list for some popular plugins:
* :pypi:`pytest-flakes`:
check source code with pyflakes.
* :pypi:`allure-pytest`:
report test results via `allure-framework <https://github.com/allure-framework/>`_.
To see a complete list of all plugins with their latest testing
status against different pytest and Python versions, please visit
:ref:`plugin-list`.

View File

@@ -69,7 +69,6 @@ It is also possible to skip the whole module using
.. code-block:: python
import sys
import pytest
if not sys.platform.startswith("win"):
@@ -410,7 +409,6 @@ test instances when using parametrize:
.. code-block:: python
import sys
import pytest

View File

@@ -104,21 +104,8 @@ The ``tmpdir`` and ``tmpdir_factory`` fixtures
The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path``
and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects
rather than standard :class:`pathlib.Path` objects.
.. note::
These days, it is preferred to use ``tmp_path`` and ``tmp_path_factory``.
In order to help modernize old code bases, one can run pytest with the legacypath
plugin disabled:
.. code-block:: bash
pytest -p no:legacypath
This will trigger errors on tests using the legacy paths.
It can also be permanently set as part of the :confval:`addopts` parameter in the
config file.
rather than standard :class:`pathlib.Path` objects. These days, prefer to
use ``tmp_path`` and ``tmp_path_factory``.
See :fixture:`tmpdir <tmpdir>` :fixture:`tmpdir_factory <tmpdir_factory>`
API for details.
@@ -131,12 +118,10 @@ The default base temporary directory
Temporary directories are by default created as sub-directories of
the system temporary directory. The base name will be ``pytest-NUM`` where
``NUM`` will be incremented with each test run.
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`.
``NUM`` will be incremented with each test run. Moreover, entries older
than 3 temporary directories will be removed.
Using the ``--basetemp``
The number of entries currently cannot be changed, but using the ``--basetemp``
option will remove the directory before every run, effectively meaning the temporary directories
of only the most recent run will be kept.

View File

@@ -27,15 +27,12 @@ Almost all ``unittest`` features are supported:
* ``setUpClass/tearDownClass``;
* ``setUpModule/tearDownModule``;
.. _`pytest-subtests`: https://github.com/pytest-dev/pytest-subtests
.. _`load_tests protocol`: https://docs.python.org/3/library/unittest.html#load-tests-protocol
Additionally, :ref:`subtests <python:subtests>` are supported by the
`pytest-subtests`_ plugin.
Up to this point pytest does not have support for the following features:
* `load_tests protocol`_;
* :ref:`subtests <python:subtests>`;
Benefits out of the box
-----------------------
@@ -118,7 +115,6 @@ fixture definition:
# content of test_unittest_db.py
import unittest
import pytest
@@ -157,7 +153,7 @@ the ``self.db`` values in the traceback:
E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
E assert 0
test_unittest_db.py:11: AssertionError
test_unittest_db.py:10: AssertionError
___________________________ MyTest.test_method2 ____________________________
self = <test_unittest_db.MyTest testMethod=test_method2>
@@ -167,7 +163,7 @@ the ``self.db`` values in the traceback:
E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
E assert 0
test_unittest_db.py:14: AssertionError
test_unittest_db.py:13: AssertionError
========================= short test summary info ==========================
FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: <conft...
FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: <conft...
@@ -198,9 +194,9 @@ creation of a per-test temporary directory:
.. code-block:: python
# content of test_unittest_cleandir.py
import unittest
import os
import pytest
import unittest
class MyTest(unittest.TestCase):

View File

@@ -183,9 +183,8 @@ You can specify additional plugins to ``pytest.main``:
.. code-block:: python
# content of myinvoke.py
import sys
import pytest
import sys
class MyPlugin:

View File

@@ -194,7 +194,7 @@ class or module can then be passed to the ``pluginmanager`` using the ``pytest_a
.. code-block:: python
def pytest_addhooks(pluginmanager):
"""This example assumes the hooks are grouped in the 'sample_hook' module."""
""" This example assumes the hooks are grouped in the 'sample_hook' module. """
from my_app.tests import sample_hook
pluginmanager.add_hookspecs(sample_hook)
@@ -249,19 +249,18 @@ and use pytest_addoption as follows:
# contents of hooks.py
# Use firstresult=True because we only want one plugin to define this
# default value
@hookspec(firstresult=True)
def pytest_config_file_default_value():
"""Return the default value for the config file command line option."""
""" Return the default value for the config file command line option. """
# contents of myplugin.py
def pytest_addhooks(pluginmanager):
"""This example assumes the hooks are grouped in the 'hooks' module."""
""" This example assumes the hooks are grouped in the 'hooks' module. """
from . import hooks
pluginmanager.add_hookspecs(hooks)

View File

@@ -147,32 +147,29 @@ Making your plugin installable by others
If you want to make your plugin externally available, you
may define a so-called entry point for your distribution so
that ``pytest`` finds your plugin module. Entry points are
a feature that is provided by :std:doc:`setuptools <setuptools:index>`.
that ``pytest`` finds your plugin module. Entry points are
a feature that is provided by :std:doc:`setuptools:index`. pytest looks up
the ``pytest11`` entrypoint to discover its
plugins and you can thus make your plugin available by defining
it in your setuptools-invocation:
pytest looks up the ``pytest11`` entrypoint to discover its
plugins, thus you can make your plugin available by defining
it in your ``pyproject.toml`` file.
.. sourcecode:: python
.. sourcecode:: toml
# sample ./setup.py file
from setuptools import setup
# sample ./pyproject.toml file
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "myproject"
classifiers = [
"Framework :: Pytest",
]
[project.entry-points.pytest11]
myproject = "myproject.pluginmodule"
setup(
name="myproject",
packages=["myproject"],
# the following makes a plugin available to pytest
entry_points={"pytest11": ["name_of_plugin = myproject.pluginmodule"]},
# custom PyPI classifier for pytest plugins
classifiers=["Framework :: Pytest"],
)
If a package is installed this way, ``pytest`` will load
``myproject.pluginmodule`` as a plugin which can define
:ref:`hooks <hook-reference>`. Confirm registration with ``pytest --trace-config``
:ref:`hooks <hook-reference>`.
.. note::
@@ -370,7 +367,7 @@ string value of ``Hello World!`` if we do not supply a value or ``Hello
def _hello(name=None):
if not name:
name = request.config.getoption("name")
return f"Hello {name}!"
return "Hello {name}!".format(name=name)
return _hello
@@ -449,8 +446,7 @@ in our ``pytest.ini`` to tell pytest where to look for example files.
$ pytest
=========================== test session starts ============================
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
test_example.py .. [100%]

View File

@@ -32,7 +32,7 @@ which will usually be called once for all the functions:
.. code-block:: python
def setup_module(module):
"""setup any state specific to the execution of the given module."""
""" setup any state specific to the execution of the given module."""
def teardown_module(module):
@@ -63,8 +63,6 @@ and after all test methods of the class are called:
setup_class.
"""
.. _xunit-method-setup:
Method and function level setup/teardown
-----------------------------------------------

View File

@@ -1,11 +1,16 @@
:orphan:
.. sidebar:: Next Open Trainings
- `PyConDE <https://2022.pycon.de/program/W93DBJ/>`__, April 11th 2022 (3h), Berlin, Germany
- `PyConIT <https://pycon.it/en/talk/pytest-simple-rapid-and-fun-testing-with-python>`__, June 3rd 2022 (4h), Florence, Italy
- `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
Also see :doc:`previous talks and blogposts <talks>`.
..
.. sidebar:: Next Open Trainings
- `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
Also see :doc:`previous talks and blogposts <talks>`.
- `Europython <https://ep2022.europython.eu/>`__, July 11th to 17th (3h), Dublin, Ireland
- `CH Open Workshoptage <https://workshoptage.ch/>`__ (German), September 6th to 8th (1 day), Bern, Switzerland
.. _features:
@@ -22,6 +27,8 @@ scale to support complex functional testing for applications and libraries.
**PyPI package name**: :pypi:`pytest`
**Documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
A quick example
---------------
@@ -97,6 +104,11 @@ Bugs/Requests
Please use the `GitHub issue tracker <https://github.com/pytest-dev/pytest/issues>`_ to submit bugs or request features.
Changelog
---------
Consult the :ref:`Changelog <changelog>` page for fixes and enhancements of each version.
Support pytest
--------------
@@ -129,3 +141,13 @@ Security
pytest has never been associated with a security vulnerability, but in any case, to report a
security vulnerability please use the `Tidelift security contact <https://tidelift.com/security>`_.
Tidelift will coordinate the fix and disclosure.
License
-------
Copyright Holger Krekel and others, 2004.
Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
.. _`MIT`: https://github.com/pytest-dev/pytest/blob/main/LICENSE

View File

@@ -0,0 +1,99 @@
Python 2.7 and 3.4 support
==========================
It is demanding on the maintainers of an open source project to support many Python versions, as
there's extra cost of keeping code compatible between all versions, while holding back on
features only made possible on newer Python versions.
In case of Python 2 and 3, the difference between the languages makes it even more prominent,
because many new Python 3 features cannot be used in a Python 2/3 compatible code base.
Python 2.7 EOL has been reached :pep:`in 2020 <0373#maintenance-releases>`, with
the last release made in April, 2020.
Python 3.4 EOL has been reached :pep:`in 2019 <0429#release-schedule>`, with the last release made in March, 2019.
For those reasons, in Jun 2019 it was decided that **pytest 4.6** series will be the last to support Python 2.7 and 3.4.
What this means for general users
---------------------------------
Thanks to the `python_requires`_ setuptools option,
Python 2.7 and Python 3.4 users using a modern pip version
will install the last pytest 4.6.X version automatically even if 5.0 or later versions
are available on PyPI.
Users should ensure they are using the latest pip and setuptools versions for this to work.
Maintenance of 4.6.X versions
-----------------------------
Until January 2020, the pytest core team ported many bug-fixes from the main release into the
``4.6.x`` branch, with several 4.6.X releases being made along the year.
From now on, the core team will **no longer actively backport patches**, but the ``4.6.x``
branch will continue to exist so the community itself can contribute patches.
The core team will be happy to accept those patches, and make new 4.6.X releases **until mid-2020**
(but consider that date as a ballpark, after that date the team might still decide to make new releases
for critical bugs).
.. _`python_requires`: https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
Technical aspects
~~~~~~~~~~~~~~~~~
(This section is a transcript from :issue:`5275`).
In this section we describe the technical aspects of the Python 2.7 and 3.4 support plan.
.. _what goes into 4.6.x releases:
What goes into 4.6.X releases
+++++++++++++++++++++++++++++
New 4.6.X releases will contain bug fixes only.
When will 4.6.X releases happen
+++++++++++++++++++++++++++++++
New 4.6.X releases will happen after we have a few bugs in place to release, or if a few weeks have
passed (say a single bug has been fixed a month after the latest 4.6.X release).
No hard rules here, just ballpark.
Who will handle applying bug fixes
++++++++++++++++++++++++++++++++++
We core maintainers expect that people still using Python 2.7/3.4 and being affected by
bugs to step up and provide patches and/or port bug fixes from the active branches.
We will be happy to guide users interested in doing so, so please don't hesitate to ask.
**Backporting changes into 4.6**
Please follow these instructions:
#. ``git fetch --all --prune``
#. ``git checkout origin/4.6.x -b backport-XXXX`` # use the PR number here
#. Locate the merge commit on the PR, in the *merged* message, for example:
nicoddemus merged commit 0f8b462 into pytest-dev:features
#. ``git cherry-pick -m1 REVISION`` # use the revision you found above (``0f8b462``).
#. Open a PR targeting ``4.6.x``:
* Prefix the message with ``[4.6]`` so it is an obvious backport
* Delete the PR body, it usually contains a duplicate commit message.
**Providing new PRs to 4.6**
Fresh pull requests to ``4.6.x`` will be accepted provided that
the equivalent code in the active branches does not contain that bug (for example, a bug is specific
to Python 2 only).
Bug fixes that also happen in the mainstream version should be first fixed
there, and then backported as per instructions above.

View File

@@ -29,11 +29,9 @@ pytest.ini
``pytest.ini`` files take precedence over other files, even when empty.
Alternatively, the hidden version ``.pytest.ini`` can be used.
.. code-block:: ini
# pytest.ini or .pytest.ini
# pytest.ini
[pytest]
minversion = 6.0
addopts = -ra -q
@@ -90,7 +88,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
setup.cfg
~~~~~~~~~
``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <python:distutils/configfile>`, and can also be used to hold pytest configuration
``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <distutils/configfile>`, and can also be used to hold pytest configuration
if they have a ``[tool:pytest]`` section.
.. code-block:: ini

View File

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

View File

@@ -8,8 +8,8 @@ Reference guides
.. toctree::
:maxdepth: 1
reference
fixtures
customize
exit-codes
plugin_list
customize
reference
exit-codes

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,10 @@
pallets-sphinx-themes
pluggy>=1.0
pygments-pytest>=2.3.0
pygments-pytest>=2.2.0
sphinx-removed-in>=0.2.0
sphinx>=5,<6
sphinx>=3.1,<4
sphinxcontrib-trio
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
# XXX: sphinx<4 is broken with latest jinja2
jinja2<3.1

View File

@@ -17,8 +17,6 @@ Books
Talks and blog postings
---------------------------------------------
- Training: `pytest - simple, rapid and fun testing with Python <https://www.youtube.com/watch?v=ofPHJrAOaTE>`_, Florian Bruhin, PyConDE 2022
- `pytest: Simple, rapid and fun testing with Python, <https://youtu.be/cSJ-X3TbQ1c?t=15752>`_ (@ 4:22:32), Florian Bruhin, WeAreDevelopers World Congress 2021
- Webinar: `pytest: Test Driven Development für Python (German) <https://bruhin.software/ins-pytest/>`_, Florian Bruhin, via mylearning.ch, 2020

View File

@@ -0,0 +1,12 @@
import sys
from distutils.core import setup
if __name__ == "__main__":
if "sdist" not in sys.argv[1:]:
raise ValueError("please use 'pytest' pypi package instead of 'py.test'")
setup(
name="py.test",
version="0.0",
description="please use 'pytest' for installation",
)

View File

@@ -3,6 +3,7 @@ requires = [
# sync with setup.py until we discard non-pep-517/518
"setuptools>=45.0",
"setuptools-scm[toml]>=6.2.3",
"wheel",
]
build-backend = "setuptools.build_meta"
@@ -37,9 +38,6 @@ filterwarnings = [
# Those are caught/handled by pyupgrade, and not easy to filter with the
# module being the filename (with .py removed).
"default:invalid escape sequence:DeprecationWarning",
# ignore not yet fixed warnings for hook markers
"default:.*not marked using pytest.hook.*",
"ignore:.*not marked using pytest.hook.*::xdist.*",
# ignore use of unregistered marks, because we use many to test the implementation
"ignore::_pytest.warning_types.PytestUnknownMarkWarning",
# https://github.com/benjaminp/six/issues/341
@@ -114,8 +112,3 @@ template = "changelog/_template.rst"
[tool.black]
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

@@ -78,23 +78,11 @@ def iter_plugins():
requires = "N/A"
if info["requires_dist"]:
for requirement in info["requires_dist"]:
if re.match(r"pytest(?![-.\w])", requirement):
if requirement == "pytest" or "pytest " in requirement:
requires = requirement
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"]
for release in sorted(releases, key=version_sort_key, reverse=True):
for release in sorted(releases, key=packaging.version.parse, reverse=True):
if releases[release]:
release_date = datetime.date.fromisoformat(
releases[release][-1]["upload_time_iso_8601"].split("T")[0]
@@ -102,9 +90,7 @@ def iter_plugins():
last_release = release_date.strftime("%b %d, %Y")
break
name = f':pypi:`{info["name"]}`'
summary = ""
if info["summary"]:
summary = escape_rst(info["summary"].replace("\n", ""))
summary = escape_rst(info["summary"].replace("\n", ""))
yield {
"name": name,
"summary": summary.strip(),
@@ -136,7 +122,7 @@ def main():
reference_dir = pathlib.Path("doc", "en", "reference")
plugin_list = reference_dir / "plugin_list.rst"
with plugin_list.open("w", encoding="UTF-8") as f:
with plugin_list.open("w") as f:
f.write(FILE_HEAD)
f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(".. only:: not latex\n\n")

View File

@@ -21,7 +21,6 @@ classifiers =
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Topic :: Software Development :: Libraries
Topic :: Software Development :: Testing
Topic :: Utilities
@@ -37,20 +36,20 @@ packages =
_pytest
_pytest._code
_pytest._io
_pytest._py
_pytest.assertion
_pytest.config
_pytest.mark
pytest
py_modules = py
install_requires =
attrs>=19.2.0
iniconfig
packaging
pluggy>=0.12,<2.0
py>=1.8.2
tomli>=1.0.0
atomicwrites>=1.0;sys_platform=="win32"
colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11"
importlib-metadata>=0.12;python_version<"3.8"
tomli>=1.0.0;python_version<"3.11"
python_requires = >=3.7
package_dir =
=src
@@ -67,7 +66,6 @@ console_scripts =
[options.extras_require]
testing =
argcomplete
attrs>=19.2.0
hypothesis>=3.56
mock
nose
@@ -96,6 +94,7 @@ mypy_path = src
check_untyped_defs = True
disallow_any_generics = True
ignore_missing_imports = True
no_implicit_optional = True
show_error_codes = True
strict_equality = True
warn_redundant_casts = True

View File

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

View File

@@ -1,5 +1,4 @@
import ast
import dataclasses
import inspect
import os
import re
@@ -33,6 +32,7 @@ from typing import TypeVar
from typing import Union
from weakref import ref
import attr
import pluggy
import _pytest
@@ -56,9 +56,6 @@ if TYPE_CHECKING:
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup
class Code:
"""Wrapper around Python code objects."""
@@ -411,13 +408,13 @@ class Traceback(List[TracebackEntry]):
"""
return Traceback(filter(fn, self), self._excinfo)
def getcrashentry(self) -> Optional[TracebackEntry]:
def getcrashentry(self) -> TracebackEntry:
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
for i in range(-1, -len(self) - 1, -1):
entry = self[i]
if not entry.ishidden():
return entry
return None
return self[-1]
def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if
@@ -445,7 +442,7 @@ E = TypeVar("E", bound=BaseException, covariant=True)
@final
@dataclasses.dataclass
@attr.s(repr=False, init=False, auto_attribs=True)
class ExceptionInfo(Generic[E]):
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
@@ -602,13 +599,11 @@ class ExceptionInfo(Generic[E]):
"""
return isinstance(self.value, exc)
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
def _getreprcrash(self) -> "ReprFileLocation":
exconly = self.exconly(tryshort=True)
entry = self.traceback.getcrashentry()
if entry:
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
return None
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
def getrepr(
self,
@@ -651,12 +646,12 @@ class ExceptionInfo(Generic[E]):
"""
if style == "native":
return ReprExceptionInfo(
reprtraceback=ReprTracebackNative(
ReprTracebackNative(
traceback.format_exception(
self.type, self.value, self.traceback[0]._rawentry
)
),
reprcrash=self._getreprcrash(),
self._getreprcrash(),
)
fmt = FormattedExcinfo(
@@ -677,16 +672,15 @@ class ExceptionInfo(Generic[E]):
If it matches `True` is returned, otherwise an `AssertionError` is raised.
"""
__tracebackhide__ = True
value = str(self.value)
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
if regexp == value:
msg += "\n Did you mean to `re.escape()` the regex?"
assert re.search(regexp, value), msg
msg = "Regex pattern {!r} does not match {!r}."
if regexp == str(self.value):
msg += " Did you mean to `re.escape()` the regex?"
assert re.search(regexp, str(self.value)), msg.format(regexp, str(self.value))
# Return True to allow for "assert excinfo.match()".
return True
@dataclasses.dataclass
@attr.s(auto_attribs=True)
class FormattedExcinfo:
"""Presenting information about failing Functions and Generators."""
@@ -701,8 +695,8 @@ class FormattedExcinfo:
funcargs: bool = False
truncate_locals: bool = True
chain: bool = True
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
default_factory=dict, init=False, repr=False
astcache: Dict[Union[str, Path], ast.AST] = attr.ib(
factory=dict, init=False, repr=False
)
def _getindent(self, source: "Source") -> int:
@@ -743,13 +737,11 @@ class FormattedExcinfo:
) -> List[str]:
"""Return formatted and marked up source lines."""
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.
if source is None or line_index >= len(source.lines):
source = Source("???")
line_index = 0
if line_index < 0:
line_index += len(source)
space_prefix = " "
if short:
lines.append(space_prefix + source.lines[line_index].strip())
@@ -931,29 +923,10 @@ class FormattedExcinfo:
while e is not None and id(e) not in seen:
seen.add(id(e))
if excinfo_:
# Fall back to native traceback as a temporary workaround until
# full support for exception groups added to ExceptionInfo.
# See https://github.com/pytest-dev/pytest/issues/9159
if isinstance(e, BaseExceptionGroup):
reprtraceback: Union[
ReprTracebackNative, ReprTraceback
] = ReprTracebackNative(
traceback.format_exception(
type(excinfo_.value),
excinfo_.value,
excinfo_.traceback[0]._rawentry,
)
)
else:
reprtraceback = self.repr_traceback(excinfo_)
# will be None if all traceback entries are hidden
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
if reprcrash:
if self.style == "value":
repr_chain += [(reprtraceback, None, descr)]
else:
repr_chain += [(reprtraceback, reprcrash, descr)]
reprtraceback = self.repr_traceback(excinfo_)
reprcrash: Optional[ReprFileLocation] = (
excinfo_._getreprcrash() if self.style != "value" else None
)
else:
# Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work.
@@ -961,8 +934,8 @@ class FormattedExcinfo:
traceback.format_exception(type(e), e, None)
)
reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)]
repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None and self.chain:
e = e.__cause__
excinfo_ = (
@@ -987,7 +960,7 @@ class FormattedExcinfo:
return ExceptionChainRepr(repr_chain)
@dataclasses.dataclass(eq=False)
@attr.s(eq=False, auto_attribs=True)
class TerminalRepr:
def __str__(self) -> str:
# FYI this is called from pytest-xdist's serialization of exception
@@ -1005,14 +978,14 @@ class TerminalRepr:
# This class is abstract -- only subclasses are instantiated.
@dataclasses.dataclass(eq=False)
@attr.s(eq=False)
class ExceptionRepr(TerminalRepr):
# Provided by subclasses.
reprtraceback: "ReprTraceback"
reprcrash: Optional["ReprFileLocation"]
sections: List[Tuple[str, str, str]] = dataclasses.field(
init=False, default_factory=list
)
reprtraceback: "ReprTraceback"
def __attrs_post_init__(self) -> None:
self.sections: List[Tuple[str, str, str]] = []
def addsection(self, name: str, content: str, sep: str = "-") -> None:
self.sections.append((name, content, sep))
@@ -1023,23 +996,16 @@ class ExceptionRepr(TerminalRepr):
tw.line(content)
@dataclasses.dataclass(eq=False)
@attr.s(eq=False, auto_attribs=True)
class ExceptionChainRepr(ExceptionRepr):
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
def __init__(
self,
chain: Sequence[
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
],
) -> None:
def __attrs_post_init__(self) -> None:
super().__attrs_post_init__()
# reprcrash and reprtraceback of the outermost (the newest) exception
# in the chain.
super().__init__(
reprtraceback=chain[-1][0],
reprcrash=chain[-1][1],
)
self.chain = chain
self.reprtraceback = self.chain[-1][0]
self.reprcrash = self.chain[-1][1]
def toterminal(self, tw: TerminalWriter) -> None:
for element in self.chain:
@@ -1050,17 +1016,17 @@ class ExceptionChainRepr(ExceptionRepr):
super().toterminal(tw)
@dataclasses.dataclass(eq=False)
@attr.s(eq=False, auto_attribs=True)
class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback"
reprcrash: Optional["ReprFileLocation"]
reprcrash: "ReprFileLocation"
def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw)
super().toterminal(tw)
@dataclasses.dataclass(eq=False)
@attr.s(eq=False, auto_attribs=True)
class ReprTraceback(TerminalRepr):
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
extraline: Optional[str]
@@ -1089,12 +1055,12 @@ class ReprTraceback(TerminalRepr):
class ReprTracebackNative(ReprTraceback):
def __init__(self, tblines: Sequence[str]) -> None:
self.style = "native"
self.reprentries = [ReprEntryNative(tblines)]
self.extraline = None
self.style = "native"
@dataclasses.dataclass(eq=False)
@attr.s(eq=False, auto_attribs=True)
class ReprEntryNative(TerminalRepr):
lines: Sequence[str]
@@ -1104,7 +1070,7 @@ class ReprEntryNative(TerminalRepr):
tw.write("".join(self.lines))
@dataclasses.dataclass(eq=False)
@attr.s(eq=False, auto_attribs=True)
class ReprEntry(TerminalRepr):
lines: Sequence[str]
reprfuncargs: Optional["ReprFuncArgs"]
@@ -1184,15 +1150,12 @@ class ReprEntry(TerminalRepr):
)
@dataclasses.dataclass(eq=False)
@attr.s(eq=False, auto_attribs=True)
class ReprFileLocation(TerminalRepr):
path: str
path: str = attr.ib(converter=str)
lineno: int
message: str
def __post_init__(self) -> None:
self.path = str(self.path)
def toterminal(self, tw: TerminalWriter) -> None:
# Filename and lineno output for each entry, using an output format
# that most editors understand.
@@ -1204,7 +1167,7 @@ class ReprFileLocation(TerminalRepr):
tw.line(f":{self.lineno}: {msg}")
@dataclasses.dataclass(eq=False)
@attr.s(eq=False, auto_attribs=True)
class ReprLocals(TerminalRepr):
lines: Sequence[str]
@@ -1213,7 +1176,7 @@ class ReprLocals(TerminalRepr):
tw.line(indent + line)
@dataclasses.dataclass(eq=False)
@attr.s(eq=False, auto_attribs=True)
class ReprFuncArgs(TerminalRepr):
args: Sequence[Tuple[str, object]]

View File

@@ -41,7 +41,7 @@ class SafeRepr(reprlib.Repr):
information on exceptions raised during the call.
"""
def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None:
def __init__(self, maxsize: Optional[int]) -> None:
"""
:param maxsize:
If not None, will truncate the resulting repr to that specific size, using ellipsis
@@ -54,15 +54,10 @@ class SafeRepr(reprlib.Repr):
# truncation.
self.maxstring = maxsize if maxsize is not None else 1_000_000_000
self.maxsize = maxsize
self.use_ascii = use_ascii
def repr(self, x: object) -> str:
try:
if self.use_ascii:
s = ascii(x)
else:
s = super().repr(x)
s = super().repr(x)
except (KeyboardInterrupt, SystemExit):
raise
except BaseException as exc:
@@ -99,9 +94,7 @@ def safeformat(obj: object) -> str:
DEFAULT_REPR_MAX_SIZE = 240
def saferepr(
obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
) -> str:
def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str:
"""Return a size-limited safe repr-string for the given object.
Failing __repr__ functions of user instances will be represented
@@ -111,27 +104,7 @@ def saferepr(
This function is a wrapper around the Repr/reprlib functionality of the
stdlib.
"""
return SafeRepr(maxsize, use_ascii).repr(obj)
def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
"""Return an unlimited-size safe repr-string for the given object.
As with saferepr, failing __repr__ functions of user instances
will be represented with a short exception info.
This function is a wrapper around simple repr.
Note: a cleaner solution would be to alter ``saferepr``this way
when maxsize=None, but that might affect some other code.
"""
try:
if use_ascii:
return ascii(obj)
return repr(obj)
except Exception as exc:
return _format_repr_exception(exc, obj)
return SafeRepr(maxsize).repr(obj)
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):

View File

@@ -1,109 +0,0 @@
"""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]

File diff suppressed because it is too large Load Diff

View File

@@ -39,7 +39,7 @@ def pytest_addoption(parser: Parser) -> None:
"enable_assertion_pass_hook",
type="bool",
default=False,
help="Enables the pytest_assertion_pass hook. "
help="Enables the pytest_assertion_pass hook."
"Make sure to delete any previously generated pyc cache files.",
)
@@ -53,7 +53,7 @@ def register_assert_rewrite(*names: str) -> None:
actually imported, usually in your __init__.py if you are a plugin
using a package.
:param names: The module names to register.
:raises TypeError: If the given module names are not strings.
"""
for name in names:
if not isinstance(name, str):

View File

@@ -44,14 +44,10 @@ from _pytest.stash import StashKey
if TYPE_CHECKING:
from _pytest.assertion import AssertionState
if sys.version_info >= (3, 8):
namedExpr = ast.NamedExpr
else:
namedExpr = ast.Expr
assertstate_key = StashKey["AssertionState"]()
# pytest caches rewritten pycs in pycache dirs
PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
PYC_EXT = ".py" + (__debug__ and "c" or "o")
@@ -184,7 +180,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
for initial_path in self.session._initialpaths:
# Make something as c:/projects/my_project/path.py ->
# ['c:', 'projects', 'my_project', 'path.py']
parts = str(initial_path).split(os.sep)
parts = str(initial_path).split(os.path.sep)
# add 'path' to basenames to be checked.
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
@@ -194,7 +190,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
return False
# For matching the name it must be as if it was a filename.
path = PurePath(*parts).with_suffix(".py")
path = PurePath(os.path.sep.join(parts) + ".py")
for pat in self.fnpats:
# if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
@@ -278,20 +274,14 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
return f.read()
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) -> TraversableResources: # type: ignore
def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore
if sys.version_info < (3, 11):
from importlib.readers import FileReader
else:
from importlib.resources.readers import FileReader
return FileReader( # type:ignore[no-any-return]
types.SimpleNamespace(path=self._rewritten_names[name])
)
return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
def _write_pyc_fp(
@@ -312,29 +302,53 @@ def _write_pyc_fp(
fp.write(marshal.dumps(co))
def _write_pyc(
state: "AssertionState",
co: types.CodeType,
source_stat: os.stat_result,
pyc: Path,
) -> bool:
proc_pyc = f"{pyc}.{os.getpid()}"
try:
with open(proc_pyc, "wb") as fp:
_write_pyc_fp(fp, source_stat, co)
except OSError as e:
state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
return False
if sys.platform == "win32":
from atomicwrites import atomic_write
try:
os.replace(proc_pyc, pyc)
except OSError as e:
state.trace(f"error writing pyc file at {pyc}: {e}")
# we ignore any failure to write the cache file
# there are many reasons, permission-denied, pycache dir being a
# file etc.
return False
return True
def _write_pyc(
state: "AssertionState",
co: types.CodeType,
source_stat: os.stat_result,
pyc: Path,
) -> bool:
try:
with atomic_write(os.fspath(pyc), mode="wb", overwrite=True) as fp:
_write_pyc_fp(fp, source_stat, co)
except OSError as e:
state.trace(f"error writing pyc file at {pyc}: {e}")
# we ignore any failure to write the cache file
# there are many reasons, permission-denied, pycache dir being a
# file etc.
return False
return True
else:
def _write_pyc(
state: "AssertionState",
co: types.CodeType,
source_stat: os.stat_result,
pyc: Path,
) -> bool:
proc_pyc = f"{pyc}.{os.getpid()}"
try:
fp = open(proc_pyc, "wb")
except OSError as e:
state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
return False
try:
_write_pyc_fp(fp, source_stat, co)
os.rename(proc_pyc, pyc)
except OSError as e:
state.trace(f"error writing pyc file at {pyc}: {e}")
# we ignore any failure to write the cache file
# there are many reasons, permission-denied, pycache dir being a
# file etc.
return False
finally:
fp.close()
return True
def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
@@ -639,12 +653,8 @@ class AssertionRewriter(ast.NodeVisitor):
.push_format_context() and .pop_format_context() which allows
to build another %-formatted string while already building one.
:variables_overwrite: A dict filled with references to variables
that change value within an assert. This happens when a variable is
reassigned with the walrus operator
This state, except the variables_overwrite, is reset on every new assert
statement visited and used by the other visitors.
This state is reset on every new assert statement visited and used
by the other visitors.
"""
def __init__(
@@ -660,7 +670,6 @@ class AssertionRewriter(ast.NodeVisitor):
else:
self.enable_assertion_pass_hook = False
self.source = source
self.variables_overwrite: Dict[str, str] = {}
def run(self, mod: ast.Module) -> None:
"""Find all assert statements in *mod* and rewrite them."""
@@ -675,7 +684,7 @@ class AssertionRewriter(ast.NodeVisitor):
if doc is not None and self.is_rewrite_disabled(doc):
return
pos = 0
item = None
lineno = 1
for item in mod.body:
if (
expect_docstring
@@ -946,18 +955,6 @@ class AssertionRewriter(ast.NodeVisitor):
ast.copy_location(node, assert_)
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(ast.Str(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), ast.Str(target_id))
return name, self.explanation_param(expr)
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
# Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable.
@@ -984,20 +981,6 @@ class AssertionRewriter(ast.NodeVisitor):
# cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
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] = pytest_temp
v.left.target.id = pytest_temp
self.push_format_context()
res, expl = self.visit(v)
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
@@ -1073,9 +1056,6 @@ class AssertionRewriter(ast.NodeVisitor):
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
self.push_format_context()
# We first check if we have overwritten a variable in the previous assert
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
comp.left.id = self.variables_overwrite[comp.left.id]
left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})"
@@ -1087,13 +1067,6 @@ class AssertionRewriter(ast.NodeVisitor):
syms = []
results = [left_res]
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.target.id
next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = f"({next_expl})"
@@ -1117,7 +1090,6 @@ class AssertionRewriter(ast.NodeVisitor):
res: ast.expr = ast.BoolOp(ast.And(), load_names)
else:
res = load_names[0]
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.
Truncates to either 8 lines, or 640 characters - whichever the input reaches
first, taking the truncation explanation into account. The remaining lines
will be replaced by a usage message.
first. The remaining lines will be replaced by a usage message.
"""
if max_lines is None:
max_lines = DEFAULT_MAX_LINES
if max_chars is None:
@@ -48,56 +48,35 @@ def _truncate_explanation(
# Check if truncation required
input_char_count = len("".join(input_lines))
# 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
):
if len(input_lines) <= max_lines and input_char_count <= max_chars:
return input_lines
# Truncate first to max_lines, and then truncate to max_chars if necessary
truncated_explanation = input_lines[:max_lines]
truncated_char = True
# We reevaluate the need to truncate chars following removal of some lines
if len("".join(truncated_explanation)) > tolerable_max_chars:
truncated_explanation = _truncate_by_char_count(
truncated_explanation, max_chars
)
else:
truncated_char = False
# Truncate first to max_lines, and then truncate to max_chars if max_chars
# is exceeded.
truncated_explanation = input_lines[:max_lines]
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
# Add ellipsis to final line
truncated_explanation[-1] = truncated_explanation[-1] + "..."
# Append useful message to explanation
truncated_line_count = len(input_lines) - len(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
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:
# 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}",
]
msg += f" ({truncated_line_count} lines hidden)"
msg += f", {USAGE_MSG}"
truncated_explanation.extend(["", str(msg)])
return truncated_explanation
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
iterated_char_count = 0
for iterated_index, input_line in enumerate(input_lines):

View File

@@ -10,13 +10,12 @@ from typing import List
from typing import Mapping
from typing import Optional
from typing import Sequence
from unicodedata import normalize
import _pytest._code
from _pytest import outcomes
from _pytest._io.saferepr import _pformat_dispatch
from _pytest._io.saferepr import safeformat
from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
from _pytest.config import Config
# The _reprcompare attribute on the util module is used by the new assertion
@@ -157,32 +156,20 @@ def has_default_eq(
return True
def assertrepr_compare(
config, op: str, left: Any, right: Any, use_ascii: bool = False
) -> Optional[List[str]]:
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
"""Return specialised explanations for some operators/operands."""
verbose = config.getoption("verbose")
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
# See issue #3246.
use_ascii = (
isinstance(left, str)
and isinstance(right, str)
and normalize("NFD", left) == normalize("NFD", right)
)
if verbose > 1:
left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
left_repr = safeformat(left)
right_repr = safeformat(right)
else:
# XXX: "15 chars indentation" is wrong
# ("E AssertionError: assert "); should use term width.
maxsize = (
80 - 15 - len(op) - 2
) // 2 # 15 chars indentation, 1 space around op
left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
left_repr = saferepr(left, maxsize=maxsize)
right_repr = saferepr(right, maxsize=maxsize)
summary = f"{left_repr} {op} {right_repr}"

View File

@@ -1,7 +1,6 @@
"""Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version.
import dataclasses
import json
import os
from pathlib import Path
@@ -13,6 +12,8 @@ from typing import Optional
from typing import Set
from typing import Union
import attr
from .pathlib import resolve_from_str
from .pathlib import rm_rf
from .reports import CollectReport
@@ -31,6 +32,7 @@ from _pytest.python import Module
from _pytest.python import Package
from _pytest.reports import TestReport
README_CONTENT = """\
# pytest cache directory #
@@ -51,12 +53,10 @@ Signature: 8a477f597d28d172789f06886806bc55
@final
@dataclasses.dataclass
@attr.s(init=False, auto_attribs=True)
class Cache:
"""Instance of the `cache` fixture."""
_cachedir: Path = dataclasses.field(repr=False)
_config: Config = dataclasses.field(repr=False)
_cachedir: Path = attr.ib(repr=False)
_config: Config = attr.ib(repr=False)
# Sub-directory under cache-dir for directories created by `mkdir()`.
_CACHE_PREFIX_DIRS = "d"
@@ -157,7 +157,7 @@ class Cache:
"""
path = self._getvaluepath(key)
try:
with path.open("r", encoding="UTF-8") as f:
with path.open("r") as f:
return json.load(f)
except (ValueError, OSError):
return default
@@ -184,9 +184,9 @@ class Cache:
return
if not cache_dir_exists_already:
self._ensure_supporting_files()
data = json.dumps(value, ensure_ascii=False, indent=2)
data = json.dumps(value, indent=2)
try:
f = path.open("w", encoding="UTF-8")
f = path.open("w")
except OSError:
self.warn("cache could not write path {path}", path=path, _ispytest=True)
else:
@@ -196,7 +196,7 @@ class Cache:
def _ensure_supporting_files(self) -> None:
"""Create supporting files in the cache dir that are not really part of the cache."""
readme_path = self._cachedir / "README.md"
readme_path.write_text(README_CONTENT, encoding="UTF-8")
readme_path.write_text(README_CONTENT)
gitignore_path = self._cachedir.joinpath(".gitignore")
msg = "# Created by pytest automatically.\n*\n"
@@ -440,7 +440,7 @@ def pytest_addoption(parser: Parser) -> None:
"--last-failed",
action="store_true",
dest="lf",
help="Rerun only the tests that failed "
help="rerun only the tests that failed "
"at the last run (or all if none failed)",
)
group.addoption(
@@ -448,7 +448,7 @@ def pytest_addoption(parser: Parser) -> None:
"--failed-first",
action="store_true",
dest="failedfirst",
help="Run all tests, but run the last failures first. "
help="run all tests, but run the last failures first.\n"
"This may re-order tests and thus lead to "
"repeated fixture setup/teardown.",
)
@@ -457,7 +457,7 @@ def pytest_addoption(parser: Parser) -> None:
"--new-first",
action="store_true",
dest="newfirst",
help="Run tests from new files first, then the rest of the tests "
help="run tests from new files first, then the rest of the tests "
"sorted by file mtime",
)
group.addoption(
@@ -466,7 +466,7 @@ def pytest_addoption(parser: Parser) -> None:
nargs="?",
dest="cacheshow",
help=(
"Show cache contents, don't perform collection or tests. "
"show cache contents, don't perform collection or tests. "
"Optional argument: glob (default: '*')."
),
)
@@ -474,12 +474,12 @@ def pytest_addoption(parser: Parser) -> None:
"--cache-clear",
action="store_true",
dest="cacheclear",
help="Remove all cache contents at start of test run",
help="remove all cache contents at start of test run.",
)
cache_dir_default = ".pytest_cache"
if "TOX_ENV_DIR" in os.environ:
cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default)
parser.addini("cache_dir", default=cache_dir_default, help="Cache directory path")
parser.addini("cache_dir", default=cache_dir_default, help="cache directory path.")
group.addoption(
"--lfnf",
"--last-failed-no-failures",
@@ -487,12 +487,12 @@ def pytest_addoption(parser: Parser) -> None:
dest="last_failed_no_failures",
choices=("all", "none"),
default="all",
help="Which tests to run with no previously (known) failures",
help="which tests to run with no previously (known) failures.",
)
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
if config.option.cacheshow and not config.option.help:
if config.option.cacheshow:
from _pytest.main import wrap_session
return wrap_session(config, cacheshow)

View File

@@ -1,26 +1,19 @@
"""Per-test stdout/stderr capturing mechanism."""
import abc
import collections
import contextlib
import functools
import io
import os
import sys
from io import UnsupportedOperation
from tempfile import TemporaryFile
from types import TracebackType
from typing import Any
from typing import AnyStr
from typing import BinaryIO
from typing import Generator
from typing import Generic
from typing import Iterable
from typing import Iterator
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import TextIO
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
@@ -36,7 +29,6 @@ from _pytest.nodes import File
from _pytest.nodes import Item
if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
@@ -50,14 +42,14 @@ def pytest_addoption(parser: Parser) -> None:
default="fd",
metavar="method",
choices=["fd", "sys", "no", "tee-sys"],
help="Per-test capturing method: one of fd|sys|no|tee-sys",
help="per-test capturing method: one of fd|sys|no|tee-sys.",
)
group._addoption(
"-s",
action="store_const",
const="no",
dest="capture",
help="Shortcut for --capture=no",
help="shortcut for --capture=no.",
)
@@ -193,151 +185,53 @@ class TeeCaptureIO(CaptureIO):
return self._other.write(s)
class DontReadFromInput(TextIO):
@property
def encoding(self) -> str:
return sys.__stdin__.encoding
class DontReadFromInput:
encoding = None
def read(self, size: int = -1) -> str:
def read(self, *args):
raise OSError(
"pytest: reading from stdin while output is captured! Consider using `-s`."
)
readline = read
readlines = read
__next__ = read
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]:
def __iter__(self):
return self
def fileno(self) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")
def flush(self) -> None:
raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()")
def isatty(self) -> bool:
return False
def close(self) -> None:
pass
def readable(self) -> bool:
return False
def seek(self, offset: int, whence: int = 0) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
def seekable(self) -> bool:
return False
def tell(self) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
def truncate(self, size: Optional[int] = None) -> int:
raise UnsupportedOperation("cannont truncate stdin")
def write(self, data: str) -> int:
raise UnsupportedOperation("cannot write to stdin")
def writelines(self, lines: Iterable[str]) -> None:
raise UnsupportedOperation("Cannot write to stdin")
def writable(self) -> bool:
return False
def __enter__(self) -> "DontReadFromInput":
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]
def buffer(self):
return self
# 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"}
class NoCapture(CaptureBase[str]):
EMPTY_BUFFER = ""
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 NoCapture:
EMPTY_BUFFER = None
__init__ = start = done = suspend = resume = lambda *args: None
class SysCaptureBase(CaptureBase[AnyStr]):
def __init__(
self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
) -> None:
class SysCaptureBinary:
EMPTY_BUFFER = b""
def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
name = patchsysdict[fd]
self._old: TextIO = getattr(sys, name)
self._old = getattr(sys, name)
self.name = name
if tmpfile is None:
if name == "stdin":
@@ -377,6 +271,14 @@ class SysCaptureBase(CaptureBase[AnyStr]):
setattr(sys, self.name, self.tmpfile)
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:
self._assert_state("done", ("initialized", "started", "suspended", "done"))
if self._state == "done":
@@ -398,43 +300,36 @@ class SysCaptureBase(CaptureBase[AnyStr]):
setattr(sys, self.name, self.tmpfile)
self._state = "started"
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:
def writeorg(self, data) -> None:
self._assert_state("writeorg", ("started", "suspended"))
self._old.flush()
self._old.buffer.write(data)
self._old.buffer.flush()
class SysCapture(SysCaptureBase[str]):
EMPTY_BUFFER = ""
class SysCapture(SysCaptureBinary):
EMPTY_BUFFER = "" # type: ignore[assignment]
def snap(self) -> str:
self._assert_state("snap", ("started", "suspended"))
assert isinstance(self.tmpfile, CaptureIO)
def snap(self):
res = self.tmpfile.getvalue()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def writeorg(self, data: str) -> None:
def writeorg(self, data):
self._assert_state("writeorg", ("started", "suspended"))
self._old.write(data)
self._old.flush()
class FDCaptureBase(CaptureBase[AnyStr]):
class FDCaptureBinary:
"""Capture IO to/from a given OS-level file descriptor.
snap() produces `bytes`.
"""
EMPTY_BUFFER = b""
def __init__(self, targetfd: int) -> None:
self.targetfd = targetfd
@@ -459,8 +354,8 @@ class FDCaptureBase(CaptureBase[AnyStr]):
self.targetfd_save = os.dup(targetfd)
if targetfd == 0:
self.tmpfile = open(os.devnull, encoding="utf-8")
self.syscapture: CaptureBase[str] = SysCapture(targetfd)
self.tmpfile = open(os.devnull)
self.syscapture = SysCapture(targetfd)
else:
self.tmpfile = EncodedFile(
TemporaryFile(buffering=0),
@@ -472,7 +367,7 @@ class FDCaptureBase(CaptureBase[AnyStr]):
if targetfd in patchsysdict:
self.syscapture = SysCapture(targetfd, self.tmpfile)
else:
self.syscapture = NoCapture(targetfd)
self.syscapture = NoCapture()
self._state = "initialized"
@@ -499,6 +394,14 @@ class FDCaptureBase(CaptureBase[AnyStr]):
self.syscapture.start()
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:
"""Stop capturing, restore streams, return original capture file,
seeked to position zero."""
@@ -531,38 +434,22 @@ class FDCaptureBase(CaptureBase[AnyStr]):
os.dup2(self.tmpfile.fileno(), self.targetfd)
self._state = "started"
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:
def writeorg(self, data):
"""Write to original file descriptor."""
self._assert_state("writeorg", ("started", "suspended"))
os.write(self.targetfd_save, data)
class FDCapture(FDCaptureBase[str]):
class FDCapture(FDCaptureBinary):
"""Capture IO to/from a given OS-level file descriptor.
snap() produces text.
"""
EMPTY_BUFFER = ""
# Ignore type because it doesn't match the type in the superclass (bytes).
EMPTY_BUFFER = "" # type: ignore
def snap(self) -> str:
def snap(self):
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.read()
@@ -570,49 +457,77 @@ class FDCapture(FDCaptureBase[str]):
self.tmpfile.truncate()
return res
def writeorg(self, data: str) -> None:
def writeorg(self, data):
"""Write to original file descriptor."""
self._assert_state("writeorg", ("started", "suspended"))
# XXX use encoding of original stream
os.write(self.targetfd_save, data.encode("utf-8"))
super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
# MultiCapture
# Generic NamedTuple only supported since Python 3.11.
if sys.version_info >= (3, 11) or TYPE_CHECKING:
# This class was a namedtuple, but due to mypy limitation[0] it could not be
# made generic, so was replaced by a regular class which tries to emulate the
# 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`."""
@final
class CaptureResult(NamedTuple, Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`."""
__slots__ = ("out", "err")
out: AnyStr
err: AnyStr
def __init__(self, out: AnyStr, err: AnyStr) -> None:
self.out: AnyStr = out
self.err: AnyStr = err
else:
def __len__(self) -> int:
return 2
class CaptureResult(
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
):
"""The result of :method:`CaptureFixture.readouterr`."""
def __iter__(self) -> Iterator[AnyStr]:
return iter((self.out, self.err))
__slots__ = ()
def __getitem__(self, item: int) -> AnyStr:
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]):
_state = None
_in_suspended = False
def __init__(
self,
in_: Optional[CaptureBase[AnyStr]],
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 __init__(self, in_, out, err) -> None:
self.in_ = in_
self.out = out
self.err = err
def __repr__(self) -> str:
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
@@ -636,10 +551,8 @@ class MultiCapture(Generic[AnyStr]):
"""Pop current snapshot out/err capture and flush to orig streams."""
out, err = self.readouterr()
if out:
assert self.out is not None
self.out.writeorg(out)
if err:
assert self.err is not None
self.err.writeorg(err)
return out, err
@@ -660,7 +573,6 @@ class MultiCapture(Generic[AnyStr]):
if self.err:
self.err.resume()
if self._in_suspended:
assert self.in_ is not None
self.in_.resume()
self._in_suspended = False
@@ -683,8 +595,7 @@ class MultiCapture(Generic[AnyStr]):
def readouterr(self) -> CaptureResult[AnyStr]:
out = self.out.snap() if self.out else ""
err = self.err.snap() if self.err else ""
# TODO: This type error is real, need to fix.
return CaptureResult(out, err) # type: ignore[arg-type]
return CaptureResult(out, err)
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
@@ -724,7 +635,7 @@ class CaptureManager:
"""
def __init__(self, method: "_CaptureMethod") -> None:
self._method: Final = method
self._method = method
self._global_capturing: Optional[MultiCapture[str]] = None
self._capture_fixture: Optional[CaptureFixture[Any]] = None
@@ -893,18 +804,14 @@ class CaptureFixture(Generic[AnyStr]):
:fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
def __init__(
self,
captureclass: Type[CaptureBase[AnyStr]],
request: SubRequest,
*,
_ispytest: bool = False,
self, captureclass, request: SubRequest, *, _ispytest: bool = False
) -> None:
check_ispytest(_ispytest)
self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
self.captureclass = captureclass
self.request = request
self._capture: Optional[MultiCapture[AnyStr]] = None
self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
self._captured_out = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER
def _start(self) -> None:
if self._capture is None:
@@ -959,9 +866,7 @@ class CaptureFixture(Generic[AnyStr]):
@contextlib.contextmanager
def disabled(self) -> Generator[None, None, None]:
"""Temporarily disable capturing while inside the ``with`` block."""
capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
"capturemanager"
)
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
with capmanager.global_and_fixture_disabled():
yield
@@ -971,25 +876,14 @@ class CaptureFixture(Generic[AnyStr]):
@fixture
def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
"""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"
"""
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -999,25 +893,14 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
@fixture
def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
r"""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()``
method calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``bytes`` objects.
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsysbinary):
print("hello")
captured = capsysbinary.readouterr()
assert captured.out == b"hello\n"
"""
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -1027,25 +910,14 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
@fixture
def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
r"""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
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_system_echo(capfd):
os.system('echo "hello"')
captured = capfd.readouterr()
assert captured.out == "hello\n"
"""
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -1055,26 +927,14 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
@fixture
def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
r"""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
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``byte`` objects.
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfdbinary):
os.system('echo "hello"')
captured = capfdbinary.readouterr()
assert captured.out == b"hello\n"
"""
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture

View File

@@ -1,7 +1,4 @@
"""Python version compatibility code."""
from __future__ import annotations
import dataclasses
import enum
import functools
import inspect
@@ -13,23 +10,17 @@ from pathlib import Path
from typing import Any
from typing import Callable
from typing import Generic
from typing import NoReturn
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
import py
# fmt: off
# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351.
# If `overload` is imported from `compat` instead of from `typing`,
# Sphinx doesn't recognize it as `overload` and the API docs for
# overloaded functions look good again. But type checkers handle
# it fine.
# fmt: on
if True:
from typing import overload as overload
if TYPE_CHECKING:
from typing import NoReturn
from typing_extensions import Final
@@ -45,7 +36,7 @@ LEGACY_PATH = py.path. local
# fmt: on
def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
return LEGACY_PATH(path)
@@ -55,15 +46,13 @@ def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum):
token = 0
NOTSET: Final = NotSetType.token # noqa: E305
NOTSET: "Final" = NotSetType.token # noqa: E305
# fmt: on
if sys.version_info >= (3, 8):
import importlib.metadata
importlib_metadata = importlib.metadata
from importlib import metadata as importlib_metadata
else:
import importlib_metadata as importlib_metadata # noqa: F401
import importlib_metadata # noqa: F401
def _format_args(func: Callable[..., Any]) -> str:
@@ -93,7 +82,7 @@ def is_async_function(func: object) -> bool:
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
def getlocation(function, curdir: str | None = None) -> str:
def getlocation(function, curdir: Optional[str] = None) -> str:
function = get_real_func(function)
fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno
@@ -131,8 +120,8 @@ def getfuncargnames(
*,
name: str = "",
is_method: bool = False,
cls: type | None = None,
) -> tuple[str, ...]:
cls: Optional[type] = None,
) -> Tuple[str, ...]:
"""Return the names of a function's mandatory arguments.
Should return the names of all function arguments that:
@@ -196,7 +185,7 @@ def getfuncargnames(
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
# getfuncargnames, to get the arguments which were excluded from its result
# because they had default values.
@@ -227,7 +216,7 @@ def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace")
def ascii_escaped(val: bytes | str) -> str:
def ascii_escaped(val: Union[bytes, str]) -> str:
r"""If val is pure ASCII, return it as an str, otherwise, escape
bytes objects into a sequence of escaped bytes:
@@ -251,7 +240,7 @@ def ascii_escaped(val: bytes | str) -> str:
return _translate_non_printable(ret)
@dataclasses.dataclass
@attr.s
class _PytestWrapper:
"""Dummy wrapper around a function object for internal use only.
@@ -260,7 +249,7 @@ class _PytestWrapper:
decorator to issue warnings when the fixture function is called directly.
"""
obj: Any
obj = attr.ib()
def get_real_func(obj):
@@ -354,6 +343,8 @@ else:
if sys.version_info >= (3, 8):
from functools import cached_property as cached_property
else:
from typing import overload
from typing import Type
class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__")
@@ -364,12 +355,12 @@ else:
@overload
def __get__(
self, instance: None, owner: type[_S] | None = ...
) -> cached_property[_S, _T]:
self, instance: None, owner: Optional[Type[_S]] = ...
) -> "cached_property[_S, _T]":
...
@overload
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T:
...
def __get__(self, instance, owner=None):
@@ -379,18 +370,6 @@ else:
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.
#
# Consider this example:
@@ -422,5 +401,5 @@ def get_user_id() -> int | None:
# previously.
#
# This also work for Enums (if you use `is` to compare) and Literals.
def assert_never(value: NoReturn) -> NoReturn:
def assert_never(value: "NoReturn") -> "NoReturn":
assert False, f"Unhandled value: {value} ({type(value).__name__})"

View File

@@ -2,9 +2,7 @@
import argparse
import collections.abc
import copy
import dataclasses
import enum
import glob
import inspect
import os
import re
@@ -15,7 +13,6 @@ import warnings
from functools import lru_cache
from pathlib import Path
from textwrap import dedent
from types import FunctionType
from types import TracebackType
from typing import Any
from typing import Callable
@@ -35,6 +32,7 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import attr
from pluggy import HookimplMarker
from pluggy import HookspecMarker
from pluggy import PluginManager
@@ -59,9 +57,9 @@ from _pytest.pathlib import ImportMode
from _pytest.pathlib import resolve_package_path
from _pytest.stash import Stash
from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter
from .argparsing import Argument
@@ -311,9 +309,7 @@ def _prepareconfig(
elif isinstance(args, os.PathLike):
args = [os.fspath(args)]
elif not isinstance(args, list):
msg = ( # type:ignore[unreachable]
"`args` parameter expected to be a list of strings, got: {!r} (type: {})"
)
msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})"
raise TypeError(msg.format(args, type(args)))
config = get_config(args, plugins)
@@ -342,38 +338,6 @@ def _get_directory(path: Path) -> Path:
return path
def _get_legacy_hook_marks(
method: Any,
hook_type: str,
opt_names: Tuple[str, ...],
) -> Dict[str, bool]:
if TYPE_CHECKING:
# abuse typeguard from importlib to avoid massive method type union thats lacking a alias
assert inspect.isroutine(method)
known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
must_warn: list[str] = []
opts: dict[str, bool] = {}
for opt_name in opt_names:
opt_attr = getattr(method, opt_name, AttributeError)
if opt_attr is not AttributeError:
must_warn.append(f"{opt_name}={opt_attr}")
opts[opt_name] = True
elif opt_name in known_marks:
must_warn.append(f"{opt_name}=True")
opts[opt_name] = True
else:
opts[opt_name] = False
if must_warn:
hook_opts = ", ".join(must_warn)
message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
type=hook_type,
fullname=method.__qualname__,
hook_opts=hook_opts,
)
warn_explicit_for(cast(FunctionType, method), message)
return opts
@final
class PytestPluginManager(PluginManager):
"""A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
@@ -447,29 +411,40 @@ class PytestPluginManager(PluginManager):
if name == "pytest_plugins":
return
opts = super().parse_hookimpl_opts(plugin, name)
if opts is not None:
return opts
method = getattr(plugin, name)
opts = super().parse_hookimpl_opts(plugin, name)
# Consider only actual functions for hooks (#3775).
if not inspect.isroutine(method):
return
# Collect unmarked hooks as long as they have the `pytest_' prefix.
return _get_legacy_hook_marks(
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
)
if opts is None and name.startswith("pytest_"):
opts = {}
if opts is not None:
# TODO: DeprecationWarning, people should use hookimpl
# https://github.com/pytest-dev/pytest/issues/4562
known_marks = {m.name for m in getattr(method, "pytestmark", [])}
for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"):
opts.setdefault(name, hasattr(method, name) or name in known_marks)
return opts
def parse_hookspec_opts(self, module_or_class, name: str):
opts = super().parse_hookspec_opts(module_or_class, name)
if opts is None:
method = getattr(module_or_class, name)
if name.startswith("pytest_"):
opts = _get_legacy_hook_marks(
method,
"spec",
("firstresult", "historic"),
)
# todo: deprecate hookspec hacks
# https://github.com/pytest-dev/pytest/issues/4562
known_marks = {m.name for m in getattr(method, "pytestmark", [])}
opts = {
"firstresult": hasattr(method, "firstresult")
or "firstresult" in known_marks,
"historic": hasattr(method, "historic")
or "historic" in known_marks,
}
return opts
def register(
@@ -511,14 +486,12 @@ class PytestPluginManager(PluginManager):
config.addinivalue_line(
"markers",
"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.",
"plugin machinery will try to call it first/as early as possible.",
)
config.addinivalue_line(
"markers",
"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.",
"plugin machinery will try to call it last/as late as possible.",
)
self._configured = True
@@ -696,7 +669,6 @@ class PytestPluginManager(PluginManager):
parg = opt[2:]
else:
continue
parg = parg.strip()
if exclude_only and not parg.startswith("no:"):
continue
self.consider_pluginarg(parg)
@@ -861,8 +833,7 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
if is_simple_module:
module_name, _ = os.path.splitext(fn)
# we ignore "setup.py" at the root of the distribution
# as well as editable installation finder modules made by setuptools
if module_name != "setup" and not module_name.startswith("__editable__"):
if module_name != "setup":
seen_some = True
yield module_name
elif is_package:
@@ -886,6 +857,10 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
yield from _iter_rewritable_modules(new_package_files)
def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
return tuple(args)
@final
class Config:
"""Access to configuration values, pluginmanager and plugin hooks.
@@ -899,7 +874,7 @@ class Config:
"""
@final
@dataclasses.dataclass(frozen=True)
@attr.s(frozen=True, auto_attribs=True)
class InvocationParams:
"""Holds parameters passed during :func:`pytest.main`.
@@ -915,37 +890,13 @@ class Config:
Plugins accessing ``InvocationParams`` must be aware of that.
"""
args: Tuple[str, ...]
args: Tuple[str, ...] = attr.ib(converter=_args_converter)
"""The command-line arguments as passed to :func:`pytest.main`."""
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
"""Extra plugins, might be `None`."""
dir: Path
"""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):
"""Indicates the source of the test arguments.
.. versionadded:: 7.2
"""
#: Command line arguments.
ARGS = enum.auto()
#: Invocation directory.
INCOVATION_DIR = enum.auto()
#: 'testpaths' configuration value.
TESTPATHS = enum.auto()
def __init__(
self,
pluginmanager: PytestPluginManager,
@@ -1005,8 +956,6 @@ class Config:
self.hook.pytest_addoption.call_historic(
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
)
self.args_source = Config.ArgsSource.ARGS
self.args: List[str] = []
if TYPE_CHECKING:
from _pytest.cacheprovider import Cache
@@ -1066,6 +1015,7 @@ class Config:
try:
self.parse(args)
except UsageError:
# Handle --version and --help here in a minimal fashion.
# This gets done via helpconfig normally, but its
# pytest_cmdline_main is not called in case of errors.
@@ -1149,11 +1099,11 @@ class Config:
self.inicfg = inicfg
self._parser.extra_info["rootdir"] = str(self.rootpath)
self._parser.extra_info["inifile"] = str(self.inipath)
self._parser.addini("addopts", "Extra command line options", "args")
self._parser.addini("minversion", "Minimally required pytest version")
self._parser.addini("addopts", "extra command line options", "args")
self._parser.addini("minversion", "minimally required pytest version")
self._parser.addini(
"required_plugins",
"Plugins that must be present for pytest to run",
"plugins that must be present for pytest to run",
type="args",
default=[],
)
@@ -1345,8 +1295,8 @@ class Config:
def parse(self, args: List[str], addopts: bool = True) -> None:
# Parse given cmdline arguments into this config object.
assert (
self.args == []
assert not hasattr(
self, "args"
), "can only parse cmdline args at most once per Config object"
self.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=self.pluginmanager)
@@ -1356,25 +1306,15 @@ class Config:
self.hook.pytest_cmdline_preparse(config=self, args=args)
self._parser.after_preparse = True # type: ignore
try:
source = Config.ArgsSource.ARGS
args = self._parser.parse_setoption(
args, self.option, namespace=self.option
)
if not args:
if self.invocation_params.dir == self.rootpath:
source = Config.ArgsSource.TESTPATHS
testpaths: List[str] = self.getini("testpaths")
if self.known_args_namespace.pyargs:
args = testpaths
else:
args = []
for path in testpaths:
args.extend(sorted(glob.iglob(path, recursive=True)))
args = self.getini("testpaths")
if not args:
source = Config.ArgsSource.INCOVATION_DIR
args = [str(self.invocation_params.dir)]
self.args = args
self.args_source = source
except PrintHelp:
pass

View File

@@ -9,7 +9,6 @@ from typing import cast
from typing import Dict
from typing import List
from typing import Mapping
from typing import NoReturn
from typing import Optional
from typing import Sequence
from typing import Tuple
@@ -25,6 +24,7 @@ from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
from _pytest.deprecated import check_ispytest
if TYPE_CHECKING:
from typing import NoReturn
from typing_extensions import Literal
FILE_OR_DIR = "file_or_dir"
@@ -48,7 +48,7 @@ class Parser:
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self._anonymous = OptionGroup("Custom options", parser=self, _ispytest=True)
self._anonymous = OptionGroup("custom options", parser=self, _ispytest=True)
self._groups: List[OptionGroup] = []
self._processopt = processopt
self._usage = usage
@@ -66,15 +66,14 @@ class Parser:
) -> "OptionGroup":
"""Get (or create) a named option Group.
:param name: Name of the option group.
:param description: Long description for --help output.
:param after: Name of another group, used for ordering --help output.
:returns: The option group.
:name: Name of the option group.
:description: Long description for --help output.
:after: Name of another group, used for ordering --help output.
The returned group object has an ``addoption`` method with the same
signature as :func:`parser.addoption <pytest.Parser.addoption>` but
will be shown in the respective group in the output of
``pytest --help``.
``pytest. --help``.
"""
for group in self._groups:
if group.name == name:
@@ -90,11 +89,10 @@ class Parser:
def addoption(self, *opts: str, **attrs: Any) -> None:
"""Register a command line option.
:param opts:
Option names, can be short or long options.
:param attrs:
Same attributes as the argparse library's :py:func:`add_argument()
<argparse.ArgumentParser.add_argument>` function accepts.
:opts: Option names, can be short or long options.
:attrs: Same attributes which the ``add_argument()`` function of the
`argparse library <https://docs.python.org/library/argparse.html>`_
accepts.
After command line parsing, options are available on the pytest config
object via ``config.option.NAME`` where ``NAME`` is usually set
@@ -150,10 +148,7 @@ class Parser:
args: Sequence[Union[str, "os.PathLike[str]"]],
namespace: Optional[argparse.Namespace] = None,
) -> argparse.Namespace:
"""Parse the known arguments at this point.
:returns: An argparse namespace object.
"""
"""Parse and return a namespace object with known arguments at this point."""
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
def parse_known_and_unknown_args(
@@ -161,13 +156,8 @@ class Parser:
args: Sequence[Union[str, "os.PathLike[str]"]],
namespace: Optional[argparse.Namespace] = None,
) -> Tuple[argparse.Namespace, List[str]]:
"""Parse the known arguments at this point, and also return the
remaining unknown arguments.
:returns:
A tuple containing an argparse namespace object for the known
arguments, and a list of the unknown arguments.
"""
"""Parse and return a namespace object with known arguments, and
the remaining arguments unknown at this point."""
optparser = self._getparser()
strargs = [os.fspath(x) for x in args]
return optparser.parse_known_args(strargs, namespace=namespace)
@@ -179,13 +169,13 @@ class Parser:
type: Optional[
"Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']"
] = None,
default: Any = None,
default=None,
) -> None:
"""Register an ini-file option.
:param name:
:name:
Name of the ini-variable.
:param type:
:type:
Type of the variable. Can be:
* ``string``: a string
@@ -199,7 +189,7 @@ class Parser:
The ``paths`` variable type.
Defaults to ``string`` if ``None`` or not passed.
:param default:
:default:
Default value if no ini-file option exists but is queried.
The value of ini-variables can be retrieved via a call to
@@ -237,7 +227,7 @@ class Argument:
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
def __init__(self, *names: str, **attrs: Any) -> None:
"""Store params in private vars for use in add_argument."""
"""Store parms in private vars for use in add_argument."""
self._attrs = attrs
self._short_opts: List[str] = []
self._long_opts: List[str] = []
@@ -364,30 +354,24 @@ class OptionGroup:
self.options: List[Argument] = []
self.parser = parser
def addoption(self, *opts: str, **attrs: Any) -> None:
def addoption(self, *optnames: str, **attrs: Any) -> None:
"""Add an option to this group.
If a shortened version of a long option is specified, it will
be suppressed in the help. ``addoption('--twowords', '--two-words')``
results in help showing ``--two-words`` only, but ``--twowords`` gets
accepted **and** the automatic destination is in ``args.twowords``.
:param opts:
Option names, can be short or long options.
:param attrs:
Same attributes as the argparse library's :py:func:`add_argument()
<argparse.ArgumentParser.add_argument>` function accepts.
"""
conflict = set(opts).intersection(
conflict = set(optnames).intersection(
name for opt in self.options for name in opt.names()
)
if conflict:
raise ValueError("option names %s already added" % conflict)
option = Argument(*opts, **attrs)
option = Argument(*optnames, **attrs)
self._addoption_instance(option, shortupper=False)
def _addoption(self, *opts: str, **attrs: Any) -> None:
option = Argument(*opts, **attrs)
def _addoption(self, *optnames: str, **attrs: Any) -> None:
option = Argument(*optnames, **attrs)
self._addoption_instance(option, shortupper=True)
def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None:
@@ -419,7 +403,7 @@ class MyOptionParser(argparse.ArgumentParser):
# an usage error to provide more contextual information to the user.
self.extra_info = extra_info if extra_info else {}
def error(self, message: str) -> NoReturn:
def error(self, message: str) -> "NoReturn":
"""Transform argparse error message into UsageError."""
msg = f"{self.prog}: error: {message}"

View File

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

View File

@@ -1,5 +1,4 @@
import os
import sys
from pathlib import Path
from typing import Dict
from typing import Iterable
@@ -65,15 +64,12 @@ def load_config_dict_from_file(
# '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
elif filepath.suffix == ".toml":
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
import tomli
toml_text = filepath.read_text(encoding="utf-8")
try:
config = tomllib.loads(toml_text)
except tomllib.TOMLDecodeError as exc:
config = tomli.loads(toml_text)
except tomli.TOMLDecodeError as exc:
raise UsageError(f"{filepath}: {exc}") from exc
result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
@@ -96,7 +92,6 @@ def locate_config(
and return a tuple of (rootdir, inifile, cfg-dict)."""
config_names = [
"pytest.ini",
".pytest.ini",
"pyproject.toml",
"tox.ini",
"setup.cfg",
@@ -203,7 +198,8 @@ def determine_setup(
else:
cwd = Path.cwd()
rootdir = get_common_ancestor([cwd, ancestor])
if is_fs_root(rootdir):
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
if is_fs_root:
rootdir = ancestor
if rootdir_cmd_arg:
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
@@ -215,11 +211,3 @@ def determine_setup(
)
assert rootdir is not None
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,7 +3,6 @@ import argparse
import functools
import sys
import types
import unittest
from typing import Any
from typing import Callable
from typing import Generator
@@ -47,21 +46,21 @@ def pytest_addoption(parser: Parser) -> None:
"--pdb",
dest="usepdb",
action="store_true",
help="Start the interactive Python debugger on errors or KeyboardInterrupt",
help="start the interactive Python debugger on errors or KeyboardInterrupt.",
)
group._addoption(
"--pdbcls",
dest="usepdb_cls",
metavar="modulename:classname",
type=_validate_usepdb_cls,
help="Specify a custom interactive Python debugger for use with --pdb."
help="specify a custom interactive Python debugger for use with --pdb."
"For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
)
group._addoption(
"--trace",
dest="trace",
action="store_true",
help="Immediately break when running each test",
help="Immediately break when running each test.",
)
@@ -294,9 +293,7 @@ class PdbInvoke:
sys.stdout.write(out)
sys.stdout.write(err)
assert call.excinfo is not None
if not isinstance(call.excinfo.value, unittest.SkipTest):
_enter_pdb(node, call.excinfo, report)
_enter_pdb(node, call.excinfo, report)
def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
tb = _postmortem_traceback(excinfo)

View File

@@ -22,21 +22,6 @@ DEPRECATED_EXTERNAL_PLUGINS = {
"pytest_faulthandler",
}
NOSE_SUPPORT = UnformattedWarning(
PytestRemovedIn8Warning,
"Support for nose tests is deprecated and will be removed in a future release.\n"
"{nodeid} is using nose method: `{method}` ({stage})\n"
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
)
NOSE_SUPPORT_METHOD = UnformattedWarning(
PytestRemovedIn8Warning,
"Support for nose tests is deprecated and will be removed in a future release.\n"
"{nodeid} is using nose-specific method: `{method}(self)`\n"
"To remove this warning, rename it to `{method}_method(self)`\n"
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
)
# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
# * If you're in the future: "could have been".
@@ -113,14 +98,6 @@ INSTANCE_COLLECTOR = PytestRemovedIn8Warning(
"The pytest.Instance collector type is deprecated and is no longer used. "
"See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector",
)
HOOK_LEGACY_MARKING = UnformattedWarning(
PytestDeprecationWarning,
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
"Please use the pytest.hook{type}({hook_opts}) decorator instead\n"
" to configure the hooks.\n"
" See https://docs.pytest.org/en/latest/deprecations.html"
"#configuring-hook-specs-impls-using-markers",
)
# You want to make some `__init__` or function "private".
#

View File

@@ -23,6 +23,7 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import pytest
from _pytest import outcomes
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ReprFileLocation
@@ -31,15 +32,11 @@ from _pytest._io import TerminalWriter
from _pytest.compat import safe_getattr
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.nodes import Collector
from _pytest.nodes import Item
from _pytest.outcomes import OutcomeException
from _pytest.outcomes import skip
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path
from _pytest.python import Module
from _pytest.python_api import approx
from _pytest.warning_types import PytestWarning
@@ -69,26 +66,26 @@ CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None
def pytest_addoption(parser: Parser) -> None:
parser.addini(
"doctest_optionflags",
"Option flags for doctests",
"option flags for doctests",
type="args",
default=["ELLIPSIS"],
)
parser.addini(
"doctest_encoding", "Encoding used for doctest files", default="utf-8"
"doctest_encoding", "encoding used for doctest files", default="utf-8"
)
group = parser.getgroup("collect")
group.addoption(
"--doctest-modules",
action="store_true",
default=False,
help="Run doctests in all .py modules",
help="run doctests in all .py modules",
dest="doctestmodules",
)
group.addoption(
"--doctest-report",
type=str.lower,
default="udiff",
help="Choose another output format for diffs on doctest failure",
help="choose another output format for diffs on doctest failure",
choices=DOCTEST_REPORT_CHOICES,
dest="doctestreport",
)
@@ -97,21 +94,21 @@ def pytest_addoption(parser: Parser) -> None:
action="append",
default=[],
metavar="pat",
help="Doctests file matching pattern, default: test*.txt",
help="doctests file matching pattern, default: test*.txt",
dest="doctestglob",
)
group.addoption(
"--doctest-ignore-import-errors",
action="store_true",
default=False,
help="Ignore doctest ImportErrors",
help="ignore doctest ImportErrors",
dest="doctest_ignore_import_errors",
)
group.addoption(
"--doctest-continue-on-failure",
action="store_true",
default=False,
help="For a given doctest, continue to run after the first failure",
help="for a given doctest, continue to run after the first failure",
dest="doctest_continue_on_failure",
)
@@ -249,7 +246,7 @@ def _get_runner(
)
class DoctestItem(Item):
class DoctestItem(pytest.Item):
def __init__(
self,
name: str,
@@ -414,7 +411,7 @@ def _get_continue_on_failure(config):
return continue_on_failure
class DoctestTextfile(Module):
class DoctestTextfile(pytest.Module):
obj = None
def collect(self) -> Iterable[DoctestItem]:
@@ -452,7 +449,7 @@ def _check_all_skipped(test: "doctest.DocTest") -> None:
all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
if all_skipped:
skip("all tests skipped by +SKIP option")
pytest.skip("all tests skipped by +SKIP option")
def _is_mocked(obj: object) -> bool:
@@ -494,7 +491,7 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
inspect.unwrap = real_unwrap
class DoctestModule(Module):
class DoctestModule(pytest.Module):
def collect(self) -> Iterable[DoctestItem]:
import doctest
@@ -531,6 +528,7 @@ class DoctestModule(Module):
if _is_mocked(obj):
return
with _patch_unwrap_mock_aware():
# Type ignored because this is a private function.
super()._find( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen
@@ -544,14 +542,10 @@ class DoctestModule(Module):
)
else:
try:
module = import_path(
self.path,
root=self.config.rootpath,
mode=self.config.getoption("importmode"),
)
module = import_path(self.path, root=self.config.rootpath)
except ImportError:
if self.config.getvalue("doctest_ignore_import_errors"):
skip("unable to import module %r" % self.path)
pytest.skip("unable to import module %r" % self.path)
else:
raise
# Uses internal doctest module parsing mechanism.
@@ -733,19 +727,8 @@ def _get_report_choice(key: str) -> int:
}[key]
@fixture(scope="session")
@pytest.fixture(scope="session")
def doctest_namespace() -> Dict[str, Any]:
"""Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.
Usually this fixture is used in conjunction with another ``autouse`` fixture:
.. code-block:: python
@pytest.fixture(autouse=True)
def add_np(doctest_namespace):
doctest_namespace["np"] = numpy
For more details: :ref:`doctest_namespace`.
"""
namespace of doctests."""
return dict()

View File

@@ -18,7 +18,7 @@ fault_handler_originally_enabled_key = StashKey[bool]()
def pytest_addoption(parser: Parser) -> None:
help = (
"Dump the traceback of all threads if a test takes "
"more than TIMEOUT seconds to finish"
"more than TIMEOUT seconds to finish."
)
parser.addini("faulthandler_timeout", help, default=0.0)

View File

@@ -1,4 +1,3 @@
import dataclasses
import functools
import inspect
import os
@@ -19,8 +18,8 @@ from typing import Iterable
from typing import Iterator
from typing import List
from typing import MutableMapping
from typing import NoReturn
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Set
from typing import Tuple
@@ -29,6 +28,8 @@ from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
import _pytest
from _pytest import nodes
from _pytest._code import getfslineno
@@ -46,7 +47,6 @@ from _pytest.compat import getimfunc
from _pytest.compat import getlocation
from _pytest.compat import is_generator
from _pytest.compat import NOTSET
from _pytest.compat import overload
from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
@@ -57,7 +57,6 @@ from _pytest.mark import Mark
from _pytest.mark import ParameterSet
from _pytest.mark.structures import MarkDecorator
from _pytest.outcomes import fail
from _pytest.outcomes import skip
from _pytest.outcomes import TEST_OUTCOME
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
@@ -68,6 +67,7 @@ from _pytest.stash import StashKey
if TYPE_CHECKING:
from typing import Deque
from typing import NoReturn
from _pytest.scope import _ScopeName
from _pytest.main import Session
@@ -102,7 +102,7 @@ _FixtureCachedResult = Union[
]
@dataclasses.dataclass(frozen=True)
@attr.s(frozen=True, auto_attribs=True)
class PseudoFixtureDef(Generic[FixtureValue]):
cached_result: "_FixtureCachedResult[FixtureValue]"
_scope: Scope
@@ -223,10 +223,15 @@ def add_funcarg_pseudo_fixture_def(
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
"""Return fixturemarker or None if it doesn't exist or raised
exceptions."""
return cast(
Optional[FixtureFunctionMarker],
safe_getattr(obj, "_pytestfixturefunction", None),
)
try:
fixturemarker: Optional[FixtureFunctionMarker] = getattr(
obj, "_pytestfixturefunction", None
)
except TEST_OUTCOME:
# some objects raise errors like request (from flask import request)
# we don't expect them to be fixture functions
return None
return fixturemarker
# Parametrized fixture key, helper alias for code below.
@@ -345,14 +350,12 @@ def reorder_items_atscope(
return items_done
def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
def get_direct_param_fixture_func(request):
return request.param
@dataclasses.dataclass
@attr.s(slots=True, auto_attribs=True)
class FuncFixtureInfo:
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
# Original function argument names.
argnames: Tuple[str, ...]
# Argnames that function immediately requires. These include argnames +
@@ -409,15 +412,6 @@ class FixtureRequest:
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
self._arg2index: Dict[str, int] = {}
self._fixturemanager: FixtureManager = pyfuncitem.session._fixturemanager
# Notes on the type of `param`:
# -`request.param` is only defined in parametrized fixtures, and will raise
# AttributeError otherwise. Python typing has no notion of "undefined", so
# this cannot be reflected in the type.
# - Technically `param` is only (possibly) defined on SubRequest, not
# FixtureRequest, but the typing of that is still in flux so this cheats.
# - In the future we might consider using a generic for the param type, but
# for now just using Any.
self.param: Any
@property
def scope(self) -> "_ScopeName":
@@ -497,7 +491,6 @@ class FixtureRequest:
@property
def path(self) -> Path:
"""Path where the test function was collected."""
if self.scope not in ("function", "class", "module", "package"):
raise AttributeError(f"path not available in {self.scope}-scoped context")
# TODO: Remove ignore once _pyfuncitem is properly typed.
@@ -515,8 +508,8 @@ class FixtureRequest:
return self._pyfuncitem.session # type: ignore[no-any-return]
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution."""
"""Add finalizer/teardown function to be called after the last test
within the requesting test context finished execution."""
# XXX usually this method is shadowed by fixturedef specific ones.
self._addfinalizer(finalizer, scope=self.scope)
@@ -531,16 +524,13 @@ class FixtureRequest:
on all function invocations.
:param marker:
An object created by a call to ``pytest.mark.NAME(...)``.
A :class:`pytest.MarkDecorator` object created by a call
to ``pytest.mark.NAME(...)``.
"""
self.node.add_marker(marker)
def raiseerror(self, msg: Optional[str]) -> NoReturn:
"""Raise a FixtureLookupError exception.
:param msg:
An optional custom error message.
"""
def raiseerror(self, msg: Optional[str]) -> "NoReturn":
"""Raise a FixtureLookupError with the given message."""
raise self._fixturemanager.FixtureLookupError(None, self, msg)
def _fillfixtures(self) -> None:
@@ -558,20 +548,11 @@ class FixtureRequest:
setup time, you may use this function to retrieve it inside a fixture
or test function body.
This method can be used during the test setup phase or the test run
phase, but during the test teardown phase a fixture's value may not
be available.
:param argname:
The fixture name.
:raises pytest.FixtureLookupError:
If the given fixture could not be found.
"""
fixturedef = self._get_active_fixturedef(argname)
assert fixturedef.cached_result is not None, (
f'The fixture value for "{argname}" is not available. '
"This can happen when the fixture has already been torn down."
)
assert fixturedef.cached_result is not None
return fixturedef.cached_result[0]
def _get_active_fixturedef(
@@ -775,8 +756,8 @@ class SubRequest(FixtureRequest):
return f"<SubRequest {self.fixturename!r} for {self._pyfuncitem!r}>"
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
"""Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution."""
"""Add finalizer/teardown function to be called after the last test
within the requesting test context finished execution."""
self._fixturedef.addfinalizer(finalizer)
def _schedule_finalizers(
@@ -883,7 +864,7 @@ class FixtureLookupErrorRepr(TerminalRepr):
tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1))
def fail_fixturefunc(fixturefunc, msg: str) -> NoReturn:
def fail_fixturefunc(fixturefunc, msg: str) -> "NoReturn":
fs, lineno = getfslineno(fixturefunc)
location = f"{fs}:{lineno + 1}"
source = _pytest._code.Source(fixturefunc)
@@ -1131,10 +1112,6 @@ def pytest_fixture_setup(
except TEST_OUTCOME:
exc_info = sys.exc_info()
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)
raise
fixturedef.cached_result = (result, my_cache_key, None)
@@ -1182,21 +1159,19 @@ def wrap_function_to_error_out_if_called_directly(
@final
@dataclasses.dataclass(frozen=True)
@attr.s(frozen=True, auto_attribs=True)
class FixtureFunctionMarker:
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
params: Optional[Tuple[object, ...]]
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
autouse: bool = False
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None
] = attr.ib(
default=None,
converter=_ensure_immutable_ids,
)
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:
if inspect.isclass(function):
raise ValueError("class fixtures not supported (maybe in the future)")
@@ -1239,7 +1214,7 @@ def fixture(
@overload
def fixture( # noqa: F811
def fixture(
fixture_function: None = ...,
*,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
@@ -1253,7 +1228,7 @@ def fixture( # noqa: F811
...
def fixture( # noqa: F811
def fixture(
fixture_function: Optional[FixtureFunction] = None,
*,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
@@ -1316,11 +1291,10 @@ def fixture( # noqa: F811
"""
fixture_marker = FixtureFunctionMarker(
scope=scope,
params=tuple(params) if params is not None else None,
params=params,
autouse=autouse,
ids=None if ids is None else ids if callable(ids) else tuple(ids),
ids=ids,
name=name,
_ispytest=True,
)
# Direct decoration.
@@ -1376,7 +1350,7 @@ def pytest_addoption(parser: Parser) -> None:
"usefixtures",
type="args",
default=[],
help="List of default fixtures to be used with this project",
help="list of default fixtures to be used with this project",
)

View File

@@ -49,7 +49,7 @@ def pytest_addoption(parser: Parser) -> None:
action="count",
default=0,
dest="version",
help="Display pytest version and information about plugins. "
help="display pytest version and information about plugins. "
"When given twice, also display information about plugins.",
)
group._addoption(
@@ -57,7 +57,7 @@ def pytest_addoption(parser: Parser) -> None:
"--help",
action=HelpAction,
dest="help",
help="Show help message and configuration info",
help="show help message and configuration info",
)
group._addoption(
"-p",
@@ -65,7 +65,7 @@ def pytest_addoption(parser: Parser) -> None:
dest="plugins",
default=[],
metavar="name",
help="Early-load given plugin module name or entry point (multi-allowed). "
help="early-load given plugin module name or entry point (multi-allowed).\n"
"To avoid loading of plugins, use the `no:` prefix, e.g. "
"`no:doctest`.",
)
@@ -74,7 +74,7 @@ def pytest_addoption(parser: Parser) -> None:
"--trace-config",
action="store_true",
default=False,
help="Trace considerations of conftest.py files",
help="trace considerations of conftest.py files.",
)
group.addoption(
"--debug",
@@ -83,17 +83,16 @@ def pytest_addoption(parser: Parser) -> None:
const="pytestdebug.log",
dest="debug",
metavar="DEBUG_FILE_NAME",
help="Store internal tracing debug information in this log file. "
"This file is opened with 'w' and truncated as a result, care advised. "
"Default: pytestdebug.log.",
help="store internal tracing debug information in this log file.\n"
"This file is opened with 'w' and truncated as a result, care advised.\n"
"Defaults to 'pytestdebug.log'.",
)
group._addoption(
"-o",
"--override-ini",
dest="override_ini",
action="append",
help='Override ini option with "option=value" style, '
"e.g. `-o xfail_strict=True -o cache_dir=cache`.",
help='override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`.',
)
@@ -164,8 +163,7 @@ def showhelp(config: Config) -> None:
tw.write(config._parser.optparser.format_help())
tw.line()
tw.line(
"[pytest] ini-options in the first "
"pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:"
"[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:"
)
tw.line()
@@ -205,12 +203,12 @@ def showhelp(config: Config) -> None:
tw.line(indent + line)
tw.line()
tw.line("Environment variables:")
tw.line("environment variables:")
vars = [
("PYTEST_ADDOPTS", "Extra command line options"),
("PYTEST_PLUGINS", "Comma-separated plugins to load during startup"),
("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "Set to disable plugin auto-loading"),
("PYTEST_DEBUG", "Set to enable debug tracing of pytest's internals"),
("PYTEST_ADDOPTS", "extra command line options"),
("PYTEST_PLUGINS", "comma-separated plugins to load during startup"),
("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "set to disable plugin auto-loading"),
("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals"),
]
for name, help in vars:
tw.line(f" {name:<24} {help}")

View File

@@ -143,7 +143,7 @@ def pytest_configure(config: "Config") -> None:
def pytest_cmdline_parse(
pluginmanager: "PytestPluginManager", args: List[str]
) -> Optional["Config"]:
"""Return an initialized :class:`~pytest.Config`, parsing the specified args.
"""Return an initialized config object, parsing the specified args.
Stops at first non-None result, see :ref:`firstresult`.
@@ -152,9 +152,8 @@ def pytest_cmdline_parse(
``plugins`` arg when using `pytest.main`_ to perform an in-process
test run.
:param pluginmanager: The pytest plugin manager.
:param args: List of arguments passed on the command line.
:returns: A pytest config object.
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
:param List[str] args: List of arguments passed on the command line.
"""
@@ -168,8 +167,8 @@ def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None:
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
:param config: The pytest config object.
:param args: Arguments passed on the command line.
:param pytest.Config config: The pytest config object.
:param List[str] args: Arguments passed on the command line.
"""
@@ -180,8 +179,7 @@ def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
Stops at first non-None result, see :ref:`firstresult`.
:param config: The pytest config object.
:returns: The exit code.
:param pytest.Config config: The pytest config object.
"""
@@ -194,9 +192,9 @@ def pytest_load_initial_conftests(
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
:param early_config: The pytest config object.
:param args: Arguments passed on the command line.
:param parser: To add command line options.
:param pytest.Config early_config: The pytest config object.
:param List[str] args: Arguments passed on the command line.
:param pytest.Parser parser: To add command line options.
"""
@@ -238,7 +236,7 @@ def pytest_collection(session: "Session") -> Optional[object]:
for example the terminal plugin uses it to start displaying the collection
counter (and returns `None`).
:param session: The pytest session object.
:param pytest.Session session: The pytest session object.
"""
@@ -248,16 +246,16 @@ def pytest_collection_modifyitems(
"""Called after collection has been performed. May filter or re-order
the items in-place.
:param session: The pytest session object.
:param config: The pytest config object.
:param items: List of item objects.
:param pytest.Session session: The pytest session object.
:param pytest.Config config: The pytest config object.
:param List[pytest.Item] items: List of item objects.
"""
def pytest_collection_finish(session: "Session") -> None:
"""Called after collection has been performed and modified.
:param session: The pytest session object.
:param pytest.Session session: The pytest session object.
"""
@@ -272,9 +270,9 @@ def pytest_ignore_collect(
Stops at first non-None result, see :ref:`firstresult`.
:param collection_path: The path to analyze.
:param path: The path to analyze (deprecated).
:param config: The pytest config object.
:param pathlib.Path collection_path : The path to analyze.
:param LEGACY_PATH path: The path to analyze (deprecated).
:param pytest.Config config: The pytest config object.
.. versionchanged:: 7.0.0
The ``collection_path`` parameter was added as a :class:`pathlib.Path`
@@ -286,12 +284,12 @@ def pytest_ignore_collect(
def pytest_collect_file(
file_path: Path, path: "LEGACY_PATH", parent: "Collector"
) -> "Optional[Collector]":
"""Create a :class:`~pytest.Collector` for the given path, or None if not relevant.
"""Create a Collector for the given path, or None if not relevant.
The new node needs to have the specified ``parent`` as a parent.
:param file_path: The path to analyze.
:param path: The path to collect (deprecated).
:param pathlib.Path file_path: The path to analyze.
:param LEGACY_PATH path: The path to collect (deprecated).
.. versionchanged:: 7.0.0
The ``file_path`` parameter was added as a :class:`pathlib.Path`
@@ -304,36 +302,21 @@ def pytest_collect_file(
def pytest_collectstart(collector: "Collector") -> None:
"""Collector starts collecting.
:param collector:
The collector.
"""
"""Collector starts collecting."""
def pytest_itemcollected(item: "Item") -> None:
"""We just collected a test item.
:param item:
The item.
"""
"""We just collected a test item."""
def pytest_collectreport(report: "CollectReport") -> None:
"""Collector finished collecting.
:param report:
The collect report.
"""
"""Collector finished collecting."""
def pytest_deselected(items: Sequence["Item"]) -> None:
"""Called for deselected test items, e.g. by keyword.
May be called multiple times.
:param items:
The items.
"""
@@ -343,9 +326,6 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
a :class:`~pytest.CollectReport`.
Stops at first non-None result, see :ref:`firstresult`.
:param collector:
The collector.
"""
@@ -358,16 +338,16 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
def pytest_pycollect_makemodule(
module_path: Path, path: "LEGACY_PATH", parent
) -> Optional["Module"]:
"""Return a :class:`pytest.Module` collector or None for the given path.
"""Return a Module collector or None for the given path.
This hook will be called for each matching test module path.
The :hook:`pytest_collect_file` hook needs to be used if you want to
The pytest_collect_file hook needs to be used if you want to
create test modules for files that do not match as a test module.
Stops at first non-None result, see :ref:`firstresult`.
:param module_path: The path of the module to collect.
:param path: The path of the module to collect (deprecated).
:param pathlib.Path module_path: The path of the module to collect.
:param LEGACY_PATH path: The path of the module to collect (deprecated).
.. versionchanged:: 7.0.0
The ``module_path`` parameter was added as a :class:`pathlib.Path`
@@ -384,15 +364,6 @@ def pytest_pycollect_makeitem(
"""Return a custom item/collector for a Python object in a module, or None.
Stops at first non-None result, see :ref:`firstresult`.
:param collector:
The module/class collector.
:param name:
The name of the object in the module/class.
:param obj:
The object.
:returns:
The created items/collectors.
"""
@@ -401,18 +372,11 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
"""Call underlying test function.
Stops at first non-None result, see :ref:`firstresult`.
:param pyfuncitem:
The function item.
"""
def pytest_generate_tests(metafunc: "Metafunc") -> None:
"""Generate (multiple) parametrized calls to a test function.
:param metafunc:
The :class:`~pytest.Metafunc` helper for the test function.
"""
"""Generate (multiple) parametrized calls to a test function."""
@hookspec(firstresult=True)
@@ -427,7 +391,7 @@ def pytest_make_parametrize_id(
Stops at first non-None result, see :ref:`firstresult`.
:param config: The pytest config object.
:param pytest.Config config: The pytest config object.
:param val: The parametrized value.
:param str argname: The automatic parameter name produced by pytest.
"""
@@ -452,7 +416,7 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
If at any point ``session.shouldfail`` or ``session.shouldstop`` are set, the
loop is terminated after the runtest protocol for the current item is finished.
:param session: The pytest session object.
:param pytest.Session session: The pytest session object.
Stops at first non-None result, see :ref:`firstresult`.
The return value is not used, but only stops further processing.
@@ -504,10 +468,8 @@ def pytest_runtest_logstart(
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
:param nodeid: Full node ID of the item.
:param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based.
:param str nodeid: Full node ID of the item.
:param location: A tuple of ``(filename, lineno, testname)``.
"""
@@ -518,10 +480,8 @@ def pytest_runtest_logfinish(
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
:param nodeid: Full node ID of the item.
:param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based.
:param str nodeid: Full node ID of the item.
:param location: A tuple of ``(filename, lineno, testname)``.
"""
@@ -532,9 +492,6 @@ def pytest_runtest_setup(item: "Item") -> None:
parents (which haven't been setup yet). This includes obtaining the
values of fixtures required by the item (which haven't been obtained
yet).
:param item:
The item.
"""
@@ -542,9 +499,6 @@ def pytest_runtest_call(item: "Item") -> None:
"""Called to run the test for test item (the call phase).
The default implementation calls ``item.runtest()``.
:param item:
The item.
"""
@@ -556,8 +510,6 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
includes running the teardown phase of fixtures required by the item (if
they go out of scope).
:param item:
The item.
:param nextitem:
The scheduled-to-be-next test item (None if no further test item is
scheduled). This argument is used to perform exact teardowns, i.e.
@@ -575,7 +527,6 @@ def pytest_runtest_makereport(
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
:param item: The item.
:param call: The :class:`~pytest.CallInfo` for the phase.
Stops at first non-None result, see :ref:`firstresult`.
@@ -596,11 +547,7 @@ def pytest_report_to_serializable(
report: Union["CollectReport", "TestReport"],
) -> Optional[Dict[str, Any]]:
"""Serialize the given report object into a data structure suitable for
sending over the wire, e.g. converted to JSON.
:param config: The pytest config object.
:param report: The report.
"""
sending over the wire, e.g. converted to JSON."""
@hookspec(firstresult=True)
@@ -609,10 +556,7 @@ def pytest_report_from_serializable(
data: Dict[str, Any],
) -> Optional[Union["CollectReport", "TestReport"]]:
"""Restore a report object previously serialized with
:hook:`pytest_report_to_serializable`.
:param config: The pytest config object.
"""
:hook:`pytest_report_to_serializable`."""
# -------------------------------------------------------------------------
@@ -626,12 +570,7 @@ def pytest_fixture_setup(
) -> Optional[object]:
"""Perform fixture setup execution.
:param fixturdef:
The fixture definition object.
:param request:
The fixture request object.
:returns:
The return value of the call to the fixture function.
:returns: The return value of the call to the fixture function.
Stops at first non-None result, see :ref:`firstresult`.
@@ -647,13 +586,7 @@ def pytest_fixture_post_finalizer(
) -> None:
"""Called after fixture teardown, but before the cache is cleared, so
the fixture result ``fixturedef.cached_result`` is still available (not
``None``).
:param fixturdef:
The fixture definition object.
:param request:
The fixture request object.
"""
``None``)."""
# -------------------------------------------------------------------------
@@ -665,7 +598,7 @@ def pytest_sessionstart(session: "Session") -> None:
"""Called after the ``Session`` object has been created and before performing collection
and entering the run test loop.
:param session: The pytest session object.
:param pytest.Session session: The pytest session object.
"""
@@ -675,15 +608,15 @@ def pytest_sessionfinish(
) -> None:
"""Called after whole test run finished, right before returning the exit status to the system.
:param session: The pytest session object.
:param exitstatus: The status which pytest will return to the system.
:param pytest.Session session: The pytest session object.
:param int exitstatus: The status which pytest will return to the system.
"""
def pytest_unconfigure(config: "Config") -> None:
"""Called before test process is exited.
:param config: The pytest config object.
:param pytest.Config config: The pytest config object.
"""
@@ -702,10 +635,7 @@ def pytest_assertrepr_compare(
*in* a string will be escaped. Note that all but the first line will
be indented slightly, the intention is for the first line to be a summary.
:param config: The pytest config object.
:param op: The operator, e.g. `"=="`, `"!="`, `"not in"`.
:param left: The left operand.
:param right: The right operand.
:param pytest.Config config: The pytest config object.
"""
@@ -730,10 +660,10 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
You need to **clean the .pyc** files in your project directory and interpreter libraries
when enabling this option, as assertions will require to be re-written.
:param item: pytest item object of current test.
:param lineno: Line number of the assert statement.
:param orig: String with the original assertion.
:param expl: String with the assert explanation.
:param pytest.Item item: pytest item object of current test.
:param int lineno: Line number of the assert statement.
:param str orig: String with the original assertion.
:param str expl: String with the assert explanation.
"""
@@ -742,14 +672,14 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
# -------------------------------------------------------------------------
def pytest_report_header( # type:ignore[empty-body]
def pytest_report_header(
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
) -> Union[str, List[str]]:
"""Return a string or list of strings to be displayed as header info for terminal reporting.
:param config: The pytest config object.
:param start_path: The starting dir.
:param startdir: The starting dir (deprecated).
:param pytest.Config config: The pytest config object.
:param Path start_path: The starting dir.
:param LEGACY_PATH startdir: The starting dir (deprecated).
.. note::
@@ -771,7 +701,7 @@ def pytest_report_header( # type:ignore[empty-body]
"""
def pytest_report_collectionfinish( # type:ignore[empty-body]
def pytest_report_collectionfinish(
config: "Config",
start_path: Path,
startdir: "LEGACY_PATH",
@@ -784,9 +714,9 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
.. versionadded:: 3.2
:param config: The pytest config object.
:param start_path: The starting dir.
:param startdir: The starting dir (deprecated).
:param pytest.Config config: The pytest config object.
:param Path start_path: The starting dir.
:param LEGACY_PATH startdir: The starting dir (deprecated).
:param items: List of pytest items that are going to be executed; this list should not be modified.
.. note::
@@ -804,7 +734,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
@hookspec(firstresult=True)
def pytest_report_teststatus( # type:ignore[empty-body]
def pytest_report_teststatus(
report: Union["CollectReport", "TestReport"], config: "Config"
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
"""Return result-category, shortletter and verbose word for status
@@ -825,7 +755,6 @@ def pytest_report_teststatus( # type:ignore[empty-body]
:param report: The report object whose status is to be returned.
:param config: The pytest config object.
:returns: The test status.
Stops at first non-None result, see :ref:`firstresult`.
"""
@@ -838,9 +767,9 @@ def pytest_terminal_summary(
) -> None:
"""Add a section to terminal summary reporting.
:param terminalreporter: The internal terminal reporter object.
:param exitstatus: The exit status that will be reported back to the OS.
:param config: The pytest config object.
:param _pytest.terminal.TerminalReporter terminalreporter: The internal terminal reporter object.
:param int exitstatus: The exit status that will be reported back to the OS.
:param pytest.Config config: The pytest config object.
.. versionadded:: 4.2
The ``config`` parameter.
@@ -856,21 +785,21 @@ def pytest_warning_recorded(
) -> None:
"""Process a warning captured by the internal pytest warnings plugin.
:param warning_message:
:param warnings.WarningMessage warning_message:
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
the same attributes as the parameters of :py:func:`warnings.showwarning`.
:param when:
:param str when:
Indicates when the warning was captured. Possible values:
* ``"config"``: during pytest configuration/initialization stage.
* ``"collect"``: during test collection.
* ``"runtest"``: during test execution.
:param nodeid:
:param str nodeid:
Full id of the item.
:param location:
:param tuple|None location:
When available, holds information about the execution context of the captured
warning (filename, linenumber, function). ``function`` evaluates to <module>
when the execution context is at the module level.
@@ -884,9 +813,7 @@ def pytest_warning_recorded(
# -------------------------------------------------------------------------
def pytest_markeval_namespace( # type:ignore[empty-body]
config: "Config",
) -> Dict[str, Any]:
def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]:
"""Called when constructing the globals dictionary used for
evaluating string conditions in xfail/skipif markers.
@@ -897,7 +824,7 @@ def pytest_markeval_namespace( # type:ignore[empty-body]
.. versionadded:: 6.2
:param config: The pytest config object.
:param pytest.Config config: The pytest config object.
:returns: A dictionary of additional globals to add.
"""
@@ -915,19 +842,13 @@ def pytest_internalerror(
Return True to suppress the fallback handling of printing an
INTERNALERROR message directly to sys.stderr.
:param excrepr: The exception repr object.
:param excinfo: The exception info.
"""
def pytest_keyboard_interrupt(
excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]",
) -> None:
"""Called for keyboard interrupt.
:param excinfo: The exception info.
"""
"""Called for keyboard interrupt."""
def pytest_exception_interact(
@@ -946,13 +867,6 @@ def pytest_exception_interact(
This hook is not called if the exception that was raised is an internal
exception like ``skip.Exception``.
:param node:
The item or collector.
:param call:
The call information. Contains the exception.
:param report:
The collection or test report.
"""
@@ -962,8 +876,8 @@ def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
Can be used by plugins to take special action just before the python
debugger enters interactive mode.
:param config: The pytest config object.
:param pdb: The Pdb instance.
:param pytest.Config config: The pytest config object.
:param pdb.Pdb pdb: The Pdb instance.
"""
@@ -973,6 +887,6 @@ def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
Can be used by plugins to take special action just after the python
debugger leaves interactive mode.
:param config: The pytest config object.
:param pdb: The Pdb instance.
:param pytest.Config config: The pytest config object.
:param pdb.Pdb pdb: The Pdb instance.
"""

View File

@@ -231,7 +231,7 @@ class _NodeReporter:
msg = f'failed on teardown with "{reason}"'
else:
msg = f'failed on setup with "{reason}"'
self._add_simple("error", bin_xml_escape(msg), str(report.longrepr))
self._add_simple("error", msg, str(report.longrepr))
def append_skipped(self, report: TestReport) -> None:
if hasattr(report, "wasxfail"):
@@ -354,10 +354,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
record_testsuite_property("ARCH", "PPC")
record_testsuite_property("STORAGE_TYPE", "CEPH")
:param name:
The property name.
:param value:
The property value. Will be converted to a string.
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
.. warning::
@@ -389,7 +386,7 @@ def pytest_addoption(parser: Parser) -> None:
metavar="path",
type=functools.partial(filename_arg, optname="--junitxml"),
default=None,
help="Create junit-xml style report file at given path",
help="create junit-xml style report file at given path.",
)
group.addoption(
"--junitprefix",
@@ -397,7 +394,7 @@ def pytest_addoption(parser: Parser) -> None:
action="store",
metavar="str",
default=None,
help="Prepend prefix to classnames in junit-xml output",
help="prepend prefix to classnames in junit-xml output",
)
parser.addini(
"junit_suite_name", "Test suite name for JUnit report", default="pytest"
@@ -645,8 +642,8 @@ class LogXML:
def pytest_sessionfinish(self) -> None:
dirname = os.path.dirname(os.path.abspath(self.logfile))
# exist_ok avoids filesystem race conditions between checking path existence and requesting creation
os.makedirs(dirname, exist_ok=True)
if not os.path.isdir(dirname):
os.makedirs(dirname)
with open(self.logfile, "w", encoding="utf-8") as logfile:
suite_stop_time = timing.time()

View File

@@ -1,5 +1,4 @@
"""Add backward compatibility support for the legacy py path type."""
import dataclasses
import shlex
import subprocess
from pathlib import Path
@@ -8,6 +7,7 @@ from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
import attr
from iniconfig import SectionWrapper
from _pytest.cacheprovider import Cache
@@ -268,17 +268,10 @@ class LegacyTestdirPlugin:
@final
@dataclasses.dataclass
@attr.s(init=False, auto_attribs=True)
class TempdirFactory:
"""Backward compatibility wrapper that implements :class:`py.path.local`
for :class:`TempPathFactory`.
.. note::
These days, it is preferred to use ``tmp_path_factory``.
:ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`.
"""
"""Backward compatibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH``
for :class:``TempPathFactory``."""
_tmppath_factory: TempPathFactory
@@ -289,11 +282,11 @@ class TempdirFactory:
self._tmppath_factory = tmppath_factory
def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH:
"""Same as :meth:`TempPathFactory.mktemp`, but returns a :class:`py.path.local` object."""
"""Same as :meth:`TempPathFactory.mktemp`, but returns a ``_pytest.compat.LEGACY_PATH`` object."""
return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve())
def getbasetemp(self) -> LEGACY_PATH:
"""Same as :meth:`TempPathFactory.getbasetemp`, but returns a :class:`py.path.local` object."""
"""Backward compat wrapper for ``_tmppath_factory.getbasetemp``."""
return legacy_path(self._tmppath_factory.getbasetemp().resolve())
@@ -319,11 +312,6 @@ class LegacyTmpdirPlugin:
The returned object is a `legacy_path`_ object.
.. note::
These days, it is preferred to use ``tmp_path``.
:ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`.
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
"""
return legacy_path(tmp_path)

View File

@@ -37,11 +37,10 @@ from _pytest.terminal import TerminalReporter
if TYPE_CHECKING:
logging_StreamHandler = logging.StreamHandler[StringIO]
from typing_extensions import Literal
else:
logging_StreamHandler = logging.StreamHandler
DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m")
@@ -219,7 +218,7 @@ def pytest_addoption(parser: Parser) -> None:
def add_option_ini(option, dest, default=None, type=None, **kwargs):
parser.addini(
dest, default=default, type=type, help="Default value for " + option
dest, default=default, type=type, help="default value for " + option
)
group.addoption(option, dest=dest, **kwargs)
@@ -229,8 +228,8 @@ def pytest_addoption(parser: Parser) -> None:
default=None,
metavar="LEVEL",
help=(
"Level of messages to catch/display."
" Not set by default, so it depends on the root/parent log handler's"
"level of messages to catch/display.\n"
"Not set by default, so it depends on the root/parent log handler's"
' effective level, where it is "WARNING" by default.'
),
)
@@ -238,58 +237,58 @@ def pytest_addoption(parser: Parser) -> None:
"--log-format",
dest="log_format",
default=DEFAULT_LOG_FORMAT,
help="Log format used by the logging module",
help="log format as used by the logging module.",
)
add_option_ini(
"--log-date-format",
dest="log_date_format",
default=DEFAULT_LOG_DATE_FORMAT,
help="Log date format used by the logging module",
help="log date format as used by the logging module.",
)
parser.addini(
"log_cli",
default=False,
type="bool",
help='Enable log display during test run (also known as "live logging")',
help='enable log display during test run (also known as "live logging").',
)
add_option_ini(
"--log-cli-level", dest="log_cli_level", default=None, help="CLI logging level"
"--log-cli-level", dest="log_cli_level", default=None, help="cli logging level."
)
add_option_ini(
"--log-cli-format",
dest="log_cli_format",
default=None,
help="Log format used by the logging module",
help="log format as used by the logging module.",
)
add_option_ini(
"--log-cli-date-format",
dest="log_cli_date_format",
default=None,
help="Log date format used by the logging module",
help="log date format as used by the logging module.",
)
add_option_ini(
"--log-file",
dest="log_file",
default=None,
help="Path to a file when logging will be written to",
help="path to a file when logging will be written to.",
)
add_option_ini(
"--log-file-level",
dest="log_file_level",
default=None,
help="Log file logging level",
help="log file logging level.",
)
add_option_ini(
"--log-file-format",
dest="log_file_format",
default=DEFAULT_LOG_FORMAT,
help="Log format used by the logging module",
help="log format as used by the logging module.",
)
add_option_ini(
"--log-file-date-format",
dest="log_file_date_format",
default=DEFAULT_LOG_DATE_FORMAT,
help="Log date format used by the logging module",
help="log date format as used by the logging module.",
)
add_option_ini(
"--log-auto-indent",
@@ -297,13 +296,6 @@ def pytest_addoption(parser: Parser) -> None:
default=None,
help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.",
)
group.addoption(
"--log-disable",
action="append",
default=[],
dest="logger_disable",
help="Disable a logger by name. Can be passed multipe times.",
)
_HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
@@ -353,10 +345,6 @@ class LogCaptureHandler(logging_StreamHandler):
self.records = []
self.stream = StringIO()
def clear(self) -> None:
self.records.clear()
self.stream = StringIO()
def handleError(self, record: logging.LogRecord) -> None:
if logging.raiseExceptions:
# Fail the test if the log message is bad (emit failed).
@@ -391,19 +379,20 @@ class LogCaptureFixture:
@property
def handler(self) -> LogCaptureHandler:
"""Get the logging handler used by the fixture."""
"""Get the logging handler used by the fixture.
:rtype: LogCaptureHandler
"""
return self._item.stash[caplog_handler_key]
def get_records(
self, when: "Literal['setup', 'call', 'teardown']"
) -> List[logging.LogRecord]:
def get_records(self, when: str) -> List[logging.LogRecord]:
"""Get the logging records for one of the possible test phases.
:param when:
Which test phase to obtain the records from.
Valid values are: "setup", "call" and "teardown".
:param str when:
Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
:returns: The list of captured records at the given stage.
:rtype: List[logging.LogRecord]
.. versionadded:: 3.4
"""
@@ -451,7 +440,7 @@ class LogCaptureFixture:
def clear(self) -> None:
"""Reset the list of log records and the captured log text."""
self.handler.clear()
self.handler.reset()
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
"""Set the level of a logger for the duration of a test.
@@ -460,8 +449,8 @@ class LogCaptureFixture:
The levels of the loggers changed by this function will be
restored to their initial values at the end of the test.
:param level: The level.
:param logger: The logger to update. If not given, the root logger.
:param int level: The level.
:param str logger: The logger to update. If not given, the root logger.
"""
logger_obj = logging.getLogger(logger)
# Save the original log-level to restore it during teardown.
@@ -479,8 +468,8 @@ class LogCaptureFixture:
the end of the 'with' statement the level is restored to its original
value.
:param level: The level.
:param logger: The logger to update. If not given, the root logger.
:param int level: The level.
:param str logger: The logger to update. If not given, the root logger.
"""
logger_obj = logging.getLogger(logger)
orig_level = logger_obj.level
@@ -601,15 +590,6 @@ class LoggingPlugin:
get_option_ini(config, "log_auto_indent"),
)
self.log_cli_handler.setFormatter(log_cli_formatter)
self._disable_loggers(loggers_to_disable=config.option.logger_disable)
def _disable_loggers(self, loggers_to_disable: List[str]) -> None:
if not loggers_to_disable:
return
for name in loggers_to_disable:
logger = logging.getLogger(name)
logger.disabled = True
def _create_formatter(self, log_format, log_date_format, auto_indent):
# Color option doesn't exist if terminal plugin is disabled.

View File

@@ -1,6 +1,5 @@
"""Core implementation of the testing process: init, session, runtest loop."""
import argparse
import dataclasses
import fnmatch
import functools
import importlib
@@ -13,6 +12,7 @@ from typing import FrozenSet
from typing import Iterator
from typing import List
from typing import Optional
from typing import overload
from typing import Sequence
from typing import Set
from typing import Tuple
@@ -20,10 +20,11 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import attr
import _pytest._code
from _pytest import nodes
from _pytest.compat import final
from _pytest.compat import overload
from _pytest.config import Config
from _pytest.config import directory_arg
from _pytest.config import ExitCode
@@ -50,7 +51,7 @@ if TYPE_CHECKING:
def pytest_addoption(parser: Parser) -> None:
parser.addini(
"norecursedirs",
"Directory patterns to avoid for recursion",
"directory patterns to avoid for recursion",
type="args",
default=[
"*.egg",
@@ -66,26 +67,26 @@ def pytest_addoption(parser: Parser) -> None:
)
parser.addini(
"testpaths",
"Directories to search for tests when no files or directories are given on the "
"command line",
"directories to search for tests when no files or directories are given in the "
"command line.",
type="args",
default=[],
)
group = parser.getgroup("general", "Running and selection options")
group = parser.getgroup("general", "running and selection options")
group._addoption(
"-x",
"--exitfirst",
action="store_const",
dest="maxfail",
const=1,
help="Exit instantly on first error or failed test",
help="exit instantly on first error or failed test.",
)
group = parser.getgroup("pytest-warnings")
group.addoption(
"-W",
"--pythonwarnings",
action="append",
help="Set which warnings to report, see -W option of Python itself",
help="set which warnings to report, see -W option of python itself.",
)
parser.addini(
"filterwarnings",
@@ -101,39 +102,37 @@ def pytest_addoption(parser: Parser) -> None:
type=int,
dest="maxfail",
default=0,
help="Exit after first num failures or errors",
help="exit after first num failures or errors.",
)
group._addoption(
"--strict-config",
action="store_true",
help="Any warnings encountered while parsing the `pytest` section of the "
"configuration file raise errors",
help="any warnings encountered while parsing the `pytest` section of the configuration file raise errors.",
)
group._addoption(
"--strict-markers",
action="store_true",
help="Markers not registered in the `markers` section of the configuration "
"file raise errors",
help="markers not registered in the `markers` section of the configuration file raise errors.",
)
group._addoption(
"--strict",
action="store_true",
help="(Deprecated) alias to --strict-markers",
help="(deprecated) alias to --strict-markers.",
)
group._addoption(
"-c",
metavar="file",
type=str,
dest="inifilename",
help="Load configuration from `file` instead of trying to locate one of the "
"implicit configuration files",
help="load configuration from `file` instead of trying to locate one of the implicit "
"configuration files.",
)
group._addoption(
"--continue-on-collection-errors",
action="store_true",
default=False,
dest="continue_on_collection_errors",
help="Force test execution even if collection errors occur",
help="Force test execution even if collection errors occur.",
)
group._addoption(
"--rootdir",
@@ -150,30 +149,30 @@ def pytest_addoption(parser: Parser) -> None:
"--collect-only",
"--co",
action="store_true",
help="Only collect tests, don't execute them",
help="only collect tests, don't execute them.",
)
group.addoption(
"--pyargs",
action="store_true",
help="Try to interpret all arguments as Python packages",
help="try to interpret all arguments as python packages.",
)
group.addoption(
"--ignore",
action="append",
metavar="path",
help="Ignore path during collection (multi-allowed)",
help="ignore path during collection (multi-allowed).",
)
group.addoption(
"--ignore-glob",
action="append",
metavar="path",
help="Ignore path pattern during collection (multi-allowed)",
help="ignore path pattern during collection (multi-allowed).",
)
group.addoption(
"--deselect",
action="append",
metavar="nodeid_prefix",
help="Deselect item (via node id prefix) during collection (multi-allowed)",
help="deselect item (via node id prefix) during collection (multi-allowed).",
)
group.addoption(
"--confcutdir",
@@ -181,14 +180,14 @@ def pytest_addoption(parser: Parser) -> None:
default=None,
metavar="dir",
type=functools.partial(directory_arg, optname="--confcutdir"),
help="Only load conftest.py's relative to specified dir",
help="only load conftest.py's relative to specified dir.",
)
group.addoption(
"--noconftest",
action="store_true",
dest="noconftest",
default=False,
help="Don't load any conftest.py files",
help="Don't load any conftest.py files.",
)
group.addoption(
"--keepduplicates",
@@ -196,7 +195,7 @@ def pytest_addoption(parser: Parser) -> None:
action="store_true",
dest="keepduplicates",
default=False,
help="Keep duplicate tests",
help="Keep duplicate tests.",
)
group.addoption(
"--collect-in-virtualenv",
@@ -210,8 +209,8 @@ def pytest_addoption(parser: Parser) -> None:
default="prepend",
choices=["prepend", "append", "importlib"],
dest="importmode",
help="Prepend/append to sys.path when importing test modules and conftest "
"files. Default: prepend.",
help="prepend/append to sys.path when importing test modules and conftest files, "
"default is to prepend.",
)
group = parser.getgroup("debugconfig", "test session debugging and configuration")
@@ -222,8 +221,8 @@ def pytest_addoption(parser: Parser) -> None:
type=validate_basetemp,
metavar="dir",
help=(
"Base temporary directory for this test run. "
"(Warning: this directory is removed if it exists.)"
"base temporary directory for this test run."
"(warning: this directory is removed if it exists)"
),
)
@@ -441,10 +440,8 @@ class Failed(Exception):
"""Signals a stop as failed test run."""
@dataclasses.dataclass
@attr.s(slots=True, auto_attribs=True)
class _bestrelpath_cache(Dict[Path, str]):
__slots__ = ("path",)
path: Path
def __missing__(self, path: Path) -> str:
@@ -598,12 +595,12 @@ class Session(nodes.FSCollector):
...
@overload
def perform_collect( # noqa: F811
def perform_collect(
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
...
def perform_collect( # noqa: F811
def perform_collect(
self, args: Optional[Sequence[str]] = None, genitems: bool = True
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
"""Perform the collection phase for this session.
@@ -648,14 +645,9 @@ class Session(nodes.FSCollector):
self.trace.root.indent -= 1
if self._notfound:
errors = []
for arg, collectors in self._notfound:
if collectors:
errors.append(
f"not found: {arg}\n(no name {arg!r} in any of {collectors!r})"
)
else:
errors.append(f"found no collectors for {arg}")
for arg, cols in self._notfound:
line = f"(no name {arg!r} in any of {cols!r})"
errors.append(f"not found: {arg}\n{line}")
raise UsageError(*errors)
if not genitems:
items = rep.result

View File

@@ -1,5 +1,4 @@
"""Generic mechanism for marking and selecting python functions."""
import dataclasses
from typing import AbstractSet
from typing import Collection
from typing import List
@@ -7,6 +6,8 @@ from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
import attr
from .expression import Expression
from .expression import ParseError
from .structures import EMPTY_PARAMETERSET_OPTION
@@ -61,8 +62,8 @@ def param(
assert eval(test_input) == expected
:param values: Variable args of the values of the parameter set, in order.
:param marks: A single mark or a list of marks to be applied to this parameter set.
:param id: The id to attribute to this parameter set.
:keyword marks: A single mark or a list of marks to be applied to this parameter set.
:keyword str id: The id to attribute to this parameter set.
"""
return ParameterSet.param(*values, marks=marks, id=id)
@@ -75,8 +76,8 @@ def pytest_addoption(parser: Parser) -> None:
dest="keyword",
default="",
metavar="EXPRESSION",
help="Only run tests which match the given substring expression. "
"An expression is a Python evaluatable expression "
help="only run tests which match the given substring expression. "
"An expression is a python evaluatable expression "
"where all names are substring-matched against test names "
"and their parent classes. Example: -k 'test_method or test_"
"other' matches all test functions and classes whose name "
@@ -95,7 +96,7 @@ def pytest_addoption(parser: Parser) -> None:
dest="markexpr",
default="",
metavar="MARKEXPR",
help="Only run tests matching given mark expression. "
help="only run tests matching given mark expression.\n"
"For example: -m 'mark1 and not mark2'.",
)
@@ -105,8 +106,8 @@ def pytest_addoption(parser: Parser) -> None:
help="show markers (builtin, plugin and per-project ones).",
)
parser.addini("markers", "Markers for test functions", "linelist")
parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets")
parser.addini("markers", "markers for test functions", "linelist")
parser.addini(EMPTY_PARAMETERSET_OPTION, "default marker for empty parametersets")
@hookimpl(tryfirst=True)
@@ -129,7 +130,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
return None
@dataclasses.dataclass
@attr.s(slots=True, auto_attribs=True)
class KeywordMatcher:
"""A matcher for keywords.
@@ -144,8 +145,6 @@ class KeywordMatcher:
any item, as well as names directly assigned to test functions.
"""
__slots__ = ("_names",)
_names: AbstractSet[str]
@classmethod
@@ -202,15 +201,13 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
items[:] = remaining
@dataclasses.dataclass
@attr.s(slots=True, auto_attribs=True)
class MarkMatcher:
"""A matcher for markers which are present.
Tries to match on any marker names, attached to the given colitem.
"""
__slots__ = ("own_mark_names",)
own_mark_names: AbstractSet[str]
@classmethod

View File

@@ -15,16 +15,20 @@ The semantics are:
- or/and/not evaluate according to the usual boolean semantics.
"""
import ast
import dataclasses
import enum
import re
import types
from typing import Callable
from typing import Iterator
from typing import Mapping
from typing import NoReturn
from typing import Optional
from typing import Sequence
from typing import TYPE_CHECKING
import attr
if TYPE_CHECKING:
from typing import NoReturn
__all__ = [
@@ -43,9 +47,8 @@ class TokenType(enum.Enum):
EOF = "end of input"
@dataclasses.dataclass(frozen=True)
@attr.s(frozen=True, slots=True, auto_attribs=True)
class Token:
__slots__ = ("type", "value", "pos")
type: TokenType
value: str
pos: int
@@ -114,7 +117,7 @@ class Scanner:
self.reject((type,))
return None
def reject(self, expected: Sequence[TokenType]) -> NoReturn:
def reject(self, expected: Sequence[TokenType]) -> "NoReturn":
raise ParseError(
self.current.pos + 1,
"expected {}; got {}".format(

View File

@@ -1,5 +1,4 @@
import collections.abc
import dataclasses
import inspect
import warnings
from typing import Any
@@ -21,6 +20,8 @@ from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
from .._code import getfslineno
from ..compat import ascii_escaped
from ..compat import final
@@ -125,12 +126,12 @@ class ParameterSet(NamedTuple):
@staticmethod
def _parse_parametrize_args(
argnames: Union[str, Sequence[str]],
argnames: Union[str, List[str], Tuple[str, ...]],
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
*args,
**kwargs,
) -> Tuple[Sequence[str], bool]:
if isinstance(argnames, str):
) -> Tuple[Union[List[str], Tuple[str, ...]], bool]:
if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1
else:
@@ -149,12 +150,12 @@ class ParameterSet(NamedTuple):
@classmethod
def _for_parametrize(
cls,
argnames: Union[str, Sequence[str]],
argnames: Union[str, List[str], Tuple[str, ...]],
argvalues: Iterable[Union["ParameterSet", Sequence[object], object]],
func,
config: Config,
nodeid: str,
) -> Tuple[Sequence[str], List["ParameterSet"]]:
) -> Tuple[Union[List[str], Tuple[str, ...]], List["ParameterSet"]]:
argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues)
parameters = cls._parse_parametrize_parameters(argvalues, force_tuple)
del argvalues
@@ -190,10 +191,8 @@ class ParameterSet(NamedTuple):
@final
@dataclasses.dataclass(frozen=True)
@attr.s(frozen=True, init=False, auto_attribs=True)
class Mark:
"""A pytest mark."""
#: Name of the mark.
name: str
#: Positional arguments of the mark decorator.
@@ -202,11 +201,9 @@ class Mark:
kwargs: Mapping[str, Any]
#: Source Mark for ids with parametrize Marks.
_param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False)
_param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False)
#: Resolved/generated ids with parametrize Marks.
_param_ids_generated: Optional[Sequence[str]] = dataclasses.field(
default=None, repr=False
)
_param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)
def __init__(
self,
@@ -264,7 +261,7 @@ class Mark:
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
@dataclasses.dataclass
@attr.s(init=False, auto_attribs=True)
class MarkDecorator:
"""A decorator for applying a mark on test functions and classes.
@@ -358,35 +355,12 @@ class MarkDecorator:
return self.with_args(*args, **kwargs)
def get_unpacked_marks(
obj: Union[object, type],
*,
consider_mro: bool = True,
) -> List[Mark]:
"""Obtain the unpacked marks that are stored on an object.
If obj is a class and consider_mro is true, return marks applied to
this class and all of its super-classes in MRO order. If consider_mro
is false, only return marks applied directly to this class.
"""
if isinstance(obj, type):
if not consider_mro:
mark_lists = [obj.__dict__.get("pytestmark", [])]
else:
mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]
mark_list = []
for item in mark_lists:
if isinstance(item, list):
mark_list.extend(item)
else:
mark_list.append(item)
else:
mark_attribute = getattr(obj, "pytestmark", [])
if isinstance(mark_attribute, list):
mark_list = mark_attribute
else:
mark_list = [mark_attribute]
return list(normalize_mark_list(mark_list))
def get_unpacked_marks(obj: object) -> Iterable[Mark]:
"""Obtain the unpacked marks that are stored on an object."""
mark_list = getattr(obj, "pytestmark", [])
if not isinstance(mark_list, list):
mark_list = [mark_list]
return normalize_mark_list(mark_list)
def normalize_mark_list(
@@ -414,7 +388,7 @@ def store_mark(obj, mark: Mark) -> None:
assert isinstance(mark, Mark), mark
# Always reassign name to avoid updating pytestmark in a reference that
# was only borrowed.
obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]
obj.pytestmark = [*get_unpacked_marks(obj), mark]
# Typing for builtin pytest marks. This is cheating; it gives builtin marks
@@ -423,7 +397,7 @@ if TYPE_CHECKING:
from _pytest.scope import _ScopeName
class _SkipMarkDecorator(MarkDecorator):
@overload # type: ignore[override,misc,no-overload-impl]
@overload # type: ignore[override,misc]
def __call__(self, arg: Markable) -> Markable:
...
@@ -441,7 +415,7 @@ if TYPE_CHECKING:
...
class _XfailMarkDecorator(MarkDecorator):
@overload # type: ignore[override,misc,no-overload-impl]
@overload # type: ignore[override,misc]
def __call__(self, arg: Markable) -> Markable:
...
@@ -460,7 +434,7 @@ if TYPE_CHECKING:
class _ParametrizeMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
self,
argnames: Union[str, Sequence[str]],
argnames: Union[str, List[str], Tuple[str, ...]],
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
*,
indirect: Union[bool, Sequence[str]] = ...,

View File

@@ -29,26 +29,21 @@ V = TypeVar("V")
def monkeypatch() -> Generator["MonkeyPatch", None, None]:
"""A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or
:data:`os.environ`:
The fixture provides these methods to modify objects, dictionaries or
os.environ::
* :meth:`monkeypatch.setattr(obj, name, value, raising=True) <pytest.MonkeyPatch.setattr>`
* :meth:`monkeypatch.delattr(obj, name, raising=True) <pytest.MonkeyPatch.delattr>`
* :meth:`monkeypatch.setitem(mapping, name, value) <pytest.MonkeyPatch.setitem>`
* :meth:`monkeypatch.delitem(obj, name, raising=True) <pytest.MonkeyPatch.delitem>`
* :meth:`monkeypatch.setenv(name, value, prepend=None) <pytest.MonkeyPatch.setenv>`
* :meth:`monkeypatch.delenv(name, raising=True) <pytest.MonkeyPatch.delenv>`
* :meth:`monkeypatch.syspath_prepend(path) <pytest.MonkeyPatch.syspath_prepend>`
* :meth:`monkeypatch.chdir(path) <pytest.MonkeyPatch.chdir>`
* :meth:`monkeypatch.context() <pytest.MonkeyPatch.context>`
monkeypatch.setattr(obj, name, value, raising=True)
monkeypatch.delattr(obj, name, raising=True)
monkeypatch.setitem(mapping, name, value)
monkeypatch.delitem(obj, name, raising=True)
monkeypatch.setenv(name, value, prepend=None)
monkeypatch.delenv(name, raising=True)
monkeypatch.syspath_prepend(path)
monkeypatch.chdir(path)
All modifications will be undone after the requesting test function or
fixture has finished. The ``raising`` parameter determines if a :class:`KeyError`
or :class:`AttributeError` will be raised if the set/deletion operation does not have the
specified target.
To undo modifications done by the fixture in a contained scope,
use :meth:`context() <pytest.MonkeyPatch.context>`.
fixture has finished. The ``raising`` parameter determines if a KeyError
or AttributeError will be raised if the set/deletion operation has no target.
"""
mpatch = MonkeyPatch()
yield mpatch
@@ -120,7 +115,7 @@ class MonkeyPatch:
Returned by the :fixture:`monkeypatch` fixture.
.. versionchanged:: 6.2
:versionchanged:: 6.2
Can now also be used directly as `pytest.MonkeyPatch()`, for when
the fixture is not available. In this case, use
:meth:`with MonkeyPatch.context() as mp: <context>` or remember to call
@@ -187,40 +182,16 @@ class MonkeyPatch:
value: object = notset,
raising: bool = True,
) -> None:
"""
Set attribute value on target, memorizing the old value.
"""Set attribute value on target, memorizing the old value.
For example:
.. code-block:: python
import os
monkeypatch.setattr(os, "getcwd", lambda: "/")
The code above replaces the :func:`os.getcwd` function by a ``lambda`` which
always returns ``"/"``.
For convenience, you can specify a string as ``target`` which
For convenience you can specify a string as ``target`` which
will be interpreted as a dotted import path, with the last part
being the attribute name:
being the attribute name. For example,
``monkeypatch.setattr("os.getcwd", lambda: "/")``
would set the ``getcwd`` function of the ``os`` module.
.. code-block:: python
monkeypatch.setattr("os.getcwd", lambda: "/")
Raises :class:`AttributeError` if the attribute does not exist, unless
Raises AttributeError if the attribute does not exist, unless
``raising`` is set to False.
**Where to patch**
``monkeypatch.setattr`` works by (temporarily) changing the object that a name points to with another one.
There can be many names pointing to any individual object, so for patching to work you must ensure
that you patch the name used by the system under test.
See the section :ref:`Where to patch <python:where-to-patch>` in the :mod:`unittest.mock`
docs for a complete explanation, which is meant for :func:`unittest.mock.patch` but
applies to ``monkeypatch.setattr`` as well.
"""
__tracebackhide__ = True
import inspect
@@ -367,8 +338,7 @@ class MonkeyPatch:
def chdir(self, path: Union[str, "os.PathLike[str]"]) -> None:
"""Change the current working directory to the specified path.
:param path:
The path to change into.
Path can be a string or a path object.
"""
if self._cwd is None:
self._cwd = os.getcwd()
@@ -383,14 +353,11 @@ class MonkeyPatch:
There is generally no need to call `undo()`, since it is
called automatically during tear-down.
.. note::
The same `monkeypatch` fixture is used across a
single test function invocation. If `monkeypatch` is used both by
the test function itself and one of the test fixtures,
calling `undo()` will undo all of the changes made in
both functions.
Prefer to use :meth:`context() <pytest.MonkeyPatch.context>` instead.
Note that the same `monkeypatch` fixture is used across a
single test function invocation. If `monkeypatch` is used both by
the test function itself and one of the test fixtures,
calling `undo()` will undo all of the changes made in
both functions.
"""
for obj, name, value in reversed(self._setattr):
if value is not notset:

View File

@@ -193,7 +193,7 @@ class Node(metaclass=NodeMeta):
nodeid: Optional[str] = None,
) -> None:
#: A unique name within the scope of the parent node.
self.name: str = name
self.name = name
#: The parent collector node.
self.parent = parent
@@ -208,7 +208,7 @@ class Node(metaclass=NodeMeta):
if session:
#: The pytest session this node is part of.
self.session: Session = session
self.session = session
else:
if not parent:
raise TypeError("session or parent must be provided")
@@ -239,7 +239,9 @@ class Node(metaclass=NodeMeta):
#: A place where plugins can store information on the node for their
#: own use.
self.stash: Stash = Stash()
#:
#: :type: Stash
self.stash = Stash()
# Deprecated alias. Was never public. Can be removed in a few releases.
self._store = self.stash
@@ -324,10 +326,7 @@ class Node(metaclass=NodeMeta):
def listchain(self) -> List["Node"]:
"""Return list of all parent collectors up to self, starting from
the root of collection tree.
:returns: The nodes.
"""
the root of collection tree."""
chain = []
item: Optional[Node] = self
while item is not None:
@@ -341,8 +340,6 @@ class Node(metaclass=NodeMeta):
) -> None:
"""Dynamically add a marker object to the node.
:param marker:
The marker.
:param append:
Whether to append the marker, or prepend it.
"""
@@ -364,7 +361,6 @@ class Node(metaclass=NodeMeta):
"""Iterate over all markers of the node.
:param name: If given, filter the results by the name attribute.
:returns: An iterator of the markers of the node.
"""
return (x[1] for x in self.iter_markers_with_node(name=name))
@@ -411,8 +407,7 @@ class Node(metaclass=NodeMeta):
return [x.name for x in self.listchain()]
def addfinalizer(self, fin: Callable[[], object]) -> None:
"""Register a function to be called without arguments when this node is
finalized.
"""Register a function to be called when this node is finalized.
This method can only be called when this node is active
in a setup chain, for example during self.setup().
@@ -421,11 +416,7 @@ class Node(metaclass=NodeMeta):
def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]:
"""Get the next parent node (including self) which is an instance of
the given class.
:param cls: The node class to search for.
:returns: The node, if found.
"""
the given class."""
current: Optional[Node] = self
while current and not isinstance(current, cls):
current = current.parent
@@ -511,7 +502,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
* "obj": a Python object that the node wraps.
* "fspath": just a path
:rtype: A tuple of (str|Path, int) with filename and 0-based line number.
:rtype: A tuple of (str|Path, int) with filename and line number.
"""
# See Item.location.
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
@@ -755,7 +746,7 @@ class Item(Node):
Returns a tuple with three elements:
- The path of the test (default ``self.path``)
- The 0-based line number of the test (default ``None``)
- The line number of the test (default ``None``)
- A name of the test to be shown (default ``""``)
.. seealso:: :ref:`non-python tests`
@@ -764,11 +755,6 @@ class Item(Node):
@cached_property
def location(self) -> Tuple[str, Optional[int], str]:
"""
Returns a tuple of ``(relfspath, lineno, testname)`` for this item
where ``relfspath`` is file path relative to ``config.rootpath``
and lineno is a 0-based line number.
"""
location = self.reportinfo()
path = absolutepath(os.fspath(location[0]))
relfspath = self.session._node_location_to_relpath(path)

View File

@@ -1,8 +1,5 @@
"""Run testsuites written for nose."""
import warnings
from _pytest.config import hookimpl
from _pytest.deprecated import NOSE_SUPPORT
from _pytest.fixtures import getfixturemarker
from _pytest.nodes import Item
from _pytest.python import Function
@@ -21,8 +18,8 @@ def pytest_runtest_setup(item: Item) -> None:
# see https://github.com/python/mypy/issues/2608
func = item
call_optional(func.obj, "setup", func.nodeid)
func.addfinalizer(lambda: call_optional(func.obj, "teardown", func.nodeid))
call_optional(func.obj, "setup")
func.addfinalizer(lambda: call_optional(func.obj, "teardown"))
# NOTE: Module- and class-level fixtures are handled in python.py
# with `pluginmanager.has_plugin("nose")` checks.
@@ -30,7 +27,7 @@ def pytest_runtest_setup(item: Item) -> None:
# it's not straightforward.
def call_optional(obj: object, name: str, nodeid: str) -> bool:
def call_optional(obj: object, name: str) -> bool:
method = getattr(obj, name, None)
if method is None:
return False
@@ -39,11 +36,6 @@ def call_optional(obj: object, name: str, nodeid: str) -> bool:
return False
if not callable(method):
return False
# Warn about deprecation of this plugin.
method_name = getattr(method, "__name__", str(method))
warnings.warn(
NOSE_SUPPORT.format(nodeid=nodeid, method=method_name, stage=name), stacklevel=2
)
# If there are any problems allow the exception to raise rather than
# silently ignoring it.
method()

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