Compare commits
91 Commits
update-plu
...
7.4.0.dev0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec8e23951d | ||
|
|
bf47357511 | ||
|
|
3683722bcb | ||
|
|
31d0b51039 | ||
|
|
2d2f69dab5 | ||
|
|
2a39ed3461 | ||
|
|
a3b39069bc | ||
|
|
172c832cbd | ||
|
|
839b90db45 | ||
|
|
549cc512f7 | ||
|
|
2369bed1db | ||
|
|
54864f0c9b | ||
|
|
ba969d2ae7 | ||
|
|
407b330fe1 | ||
|
|
431ec6d34e | ||
|
|
eada68b2b3 | ||
|
|
ab069247cd | ||
|
|
7af1e4e4ed | ||
|
|
0ae04ae629 | ||
|
|
723035be7f | ||
|
|
6e478b0947 | ||
|
|
a869141b3d | ||
|
|
5e98aefc92 | ||
|
|
5f47e423b2 | ||
|
|
5a61ec3d4a | ||
|
|
b3b44ea814 | ||
|
|
1d48b3021d | ||
|
|
d5dda84ef3 | ||
|
|
517e02e59e | ||
|
|
4e259590c9 | ||
|
|
97a2761d72 | ||
|
|
88c9e92258 | ||
|
|
72ad32411f | ||
|
|
cb9e8be301 | ||
|
|
d72da480c4 | ||
|
|
07e7deb4a7 | ||
|
|
572b5657d7 | ||
|
|
44afed9b13 | ||
|
|
13ea4780b8 | ||
|
|
135600fca3 | ||
|
|
c237297b3d | ||
|
|
9ccae9a8e3 | ||
|
|
77152d26e7 | ||
|
|
da626e7186 | ||
|
|
051f8f1f0f | ||
|
|
31ad577325 | ||
|
|
835cac8d8b | ||
|
|
464f29901f | ||
|
|
aa72496d24 | ||
|
|
e9f3a01392 | ||
|
|
00c94ab01b | ||
|
|
9048621002 | ||
|
|
27165cf8db | ||
|
|
7a829cb57d | ||
|
|
5e1c3d2477 | ||
|
|
59e7d2bbc9 | ||
|
|
af99040123 | ||
|
|
a2b7db7655 | ||
|
|
9c93c96b14 | ||
|
|
4a46ee8bc9 | ||
|
|
5dbfb8e108 | ||
|
|
86a1beba07 | ||
|
|
ca40380e99 | ||
|
|
05eee78aaa | ||
|
|
02893139f9 | ||
|
|
8c53dbf9d7 | ||
|
|
54b8b40f83 | ||
|
|
54911acf8d | ||
|
|
c746d2b016 | ||
|
|
a3693ce503 | ||
|
|
af4143729f | ||
|
|
bd7919e03d | ||
|
|
7d4b40337b | ||
|
|
6a714d7b70 | ||
|
|
5a23eeff7a | ||
|
|
310b67b227 | ||
|
|
4d4ed42c34 | ||
|
|
096b942ec4 | ||
|
|
61cfaacec6 | ||
|
|
95c62eb527 | ||
|
|
03b19945fb | ||
|
|
b2ac31cc9f | ||
|
|
1a96f16401 | ||
|
|
7421f3bb94 | ||
|
|
5e0583f4b9 | ||
|
|
8efb4bb9c1 | ||
|
|
3ad4344656 | ||
|
|
6bf7f55555 | ||
|
|
61f70a5a75 | ||
|
|
326ae0cd88 | ||
|
|
10220d3f31 |
29
.github/workflows/deploy.yml
vendored
29
.github/workflows/deploy.yml
vendored
@@ -28,25 +28,30 @@ jobs:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
python-version: "3.7"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade build tox
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
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
|
||||
with:
|
||||
python-version: "3.7"
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade tox
|
||||
|
||||
- name: Publish GitHub release notes
|
||||
env:
|
||||
GH_RELEASE_NOTES_TOKEN: ${{ github.token }}
|
||||
|
||||
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@@ -18,6 +18,11 @@ 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: {}
|
||||
|
||||
@@ -189,3 +194,10 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
files: ./coverage.xml
|
||||
verbose: true
|
||||
|
||||
check-package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
2
.github/workflows/update-plugin-list.yml
vendored
2
.github/workflows/update-plugin-list.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@2b011faafdcbc9ceb11414d64d0573f37c774b04
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
||||
@@ -2,15 +2,15 @@ default_language_version:
|
||||
python: "3.10"
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.12.0
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: v1.12.1
|
||||
rev: 1.13.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==20.8b1]
|
||||
additional_dependencies: [black==23.1.0]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
@@ -23,7 +23,7 @@ repos:
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.0.0
|
||||
rev: v2.0.2
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
@@ -54,11 +54,11 @@ repos:
|
||||
- id: setup-cfg-fmt
|
||||
args: ["--max-py-version=3.11", "--include-version-classifiers"]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.9.0
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.991
|
||||
rev: v1.1.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
||||
10
AUTHORS
10
AUTHORS
@@ -12,6 +12,7 @@ Adam Uhlir
|
||||
Ahn Ki-Wook
|
||||
Akiomi Kamakura
|
||||
Alan Velasco
|
||||
Alessio Izzo
|
||||
Alexander Johnson
|
||||
Alexander King
|
||||
Alexei Kozlenok
|
||||
@@ -127,6 +128,7 @@ Erik M. Bray
|
||||
Evan Kepner
|
||||
Fabien Zarifian
|
||||
Fabio Zadrozny
|
||||
Felix Hofstätter
|
||||
Felix Nieuwenhuizen
|
||||
Feng Ma
|
||||
Florian Bruhin
|
||||
@@ -161,6 +163,7 @@ Ionuț Turturică
|
||||
Itxaso Aizpurua
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
Jake VanderPlas
|
||||
Jakob van Santen
|
||||
Jakub Mitoraj
|
||||
James Bourbeau
|
||||
@@ -290,12 +293,14 @@ Prashant Sharma
|
||||
Pulkit Goyal
|
||||
Punyashloka Biswal
|
||||
Quentin Pradet
|
||||
q0w
|
||||
Ralf Schmitt
|
||||
Ralph Giles
|
||||
Ram Rachum
|
||||
Ran Benita
|
||||
Raphael Castaneda
|
||||
Raphael Pierzina
|
||||
Rafal Semik
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
Robert Holt
|
||||
@@ -315,6 +320,7 @@ Samuel Searles-Bryant
|
||||
Samuele Pedroni
|
||||
Sanket Duthade
|
||||
Sankt Petersbug
|
||||
Saravanan Padmanaban
|
||||
Segev Finer
|
||||
Serhii Mozghovyi
|
||||
Seth Junot
|
||||
@@ -328,6 +334,7 @@ Srinivas Reddy Thatiparthy
|
||||
Stefan Farmbauer
|
||||
Stefan Scherfke
|
||||
Stefan Zimmermann
|
||||
Stefanie Molin
|
||||
Stefano Taschini
|
||||
Steffen Allner
|
||||
Stephan Obermann
|
||||
@@ -346,6 +353,7 @@ Thomas Grainger
|
||||
Thomas Hisch
|
||||
Tim Hoffmann
|
||||
Tim Strazny
|
||||
TJ Bruno
|
||||
Tobias Diez
|
||||
Tom Dalton
|
||||
Tom Viner
|
||||
@@ -376,7 +384,9 @@ Wouter van Ackooy
|
||||
Xixi Zhao
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Yannick Péroux
|
||||
Yoav Caspi
|
||||
Yuliang Shao
|
||||
Yusuke Kadowaki
|
||||
Yuval Shimon
|
||||
Zac Hatfield-Dodds
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
|
||||
@@ -1 +0,0 @@
|
||||
Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12.
|
||||
@@ -1 +0,0 @@
|
||||
If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location.
|
||||
@@ -1 +0,0 @@
|
||||
Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows.
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
@@ -1,2 +0,0 @@
|
||||
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.
|
||||
@@ -1 +0,0 @@
|
||||
``--log-disable`` CLI option added to disable individual loggers.
|
||||
@@ -1,2 +0,0 @@
|
||||
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.
|
||||
The default behavior has changed to keep only directories for failed tests, equivalent to `tmp_path_retention_policy="failed"`.
|
||||
@@ -6,6 +6,9 @@ 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
|
||||
|
||||
25
doc/en/announce/release-7.2.1.rst
Normal file
25
doc/en/announce/release-7.2.1.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-7.2.1
|
||||
=======================================
|
||||
|
||||
pytest 7.2.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Valenzuela
|
||||
* Kadino
|
||||
* Prerak Patel
|
||||
* Ronny Pfannschmidt
|
||||
* Santiago Castro
|
||||
* s-padmanaban
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
25
doc/en/announce/release-7.2.2.rst
Normal file
25
doc/en/announce/release-7.2.2.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-7.2.2
|
||||
=======================================
|
||||
|
||||
pytest 7.2.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Garvit Shubham
|
||||
* Mahesh Vashishtha
|
||||
* Ramsey
|
||||
* Ronny Pfannschmidt
|
||||
* Teejay
|
||||
* q0w
|
||||
* vin01
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
130
doc/en/announce/release-7.3.0.rst
Normal file
130
doc/en/announce/release-7.3.0.rst
Normal file
@@ -0,0 +1,130 @@
|
||||
pytest-7.3.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 7.3.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Aaron Berdy
|
||||
* Adam Turner
|
||||
* Albert Villanova del Moral
|
||||
* Alessio Izzo
|
||||
* Alex Hadley
|
||||
* Alice Purcell
|
||||
* Anthony Sottile
|
||||
* Anton Yakutovich
|
||||
* Ashish Kurmi
|
||||
* Babak Keyvani
|
||||
* Billy
|
||||
* Brandon Chinn
|
||||
* Bruno Oliveira
|
||||
* Cal Jacobson
|
||||
* Chanvin Xiao
|
||||
* Cheuk Ting Ho
|
||||
* Chris Wheeler
|
||||
* Daniel Garcia Moreno
|
||||
* Daniel Scheffler
|
||||
* Daniel Valenzuela
|
||||
* EmptyRabbit
|
||||
* Ezio Melotti
|
||||
* Felix Hofstätter
|
||||
* Florian Best
|
||||
* Florian Bruhin
|
||||
* Fredrik Berndtsson
|
||||
* Gabriel Landau
|
||||
* Garvit Shubham
|
||||
* Gergely Kalmár
|
||||
* HTRafal
|
||||
* Hugo van Kemenade
|
||||
* Ilya Konstantinov
|
||||
* Itxaso Aizpurua
|
||||
* James Gerity
|
||||
* Jay
|
||||
* John Litborn
|
||||
* Jon Parise
|
||||
* Jouke Witteveen
|
||||
* Kadino
|
||||
* Kevin C
|
||||
* Kian Eliasi
|
||||
* Klaus Rettinghaus
|
||||
* Kodi Arfer
|
||||
* Mahesh Vashishtha
|
||||
* Manuel Jacob
|
||||
* Marko Pacak
|
||||
* MatthewFlamm
|
||||
* Miro Hrončok
|
||||
* Nate Meyvis
|
||||
* Neil Girdhar
|
||||
* Nhieuvu1802
|
||||
* Nipunn Koorapati
|
||||
* Ofek Lev
|
||||
* Paul Kehrer
|
||||
* Paul Müller
|
||||
* Paul Reece
|
||||
* Pax
|
||||
* Pete Baughman
|
||||
* Peyman Salehi
|
||||
* Philipp A
|
||||
* Pierre Sassoulas
|
||||
* Prerak Patel
|
||||
* Ramsey
|
||||
* Ran Benita
|
||||
* Robert O'Shea
|
||||
* Ronny Pfannschmidt
|
||||
* Rowin
|
||||
* Ruth Comer
|
||||
* Samuel Colvin
|
||||
* Samuel Gaist
|
||||
* Sandro Tosi
|
||||
* Santiago Castro
|
||||
* Shantanu
|
||||
* Simon K
|
||||
* Stefanie Molin
|
||||
* Stephen Rosen
|
||||
* Sviatoslav Sydorenko
|
||||
* Tatiana Ovary
|
||||
* Teejay
|
||||
* Thierry Moisan
|
||||
* Thomas Grainger
|
||||
* Tim Hoffmann
|
||||
* Tobias Diez
|
||||
* Tony Narlock
|
||||
* Vivaan Verma
|
||||
* Wolfremium
|
||||
* Yannick PÉROUX
|
||||
* Yusuke Kadowaki
|
||||
* Zac Hatfield-Dodds
|
||||
* Zach OBrien
|
||||
* aizpurua23a
|
||||
* bitzge
|
||||
* bluthej
|
||||
* gresm
|
||||
* holesch
|
||||
* itxasos23
|
||||
* johnkangw
|
||||
* q0w
|
||||
* rdb
|
||||
* s-padmanaban
|
||||
* skhomuti
|
||||
* sommersoft
|
||||
* vin01
|
||||
* wim glenn
|
||||
* wodny
|
||||
* zx.qiu
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -33,25 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
Values can be any object handled by the json stdlib module.
|
||||
|
||||
capsys -- .../_pytest/capture.py:905
|
||||
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsys.readouterr()`` method
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsys):
|
||||
print("hello")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
capsysbinary -- .../_pytest/capture.py:933
|
||||
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()``
|
||||
@@ -69,7 +51,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsysbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capfd -- .../_pytest/capture.py:961
|
||||
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
|
||||
@@ -87,7 +69,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capfd.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
capfdbinary -- .../_pytest/capture.py:989
|
||||
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
|
||||
@@ -105,7 +87,25 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capfdbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:738
|
||||
capsys -- .../_pytest/capture.py:973
|
||||
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsys.readouterr()`` method
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsys):
|
||||
print("hello")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
@@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1351
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
@@ -196,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
caplog -- .../_pytest/logging.py:491
|
||||
caplog -- .../_pytest/logging.py:498
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following properties/methods::
|
||||
@@ -237,17 +237,19 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
||||
on warning categories.
|
||||
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:188
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:245
|
||||
Return a :class:`pytest.TempPathFactory` instance for the test session.
|
||||
|
||||
tmp_path -- .../_pytest/tmpdir.py:203
|
||||
tmp_path -- .../_pytest/tmpdir.py:260
|
||||
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. 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.
|
||||
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a :class:`pathlib.Path` object.
|
||||
|
||||
@@ -28,6 +28,133 @@ 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)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -1052,7 +1052,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 +1067,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:
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ def b(a, order):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def c(a, b, order):
|
||||
def c(b, order):
|
||||
order.append("c")
|
||||
|
||||
|
||||
|
||||
@@ -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:29: 'python3.5' not found
|
||||
SKIPPED [9] multipython.py:29: 'python3.6' not found
|
||||
SKIPPED [9] multipython.py:29: 'python3.7' not found
|
||||
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
|
||||
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] conftest.py:12: could not import 'opt2': No module named 'opt2'
|
||||
SKIPPED [1] test_module.py:3: could not import 'opt2': No module named 'opt2'
|
||||
======================= 1 passed, 1 skipped in 0.12s =======================
|
||||
|
||||
You'll see that we don't have an ``opt2`` module and thus the second test run
|
||||
|
||||
@@ -148,7 +148,8 @@ 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>
|
||||
@@ -209,7 +210,8 @@ 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>
|
||||
@@ -290,7 +292,8 @@ 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 ========================
|
||||
|
||||
@@ -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 (7 lines hidden), use '-vv' to show
|
||||
E ...Full output truncated (6 lines hidden), use '-vv' to show
|
||||
|
||||
failure_demo.py:60: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||
@@ -184,9 +184,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
E Left contains 1 more item:
|
||||
E {'c': 0}
|
||||
E Right contains 1 more item:
|
||||
E {'d': 0}...
|
||||
E
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
E {'d': 0}
|
||||
E Use -v to get more diff
|
||||
|
||||
failure_demo.py:71: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||
@@ -195,16 +194,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
|
||||
def test_eq_set(self):
|
||||
> assert {0, 10, 11, 12} == {0, 20, 21}
|
||||
E AssertionError: assert {0, 10, 11, 12} == {0, 20, 21}
|
||||
E assert {0, 10, 11, 12} == {0, 20, 21}
|
||||
E Extra items in the left set:
|
||||
E 10
|
||||
E 11
|
||||
E 12
|
||||
E Extra items in the right set:
|
||||
E 20
|
||||
E 21...
|
||||
E
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
E 21
|
||||
E Use -v to get more diff
|
||||
|
||||
failure_demo.py:74: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
||||
@@ -241,9 +239,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
E which
|
||||
E includes foo
|
||||
E ? +++
|
||||
E and a...
|
||||
E
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
E and a
|
||||
E tail
|
||||
|
||||
failure_demo.py:84: AssertionError
|
||||
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
||||
@@ -307,9 +304,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
E ['b']
|
||||
E
|
||||
E Drill down into differing attribute b:
|
||||
E b: 'b' != 'c'...
|
||||
E
|
||||
E ...Full output truncated (3 lines hidden), use '-vv' to show
|
||||
E b: 'b' != 'c'
|
||||
E - c
|
||||
E + b
|
||||
|
||||
failure_demo.py:108: AssertionError
|
||||
________________ TestSpecialisedExplanations.test_eq_attrs _________________
|
||||
@@ -334,9 +331,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
|
||||
E ...Full output truncated (3 lines hidden), use '-vv' to show
|
||||
E b: 'b' != 'c'
|
||||
E - c
|
||||
E + b
|
||||
|
||||
failure_demo.py:120: AssertionError
|
||||
______________________________ test_attribute ______________________________
|
||||
@@ -673,7 +670,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list - asser...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_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
|
||||
|
||||
@@ -892,8 +892,9 @@ 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]]()
|
||||
|
||||
@@ -956,8 +957,8 @@ and run it:
|
||||
rootdir: /home/sweet/project
|
||||
collected 3 items
|
||||
|
||||
test_module.py Esetting up a test failed! test_module.py::test_setup_fails
|
||||
Fexecuting test failed test_module.py::test_call_fails
|
||||
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
|
||||
F
|
||||
|
||||
================================== ERRORS ==================================
|
||||
|
||||
@@ -24,8 +24,9 @@ The first few lines should look like this:
|
||||
|
||||
[project]
|
||||
name = "PACKAGENAME"
|
||||
version = "PACKAGEVERSION"
|
||||
|
||||
where ``PACKAGENAME`` is the name of your package.
|
||||
where ``PACKAGENAME`` and ``PACKAGEVERSION`` are the name and version of your package respectively.
|
||||
|
||||
You can then install your package in "editable" mode by running from the same directory:
|
||||
|
||||
|
||||
@@ -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:`__import__ <__import__>` builtin.
|
||||
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
|
||||
|
||||
This requires test module names to be unique when the test directory tree is not arranged in
|
||||
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 ``__import__``.
|
||||
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
||||
|
||||
This better allows to run test modules against installed versions of a package even if the
|
||||
package under test has the same import root. For example:
|
||||
@@ -43,7 +43,7 @@ these values:
|
||||
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
||||
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
|
||||
|
||||
* ``importlib``: new in pytest-6.0, this mode uses :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
|
||||
* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
|
||||
|
||||
For this reason this doesn't require test module names to be unique.
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.2.0
|
||||
pytest 7.3.0
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -109,6 +109,18 @@ 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``
|
||||
@@ -270,20 +282,34 @@ which works in a similar manner to :ref:`raises <assertraises>` (except that
|
||||
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::
|
||||
argument ``match`` to assert that the warning matches a text or regex.
|
||||
To match a literal string that may contain regular expression metacharacters like ``(`` or ``.``, the pattern can
|
||||
first be escaped with ``re.escape``.
|
||||
|
||||
>>> with warns(UserWarning, match='must be 0 or None'):
|
||||
Some examples:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
|
||||
>>> with warns(UserWarning, match="must be 0 or None"):
|
||||
... warnings.warn("value must be 0 or None", UserWarning)
|
||||
...
|
||||
|
||||
>>> 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
|
||||
|
||||
@@ -1237,7 +1237,6 @@ 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):
|
||||
|
||||
@@ -135,10 +135,10 @@ This can be done in our test file by defining a class to represent ``r``.
|
||||
# this is the previous code block example
|
||||
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,7 +146,6 @@ 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):
|
||||
@@ -181,6 +180,7 @@ 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,7 +358,6 @@ 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")
|
||||
@@ -383,7 +382,6 @@ 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)
|
||||
|
||||
@@ -404,6 +402,7 @@ 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):
|
||||
@@ -425,7 +424,6 @@ separate fixtures for each potential mock and reference them in the needed tests
|
||||
|
||||
# tests reference only the fixture mocks that are needed
|
||||
def test_connection(mock_test_user, mock_test_database):
|
||||
|
||||
expected = "User Id=test_user; Location=test_db;"
|
||||
|
||||
result = app.create_connection_string()
|
||||
@@ -433,7 +431,6 @@ separate fixtures for each potential mock and reference them in the needed tests
|
||||
|
||||
|
||||
def test_missing_user(mock_missing_default_user):
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
_ = app.create_connection_string()
|
||||
|
||||
|
||||
@@ -167,9 +167,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 ...Full output truncated (3 lines hidden), use '-vv' to show
|
||||
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
|
||||
E ? - - - - - - - -
|
||||
E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
|
||||
|
||||
test_verbosity_example.py:14: AssertionError
|
||||
___________________________ test_long_text_fail ____________________________
|
||||
|
||||
@@ -132,8 +132,7 @@ 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, only the directories of failed tests will be kept.
|
||||
Also only the last 3 directries will remain at most.
|
||||
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`.
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ 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)
|
||||
|
||||
@@ -167,13 +167,8 @@ it in your ``pyproject.toml`` file.
|
||||
"Framework :: Pytest",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["myproject"]
|
||||
|
||||
[project.entry_points]
|
||||
pytest11 = [
|
||||
"myproject = myproject.pluginmodule",
|
||||
]
|
||||
[project.entry-points.pytest11]
|
||||
myproject = "myproject.pluginmodule"
|
||||
|
||||
If a package is installed this way, ``pytest`` will load
|
||||
``myproject.pluginmodule`` as a plugin which can define
|
||||
@@ -454,7 +449,8 @@ in our ``pytest.ini`` to tell pytest where to look for example files.
|
||||
$ pytest
|
||||
=========================== 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%]
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
:orphan:
|
||||
|
||||
.. sidebar:: Next Open Trainings
|
||||
..
|
||||
.. 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 and Leipzig, Germany
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
.. _features:
|
||||
|
||||
|
||||
@@ -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 look like this:
|
||||
If we map out what depends on what, we get something that looks like this:
|
||||
|
||||
.. image:: /example/fixtures/test_fixtures_order_dependencies.*
|
||||
:align: center
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1047,6 +1047,14 @@ Environment Variables
|
||||
|
||||
Environment variables that can be used to change pytest's behavior.
|
||||
|
||||
.. envvar:: CI
|
||||
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to ``BUILD_NUMBER`` variable.
|
||||
|
||||
.. envvar:: BUILD_NUMBER
|
||||
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to CI variable.
|
||||
|
||||
.. envvar:: PYTEST_ADDOPTS
|
||||
|
||||
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given
|
||||
@@ -1212,6 +1220,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
* ``classic``: classic pytest output.
|
||||
* ``progress``: like classic pytest output, but with a progress indicator.
|
||||
* ``progress-even-when-capture-no``: allows the use of the progress indicator even when ``capture=no``.
|
||||
* ``count``: like progress, but shows progress as the number of tests completed instead of a percent.
|
||||
|
||||
The default is ``progress``, but you can fallback to ``classic`` if you prefer or
|
||||
@@ -1754,7 +1763,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
tmp_path_retention_policy = "all"
|
||||
|
||||
Default: failed
|
||||
Default: all
|
||||
|
||||
|
||||
.. confval:: usefixtures
|
||||
@@ -1986,8 +1995,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
--log-auto-indent=LOG_AUTO_INDENT
|
||||
Auto-indent multiline messages passed to the logging
|
||||
module. Accepts true|on, false|off or an integer.
|
||||
--log-disable=LOGGER_DISABLE
|
||||
Disable a logger by name. Can be passed multipe
|
||||
times.
|
||||
|
||||
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:
|
||||
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
||||
|
||||
markers (linelist): Markers for test functions
|
||||
empty_parameter_set_mark (string):
|
||||
@@ -2015,9 +2027,18 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
console_output_style (string):
|
||||
Console output: "classic", or with additional
|
||||
progress information ("progress" (percentage) |
|
||||
"count")
|
||||
"count" | "progress-even-when-capture-no" (forces
|
||||
progress even when capture=no)
|
||||
xfail_strict (bool): Default for the strict parameter of xfail markers
|
||||
when not given explicitly (default: False)
|
||||
tmp_path_retention_count (string):
|
||||
How many sessions should we keep the `tmp_path`
|
||||
directories, according to
|
||||
`tmp_path_retention_policy`.
|
||||
tmp_path_retention_policy (string):
|
||||
Controls which directories created by the `tmp_path`
|
||||
fixture are kept around, based on test outcome.
|
||||
(all/failed/none)
|
||||
enable_assertion_pass_hook (bool):
|
||||
Enables the pytest_assertion_pass hook. Make sure to
|
||||
delete any previously generated pyc cache files.
|
||||
|
||||
@@ -114,3 +114,8 @@ 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"
|
||||
|
||||
@@ -44,7 +44,6 @@ packages =
|
||||
pytest
|
||||
py_modules = py
|
||||
install_requires =
|
||||
attrs>=19.2.0
|
||||
iniconfig
|
||||
packaging
|
||||
pluggy>=0.12,<2.0
|
||||
@@ -68,6 +67,7 @@ console_scripts =
|
||||
[options.extras_require]
|
||||
testing =
|
||||
argcomplete
|
||||
attrs>=19.2.0
|
||||
hypothesis>=3.56
|
||||
mock
|
||||
nose
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ast
|
||||
import dataclasses
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
@@ -32,7 +33,6 @@ from typing import TypeVar
|
||||
from typing import Union
|
||||
from weakref import ref
|
||||
|
||||
import attr
|
||||
import pluggy
|
||||
|
||||
import _pytest
|
||||
@@ -411,13 +411,13 @@ class Traceback(List[TracebackEntry]):
|
||||
"""
|
||||
return Traceback(filter(fn, self), self._excinfo)
|
||||
|
||||
def getcrashentry(self) -> TracebackEntry:
|
||||
def getcrashentry(self) -> Optional[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 self[-1]
|
||||
return None
|
||||
|
||||
def recursionindex(self) -> Optional[int]:
|
||||
"""Return the index of the frame/TracebackEntry where recursion originates if
|
||||
@@ -445,7 +445,7 @@ E = TypeVar("E", bound=BaseException, covariant=True)
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(repr=False, init=False, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class ExceptionInfo(Generic[E]):
|
||||
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
|
||||
|
||||
@@ -602,11 +602,13 @@ class ExceptionInfo(Generic[E]):
|
||||
"""
|
||||
return isinstance(self.value, exc)
|
||||
|
||||
def _getreprcrash(self) -> "ReprFileLocation":
|
||||
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
||||
exconly = self.exconly(tryshort=True)
|
||||
entry = self.traceback.getcrashentry()
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
if entry:
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
return None
|
||||
|
||||
def getrepr(
|
||||
self,
|
||||
@@ -649,12 +651,12 @@ class ExceptionInfo(Generic[E]):
|
||||
"""
|
||||
if style == "native":
|
||||
return ReprExceptionInfo(
|
||||
ReprTracebackNative(
|
||||
reprtraceback=ReprTracebackNative(
|
||||
traceback.format_exception(
|
||||
self.type, self.value, self.traceback[0]._rawentry
|
||||
)
|
||||
),
|
||||
self._getreprcrash(),
|
||||
reprcrash=self._getreprcrash(),
|
||||
)
|
||||
|
||||
fmt = FormattedExcinfo(
|
||||
@@ -684,7 +686,7 @@ class ExceptionInfo(Generic[E]):
|
||||
return True
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class FormattedExcinfo:
|
||||
"""Presenting information about failing Functions and Generators."""
|
||||
|
||||
@@ -699,8 +701,8 @@ class FormattedExcinfo:
|
||||
funcargs: bool = False
|
||||
truncate_locals: bool = True
|
||||
chain: bool = True
|
||||
astcache: Dict[Union[str, Path], ast.AST] = attr.ib(
|
||||
factory=dict, init=False, repr=False
|
||||
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
|
||||
default_factory=dict, init=False, repr=False
|
||||
)
|
||||
|
||||
def _getindent(self, source: "Source") -> int:
|
||||
@@ -741,11 +743,13 @@ class FormattedExcinfo:
|
||||
) -> List[str]:
|
||||
"""Return formatted and marked up source lines."""
|
||||
lines = []
|
||||
if source is None or line_index >= len(source.lines):
|
||||
if source is not None and line_index < 0:
|
||||
line_index += len(source)
|
||||
if source is None or line_index >= len(source.lines) or line_index < 0:
|
||||
# `line_index` could still be outside `range(len(source.lines))` if
|
||||
# we're processing AST with pathological position attributes.
|
||||
source = Source("???")
|
||||
line_index = 0
|
||||
if line_index < 0:
|
||||
line_index += len(source)
|
||||
space_prefix = " "
|
||||
if short:
|
||||
lines.append(space_prefix + source.lines[line_index].strip())
|
||||
@@ -942,9 +946,14 @@ class FormattedExcinfo:
|
||||
)
|
||||
else:
|
||||
reprtraceback = self.repr_traceback(excinfo_)
|
||||
reprcrash: Optional[ReprFileLocation] = (
|
||||
excinfo_._getreprcrash() if self.style != "value" else None
|
||||
)
|
||||
|
||||
# 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)]
|
||||
else:
|
||||
# Fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work.
|
||||
@@ -952,8 +961,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_ = (
|
||||
@@ -978,7 +987,7 @@ class FormattedExcinfo:
|
||||
return ExceptionChainRepr(repr_chain)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class TerminalRepr:
|
||||
def __str__(self) -> str:
|
||||
# FYI this is called from pytest-xdist's serialization of exception
|
||||
@@ -996,14 +1005,14 @@ class TerminalRepr:
|
||||
|
||||
|
||||
# This class is abstract -- only subclasses are instantiated.
|
||||
@attr.s(eq=False)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ExceptionRepr(TerminalRepr):
|
||||
# Provided by subclasses.
|
||||
reprcrash: Optional["ReprFileLocation"]
|
||||
reprtraceback: "ReprTraceback"
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
self.sections: List[Tuple[str, str, str]] = []
|
||||
reprcrash: Optional["ReprFileLocation"]
|
||||
sections: List[Tuple[str, str, str]] = dataclasses.field(
|
||||
init=False, default_factory=list
|
||||
)
|
||||
|
||||
def addsection(self, name: str, content: str, sep: str = "-") -> None:
|
||||
self.sections.append((name, content, sep))
|
||||
@@ -1014,16 +1023,23 @@ class ExceptionRepr(TerminalRepr):
|
||||
tw.line(content)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ExceptionChainRepr(ExceptionRepr):
|
||||
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
super().__attrs_post_init__()
|
||||
def __init__(
|
||||
self,
|
||||
chain: Sequence[
|
||||
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
|
||||
],
|
||||
) -> None:
|
||||
# reprcrash and reprtraceback of the outermost (the newest) exception
|
||||
# in the chain.
|
||||
self.reprtraceback = self.chain[-1][0]
|
||||
self.reprcrash = self.chain[-1][1]
|
||||
super().__init__(
|
||||
reprtraceback=chain[-1][0],
|
||||
reprcrash=chain[-1][1],
|
||||
)
|
||||
self.chain = chain
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
for element in self.chain:
|
||||
@@ -1034,17 +1050,17 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||
super().toterminal(tw)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprExceptionInfo(ExceptionRepr):
|
||||
reprtraceback: "ReprTraceback"
|
||||
reprcrash: "ReprFileLocation"
|
||||
reprcrash: Optional["ReprFileLocation"]
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
self.reprtraceback.toterminal(tw)
|
||||
super().toterminal(tw)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprTraceback(TerminalRepr):
|
||||
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
|
||||
extraline: Optional[str]
|
||||
@@ -1073,12 +1089,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"
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprEntryNative(TerminalRepr):
|
||||
lines: Sequence[str]
|
||||
|
||||
@@ -1088,7 +1104,7 @@ class ReprEntryNative(TerminalRepr):
|
||||
tw.write("".join(self.lines))
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprEntry(TerminalRepr):
|
||||
lines: Sequence[str]
|
||||
reprfuncargs: Optional["ReprFuncArgs"]
|
||||
@@ -1168,12 +1184,15 @@ class ReprEntry(TerminalRepr):
|
||||
)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprFileLocation(TerminalRepr):
|
||||
path: str = attr.ib(converter=str)
|
||||
path: 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.
|
||||
@@ -1185,7 +1204,7 @@ class ReprFileLocation(TerminalRepr):
|
||||
tw.line(f":{self.lineno}: {msg}")
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprLocals(TerminalRepr):
|
||||
lines: Sequence[str]
|
||||
|
||||
@@ -1194,7 +1213,7 @@ class ReprLocals(TerminalRepr):
|
||||
tw.line(indent + line)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprFuncArgs(TerminalRepr):
|
||||
args: Sequence[Tuple[str, object]]
|
||||
|
||||
|
||||
@@ -44,10 +44,14 @@ 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")
|
||||
@@ -274,7 +278,6 @@ 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:
|
||||
@@ -636,8 +639,12 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
.push_format_context() and .pop_format_context() which allows
|
||||
to build another %-formatted string while already building one.
|
||||
|
||||
This state is reset on every new assert statement visited and used
|
||||
by the other visitors.
|
||||
: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.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -653,6 +660,7 @@ 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."""
|
||||
@@ -667,7 +675,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if doc is not None and self.is_rewrite_disabled(doc):
|
||||
return
|
||||
pos = 0
|
||||
lineno = 1
|
||||
item = None
|
||||
for item in mod.body:
|
||||
if (
|
||||
expect_docstring
|
||||
@@ -938,6 +946,18 @@ 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.
|
||||
@@ -964,6 +984,20 @@ 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))
|
||||
@@ -1039,6 +1073,9 @@ 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})"
|
||||
@@ -1050,6 +1087,13 @@ 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})"
|
||||
@@ -1073,6 +1117,7 @@ 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))
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""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
|
||||
@@ -12,8 +13,6 @@ 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
|
||||
@@ -32,7 +31,6 @@ from _pytest.python import Module
|
||||
from _pytest.python import Package
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
README_CONTENT = """\
|
||||
# pytest cache directory #
|
||||
|
||||
@@ -53,10 +51,12 @@ Signature: 8a477f597d28d172789f06886806bc55
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(init=False, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class Cache:
|
||||
_cachedir: Path = attr.ib(repr=False)
|
||||
_config: Config = attr.ib(repr=False)
|
||||
"""Instance of the `cache` fixture."""
|
||||
|
||||
_cachedir: Path = dataclasses.field(repr=False)
|
||||
_config: Config = dataclasses.field(repr=False)
|
||||
|
||||
# Sub-directory under cache-dir for directories created by `mkdir()`.
|
||||
_CACHE_PREFIX_DIRS = "d"
|
||||
@@ -492,7 +492,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
|
||||
|
||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
if config.option.cacheshow:
|
||||
if config.option.cacheshow and not config.option.help:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, cacheshow)
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
"""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
|
||||
|
||||
@@ -29,6 +36,7 @@ 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"]
|
||||
@@ -185,19 +193,27 @@ class TeeCaptureIO(CaptureIO):
|
||||
return self._other.write(s)
|
||||
|
||||
|
||||
class DontReadFromInput:
|
||||
encoding = None
|
||||
class DontReadFromInput(TextIO):
|
||||
@property
|
||||
def encoding(self) -> str:
|
||||
return sys.__stdin__.encoding
|
||||
|
||||
def read(self, *args):
|
||||
def read(self, size: int = -1) -> str:
|
||||
raise OSError(
|
||||
"pytest: reading from stdin while output is captured! Consider using `-s`."
|
||||
)
|
||||
|
||||
readline = read
|
||||
readlines = read
|
||||
__next__ = read
|
||||
|
||||
def __iter__(self):
|
||||
def __next__(self) -> str:
|
||||
return self.readline()
|
||||
|
||||
def readlines(self, hint: Optional[int] = -1) -> List[str]:
|
||||
raise OSError(
|
||||
"pytest: reading from stdin while output is captured! Consider using `-s`."
|
||||
)
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return self
|
||||
|
||||
def fileno(self) -> int:
|
||||
@@ -215,7 +231,7 @@ class DontReadFromInput:
|
||||
def readable(self) -> bool:
|
||||
return False
|
||||
|
||||
def seek(self, offset: int) -> int:
|
||||
def seek(self, offset: int, whence: int = 0) -> int:
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
|
||||
|
||||
def seekable(self) -> bool:
|
||||
@@ -224,41 +240,104 @@ class DontReadFromInput:
|
||||
def tell(self) -> int:
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
||||
|
||||
def truncate(self, size: int) -> None:
|
||||
def truncate(self, size: Optional[int] = None) -> int:
|
||||
raise UnsupportedOperation("cannont truncate stdin")
|
||||
|
||||
def write(self, *args) -> None:
|
||||
def write(self, data: str) -> int:
|
||||
raise UnsupportedOperation("cannot write to stdin")
|
||||
|
||||
def writelines(self, *args) -> None:
|
||||
def writelines(self, lines: Iterable[str]) -> None:
|
||||
raise UnsupportedOperation("Cannot write to stdin")
|
||||
|
||||
def writable(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def buffer(self):
|
||||
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]
|
||||
|
||||
|
||||
# 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:
|
||||
EMPTY_BUFFER = None
|
||||
__init__ = start = done = suspend = resume = lambda *args: None
|
||||
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 SysCaptureBinary:
|
||||
|
||||
EMPTY_BUFFER = b""
|
||||
|
||||
def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
|
||||
class SysCaptureBase(CaptureBase[AnyStr]):
|
||||
def __init__(
|
||||
self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
|
||||
) -> None:
|
||||
name = patchsysdict[fd]
|
||||
self._old = getattr(sys, name)
|
||||
self._old: TextIO = getattr(sys, name)
|
||||
self.name = name
|
||||
if tmpfile is None:
|
||||
if name == "stdin":
|
||||
@@ -298,14 +377,6 @@ class SysCaptureBinary:
|
||||
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":
|
||||
@@ -327,36 +398,43 @@ class SysCaptureBinary:
|
||||
setattr(sys, self.name, self.tmpfile)
|
||||
self._state = "started"
|
||||
|
||||
def writeorg(self, data) -> None:
|
||||
|
||||
class SysCaptureBinary(SysCaptureBase[bytes]):
|
||||
EMPTY_BUFFER = b""
|
||||
|
||||
def snap(self) -> bytes:
|
||||
self._assert_state("snap", ("started", "suspended"))
|
||||
self.tmpfile.seek(0)
|
||||
res = self.tmpfile.buffer.read()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def writeorg(self, data: bytes) -> None:
|
||||
self._assert_state("writeorg", ("started", "suspended"))
|
||||
self._old.flush()
|
||||
self._old.buffer.write(data)
|
||||
self._old.buffer.flush()
|
||||
|
||||
|
||||
class SysCapture(SysCaptureBinary):
|
||||
EMPTY_BUFFER = "" # type: ignore[assignment]
|
||||
class SysCapture(SysCaptureBase[str]):
|
||||
EMPTY_BUFFER = ""
|
||||
|
||||
def snap(self):
|
||||
def snap(self) -> str:
|
||||
self._assert_state("snap", ("started", "suspended"))
|
||||
assert isinstance(self.tmpfile, CaptureIO)
|
||||
res = self.tmpfile.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def writeorg(self, data):
|
||||
def writeorg(self, data: str) -> None:
|
||||
self._assert_state("writeorg", ("started", "suspended"))
|
||||
self._old.write(data)
|
||||
self._old.flush()
|
||||
|
||||
|
||||
class FDCaptureBinary:
|
||||
"""Capture IO to/from a given OS-level file descriptor.
|
||||
|
||||
snap() produces `bytes`.
|
||||
"""
|
||||
|
||||
EMPTY_BUFFER = b""
|
||||
|
||||
class FDCaptureBase(CaptureBase[AnyStr]):
|
||||
def __init__(self, targetfd: int) -> None:
|
||||
self.targetfd = targetfd
|
||||
|
||||
@@ -382,7 +460,7 @@ class FDCaptureBinary:
|
||||
|
||||
if targetfd == 0:
|
||||
self.tmpfile = open(os.devnull, encoding="utf-8")
|
||||
self.syscapture = SysCapture(targetfd)
|
||||
self.syscapture: CaptureBase[str] = SysCapture(targetfd)
|
||||
else:
|
||||
self.tmpfile = EncodedFile(
|
||||
TemporaryFile(buffering=0),
|
||||
@@ -394,7 +472,7 @@ class FDCaptureBinary:
|
||||
if targetfd in patchsysdict:
|
||||
self.syscapture = SysCapture(targetfd, self.tmpfile)
|
||||
else:
|
||||
self.syscapture = NoCapture()
|
||||
self.syscapture = NoCapture(targetfd)
|
||||
|
||||
self._state = "initialized"
|
||||
|
||||
@@ -421,14 +499,6 @@ class FDCaptureBinary:
|
||||
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."""
|
||||
@@ -461,22 +531,38 @@ class FDCaptureBinary:
|
||||
os.dup2(self.tmpfile.fileno(), self.targetfd)
|
||||
self._state = "started"
|
||||
|
||||
def writeorg(self, data):
|
||||
|
||||
class FDCaptureBinary(FDCaptureBase[bytes]):
|
||||
"""Capture IO to/from a given OS-level file descriptor.
|
||||
|
||||
snap() produces `bytes`.
|
||||
"""
|
||||
|
||||
EMPTY_BUFFER = b""
|
||||
|
||||
def snap(self) -> bytes:
|
||||
self._assert_state("snap", ("started", "suspended"))
|
||||
self.tmpfile.seek(0)
|
||||
res = self.tmpfile.buffer.read()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def writeorg(self, data: bytes) -> None:
|
||||
"""Write to original file descriptor."""
|
||||
self._assert_state("writeorg", ("started", "suspended"))
|
||||
os.write(self.targetfd_save, data)
|
||||
|
||||
|
||||
class FDCapture(FDCaptureBinary):
|
||||
class FDCapture(FDCaptureBase[str]):
|
||||
"""Capture IO to/from a given OS-level file descriptor.
|
||||
|
||||
snap() produces text.
|
||||
"""
|
||||
|
||||
# Ignore type because it doesn't match the type in the superclass (bytes).
|
||||
EMPTY_BUFFER = "" # type: ignore
|
||||
EMPTY_BUFFER = ""
|
||||
|
||||
def snap(self):
|
||||
def snap(self) -> str:
|
||||
self._assert_state("snap", ("started", "suspended"))
|
||||
self.tmpfile.seek(0)
|
||||
res = self.tmpfile.read()
|
||||
@@ -484,77 +570,49 @@ class FDCapture(FDCaptureBinary):
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def writeorg(self, data):
|
||||
def writeorg(self, data: str) -> None:
|
||||
"""Write to original file descriptor."""
|
||||
super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
|
||||
self._assert_state("writeorg", ("started", "suspended"))
|
||||
# XXX use encoding of original stream
|
||||
os.write(self.targetfd_save, data.encode("utf-8"))
|
||||
|
||||
|
||||
# MultiCapture
|
||||
|
||||
|
||||
# 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`."""
|
||||
# Generic NamedTuple only supported since Python 3.11.
|
||||
if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
||||
|
||||
__slots__ = ("out", "err")
|
||||
@final
|
||||
class CaptureResult(NamedTuple, Generic[AnyStr]):
|
||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
||||
|
||||
def __init__(self, out: AnyStr, err: AnyStr) -> None:
|
||||
self.out: AnyStr = out
|
||||
self.err: AnyStr = err
|
||||
out: AnyStr
|
||||
err: AnyStr
|
||||
|
||||
def __len__(self) -> int:
|
||||
return 2
|
||||
else:
|
||||
|
||||
def __iter__(self) -> Iterator[AnyStr]:
|
||||
return iter((self.out, self.err))
|
||||
class CaptureResult(
|
||||
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
||||
):
|
||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
||||
|
||||
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})"
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class MultiCapture(Generic[AnyStr]):
|
||||
_state = None
|
||||
_in_suspended = False
|
||||
|
||||
def __init__(self, in_, out, err) -> None:
|
||||
self.in_ = in_
|
||||
self.out = out
|
||||
self.err = err
|
||||
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 __repr__(self) -> str:
|
||||
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
|
||||
@@ -578,8 +636,10 @@ class MultiCapture(Generic[AnyStr]):
|
||||
"""Pop current snapshot out/err capture and flush to orig streams."""
|
||||
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
|
||||
|
||||
@@ -600,6 +660,7 @@ 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
|
||||
|
||||
@@ -622,7 +683,8 @@ 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 ""
|
||||
return CaptureResult(out, err)
|
||||
# TODO: This type error is real, need to fix.
|
||||
return CaptureResult(out, err) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
|
||||
@@ -662,7 +724,7 @@ class CaptureManager:
|
||||
"""
|
||||
|
||||
def __init__(self, method: "_CaptureMethod") -> None:
|
||||
self._method = method
|
||||
self._method: Final = method
|
||||
self._global_capturing: Optional[MultiCapture[str]] = None
|
||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||
|
||||
@@ -831,14 +893,18 @@ class CaptureFixture(Generic[AnyStr]):
|
||||
:fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
|
||||
|
||||
def __init__(
|
||||
self, captureclass, request: SubRequest, *, _ispytest: bool = False
|
||||
self,
|
||||
captureclass: Type[CaptureBase[AnyStr]],
|
||||
request: SubRequest,
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self.captureclass = captureclass
|
||||
self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
|
||||
self.request = request
|
||||
self._capture: Optional[MultiCapture[AnyStr]] = None
|
||||
self._captured_out = self.captureclass.EMPTY_BUFFER
|
||||
self._captured_err = self.captureclass.EMPTY_BUFFER
|
||||
self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
|
||||
self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
|
||||
|
||||
def _start(self) -> None:
|
||||
if self._capture is None:
|
||||
@@ -893,7 +959,9 @@ class CaptureFixture(Generic[AnyStr]):
|
||||
@contextlib.contextmanager
|
||||
def disabled(self) -> Generator[None, None, None]:
|
||||
"""Temporarily disable capturing while inside the ``with`` block."""
|
||||
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
|
||||
capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
|
||||
"capturemanager"
|
||||
)
|
||||
with capmanager.global_and_fixture_disabled():
|
||||
yield
|
||||
|
||||
@@ -920,8 +988,8 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
|
||||
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
@@ -948,8 +1016,8 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
|
||||
captured = capsysbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
|
||||
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
@@ -976,8 +1044,8 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
captured = capfd.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
|
||||
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
@@ -1005,8 +1073,8 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
|
||||
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Python version compatibility code."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import functools
|
||||
import inspect
|
||||
@@ -11,13 +14,8 @@ 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
|
||||
|
||||
@@ -47,7 +45,7 @@ LEGACY_PATH = py.path. local
|
||||
# fmt: on
|
||||
|
||||
|
||||
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
|
||||
def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
|
||||
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
|
||||
return LEGACY_PATH(path)
|
||||
|
||||
@@ -57,7 +55,7 @@ def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
|
||||
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||
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):
|
||||
@@ -95,7 +93,7 @@ def is_async_function(func: object) -> bool:
|
||||
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
||||
|
||||
|
||||
def getlocation(function, curdir: Optional[str] = None) -> str:
|
||||
def getlocation(function, curdir: str | None = None) -> str:
|
||||
function = get_real_func(function)
|
||||
fn = Path(inspect.getfile(function))
|
||||
lineno = function.__code__.co_firstlineno
|
||||
@@ -133,8 +131,8 @@ def getfuncargnames(
|
||||
*,
|
||||
name: str = "",
|
||||
is_method: bool = False,
|
||||
cls: Optional[type] = None,
|
||||
) -> Tuple[str, ...]:
|
||||
cls: type | None = None,
|
||||
) -> tuple[str, ...]:
|
||||
"""Return the names of a function's mandatory arguments.
|
||||
|
||||
Should return the names of all function arguments that:
|
||||
@@ -198,7 +196,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.
|
||||
@@ -229,7 +227,7 @@ def _bytes_to_ascii(val: bytes) -> str:
|
||||
return val.decode("ascii", "backslashreplace")
|
||||
|
||||
|
||||
def ascii_escaped(val: Union[bytes, str]) -> str:
|
||||
def ascii_escaped(val: bytes | str) -> str:
|
||||
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
||||
bytes objects into a sequence of escaped bytes:
|
||||
|
||||
@@ -253,7 +251,7 @@ def ascii_escaped(val: Union[bytes, str]) -> str:
|
||||
return _translate_non_printable(ret)
|
||||
|
||||
|
||||
@attr.s
|
||||
@dataclasses.dataclass
|
||||
class _PytestWrapper:
|
||||
"""Dummy wrapper around a function object for internal use only.
|
||||
|
||||
@@ -262,7 +260,7 @@ class _PytestWrapper:
|
||||
decorator to issue warnings when the fixture function is called directly.
|
||||
"""
|
||||
|
||||
obj = attr.ib()
|
||||
obj: Any
|
||||
|
||||
|
||||
def get_real_func(obj):
|
||||
@@ -356,7 +354,6 @@ else:
|
||||
if sys.version_info >= (3, 8):
|
||||
from functools import cached_property as cached_property
|
||||
else:
|
||||
from typing import Type
|
||||
|
||||
class cached_property(Generic[_S, _T]):
|
||||
__slots__ = ("func", "__doc__")
|
||||
@@ -367,12 +364,12 @@ else:
|
||||
|
||||
@overload
|
||||
def __get__(
|
||||
self, instance: None, owner: Optional[Type[_S]] = ...
|
||||
) -> "cached_property[_S, _T]":
|
||||
self, instance: None, owner: type[_S] | None = ...
|
||||
) -> cached_property[_S, _T]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T:
|
||||
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
|
||||
...
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
@@ -382,6 +379,18 @@ 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:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import argparse
|
||||
import collections.abc
|
||||
import copy
|
||||
import dataclasses
|
||||
import enum
|
||||
import glob
|
||||
import inspect
|
||||
@@ -34,7 +35,6 @@ 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
|
||||
@@ -62,7 +62,6 @@ 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
|
||||
@@ -697,6 +696,7 @@ class PytestPluginManager(PluginManager):
|
||||
parg = opt[2:]
|
||||
else:
|
||||
continue
|
||||
parg = parg.strip()
|
||||
if exclude_only and not parg.startswith("no:"):
|
||||
continue
|
||||
self.consider_pluginarg(parg)
|
||||
@@ -886,10 +886,6 @@ 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.
|
||||
@@ -903,7 +899,7 @@ class Config:
|
||||
"""
|
||||
|
||||
@final
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class InvocationParams:
|
||||
"""Holds parameters passed during :func:`pytest.main`.
|
||||
|
||||
@@ -919,13 +915,24 @@ class Config:
|
||||
Plugins accessing ``InvocationParams`` must be aware of that.
|
||||
"""
|
||||
|
||||
args: Tuple[str, ...] = attr.ib(converter=_args_converter)
|
||||
args: Tuple[str, ...]
|
||||
"""The command-line arguments as passed to :func:`pytest.main`."""
|
||||
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.
|
||||
|
||||
@@ -998,6 +1005,8 @@ 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
|
||||
@@ -1057,7 +1066,6 @@ 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.
|
||||
@@ -1337,8 +1345,8 @@ class Config:
|
||||
|
||||
def parse(self, args: List[str], addopts: bool = True) -> None:
|
||||
# Parse given cmdline arguments into this config object.
|
||||
assert not hasattr(
|
||||
self, "args"
|
||||
assert (
|
||||
self.args == []
|
||||
), "can only parse cmdline args at most once per Config object"
|
||||
self.hook.pytest_addhooks.call_historic(
|
||||
kwargs=dict(pluginmanager=self.pluginmanager)
|
||||
|
||||
@@ -43,7 +43,6 @@ 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:
|
||||
|
||||
@@ -531,7 +531,6 @@ 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
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import dataclasses
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
@@ -28,8 +29,6 @@ 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
|
||||
@@ -103,7 +102,7 @@ _FixtureCachedResult = Union[
|
||||
]
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class PseudoFixtureDef(Generic[FixtureValue]):
|
||||
cached_result: "_FixtureCachedResult[FixtureValue]"
|
||||
_scope: Scope
|
||||
@@ -350,8 +349,10 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
|
||||
return request.param
|
||||
|
||||
|
||||
@attr.s(slots=True, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class FuncFixtureInfo:
|
||||
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
|
||||
|
||||
# Original function argument names.
|
||||
argnames: Tuple[str, ...]
|
||||
# Argnames that function immediately requires. These include argnames +
|
||||
@@ -1181,19 +1182,21 @@ def wrap_function_to_error_out_if_called_directly(
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class FixtureFunctionMarker:
|
||||
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
|
||||
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
|
||||
params: Optional[Tuple[object, ...]]
|
||||
autouse: bool = False
|
||||
ids: Optional[
|
||||
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
|
||||
] = attr.ib(
|
||||
default=None,
|
||||
converter=_ensure_immutable_ids,
|
||||
)
|
||||
] = None
|
||||
name: Optional[str] = None
|
||||
|
||||
_ispytest: dataclasses.InitVar[bool] = False
|
||||
|
||||
def __post_init__(self, _ispytest: bool) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
|
||||
def __call__(self, function: FixtureFunction) -> FixtureFunction:
|
||||
if inspect.isclass(function):
|
||||
raise ValueError("class fixtures not supported (maybe in the future)")
|
||||
@@ -1313,10 +1316,11 @@ def fixture( # noqa: F811
|
||||
"""
|
||||
fixture_marker = FixtureFunctionMarker(
|
||||
scope=scope,
|
||||
params=params,
|
||||
params=tuple(params) if params is not None else None,
|
||||
autouse=autouse,
|
||||
ids=ids,
|
||||
ids=None if ids is None else ids if callable(ids) else tuple(ids),
|
||||
name=name,
|
||||
_ispytest=True,
|
||||
)
|
||||
|
||||
# Direct decoration.
|
||||
|
||||
@@ -164,7 +164,8 @@ 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 file found:"
|
||||
"[pytest] ini-options in the first "
|
||||
"pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:"
|
||||
)
|
||||
tw.line()
|
||||
|
||||
|
||||
@@ -505,7 +505,9 @@ 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)``.
|
||||
:param location: A tuple of ``(filename, lineno, testname)``
|
||||
where ``filename`` is a file path relative to ``config.rootpath``
|
||||
and ``lineno`` is 0-based.
|
||||
"""
|
||||
|
||||
|
||||
@@ -517,7 +519,9 @@ 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)``.
|
||||
:param location: A tuple of ``(filename, lineno, testname)``
|
||||
where ``filename`` is a file path relative to ``config.rootpath``
|
||||
and ``lineno`` is 0-based.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -645,8 +645,8 @@ class LogXML:
|
||||
|
||||
def pytest_sessionfinish(self) -> None:
|
||||
dirname = os.path.dirname(os.path.abspath(self.logfile))
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
# exist_ok avoids filesystem race conditions between checking path existence and requesting creation
|
||||
os.makedirs(dirname, exist_ok=True)
|
||||
|
||||
with open(self.logfile, "w", encoding="utf-8") as logfile:
|
||||
suite_stop_time = timing.time()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Add backward compatibility support for the legacy py path type."""
|
||||
import dataclasses
|
||||
import shlex
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
@@ -7,7 +8,6 @@ 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,7 +268,7 @@ class LegacyTestdirPlugin:
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(init=False, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class TempdirFactory:
|
||||
"""Backward compatibility wrapper that implements :class:`py.path.local`
|
||||
for :class:`TempPathFactory`.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Core implementation of the testing process: init, session, runtest loop."""
|
||||
import argparse
|
||||
import dataclasses
|
||||
import fnmatch
|
||||
import functools
|
||||
import importlib
|
||||
@@ -19,8 +20,6 @@ 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
|
||||
@@ -442,8 +441,10 @@ class Failed(Exception):
|
||||
"""Signals a stop as failed test run."""
|
||||
|
||||
|
||||
@attr.s(slots=True, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class _bestrelpath_cache(Dict[Path, str]):
|
||||
__slots__ = ("path",)
|
||||
|
||||
path: Path
|
||||
|
||||
def __missing__(self, path: Path) -> str:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Generic mechanism for marking and selecting python functions."""
|
||||
import dataclasses
|
||||
from typing import AbstractSet
|
||||
from typing import Collection
|
||||
from typing import List
|
||||
@@ -6,8 +7,6 @@ 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
|
||||
@@ -130,7 +129,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
return None
|
||||
|
||||
|
||||
@attr.s(slots=True, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class KeywordMatcher:
|
||||
"""A matcher for keywords.
|
||||
|
||||
@@ -145,6 +144,8 @@ class KeywordMatcher:
|
||||
any item, as well as names directly assigned to test functions.
|
||||
"""
|
||||
|
||||
__slots__ = ("_names",)
|
||||
|
||||
_names: AbstractSet[str]
|
||||
|
||||
@classmethod
|
||||
@@ -201,13 +202,15 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
|
||||
items[:] = remaining
|
||||
|
||||
|
||||
@attr.s(slots=True, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
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
|
||||
|
||||
@@ -15,6 +15,7 @@ The semantics are:
|
||||
- or/and/not evaluate according to the usual boolean semantics.
|
||||
"""
|
||||
import ast
|
||||
import dataclasses
|
||||
import enum
|
||||
import re
|
||||
import types
|
||||
@@ -25,8 +26,6 @@ from typing import NoReturn
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
|
||||
import attr
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Expression",
|
||||
@@ -44,8 +43,9 @@ class TokenType(enum.Enum):
|
||||
EOF = "end of input"
|
||||
|
||||
|
||||
@attr.s(frozen=True, slots=True, auto_attribs=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Token:
|
||||
__slots__ = ("type", "value", "pos")
|
||||
type: TokenType
|
||||
value: str
|
||||
pos: int
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import collections.abc
|
||||
import dataclasses
|
||||
import inspect
|
||||
import warnings
|
||||
from typing import Any
|
||||
@@ -20,8 +21,6 @@ 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
|
||||
@@ -191,8 +190,10 @@ class ParameterSet(NamedTuple):
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(frozen=True, init=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Mark:
|
||||
"""A pytest mark."""
|
||||
|
||||
#: Name of the mark.
|
||||
name: str
|
||||
#: Positional arguments of the mark decorator.
|
||||
@@ -201,9 +202,11 @@ class Mark:
|
||||
kwargs: Mapping[str, Any]
|
||||
|
||||
#: Source Mark for ids with parametrize Marks.
|
||||
_param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False)
|
||||
_param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False)
|
||||
#: Resolved/generated ids with parametrize Marks.
|
||||
_param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)
|
||||
_param_ids_generated: Optional[Sequence[str]] = dataclasses.field(
|
||||
default=None, repr=False
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -261,7 +264,7 @@ class Mark:
|
||||
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
|
||||
|
||||
|
||||
@attr.s(init=False, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class MarkDecorator:
|
||||
"""A decorator for applying a mark on test functions and classes.
|
||||
|
||||
|
||||
@@ -511,7 +511,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 line number.
|
||||
:rtype: A tuple of (str|Path, int) with filename and 0-based line number.
|
||||
"""
|
||||
# See Item.location.
|
||||
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
|
||||
@@ -755,7 +755,7 @@ class Item(Node):
|
||||
Returns a tuple with three elements:
|
||||
|
||||
- The path of the test (default ``self.path``)
|
||||
- The line number of the test (default ``None``)
|
||||
- The 0-based line number of the test (default ``None``)
|
||||
- A name of the test to be shown (default ``""``)
|
||||
|
||||
.. seealso:: :ref:`non-python tests`
|
||||
@@ -764,6 +764,11 @@ 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)
|
||||
|
||||
@@ -157,8 +157,12 @@ def skip(
|
||||
The message to show the user as reason for the skip.
|
||||
|
||||
:param allow_module_level:
|
||||
Allows this function to be called at module level, skipping the rest
|
||||
of the module. Defaults to False.
|
||||
Allows this function to be called at module level.
|
||||
Raising the skip exception at module level will stop
|
||||
the execution of the module and prevent the collection of all tests in the module,
|
||||
even those defined before the `skip` call.
|
||||
|
||||
Defaults to False.
|
||||
|
||||
:param msg:
|
||||
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
|
||||
@@ -219,7 +223,6 @@ def _resolve_msg_to_reason(
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if msg is not None:
|
||||
|
||||
if reason:
|
||||
from pytest import UsageError
|
||||
|
||||
|
||||
@@ -477,14 +477,14 @@ def import_path(
|
||||
|
||||
* `mode == ImportMode.prepend`: the directory containing the module (or package, taking
|
||||
`__init__.py` files into account) will be put at the *start* of `sys.path` before
|
||||
being imported with `__import__.
|
||||
being imported with `importlib.import_module`.
|
||||
|
||||
* `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
|
||||
to the end of `sys.path`, if not already in `sys.path`.
|
||||
|
||||
* `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib`
|
||||
to import the module, which avoids having to use `__import__` and muck with `sys.path`
|
||||
at all. It effectively allows having same-named test modules in different places.
|
||||
to import the module, which avoids having to muck with `sys.path` at all. It effectively
|
||||
allows having same-named test modules in different places.
|
||||
|
||||
:param root:
|
||||
Used as an anchor when mode == ImportMode.importlib to obtain
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Python test discovery, setup and run of test functions."""
|
||||
import dataclasses
|
||||
import enum
|
||||
import fnmatch
|
||||
import inspect
|
||||
@@ -27,8 +28,6 @@ from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
|
||||
import _pytest
|
||||
from _pytest import fixtures
|
||||
from _pytest import nodes
|
||||
@@ -790,7 +789,8 @@ def _call_with_optional_argument(func, arg) -> None:
|
||||
|
||||
def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]:
|
||||
"""Return the attribute from the given object to be used as a setup/teardown
|
||||
xunit-style function, but only if not marked as a fixture to avoid calling it twice."""
|
||||
xunit-style function, but only if not marked as a fixture to avoid calling it twice.
|
||||
"""
|
||||
for name in names:
|
||||
meth: Optional[object] = getattr(obj, name, None)
|
||||
if meth is not None and fixtures.getfixturemarker(meth) is None:
|
||||
@@ -848,7 +848,7 @@ class Class(PyCollector):
|
||||
other fixtures (#517).
|
||||
"""
|
||||
setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
|
||||
teardown_class = getattr(self.obj, "teardown_class", None)
|
||||
teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",))
|
||||
if setup_class is None and teardown_class is None:
|
||||
return
|
||||
|
||||
@@ -885,12 +885,12 @@ class Class(PyCollector):
|
||||
emit_nose_setup_warning = True
|
||||
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
|
||||
teardown_name = "teardown_method"
|
||||
teardown_method = getattr(self.obj, teardown_name, None)
|
||||
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
|
||||
emit_nose_teardown_warning = False
|
||||
if teardown_method is None and has_nose:
|
||||
teardown_name = "teardown"
|
||||
emit_nose_teardown_warning = True
|
||||
teardown_method = getattr(self.obj, teardown_name, None)
|
||||
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
|
||||
if setup_method is None and teardown_method is None:
|
||||
return
|
||||
|
||||
@@ -956,10 +956,20 @@ def hasnew(obj: object) -> bool:
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(frozen=True, auto_attribs=True, slots=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class IdMaker:
|
||||
"""Make IDs for a parametrization."""
|
||||
|
||||
__slots__ = (
|
||||
"argnames",
|
||||
"parametersets",
|
||||
"idfn",
|
||||
"ids",
|
||||
"config",
|
||||
"nodeid",
|
||||
"func_name",
|
||||
)
|
||||
|
||||
# The argnames of the parametrization.
|
||||
argnames: Sequence[str]
|
||||
# The ParameterSets of the parametrization.
|
||||
@@ -1109,7 +1119,7 @@ class IdMaker:
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(frozen=True, slots=True, auto_attribs=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class CallSpec2:
|
||||
"""A planned parameterized invocation of a test function.
|
||||
|
||||
@@ -1120,18 +1130,18 @@ class CallSpec2:
|
||||
|
||||
# arg name -> arg value which will be passed to the parametrized test
|
||||
# function (direct parameterization).
|
||||
funcargs: Dict[str, object] = attr.Factory(dict)
|
||||
funcargs: Dict[str, object] = dataclasses.field(default_factory=dict)
|
||||
# arg name -> arg value which will be passed to a fixture of the same name
|
||||
# (indirect parametrization).
|
||||
params: Dict[str, object] = attr.Factory(dict)
|
||||
params: Dict[str, object] = dataclasses.field(default_factory=dict)
|
||||
# arg name -> arg index.
|
||||
indices: Dict[str, int] = attr.Factory(dict)
|
||||
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
|
||||
# Used for sorting parametrized resources.
|
||||
_arg2scope: Dict[str, Scope] = attr.Factory(dict)
|
||||
_arg2scope: Dict[str, Scope] = dataclasses.field(default_factory=dict)
|
||||
# Parts which will be added to the item's name in `[..]` separated by "-".
|
||||
_idlist: List[str] = attr.Factory(list)
|
||||
_idlist: List[str] = dataclasses.field(default_factory=list)
|
||||
# Marks which will be applied to the item.
|
||||
marks: List[Mark] = attr.Factory(list)
|
||||
marks: List[Mark] = dataclasses.field(default_factory=list)
|
||||
|
||||
def setmulti(
|
||||
self,
|
||||
@@ -1163,9 +1173,9 @@ class CallSpec2:
|
||||
return CallSpec2(
|
||||
funcargs=funcargs,
|
||||
params=params,
|
||||
arg2scope=arg2scope,
|
||||
indices=indices,
|
||||
idlist=[*self._idlist, id],
|
||||
_arg2scope=arg2scope,
|
||||
_idlist=[*self._idlist, id],
|
||||
marks=[*self.marks, *normalize_mark_list(marks)],
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Generic
|
||||
from typing import ContextManager
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
@@ -269,10 +269,16 @@ class ApproxMapping(ApproxBase):
|
||||
max_abs_diff = max(
|
||||
max_abs_diff, abs(approx_value.expected - other_value)
|
||||
)
|
||||
max_rel_diff = max(
|
||||
max_rel_diff,
|
||||
abs((approx_value.expected - other_value) / approx_value.expected),
|
||||
)
|
||||
if approx_value.expected == 0.0:
|
||||
max_rel_diff = math.inf
|
||||
else:
|
||||
max_rel_diff = max(
|
||||
max_rel_diff,
|
||||
abs(
|
||||
(approx_value.expected - other_value)
|
||||
/ approx_value.expected
|
||||
),
|
||||
)
|
||||
different_ids.append(approx_key)
|
||||
|
||||
message_data = [
|
||||
@@ -802,7 +808,7 @@ def raises( # noqa: F811
|
||||
|
||||
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
|
||||
The expected exception type, or a tuple if one of multiple possible
|
||||
exception types are excepted.
|
||||
exception types are expected.
|
||||
:kwparam str | typing.Pattern[str] | None match:
|
||||
If specified, a string containing a regular expression,
|
||||
or a regular expression object, that is tested against the string
|
||||
@@ -918,10 +924,10 @@ def raises( # noqa: F811
|
||||
f"any special code to say 'this should never raise an exception'."
|
||||
)
|
||||
if isinstance(expected_exception, type):
|
||||
excepted_exceptions: Tuple[Type[E], ...] = (expected_exception,)
|
||||
expected_exceptions: Tuple[Type[E], ...] = (expected_exception,)
|
||||
else:
|
||||
excepted_exceptions = expected_exception
|
||||
for exc in excepted_exceptions:
|
||||
expected_exceptions = expected_exception
|
||||
for exc in expected_exceptions:
|
||||
if not isinstance(exc, type) or not issubclass(exc, BaseException):
|
||||
msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
|
||||
not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
|
||||
@@ -957,7 +963,7 @@ raises.Exception = fail.Exception # type: ignore
|
||||
|
||||
|
||||
@final
|
||||
class RaisesContext(Generic[E]):
|
||||
class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
|
||||
def __init__(
|
||||
self,
|
||||
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import dataclasses
|
||||
import os
|
||||
from io import StringIO
|
||||
from pprint import pprint
|
||||
@@ -16,8 +17,6 @@ from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
|
||||
from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import ExceptionRepr
|
||||
@@ -263,6 +262,8 @@ class TestReport(BaseReport):
|
||||
when: "Literal['setup', 'call', 'teardown']",
|
||||
sections: Iterable[Tuple[str, str]] = (),
|
||||
duration: float = 0,
|
||||
start: float = 0,
|
||||
stop: float = 0,
|
||||
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
|
||||
**extra,
|
||||
) -> None:
|
||||
@@ -272,6 +273,8 @@ class TestReport(BaseReport):
|
||||
#: A (filesystempath, lineno, domaininfo) tuple indicating the
|
||||
#: actual location of a test item - it might be different from the
|
||||
#: collected one e.g. if a method is inherited from a different module.
|
||||
#: The filesystempath may be relative to ``config.rootdir``.
|
||||
#: The line number is 0-based.
|
||||
self.location: Tuple[str, Optional[int], str] = location
|
||||
|
||||
#: A name -> value dictionary containing all keywords and
|
||||
@@ -300,6 +303,11 @@ class TestReport(BaseReport):
|
||||
#: Time it took to run just the test.
|
||||
self.duration: float = duration
|
||||
|
||||
#: The system time when the call started, in seconds since the epoch.
|
||||
self.start: float = start
|
||||
#: The system time when the call ended, in seconds since the epoch.
|
||||
self.stop: float = stop
|
||||
|
||||
self.__dict__.update(extra)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -318,6 +326,8 @@ class TestReport(BaseReport):
|
||||
# Remove "collect" from the Literal type -- only for collection calls.
|
||||
assert when != "collect"
|
||||
duration = call.duration
|
||||
start = call.start
|
||||
stop = call.stop
|
||||
keywords = {x: 1 for x in item.keywords}
|
||||
excinfo = call.excinfo
|
||||
sections = []
|
||||
@@ -337,6 +347,10 @@ class TestReport(BaseReport):
|
||||
elif isinstance(excinfo.value, skip.Exception):
|
||||
outcome = "skipped"
|
||||
r = excinfo._getreprcrash()
|
||||
if r is None:
|
||||
raise ValueError(
|
||||
"There should always be a traceback entry for skipping a test."
|
||||
)
|
||||
if excinfo.value._use_item_location:
|
||||
path, line = item.reportinfo()[:2]
|
||||
assert line is not None
|
||||
@@ -362,6 +376,8 @@ class TestReport(BaseReport):
|
||||
when,
|
||||
sections,
|
||||
duration,
|
||||
start,
|
||||
stop,
|
||||
user_properties=item.user_properties,
|
||||
)
|
||||
|
||||
@@ -407,7 +423,9 @@ class CollectReport(BaseReport):
|
||||
self.__dict__.update(extra)
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
def location( # type:ignore[override]
|
||||
self,
|
||||
) -> Optional[Tuple[str, Optional[int], str]]:
|
||||
return (self.fspath, None, self.fspath)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
@@ -459,15 +477,15 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
|
||||
def serialize_repr_entry(
|
||||
entry: Union[ReprEntry, ReprEntryNative]
|
||||
) -> Dict[str, Any]:
|
||||
data = attr.asdict(entry)
|
||||
data = dataclasses.asdict(entry)
|
||||
for key, value in data.items():
|
||||
if hasattr(value, "__dict__"):
|
||||
data[key] = attr.asdict(value)
|
||||
data[key] = dataclasses.asdict(value)
|
||||
entry_data = {"type": type(entry).__name__, "data": data}
|
||||
return entry_data
|
||||
|
||||
def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
|
||||
result = attr.asdict(reprtraceback)
|
||||
result = dataclasses.asdict(reprtraceback)
|
||||
result["reprentries"] = [
|
||||
serialize_repr_entry(x) for x in reprtraceback.reprentries
|
||||
]
|
||||
@@ -477,7 +495,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
|
||||
reprcrash: Optional[ReprFileLocation],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if reprcrash is not None:
|
||||
return attr.asdict(reprcrash)
|
||||
return dataclasses.asdict(reprcrash)
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -573,7 +591,6 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
and "reprcrash" in reportdict["longrepr"]
|
||||
and "reprtraceback" in reportdict["longrepr"]
|
||||
):
|
||||
|
||||
reprtraceback = deserialize_repr_traceback(
|
||||
reportdict["longrepr"]["reprtraceback"]
|
||||
)
|
||||
@@ -594,7 +611,10 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
|
||||
ExceptionChainRepr, ReprExceptionInfo
|
||||
] = ExceptionChainRepr(chain)
|
||||
else:
|
||||
exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
|
||||
exception_info = ReprExceptionInfo(
|
||||
reprtraceback=reprtraceback,
|
||||
reprcrash=reprcrash,
|
||||
)
|
||||
|
||||
for section in reportdict["longrepr"]["sections"]:
|
||||
exception_info.addsection(*section)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Basic collect and runtest protocol implementations."""
|
||||
import bdb
|
||||
import dataclasses
|
||||
import os
|
||||
import sys
|
||||
from typing import Callable
|
||||
@@ -14,8 +15,6 @@ from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
|
||||
from .reports import BaseReport
|
||||
from .reports import CollectErrorRepr
|
||||
from .reports import CollectReport
|
||||
@@ -268,7 +267,7 @@ TResult = TypeVar("TResult", covariant=True)
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(repr=False, init=False, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class CallInfo(Generic[TResult]):
|
||||
"""Result/Exception info of a function invocation."""
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Support for skip/xfail functions and markers."""
|
||||
import dataclasses
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
@@ -9,8 +10,6 @@ from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
|
||||
import attr
|
||||
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
@@ -157,7 +156,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
|
||||
return result, reason
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Skip:
|
||||
"""The result of evaluate_skip_marks()."""
|
||||
|
||||
@@ -192,10 +191,12 @@ def evaluate_skip_marks(item: Item) -> Optional[Skip]:
|
||||
return None
|
||||
|
||||
|
||||
@attr.s(slots=True, frozen=True, auto_attribs=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class Xfail:
|
||||
"""The result of evaluate_xfail_marks()."""
|
||||
|
||||
__slots__ = ("reason", "run", "strict", "raises")
|
||||
|
||||
reason: str
|
||||
run: bool
|
||||
strict: bool
|
||||
|
||||
@@ -48,6 +48,10 @@ def pytest_configure(config: Config) -> None:
|
||||
def pytest_sessionfinish(session: Session) -> None:
|
||||
if not session.config.getoption("stepwise"):
|
||||
assert session.config.cache is not None
|
||||
if hasattr(session.config, "workerinput"):
|
||||
# Do not update cache if this process is a xdist worker to prevent
|
||||
# race conditions (#10641).
|
||||
return
|
||||
# Clear the list of failing tests if the plugin is not active.
|
||||
session.config.cache.set(STEPWISE_CACHE_DIR, [])
|
||||
|
||||
@@ -119,4 +123,8 @@ class StepwisePlugin:
|
||||
return None
|
||||
|
||||
def pytest_sessionfinish(self) -> None:
|
||||
if hasattr(self.config, "workerinput"):
|
||||
# Do not update cache if this process is a xdist worker to prevent
|
||||
# race conditions (#10641).
|
||||
return
|
||||
self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
This is a good source for looking at the various reporting hooks.
|
||||
"""
|
||||
import argparse
|
||||
import dataclasses
|
||||
import datetime
|
||||
import inspect
|
||||
import platform
|
||||
@@ -27,7 +28,6 @@ from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
import pluggy
|
||||
|
||||
import _pytest._version
|
||||
@@ -229,7 +229,8 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
parser.addini(
|
||||
"console_output_style",
|
||||
help='Console output: "classic", or with additional progress information '
|
||||
'("progress" (percentage) | "count")',
|
||||
'("progress" (percentage) | "count" | "progress-even-when-capture-no" (forces '
|
||||
"progress even when capture=no)",
|
||||
default="progress",
|
||||
)
|
||||
|
||||
@@ -287,7 +288,7 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
|
||||
return outcome, letter, outcome.upper()
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class WarningReport:
|
||||
"""Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
|
||||
|
||||
@@ -346,14 +347,19 @@ class TerminalReporter:
|
||||
|
||||
def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
|
||||
"""Return whether we should display progress information based on the current config."""
|
||||
# do not show progress if we are not capturing output (#3038)
|
||||
if self.config.getoption("capture", "no") == "no":
|
||||
# do not show progress if we are not capturing output (#3038) unless explicitly
|
||||
# overridden by progress-even-when-capture-no
|
||||
if (
|
||||
self.config.getoption("capture", "no") == "no"
|
||||
and self.config.getini("console_output_style")
|
||||
!= "progress-even-when-capture-no"
|
||||
):
|
||||
return False
|
||||
# do not show progress if we are showing fixture setup/teardown
|
||||
if self.config.getoption("setupshow", False):
|
||||
return False
|
||||
cfg: str = self.config.getini("console_output_style")
|
||||
if cfg == "progress":
|
||||
if cfg == "progress" or cfg == "progress-even-when-capture-no":
|
||||
return "progress"
|
||||
elif cfg == "count":
|
||||
return "count"
|
||||
@@ -733,16 +739,14 @@ class TerminalReporter:
|
||||
self.write_line(line)
|
||||
|
||||
def pytest_report_header(self, config: Config) -> List[str]:
|
||||
line = "rootdir: %s" % config.rootpath
|
||||
result = [f"rootdir: {config.rootpath}"]
|
||||
|
||||
if config.inipath:
|
||||
line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
|
||||
result.append("configfile: " + bestrelpath(config.rootpath, config.inipath))
|
||||
|
||||
if config.args_source == Config.ArgsSource.TESTPATHS:
|
||||
testpaths: List[str] = config.getini("testpaths")
|
||||
line += ", testpaths: {}".format(", ".join(testpaths))
|
||||
|
||||
result = [line]
|
||||
result.append("testpaths: {}".format(", ".join(testpaths)))
|
||||
|
||||
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
||||
if plugininfo:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Support for providing temporary directories to test functions."""
|
||||
import dataclasses
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import Optional
|
||||
@@ -21,7 +22,6 @@ if TYPE_CHECKING:
|
||||
RetentionType = Literal["all", "failed", "none"]
|
||||
|
||||
|
||||
import attr
|
||||
from _pytest.config.argparsing import Parser
|
||||
|
||||
from .pathlib import LOCK_TIMEOUT
|
||||
@@ -29,7 +29,7 @@ from .pathlib import make_numbered_dir
|
||||
from .pathlib import make_numbered_dir_with_cleanup
|
||||
from .pathlib import rm_rf
|
||||
from .pathlib import cleanup_dead_symlink
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import final, get_user_id
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
@@ -42,18 +42,19 @@ tmppath_result_key = StashKey[Dict[str, bool]]()
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(init=False)
|
||||
@dataclasses.dataclass
|
||||
class TempPathFactory:
|
||||
"""Factory for temporary directories under the common base temp directory.
|
||||
|
||||
The base directory can be configured using the ``--basetemp`` option.
|
||||
"""
|
||||
|
||||
_given_basetemp = attr.ib(type=Optional[Path])
|
||||
_trace = attr.ib()
|
||||
_basetemp = attr.ib(type=Optional[Path])
|
||||
_retention_count = attr.ib(type=int)
|
||||
_retention_policy = attr.ib(type="RetentionType")
|
||||
_given_basetemp: Optional[Path]
|
||||
# pluggy TagTracerSub, not currently exposed, so Any.
|
||||
_trace: Any
|
||||
_basetemp: Optional[Path]
|
||||
_retention_count: int
|
||||
_retention_policy: "RetentionType"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -174,19 +175,16 @@ class TempPathFactory:
|
||||
# Also, to keep things private, fixup any world-readable temp
|
||||
# rootdir's permissions. Historically 0o755 was used, so we can't
|
||||
# just error out on this, at least for a while.
|
||||
if sys.platform != "win32":
|
||||
uid = os.getuid()
|
||||
uid = get_user_id()
|
||||
if uid is not None:
|
||||
rootdir_stat = rootdir.stat()
|
||||
# getuid shouldn't fail, but cpython defines such a case.
|
||||
# Let's hope for the best.
|
||||
if uid != -1:
|
||||
if rootdir_stat.st_uid != uid:
|
||||
raise OSError(
|
||||
f"The temporary directory {rootdir} is not owned by the current user. "
|
||||
"Fix this and try again."
|
||||
)
|
||||
if (rootdir_stat.st_mode & 0o077) != 0:
|
||||
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
|
||||
if rootdir_stat.st_uid != uid:
|
||||
raise OSError(
|
||||
f"The temporary directory {rootdir} is not owned by the current user. "
|
||||
"Fix this and try again."
|
||||
)
|
||||
if (rootdir_stat.st_mode & 0o077) != 0:
|
||||
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
|
||||
keep = self._retention_count
|
||||
if self._retention_policy == "none":
|
||||
keep = 0
|
||||
@@ -239,7 +237,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"tmp_path_retention_policy",
|
||||
help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. "
|
||||
"(all/failed/none)",
|
||||
default="failed",
|
||||
default="all",
|
||||
)
|
||||
|
||||
|
||||
@@ -267,8 +265,8 @@ def tmp_path(
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and only the base of failed session is kept. Also it only keeps the last 3 bases
|
||||
at most. This can be configured with :confval:`tmp_path_retention_count` and
|
||||
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
|
||||
temporary directory`.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import dataclasses
|
||||
import inspect
|
||||
import warnings
|
||||
from types import FunctionType
|
||||
@@ -6,8 +7,6 @@ from typing import Generic
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
|
||||
import attr
|
||||
|
||||
from _pytest.compat import final
|
||||
|
||||
|
||||
@@ -130,7 +129,7 @@ _W = TypeVar("_W", bound=PytestWarning)
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class UnformattedWarning(Generic[_W]):
|
||||
"""A warning meant to be formatted during runtime.
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import dataclasses
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
import attr
|
||||
|
||||
import pytest
|
||||
from _pytest.compat import importlib_metadata
|
||||
from _pytest.config import ExitCode
|
||||
@@ -115,11 +114,11 @@ class TestGeneralUsage:
|
||||
|
||||
loaded = []
|
||||
|
||||
@attr.s
|
||||
@dataclasses.dataclass
|
||||
class DummyEntryPoint:
|
||||
name = attr.ib()
|
||||
module = attr.ib()
|
||||
group = "pytest11"
|
||||
name: str
|
||||
module: str
|
||||
group: str = "pytest11"
|
||||
|
||||
def load(self):
|
||||
__import__(self.module)
|
||||
@@ -132,10 +131,10 @@ class TestGeneralUsage:
|
||||
DummyEntryPoint("mycov", "mycov_module"),
|
||||
]
|
||||
|
||||
@attr.s
|
||||
@dataclasses.dataclass
|
||||
class DummyDist:
|
||||
entry_points = attr.ib()
|
||||
files = ()
|
||||
entry_points: object
|
||||
files: object = ()
|
||||
|
||||
def my_dists():
|
||||
return (DummyDist(entry_points),)
|
||||
@@ -694,7 +693,14 @@ class TestInvocationVariants:
|
||||
|
||||
# mixed module and filenames:
|
||||
monkeypatch.chdir("world")
|
||||
result = pytester.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world")
|
||||
|
||||
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
|
||||
# While we could change the test to use implicit namespace packages, seems better
|
||||
# to still ensure the old declaration via declare_namespace still works.
|
||||
ignore_w = r"-Wignore:Deprecated call to `pkg_resources.declare_namespace"
|
||||
result = pytester.runpytest(
|
||||
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", ignore_w
|
||||
)
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
@@ -872,7 +878,6 @@ class TestDurations:
|
||||
)
|
||||
|
||||
def test_calls_show_2(self, pytester: Pytester, mock_timing) -> None:
|
||||
|
||||
pytester.makepyfile(self.source)
|
||||
result = pytester.runpytest_inprocess("--durations=2")
|
||||
assert result.ret == 0
|
||||
@@ -1037,14 +1042,14 @@ def test_fixture_values_leak(pytester: Pytester) -> None:
|
||||
"""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import attr
|
||||
import dataclasses
|
||||
import gc
|
||||
import pytest
|
||||
import weakref
|
||||
|
||||
@attr.s
|
||||
class SomeObj(object):
|
||||
name = attr.ib()
|
||||
@dataclasses.dataclass
|
||||
class SomeObj:
|
||||
name: str
|
||||
|
||||
fix_of_test1_ref = None
|
||||
session_ref = None
|
||||
|
||||
@@ -294,6 +294,7 @@ class TestTraceback_f_g_h:
|
||||
excinfo = pytest.raises(ValueError, f)
|
||||
tb = excinfo.traceback
|
||||
entry = tb.getcrashentry()
|
||||
assert entry is not None
|
||||
co = _pytest._code.Code.from_function(h)
|
||||
assert entry.frame.code.path == co.path
|
||||
assert entry.lineno == co.firstlineno + 1
|
||||
@@ -311,10 +312,7 @@ class TestTraceback_f_g_h:
|
||||
excinfo = pytest.raises(ValueError, f)
|
||||
tb = excinfo.traceback
|
||||
entry = tb.getcrashentry()
|
||||
co = _pytest._code.Code.from_function(g)
|
||||
assert entry.frame.code.path == co.path
|
||||
assert entry.lineno == co.firstlineno + 2
|
||||
assert entry.frame.code.name == "g"
|
||||
assert entry is None
|
||||
|
||||
|
||||
def test_excinfo_exconly():
|
||||
@@ -463,6 +461,24 @@ class TestFormattedExcinfo:
|
||||
assert lines[0] == "| def f(x):"
|
||||
assert lines[1] == " pass"
|
||||
|
||||
def test_repr_source_out_of_bounds(self):
|
||||
pr = FormattedExcinfo()
|
||||
source = _pytest._code.Source(
|
||||
"""\
|
||||
def f(x):
|
||||
pass
|
||||
"""
|
||||
).strip()
|
||||
pr.flow_marker = "|" # type: ignore[misc]
|
||||
|
||||
lines = pr.get_source(source, 100)
|
||||
assert len(lines) == 1
|
||||
assert lines[0] == "| ???"
|
||||
|
||||
lines = pr.get_source(source, -100)
|
||||
assert len(lines) == 1
|
||||
assert lines[0] == "| ???"
|
||||
|
||||
def test_repr_source_excinfo(self) -> None:
|
||||
"""Check if indentation is right."""
|
||||
try:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import dataclasses
|
||||
import re
|
||||
import sys
|
||||
from typing import List
|
||||
@@ -157,6 +158,7 @@ def color_mapping():
|
||||
"number": "\x1b[94m",
|
||||
"str": "\x1b[33m",
|
||||
"print": "\x1b[96m",
|
||||
"endline": "\x1b[90m\x1b[39;49;00m",
|
||||
}
|
||||
RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()}
|
||||
|
||||
@@ -191,20 +193,18 @@ def mock_timing(monkeypatch: MonkeyPatch):
|
||||
Time is static, and only advances through `sleep` calls, thus tests might sleep over large
|
||||
numbers and obtain accurate time() calls at the end, making tests reliable and instant.
|
||||
"""
|
||||
import attr
|
||||
|
||||
@attr.s
|
||||
@dataclasses.dataclass
|
||||
class MockTiming:
|
||||
_current_time: float = 1590150050.0
|
||||
|
||||
_current_time = attr.ib(default=1590150050.0)
|
||||
|
||||
def sleep(self, seconds):
|
||||
def sleep(self, seconds: float) -> None:
|
||||
self._current_time += seconds
|
||||
|
||||
def time(self):
|
||||
def time(self) -> float:
|
||||
return self._current_time
|
||||
|
||||
def patch(self):
|
||||
def patch(self) -> None:
|
||||
from _pytest import timing
|
||||
|
||||
monkeypatch.setattr(timing, "sleep", self.sleep)
|
||||
|
||||
@@ -254,7 +254,7 @@ class TestTerminalWriterLineWidth:
|
||||
pytest.param(
|
||||
True,
|
||||
True,
|
||||
"{kw}assert{hl-reset} {number}0{hl-reset}\n",
|
||||
"{kw}assert{hl-reset} {number}0{hl-reset}{endline}\n",
|
||||
id="with markup and code_highlight",
|
||||
),
|
||||
pytest.param(
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
anyio[curio,trio]==3.6.2
|
||||
django==4.1.3
|
||||
pytest-asyncio==0.20.2
|
||||
django==4.1.7
|
||||
pytest-asyncio==0.21.0
|
||||
pytest-bdd==6.1.1
|
||||
pytest-cov==4.0.0
|
||||
pytest-django==4.5.2
|
||||
pytest-flakes==4.0.5
|
||||
pytest-html==3.2.0
|
||||
pytest-mock==3.10.0
|
||||
pytest-rerunfailures==10.3
|
||||
pytest-rerunfailures==11.1.2
|
||||
pytest-sugar==0.9.5
|
||||
pytest-trio==0.7.0
|
||||
pytest-twisted==1.14.0
|
||||
|
||||
@@ -630,6 +630,19 @@ class TestApprox:
|
||||
def test_dict_vs_other(self):
|
||||
assert 1 != approx({"a": 0})
|
||||
|
||||
def test_dict_for_div_by_zero(self, assert_approx_raises_regex):
|
||||
assert_approx_raises_regex(
|
||||
{"foo": 42.0},
|
||||
{"foo": 0.0},
|
||||
[
|
||||
r" comparison failed. Mismatched elements: 1 / 1:",
|
||||
rf" Max absolute difference: {SOME_FLOAT}",
|
||||
r" Max relative difference: inf",
|
||||
r" Index \| Obtained\s+\| Expected ",
|
||||
rf" foo | {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
|
||||
],
|
||||
)
|
||||
|
||||
def test_numpy_array(self):
|
||||
np = pytest.importorskip("numpy")
|
||||
|
||||
|
||||
@@ -3338,6 +3338,10 @@ class TestShowFixtures:
|
||||
config = pytester.parseconfigure("--funcargs")
|
||||
assert config.option.showfixtures
|
||||
|
||||
def test_show_help(self, pytester: Pytester) -> None:
|
||||
result = pytester.runpytest("--fixtures", "--help")
|
||||
assert not result.ret
|
||||
|
||||
def test_show_fixtures(self, pytester: Pytester) -> None:
|
||||
result = pytester.runpytest("--fixtures")
|
||||
result.stdout.fnmatch_lines(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import dataclasses
|
||||
import itertools
|
||||
import re
|
||||
import sys
|
||||
@@ -12,7 +13,6 @@ from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
import hypothesis
|
||||
from hypothesis import strategies
|
||||
|
||||
@@ -39,14 +39,14 @@ class TestMetafunc:
|
||||
def __init__(self, names):
|
||||
self.names_closure = names
|
||||
|
||||
@attr.s
|
||||
@dataclasses.dataclass
|
||||
class DefinitionMock(python.FunctionDefinition):
|
||||
obj = attr.ib()
|
||||
_nodeid = attr.ib()
|
||||
_nodeid: str
|
||||
obj: object
|
||||
|
||||
names = getfuncargnames(func)
|
||||
fixtureinfo: Any = FuncFixtureInfoMock(names)
|
||||
definition: Any = DefinitionMock._create(func, "mock::nodeid")
|
||||
definition: Any = DefinitionMock._create(obj=func, _nodeid="mock::nodeid")
|
||||
return python.Metafunc(definition, fixtureinfo, config, _ispytest=True)
|
||||
|
||||
def test_no_funcargs(self) -> None:
|
||||
@@ -140,9 +140,9 @@ class TestMetafunc:
|
||||
"""Unit test for _find_parametrized_scope (#3941)."""
|
||||
from _pytest.python import _find_parametrized_scope
|
||||
|
||||
@attr.s
|
||||
@dataclasses.dataclass
|
||||
class DummyFixtureDef:
|
||||
_scope = attr.ib()
|
||||
_scope: Scope
|
||||
|
||||
fixtures_defs = cast(
|
||||
Dict[str, Sequence[fixtures.FixtureDef[object]]],
|
||||
|
||||
@@ -1265,6 +1265,177 @@ class TestIssue2121:
|
||||
result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"])
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info < (3, 8), reason="walrus operator not available in py<38"
|
||||
)
|
||||
class TestIssue10743:
|
||||
def test_assertion_walrus_operator(self, pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def my_func(before, after):
|
||||
return before == after
|
||||
|
||||
def change_value(value):
|
||||
return value.lower()
|
||||
|
||||
def test_walrus_conversion():
|
||||
a = "Hello"
|
||||
assert not my_func(a, a := change_value(a))
|
||||
assert a == "hello"
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_walrus_operator_dont_rewrite(self, pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
'PYTEST_DONT_REWRITE'
|
||||
def my_func(before, after):
|
||||
return before == after
|
||||
|
||||
def change_value(value):
|
||||
return value.lower()
|
||||
|
||||
def test_walrus_conversion_dont_rewrite():
|
||||
a = "Hello"
|
||||
assert not my_func(a, a := change_value(a))
|
||||
assert a == "hello"
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_inline_walrus_operator(self, pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def my_func(before, after):
|
||||
return before == after
|
||||
|
||||
def test_walrus_conversion_inline():
|
||||
a = "Hello"
|
||||
assert not my_func(a, a := a.lower())
|
||||
assert a == "hello"
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_inline_walrus_operator_reverse(self, pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def my_func(before, after):
|
||||
return before == after
|
||||
|
||||
def test_walrus_conversion_reverse():
|
||||
a = "Hello"
|
||||
assert my_func(a := a.lower(), a)
|
||||
assert a == 'hello'
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_walrus_no_variable_name_conflict(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_walrus_conversion_no_conflict():
|
||||
a = "Hello"
|
||||
assert a == (b := a.lower())
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(["*AssertionError: assert 'Hello' == 'hello'"])
|
||||
|
||||
def test_assertion_walrus_operator_true_assertion_and_changes_variable_value(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_walrus_conversion_succeed():
|
||||
a = "Hello"
|
||||
assert a != (a := a.lower())
|
||||
assert a == 'hello'
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_walrus_operator_fail_assertion(self, pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_walrus_conversion_fails():
|
||||
a = "Hello"
|
||||
assert a == (a := a.lower())
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(["*AssertionError: assert 'Hello' == 'hello'"])
|
||||
|
||||
def test_assertion_walrus_operator_boolean_composite(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_walrus_operator_change_boolean_value():
|
||||
a = True
|
||||
assert a and True and ((a := False) is False) and (a is False) and ((a := None) is None)
|
||||
assert a is None
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
def test_assertion_walrus_operator_compare_boolean_fails(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_walrus_operator_change_boolean_value():
|
||||
a = True
|
||||
assert not (a and ((a := False) is False))
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(["*assert not (True and False is False)"])
|
||||
|
||||
def test_assertion_walrus_operator_boolean_none_fails(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_walrus_operator_change_boolean_value():
|
||||
a = True
|
||||
assert not (a and ((a := None) is None))
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(["*assert not (True and None is None)"])
|
||||
|
||||
def test_assertion_walrus_operator_value_changes_cleared_after_each_test(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_walrus_operator_change_value():
|
||||
a = True
|
||||
assert (a := None) is None
|
||||
|
||||
def test_walrus_operator_not_override_value():
|
||||
a = True
|
||||
assert a is True
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||
)
|
||||
|
||||
@@ -494,7 +494,6 @@ class TestLastFailed:
|
||||
def test_lastfailed_collectfailure(
|
||||
self, pytester: Pytester, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
pytester.makepyfile(
|
||||
test_maybe="""
|
||||
import os
|
||||
@@ -1249,3 +1248,8 @@ def test_cachedir_tag(pytester: Pytester) -> None:
|
||||
cache.set("foo", "bar")
|
||||
cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG")
|
||||
assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT
|
||||
|
||||
|
||||
def test_clioption_with_cacheshow_and_help(pytester: Pytester) -> None:
|
||||
result = pytester.runpytest("--cache-show", "--help")
|
||||
assert result.ret == 0
|
||||
|
||||
@@ -890,7 +890,7 @@ def test_dontreadfrominput() -> None:
|
||||
from _pytest.capture import DontReadFromInput
|
||||
|
||||
f = DontReadFromInput()
|
||||
assert f.buffer is f
|
||||
assert f.buffer is f # type: ignore[comparison-overlap]
|
||||
assert not f.isatty()
|
||||
pytest.raises(OSError, f.read)
|
||||
pytest.raises(OSError, f.readlines)
|
||||
@@ -906,7 +906,10 @@ def test_dontreadfrominput() -> None:
|
||||
pytest.raises(UnsupportedOperation, f.write, b"")
|
||||
pytest.raises(UnsupportedOperation, f.writelines, [])
|
||||
assert not f.writable()
|
||||
assert isinstance(f.encoding, str)
|
||||
f.close() # just for completeness
|
||||
with f:
|
||||
pass
|
||||
|
||||
|
||||
def test_captureresult() -> None:
|
||||
@@ -1049,6 +1052,7 @@ class TestFDCapture:
|
||||
)
|
||||
)
|
||||
# Should not crash with missing "_old".
|
||||
assert isinstance(cap.syscapture, capture.SysCapture)
|
||||
assert repr(cap.syscapture) == (
|
||||
"<SysCapture stdout _old=<UNSET> _state='done' tmpfile={!r}>".format(
|
||||
cap.syscapture.tmpfile
|
||||
@@ -1349,6 +1353,7 @@ def test_capsys_results_accessible_by_attribute(capsys: CaptureFixture[str]) ->
|
||||
|
||||
def test_fdcapture_tmpfile_remains_the_same() -> None:
|
||||
cap = StdCaptureFD(out=False, err=True)
|
||||
assert isinstance(cap.err, capture.FDCapture)
|
||||
try:
|
||||
cap.start_capturing()
|
||||
capfile = cap.err.tmpfile
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import dataclasses
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
@@ -10,8 +11,6 @@ from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
|
||||
import _pytest._code
|
||||
import pytest
|
||||
from _pytest.compat import importlib_metadata
|
||||
@@ -75,7 +74,7 @@ class TestParseIni:
|
||||
% p1.name,
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["*, configfile: setup.cfg, *", "* 1 passed in *"])
|
||||
result.stdout.fnmatch_lines(["configfile: setup.cfg", "* 1 passed in *"])
|
||||
assert result.ret == 0
|
||||
|
||||
def test_append_parse_args(
|
||||
@@ -423,11 +422,11 @@ class TestParseIni:
|
||||
This test installs a mock "myplugin-1.5" which is used in the parametrized test cases.
|
||||
"""
|
||||
|
||||
@attr.s
|
||||
@dataclasses.dataclass
|
||||
class DummyEntryPoint:
|
||||
name = attr.ib()
|
||||
module = attr.ib()
|
||||
group = "pytest11"
|
||||
name: str
|
||||
module: str
|
||||
group: str = "pytest11"
|
||||
|
||||
def load(self):
|
||||
__import__(self.module)
|
||||
@@ -437,11 +436,11 @@ class TestParseIni:
|
||||
DummyEntryPoint("myplugin1", "myplugin1_module"),
|
||||
]
|
||||
|
||||
@attr.s
|
||||
@dataclasses.dataclass
|
||||
class DummyDist:
|
||||
entry_points = attr.ib()
|
||||
files = ()
|
||||
version = plugin_version
|
||||
entry_points: object
|
||||
files: object = ()
|
||||
version: str = plugin_version
|
||||
|
||||
@property
|
||||
def metadata(self):
|
||||
@@ -1809,6 +1808,10 @@ def test_config_does_not_load_blocked_plugin_from_args(pytester: Pytester) -> No
|
||||
result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"])
|
||||
assert result.ret == ExitCode.USAGE_ERROR
|
||||
|
||||
result = pytester.runpytest(str(p), "-p no:capture", "-s")
|
||||
result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"])
|
||||
assert result.ret == ExitCode.USAGE_ERROR
|
||||
|
||||
|
||||
def test_invocation_args(pytester: Pytester) -> None:
|
||||
"""Ensure that Config.invocation_* arguments are correctly defined"""
|
||||
|
||||
@@ -1236,7 +1236,6 @@ class TestDoctestSkips:
|
||||
|
||||
|
||||
class TestDoctestAutoUseFixtures:
|
||||
|
||||
SCOPES = ["module", "session", "class", "function"]
|
||||
|
||||
def test_doctest_module_session_fixture(self, pytester: Pytester):
|
||||
@@ -1379,7 +1378,6 @@ class TestDoctestAutoUseFixtures:
|
||||
|
||||
|
||||
class TestDoctestNamespaceFixture:
|
||||
|
||||
SCOPES = ["module", "session", "class", "function"]
|
||||
|
||||
@pytest.mark.parametrize("scope", SCOPES)
|
||||
|
||||
@@ -253,7 +253,6 @@ class TestPython:
|
||||
duration_report: str,
|
||||
run_and_parse: RunAndParse,
|
||||
) -> None:
|
||||
|
||||
# mock LogXML.node_reporter so it always sets a known duration to each test report object
|
||||
original_node_reporter = LogXML.node_reporter
|
||||
|
||||
@@ -603,7 +602,6 @@ class TestPython:
|
||||
node.assert_attr(failures=3, tests=3)
|
||||
|
||||
for index, char in enumerate("<&'"):
|
||||
|
||||
tnode = node.find_nth_by_tag("testcase", index)
|
||||
tnode.assert_attr(
|
||||
classname="test_failure_escape", name="test_func[%s]" % char
|
||||
|
||||
@@ -425,6 +425,9 @@ def test_context_classmethod() -> None:
|
||||
assert A.x == 1
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings(
|
||||
"ignore:Deprecated call to `pkg_resources.declare_namespace"
|
||||
)
|
||||
def test_syspath_prepend_with_namespace_packages(
|
||||
pytester: Pytester, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
@@ -496,3 +496,24 @@ def test_nose_setup_skipped_if_non_callable(pytester: Pytester) -> None:
|
||||
)
|
||||
result = pytester.runpytest(p, "-p", "nose")
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("fixture_name", ("teardown", "teardown_class"))
|
||||
def test_teardown_fixture_not_called_directly(fixture_name, pytester: Pytester) -> None:
|
||||
"""Regression test for #10597."""
|
||||
p = pytester.makepyfile(
|
||||
f"""
|
||||
import pytest
|
||||
|
||||
class TestHello:
|
||||
|
||||
@pytest.fixture
|
||||
def {fixture_name}(self):
|
||||
yield
|
||||
|
||||
def test_hello(self, {fixture_name}):
|
||||
assert True
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest(p, "-p", "nose")
|
||||
assert result.ret == 0
|
||||
|
||||
@@ -518,10 +518,10 @@ class TestImportLibMode:
|
||||
fn1.write_text(
|
||||
dedent(
|
||||
"""
|
||||
import attr
|
||||
import dataclasses
|
||||
import pickle
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class Data:
|
||||
x: int = 42
|
||||
"""
|
||||
@@ -533,10 +533,10 @@ class TestImportLibMode:
|
||||
fn2.write_text(
|
||||
dedent(
|
||||
"""
|
||||
import attr
|
||||
import dataclasses
|
||||
import pickle
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class Data:
|
||||
x: str = ""
|
||||
"""
|
||||
|
||||
@@ -6,6 +6,7 @@ from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest._code.code import ExceptionRepr
|
||||
from _pytest.config import Config
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.python_api import approx
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
@@ -415,6 +416,26 @@ class TestReportSerialization:
|
||||
result.stdout.fnmatch_lines(["E *Error: No module named 'unknown'"])
|
||||
result.stdout.no_fnmatch_line("ERROR - *ConftestImportFailure*")
|
||||
|
||||
def test_report_timestamps_match_duration(self, pytester: Pytester, mock_timing):
|
||||
reprec = pytester.inline_runsource(
|
||||
"""
|
||||
import pytest
|
||||
from _pytest import timing
|
||||
@pytest.fixture
|
||||
def fixture_():
|
||||
timing.sleep(5)
|
||||
yield
|
||||
timing.sleep(5)
|
||||
def test_1(fixture_): timing.sleep(10)
|
||||
"""
|
||||
)
|
||||
reports = reprec.getreports("pytest_runtest_logreport")
|
||||
assert len(reports) == 3
|
||||
for report in reports:
|
||||
data = report._to_json()
|
||||
loaded_report = TestReport._from_json(data)
|
||||
assert loaded_report.stop - loaded_report.start == approx(report.duration)
|
||||
|
||||
|
||||
class TestHooks:
|
||||
"""Test that the hooks are working correctly for plugins"""
|
||||
|
||||
@@ -473,6 +473,7 @@ class TestSessionReports:
|
||||
assert not rep.skipped
|
||||
assert rep.passed
|
||||
locinfo = rep.location
|
||||
assert locinfo is not None
|
||||
assert locinfo[0] == col.path.name
|
||||
assert not locinfo[1]
|
||||
assert locinfo[2] == col.path.name
|
||||
@@ -906,6 +907,7 @@ def test_makereport_getsource_dynamic_code(
|
||||
def test_store_except_info_on_error() -> None:
|
||||
"""Test that upon test failure, the exception info is stored on
|
||||
sys.last_traceback and friends."""
|
||||
|
||||
# Simulate item that might raise a specific exception, depending on `raise_error` class var
|
||||
class ItemMightRaise:
|
||||
nodeid = "item_that_raises"
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from _pytest.cacheprovider import Cache
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.stepwise import STEPWISE_CACHE_DIR
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -278,3 +282,76 @@ def test_stepwise_skip_is_independent(pytester: Pytester) -> None:
|
||||
def test_sw_skip_help(pytester: Pytester) -> None:
|
||||
result = pytester.runpytest("-h")
|
||||
result.stdout.fnmatch_lines("*Implicitly enables --stepwise.")
|
||||
|
||||
|
||||
def test_stepwise_xdist_dont_store_lastfailed(pytester: Pytester) -> None:
|
||||
pytester.makefile(
|
||||
ext=".ini",
|
||||
pytest=f"[pytest]\ncache_dir = {pytester.path}\n",
|
||||
)
|
||||
|
||||
pytester.makepyfile(
|
||||
conftest="""
|
||||
import pytest
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_configure(config) -> None:
|
||||
config.workerinput = True
|
||||
"""
|
||||
)
|
||||
pytester.makepyfile(
|
||||
test_one="""
|
||||
def test_one():
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("--stepwise")
|
||||
assert result.ret == pytest.ExitCode.INTERRUPTED
|
||||
|
||||
stepwise_cache_file = (
|
||||
pytester.path / Cache._CACHE_PREFIX_VALUES / STEPWISE_CACHE_DIR
|
||||
)
|
||||
assert not Path(stepwise_cache_file).exists()
|
||||
|
||||
|
||||
def test_disabled_stepwise_xdist_dont_clear_cache(pytester: Pytester) -> None:
|
||||
pytester.makefile(
|
||||
ext=".ini",
|
||||
pytest=f"[pytest]\ncache_dir = {pytester.path}\n",
|
||||
)
|
||||
|
||||
stepwise_cache_file = (
|
||||
pytester.path / Cache._CACHE_PREFIX_VALUES / STEPWISE_CACHE_DIR
|
||||
)
|
||||
stepwise_cache_dir = stepwise_cache_file.parent
|
||||
stepwise_cache_dir.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
stepwise_cache_file_relative = f"{Cache._CACHE_PREFIX_VALUES}/{STEPWISE_CACHE_DIR}"
|
||||
|
||||
expected_value = '"test_one.py::test_one"'
|
||||
content = {f"{stepwise_cache_file_relative}": expected_value}
|
||||
|
||||
pytester.makefile(ext="", **content)
|
||||
|
||||
pytester.makepyfile(
|
||||
conftest="""
|
||||
import pytest
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_configure(config) -> None:
|
||||
config.workerinput = True
|
||||
"""
|
||||
)
|
||||
pytester.makepyfile(
|
||||
test_one="""
|
||||
def test_one():
|
||||
assert True
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 0
|
||||
|
||||
assert Path(stepwise_cache_file).exists()
|
||||
with stepwise_cache_file.open() as file_handle:
|
||||
observed_value = file_handle.readlines()
|
||||
assert [expected_value] == observed_value
|
||||
|
||||
@@ -909,7 +909,7 @@ class TestTerminalFunctional:
|
||||
# with configfile
|
||||
pytester.makeini("""[pytest]""")
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"])
|
||||
result.stdout.fnmatch_lines(["rootdir: *test_header0", "configfile: tox.ini"])
|
||||
|
||||
# with testpaths option, and not passing anything in the command-line
|
||||
pytester.makeini(
|
||||
@@ -920,12 +920,12 @@ class TestTerminalFunctional:
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
["rootdir: *test_header0, configfile: tox.ini, testpaths: tests, gui"]
|
||||
["rootdir: *test_header0", "configfile: tox.ini", "testpaths: tests, gui"]
|
||||
)
|
||||
|
||||
# with testpaths option, passing directory in command-line: do not show testpaths then
|
||||
result = pytester.runpytest("tests")
|
||||
result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"])
|
||||
result.stdout.fnmatch_lines(["rootdir: *test_header0", "configfile: tox.ini"])
|
||||
|
||||
def test_header_absolute_testpath(
|
||||
self, pytester: Pytester, monkeypatch: MonkeyPatch
|
||||
@@ -944,9 +944,9 @@ class TestTerminalFunctional:
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"rootdir: *absolute_testpath0, configfile: pyproject.toml, testpaths: {}".format(
|
||||
tests
|
||||
)
|
||||
"rootdir: *absolute_testpath0",
|
||||
"configfile: pyproject.toml",
|
||||
f"testpaths: {tests}",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1265,14 +1265,14 @@ def test_color_yes(pytester: Pytester, color_mapping) -> None:
|
||||
"=*= FAILURES =*=",
|
||||
"{red}{bold}_*_ test_this _*_{reset}",
|
||||
"",
|
||||
" {kw}def{hl-reset} {function}test_this{hl-reset}():",
|
||||
"> fail()",
|
||||
" {kw}def{hl-reset} {function}test_this{hl-reset}():{endline}",
|
||||
"> fail(){endline}",
|
||||
"",
|
||||
"{bold}{red}test_color_yes.py{reset}:5: ",
|
||||
"_ _ * _ _*",
|
||||
"",
|
||||
" {kw}def{hl-reset} {function}fail{hl-reset}():",
|
||||
"> {kw}assert{hl-reset} {number}0{hl-reset}",
|
||||
" {kw}def{hl-reset} {function}fail{hl-reset}():{endline}",
|
||||
"> {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
||||
"{bold}{red}E assert 0{reset}",
|
||||
"",
|
||||
"{bold}{red}test_color_yes.py{reset}:2: AssertionError",
|
||||
@@ -1292,9 +1292,9 @@ def test_color_yes(pytester: Pytester, color_mapping) -> None:
|
||||
"=*= FAILURES =*=",
|
||||
"{red}{bold}_*_ test_this _*_{reset}",
|
||||
"{bold}{red}test_color_yes.py{reset}:5: in test_this",
|
||||
" fail()",
|
||||
" fail(){endline}",
|
||||
"{bold}{red}test_color_yes.py{reset}:2: in fail",
|
||||
" {kw}assert{hl-reset} {number}0{hl-reset}",
|
||||
" {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
||||
"{bold}{red}E assert 0{reset}",
|
||||
"{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}",
|
||||
]
|
||||
@@ -2213,6 +2213,24 @@ class TestProgressOutputStyle:
|
||||
output = pytester.runpytest("--capture=no")
|
||||
output.stdout.no_fnmatch_line("*%]*")
|
||||
|
||||
def test_capture_no_progress_enabled(
|
||||
self, many_tests_files, pytester: Pytester
|
||||
) -> None:
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
console_output_style = progress-even-when-capture-no
|
||||
"""
|
||||
)
|
||||
output = pytester.runpytest("-s")
|
||||
output.stdout.re_match_lines(
|
||||
[
|
||||
r"test_bar.py \.{10} \s+ \[ 50%\]",
|
||||
r"test_foo.py \.{5} \s+ \[ 75%\]",
|
||||
r"test_foobar.py \.{5} \s+ \[100%\]",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class TestProgressWithTeardown:
|
||||
"""Ensure we show the correct percentages for tests that fail during teardown (#3088)"""
|
||||
@@ -2472,8 +2490,8 @@ class TestCodeHighlight:
|
||||
result.stdout.fnmatch_lines(
|
||||
color_mapping.format_for_fnmatch(
|
||||
[
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
|
||||
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}",
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
||||
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
|
||||
"{bold}{red}E assert 1 == 10{reset}",
|
||||
]
|
||||
)
|
||||
@@ -2494,9 +2512,9 @@ class TestCodeHighlight:
|
||||
result.stdout.fnmatch_lines(
|
||||
color_mapping.format_for_fnmatch(
|
||||
[
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
||||
" {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}",
|
||||
"> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}",
|
||||
"> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
|
||||
"{bold}{red}E assert 0{reset}",
|
||||
]
|
||||
)
|
||||
@@ -2517,8 +2535,8 @@ class TestCodeHighlight:
|
||||
result.stdout.fnmatch_lines(
|
||||
color_mapping.format_for_fnmatch(
|
||||
[
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
|
||||
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}",
|
||||
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
|
||||
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
|
||||
"{bold}{red}E assert 1 == 10{reset}",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import dataclasses
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
@@ -6,8 +7,7 @@ from pathlib import Path
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import List
|
||||
|
||||
import attr
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
from _pytest import pathlib
|
||||
@@ -31,9 +31,9 @@ def test_tmp_path_fixture(pytester: Pytester) -> None:
|
||||
results.stdout.fnmatch_lines(["*1 passed*"])
|
||||
|
||||
|
||||
@attr.s
|
||||
@dataclasses.dataclass
|
||||
class FakeConfig:
|
||||
basetemp = attr.ib()
|
||||
basetemp: Union[str, Path]
|
||||
|
||||
@property
|
||||
def trace(self):
|
||||
@@ -46,7 +46,7 @@ class FakeConfig:
|
||||
if name == "tmp_path_retention_count":
|
||||
return 3
|
||||
elif name == "tmp_path_retention_policy":
|
||||
return "failed"
|
||||
return "all"
|
||||
else:
|
||||
assert False
|
||||
|
||||
@@ -56,7 +56,7 @@ class FakeConfig:
|
||||
|
||||
|
||||
class TestTmpPathHandler:
|
||||
def test_mktemp(self, tmp_path):
|
||||
def test_mktemp(self, tmp_path: Path) -> None:
|
||||
config = cast(Config, FakeConfig(tmp_path))
|
||||
t = TempPathFactory.from_config(config, _ispytest=True)
|
||||
tmp = t.mktemp("world")
|
||||
@@ -67,7 +67,9 @@ class TestTmpPathHandler:
|
||||
assert str(tmp2.relative_to(t.getbasetemp())).startswith("this")
|
||||
assert tmp2 != tmp
|
||||
|
||||
def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch):
|
||||
def test_tmppath_relative_basetemp_absolute(
|
||||
self, tmp_path: Path, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
"""#4425"""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
config = cast(Config, FakeConfig("hello"))
|
||||
@@ -101,6 +103,12 @@ class TestConfigTmpPath:
|
||||
assert 0 == 1
|
||||
"""
|
||||
)
|
||||
pytester.makepyprojecttoml(
|
||||
"""
|
||||
[tool.pytest.ini_options]
|
||||
tmp_path_retention_policy = "failed"
|
||||
"""
|
||||
)
|
||||
|
||||
pytester.inline_run(p)
|
||||
root = pytester._test_tmproot
|
||||
@@ -128,6 +136,12 @@ class TestConfigTmpPath:
|
||||
assert 0 == 0
|
||||
"""
|
||||
)
|
||||
pytester.makepyprojecttoml(
|
||||
"""
|
||||
[tool.pytest.ini_options]
|
||||
tmp_path_retention_policy = "failed"
|
||||
"""
|
||||
)
|
||||
|
||||
pytester.inline_run(p)
|
||||
root = pytester._test_tmproot
|
||||
@@ -155,6 +169,13 @@ class TestConfigTmpPath:
|
||||
pass
|
||||
"""
|
||||
)
|
||||
pytester.makepyprojecttoml(
|
||||
"""
|
||||
[tool.pytest.ini_options]
|
||||
tmp_path_retention_policy = "failed"
|
||||
"""
|
||||
)
|
||||
|
||||
pytester.inline_run(p)
|
||||
|
||||
# Check if the whole directory is removed
|
||||
@@ -570,7 +591,7 @@ def test_tmp_path_factory_create_directory_with_safe_permissions(
|
||||
"""Verify that pytest creates directories under /tmp with private permissions."""
|
||||
# Use the test's tmp_path as the system temproot (/tmp).
|
||||
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
|
||||
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
|
||||
tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True)
|
||||
basetemp = tmp_factory.getbasetemp()
|
||||
|
||||
# No world-readable permissions.
|
||||
@@ -590,14 +611,14 @@ def test_tmp_path_factory_fixes_up_world_readable_permissions(
|
||||
"""
|
||||
# Use the test's tmp_path as the system temproot (/tmp).
|
||||
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
|
||||
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
|
||||
tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True)
|
||||
basetemp = tmp_factory.getbasetemp()
|
||||
|
||||
# Before - simulate bad perms.
|
||||
os.chmod(basetemp.parent, 0o777)
|
||||
assert (basetemp.parent.stat().st_mode & 0o077) != 0
|
||||
|
||||
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
|
||||
tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True)
|
||||
basetemp = tmp_factory.getbasetemp()
|
||||
|
||||
# After - fixed.
|
||||
|
||||
25
testing/test_tracebackhide.py
Normal file
25
testing/test_tracebackhide.py
Normal file
@@ -0,0 +1,25 @@
|
||||
def test_tbh_chained(testdir):
|
||||
"""Ensure chained exceptions whose frames contain "__tracebackhide__" are not shown (#1904)."""
|
||||
p = testdir.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
|
||||
def f1():
|
||||
__tracebackhide__ = True
|
||||
try:
|
||||
return f1.meh
|
||||
except AttributeError:
|
||||
pytest.fail("fail")
|
||||
|
||||
@pytest.fixture
|
||||
def fix():
|
||||
f1()
|
||||
|
||||
|
||||
def test(fix):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest(str(p))
|
||||
assert "'function' object has no attribute 'meh'" not in result.stdout.str()
|
||||
assert result.ret == 1
|
||||
@@ -3,6 +3,11 @@
|
||||
This file is not executed, it is only checked by mypy to ensure that
|
||||
none of the code triggers any mypy errors.
|
||||
"""
|
||||
import contextlib
|
||||
from typing import Optional
|
||||
|
||||
from typing_extensions import assert_type
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -22,3 +27,9 @@ def check_fixture_ids_callable() -> None:
|
||||
@pytest.mark.parametrize("func", [str, int], ids=lambda x: str(x.__name__))
|
||||
def check_parametrize_ids_callable(func) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def check_raises_is_a_context_manager(val: bool) -> None:
|
||||
with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo:
|
||||
pass
|
||||
assert_type(excinfo, Optional[pytest.ExceptionInfo[RuntimeError]])
|
||||
|
||||
Reference in New Issue
Block a user