Compare commits
36 Commits
Author | SHA1 | Date |
---|---|---|
|
bfe8d565e4 | |
|
e19b287b5f | |
|
5dcd2be466 | |
|
9d47a39bdd | |
|
d66697ed9a | |
|
8e1bbe1a94 | |
|
d054a68931 | |
|
30a112583e | |
|
682fc81781 | |
|
331bc1be46 | |
|
69689c6eb5 | |
|
6c95cb607d | |
|
efcb81c492 | |
|
fecf977e56 | |
|
bcada5138a | |
|
8082d27d11 | |
|
25ebf53f72 | |
|
920512435a | |
|
2c1913e709 | |
|
94d6922261 | |
|
384d54e11f | |
|
2ae187e78d | |
|
c403dc538b | |
|
f72c4cf35b | |
|
6193bf73ca | |
|
9a413e4cb8 | |
|
a1f7a204df | |
|
dab199281c | |
|
c3d9dacd39 | |
|
06d759619d | |
|
a4121aa0b6 | |
|
6e26c2bf9b | |
|
23cf1feb97 | |
|
1a427d32d6 | |
|
cec5bfe058 | |
|
ef982aaf2b |
|
@ -43,6 +43,7 @@ jobs:
|
||||||
"windows-py39",
|
"windows-py39",
|
||||||
"windows-py310",
|
"windows-py310",
|
||||||
"windows-py311",
|
"windows-py311",
|
||||||
|
"windows-py312",
|
||||||
|
|
||||||
"ubuntu-py37",
|
"ubuntu-py37",
|
||||||
"ubuntu-py37-pluggy",
|
"ubuntu-py37-pluggy",
|
||||||
|
@ -51,12 +52,13 @@ jobs:
|
||||||
"ubuntu-py39",
|
"ubuntu-py39",
|
||||||
"ubuntu-py310",
|
"ubuntu-py310",
|
||||||
"ubuntu-py311",
|
"ubuntu-py311",
|
||||||
|
"ubuntu-py312",
|
||||||
"ubuntu-pypy3",
|
"ubuntu-pypy3",
|
||||||
|
|
||||||
"macos-py37",
|
"macos-py37",
|
||||||
"macos-py38",
|
|
||||||
"macos-py39",
|
"macos-py39",
|
||||||
"macos-py310",
|
"macos-py310",
|
||||||
|
"macos-py312",
|
||||||
|
|
||||||
"docs",
|
"docs",
|
||||||
"doctesting",
|
"doctesting",
|
||||||
|
@ -86,9 +88,13 @@ jobs:
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py310-xdist"
|
tox_env: "py310-xdist"
|
||||||
- name: "windows-py311"
|
- name: "windows-py311"
|
||||||
python: "3.11-dev"
|
python: "3.11"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
tox_env: "py311"
|
tox_env: "py311"
|
||||||
|
- name: "windows-py312"
|
||||||
|
python: "3.12-dev"
|
||||||
|
os: windows-latest
|
||||||
|
tox_env: "py312"
|
||||||
|
|
||||||
- name: "ubuntu-py37"
|
- name: "ubuntu-py37"
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
|
@ -116,10 +122,15 @@ jobs:
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py310-xdist"
|
tox_env: "py310-xdist"
|
||||||
- name: "ubuntu-py311"
|
- name: "ubuntu-py311"
|
||||||
python: "3.11-dev"
|
python: "3.11"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
tox_env: "py311"
|
tox_env: "py311"
|
||||||
use_coverage: true
|
use_coverage: true
|
||||||
|
- name: "ubuntu-py312"
|
||||||
|
python: "3.12-dev"
|
||||||
|
os: ubuntu-latest
|
||||||
|
tox_env: "py312"
|
||||||
|
use_coverage: true
|
||||||
- name: "ubuntu-pypy3"
|
- name: "ubuntu-pypy3"
|
||||||
python: "pypy-3.7"
|
python: "pypy-3.7"
|
||||||
os: ubuntu-latest
|
os: ubuntu-latest
|
||||||
|
@ -129,19 +140,19 @@ jobs:
|
||||||
python: "3.7"
|
python: "3.7"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py37-xdist"
|
tox_env: "py37-xdist"
|
||||||
- name: "macos-py38"
|
|
||||||
python: "3.8"
|
|
||||||
os: macos-latest
|
|
||||||
tox_env: "py38-xdist"
|
|
||||||
use_coverage: true
|
|
||||||
- name: "macos-py39"
|
- name: "macos-py39"
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py39-xdist"
|
tox_env: "py39-xdist"
|
||||||
|
use_coverage: true
|
||||||
- name: "macos-py310"
|
- name: "macos-py310"
|
||||||
python: "3.10"
|
python: "3.10"
|
||||||
os: macos-latest
|
os: macos-latest
|
||||||
tox_env: "py310-xdist"
|
tox_env: "py310-xdist"
|
||||||
|
- name: "macos-py312"
|
||||||
|
python: "3.12-dev"
|
||||||
|
os: macos-latest
|
||||||
|
tox_env: "py312-xdist"
|
||||||
|
|
||||||
- name: "plugins"
|
- name: "plugins"
|
||||||
python: "3.9"
|
python: "3.9"
|
||||||
|
@ -168,6 +179,7 @@ jobs:
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v4
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|
|
@ -52,7 +52,7 @@ repos:
|
||||||
rev: v2.2.0
|
rev: v2.2.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: setup-cfg-fmt
|
- id: setup-cfg-fmt
|
||||||
args: ["--max-py-version=3.11", "--include-version-classifiers"]
|
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||||
rev: v1.10.0
|
rev: v1.10.0
|
||||||
hooks:
|
hooks:
|
||||||
|
|
3
AUTHORS
3
AUTHORS
|
@ -8,6 +8,7 @@ Abdeali JK
|
||||||
Abdelrahman Elbehery
|
Abdelrahman Elbehery
|
||||||
Abhijeet Kasurde
|
Abhijeet Kasurde
|
||||||
Adam Johnson
|
Adam Johnson
|
||||||
|
Adam Stewart
|
||||||
Adam Uhlir
|
Adam Uhlir
|
||||||
Ahn Ki-Wook
|
Ahn Ki-Wook
|
||||||
Akiomi Kamakura
|
Akiomi Kamakura
|
||||||
|
@ -128,7 +129,6 @@ Erik M. Bray
|
||||||
Evan Kepner
|
Evan Kepner
|
||||||
Fabien Zarifian
|
Fabien Zarifian
|
||||||
Fabio Zadrozny
|
Fabio Zadrozny
|
||||||
Felix Hofstätter
|
|
||||||
Felix Nieuwenhuizen
|
Felix Nieuwenhuizen
|
||||||
Feng Ma
|
Feng Ma
|
||||||
Florian Bruhin
|
Florian Bruhin
|
||||||
|
@ -167,6 +167,7 @@ Jake VanderPlas
|
||||||
Jakob van Santen
|
Jakob van Santen
|
||||||
Jakub Mitoraj
|
Jakub Mitoraj
|
||||||
James Bourbeau
|
James Bourbeau
|
||||||
|
James Frost
|
||||||
Jan Balster
|
Jan Balster
|
||||||
Janne Vanhala
|
Janne Vanhala
|
||||||
Jason R. Coombs
|
Jason R. Coombs
|
||||||
|
|
|
@ -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 @@
|
||||||
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,3 +0,0 @@
|
||||||
Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of
|
|
||||||
``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration
|
|
||||||
file.
|
|
|
@ -1 +0,0 @@
|
||||||
pytest no longer depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported).
|
|
|
@ -1 +0,0 @@
|
||||||
Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.
|
|
|
@ -1 +0,0 @@
|
||||||
Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.
|
|
|
@ -1 +0,0 @@
|
||||||
The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.
|
|
|
@ -1 +0,0 @@
|
||||||
: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.
|
|
|
@ -1 +0,0 @@
|
||||||
Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.
|
|
|
@ -1 +0,0 @@
|
||||||
Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.
|
|
|
@ -1 +0,0 @@
|
||||||
pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`.
|
|
|
@ -0,0 +1 @@
|
||||||
|
The documentation webpages now links to a canonical version to reduce outdated documentation in search engine results.
|
|
@ -1 +0,0 @@
|
||||||
Correctly handle ``__tracebackhide__`` for chained exceptions.
|
|
|
@ -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 +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.
|
|
|
@ -6,6 +6,9 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-7.3.2
|
||||||
|
release-7.3.1
|
||||||
|
release-7.3.0
|
||||||
release-7.2.2
|
release-7.2.2
|
||||||
release-7.2.1
|
release-7.2.1
|
||||||
release-7.2.0
|
release-7.2.0
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1,18 @@
|
||||||
|
pytest-7.3.1
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.3.1 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,21 @@
|
||||||
|
pytest-7.3.2
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.3.2 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Adam J. Stewart
|
||||||
|
* Alessio Izzo
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -92,3 +92,5 @@ pytest version min. Python version
|
||||||
5.0 - 6.1 3.5+
|
5.0 - 6.1 3.5+
|
||||||
3.3 - 4.6 2.7, 3.4+
|
3.3 - 4.6 2.7, 3.4+
|
||||||
============== ===================
|
============== ===================
|
||||||
|
|
||||||
|
`Status of Python Versions <https://devguide.python.org/versions/>`__.
|
||||||
|
|
|
@ -207,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||||
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
||||||
* caplog.clear() -> clear captured records and formatted log output string
|
* caplog.clear() -> clear captured records and formatted log output string
|
||||||
|
|
||||||
monkeypatch -- .../_pytest/monkeypatch.py:29
|
monkeypatch -- .../_pytest/monkeypatch.py:30
|
||||||
A convenient fixture for monkey-patching.
|
A convenient fixture for monkey-patching.
|
||||||
|
|
||||||
The fixture provides these methods to modify objects, dictionaries, or
|
The fixture provides these methods to modify objects, dictionaries, or
|
||||||
|
|
|
@ -28,6 +28,129 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 7.3.2 (2023-06-10)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
|
||||||
|
|
||||||
|
|
||||||
|
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
|
||||||
|
|
||||||
|
|
||||||
|
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.3.1 (2023-04-14)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#10875 <https://github.com/pytest-dev/pytest/issues/10875>`_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10890 <https://github.com/pytest-dev/pytest/issues/10890>`_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10896 <https://github.com/pytest-dev/pytest/issues/10896>`_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10903 <https://github.com/pytest-dev/pytest/issues/10903>`_: Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden.
|
||||||
|
This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.3.0 (2023-04-08)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- `#10525 <https://github.com/pytest-dev/pytest/issues/10525>`_: Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10755 <https://github.com/pytest-dev/pytest/issues/10755>`_: :confval:`console_output_style` now supports ``progress-even-when-capture-no`` to force the use of the progress output even when capture is disabled. This is useful in large test suites where capture may have significant performance impact.
|
||||||
|
|
||||||
|
|
||||||
|
- `#7431 <https://github.com/pytest-dev/pytest/issues/7431>`_: ``--log-disable`` CLI option added to disable individual loggers.
|
||||||
|
|
||||||
|
|
||||||
|
- `#8141 <https://github.com/pytest-dev/pytest/issues/8141>`_: Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#10226 <https://github.com/pytest-dev/pytest/issues/10226>`_: If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10658 <https://github.com/pytest-dev/pytest/issues/10658>`_: Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of
|
||||||
|
``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration
|
||||||
|
file.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10710 <https://github.com/pytest-dev/pytest/issues/10710>`_: Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10727 <https://github.com/pytest-dev/pytest/issues/10727>`_: Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10840 <https://github.com/pytest-dev/pytest/issues/10840>`_: pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6267 <https://github.com/pytest-dev/pytest/issues/6267>`_: The full output of a test is no longer truncated if the truncation message would be longer than
|
||||||
|
the hidden text. The line number shown has also been fixed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10743 <https://github.com/pytest-dev/pytest/issues/10743>`_: The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10765 <https://github.com/pytest-dev/pytest/issues/10765>`_: Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions.
|
||||||
|
NOTE: This change was reverted in version 7.3.1.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- `#10782 <https://github.com/pytest-dev/pytest/issues/10782>`_: Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Trivial/Internal Changes
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer directly depends on the `attrs <https://www.attrs.org/en/stable/>`__ package. While
|
||||||
|
we at pytest all love the package dearly and would like to thank the ``attrs`` team for many years of cooperation and support,
|
||||||
|
it makes sense for ``pytest`` to have as little external dependencies as possible, as this helps downstream projects.
|
||||||
|
With that in mind, we have replaced the pytest's limited internal usage to use the standard library's ``dataclasses`` instead.
|
||||||
|
|
||||||
|
Nice diffs for ``attrs`` classes are still supported though.
|
||||||
|
|
||||||
|
|
||||||
pytest 7.2.2 (2023-03-03)
|
pytest 7.2.2 (2023-03-03)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
@ -468,7 +591,7 @@ Breaking Changes
|
||||||
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
||||||
|
|
||||||
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
||||||
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
|
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
|
||||||
|
|
||||||
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
||||||
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
||||||
|
@ -3968,7 +4091,7 @@ Removals
|
||||||
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
|
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
|
||||||
|
|
||||||
Use ``Node.get_closest_marker(name)`` as a replacement.
|
Use ``Node.get_closest_marker(name)`` as a replacement.
|
||||||
|
|
||||||
|
|
|
@ -273,6 +273,9 @@ html_show_sourcelink = False
|
||||||
# Output file base name for HTML help builder.
|
# Output file base name for HTML help builder.
|
||||||
htmlhelp_basename = "pytestdoc"
|
htmlhelp_basename = "pytestdoc"
|
||||||
|
|
||||||
|
# The base URL which points to the root of the HTML documentation. It is used
|
||||||
|
# to indicate the location of document using the canonical link relation (#12363).
|
||||||
|
html_baseurl = "https://docs.pytest.org/en/stable/"
|
||||||
|
|
||||||
# -- Options for LaTeX output --------------------------------------------------
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
|
@ -341,7 +344,7 @@ epub_copyright = "2013, holger krekel et alii"
|
||||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
||||||
# epub_scheme = ''
|
# epub_scheme = ''
|
||||||
|
|
||||||
# The unique identifier of the text. This can be a ISBN number
|
# The unique identifier of the text. This can be an ISBN number
|
||||||
# or the project homepage.
|
# or the project homepage.
|
||||||
# epub_identifier = ''
|
# epub_identifier = ''
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
|
||||||
" no further details known at this point.",
|
" no further details known at this point.",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
return super().repr_failure(excinfo)
|
||||||
|
|
||||||
def reportinfo(self):
|
def reportinfo(self):
|
||||||
return self.path, 0, f"usecase: {self.name}"
|
return self.path, 0, f"usecase: {self.name}"
|
||||||
|
|
|
@ -502,8 +502,12 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||||
.. code-block:: pytest
|
.. code-block:: pytest
|
||||||
|
|
||||||
. $ pytest -rs -q multipython.py
|
. $ pytest -rs -q multipython.py
|
||||||
........................... [100%]
|
sssssssssssssssssssssssssss [100%]
|
||||||
27 passed in 0.12s
|
========================= short test summary info ==========================
|
||||||
|
SKIPPED [9] multipython.py:69: 'python3.5' not found
|
||||||
|
SKIPPED [9] multipython.py:69: 'python3.6' not found
|
||||||
|
SKIPPED [9] multipython.py:69: 'python3.7' not found
|
||||||
|
27 skipped in 0.12s
|
||||||
|
|
||||||
Indirect parametrization of optional implementations/imports
|
Indirect parametrization of optional implementations/imports
|
||||||
--------------------------------------------------------------------
|
--------------------------------------------------------------------
|
||||||
|
|
|
@ -70,12 +70,12 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
|
|
||||||
> assert not f()
|
> assert not f()
|
||||||
E assert not 42
|
E assert not 42
|
||||||
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef0002>()
|
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef0006>()
|
||||||
|
|
||||||
failure_demo.py:39: AssertionError
|
failure_demo.py:39: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_eq_text _________________
|
_________________ TestSpecialisedExplanations.test_eq_text _________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0006>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0007>
|
||||||
|
|
||||||
def test_eq_text(self):
|
def test_eq_text(self):
|
||||||
> assert "spam" == "eggs"
|
> assert "spam" == "eggs"
|
||||||
|
@ -86,7 +86,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:44: AssertionError
|
failure_demo.py:44: AssertionError
|
||||||
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0007>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0008>
|
||||||
|
|
||||||
def test_eq_similar_text(self):
|
def test_eq_similar_text(self):
|
||||||
> assert "foo 1 bar" == "foo 2 bar"
|
> assert "foo 1 bar" == "foo 2 bar"
|
||||||
|
@ -99,7 +99,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:47: AssertionError
|
failure_demo.py:47: AssertionError
|
||||||
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
|
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0008>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0009>
|
||||||
|
|
||||||
def test_eq_multiline_text(self):
|
def test_eq_multiline_text(self):
|
||||||
> assert "foo\nspam\nbar" == "foo\neggs\nbar"
|
> assert "foo\nspam\nbar" == "foo\neggs\nbar"
|
||||||
|
@ -112,7 +112,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:50: AssertionError
|
failure_demo.py:50: AssertionError
|
||||||
______________ TestSpecialisedExplanations.test_eq_long_text _______________
|
______________ TestSpecialisedExplanations.test_eq_long_text _______________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0009>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000a>
|
||||||
|
|
||||||
def test_eq_long_text(self):
|
def test_eq_long_text(self):
|
||||||
a = "1" * 100 + "a" + "2" * 100
|
a = "1" * 100 + "a" + "2" * 100
|
||||||
|
@ -129,7 +129,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:55: AssertionError
|
failure_demo.py:55: AssertionError
|
||||||
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
|
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000a>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000b>
|
||||||
|
|
||||||
def test_eq_long_text_multiline(self):
|
def test_eq_long_text_multiline(self):
|
||||||
a = "1\n" * 100 + "a" + "2\n" * 100
|
a = "1\n" * 100 + "a" + "2\n" * 100
|
||||||
|
@ -149,7 +149,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:60: AssertionError
|
failure_demo.py:60: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000b>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000c>
|
||||||
|
|
||||||
def test_eq_list(self):
|
def test_eq_list(self):
|
||||||
> assert [0, 1, 2] == [0, 1, 3]
|
> assert [0, 1, 2] == [0, 1, 3]
|
||||||
|
@ -160,7 +160,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:63: AssertionError
|
failure_demo.py:63: AssertionError
|
||||||
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000c>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000d>
|
||||||
|
|
||||||
def test_eq_list_long(self):
|
def test_eq_list_long(self):
|
||||||
a = [0] * 100 + [1] + [3] * 100
|
a = [0] * 100 + [1] + [3] * 100
|
||||||
|
@ -173,7 +173,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:68: AssertionError
|
failure_demo.py:68: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000d>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
|
||||||
|
|
||||||
def test_eq_dict(self):
|
def test_eq_dict(self):
|
||||||
> assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}
|
> assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}
|
||||||
|
@ -190,7 +190,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:71: AssertionError
|
failure_demo.py:71: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000f>
|
||||||
|
|
||||||
def test_eq_set(self):
|
def test_eq_set(self):
|
||||||
> assert {0, 10, 11, 12} == {0, 20, 21}
|
> assert {0, 10, 11, 12} == {0, 20, 21}
|
||||||
|
@ -207,7 +207,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:74: AssertionError
|
failure_demo.py:74: AssertionError
|
||||||
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000f>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0010>
|
||||||
|
|
||||||
def test_eq_longer_list(self):
|
def test_eq_longer_list(self):
|
||||||
> assert [1, 2] == [1, 2, 3]
|
> assert [1, 2] == [1, 2, 3]
|
||||||
|
@ -218,7 +218,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:77: AssertionError
|
failure_demo.py:77: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_in_list _________________
|
_________________ TestSpecialisedExplanations.test_in_list _________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0010>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0011>
|
||||||
|
|
||||||
def test_in_list(self):
|
def test_in_list(self):
|
||||||
> assert 1 in [0, 2, 3, 4, 5]
|
> assert 1 in [0, 2, 3, 4, 5]
|
||||||
|
@ -227,7 +227,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:80: AssertionError
|
failure_demo.py:80: AssertionError
|
||||||
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________
|
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0011>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0012>
|
||||||
|
|
||||||
def test_not_in_text_multiline(self):
|
def test_not_in_text_multiline(self):
|
||||||
text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail"
|
text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail"
|
||||||
|
@ -245,7 +245,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:84: AssertionError
|
failure_demo.py:84: AssertionError
|
||||||
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0012>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0013>
|
||||||
|
|
||||||
def test_not_in_text_single(self):
|
def test_not_in_text_single(self):
|
||||||
text = "single foo line"
|
text = "single foo line"
|
||||||
|
@ -258,7 +258,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:88: AssertionError
|
failure_demo.py:88: AssertionError
|
||||||
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
|
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0013>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0014>
|
||||||
|
|
||||||
def test_not_in_text_single_long(self):
|
def test_not_in_text_single_long(self):
|
||||||
text = "head " * 50 + "foo " + "tail " * 20
|
text = "head " * 50 + "foo " + "tail " * 20
|
||||||
|
@ -271,7 +271,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:92: AssertionError
|
failure_demo.py:92: AssertionError
|
||||||
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
|
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0014>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0015>
|
||||||
|
|
||||||
def test_not_in_text_single_long_term(self):
|
def test_not_in_text_single_long_term(self):
|
||||||
text = "head " * 50 + "f" * 70 + "tail " * 20
|
text = "head " * 50 + "f" * 70 + "tail " * 20
|
||||||
|
@ -284,7 +284,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:96: AssertionError
|
failure_demo.py:96: AssertionError
|
||||||
______________ TestSpecialisedExplanations.test_eq_dataclass _______________
|
______________ TestSpecialisedExplanations.test_eq_dataclass _______________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0015>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
|
||||||
|
|
||||||
def test_eq_dataclass(self):
|
def test_eq_dataclass(self):
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
@ -311,7 +311,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:108: AssertionError
|
failure_demo.py:108: AssertionError
|
||||||
________________ TestSpecialisedExplanations.test_eq_attrs _________________
|
________________ TestSpecialisedExplanations.test_eq_attrs _________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0017>
|
||||||
|
|
||||||
def test_eq_attrs(self):
|
def test_eq_attrs(self):
|
||||||
import attr
|
import attr
|
||||||
|
@ -345,7 +345,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
i = Foo()
|
i = Foo()
|
||||||
> assert i.b == 2
|
> assert i.b == 2
|
||||||
E assert 1 == 2
|
E assert 1 == 2
|
||||||
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef0017>.b
|
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef0018>.b
|
||||||
|
|
||||||
failure_demo.py:128: AssertionError
|
failure_demo.py:128: AssertionError
|
||||||
_________________________ test_attribute_instance __________________________
|
_________________________ test_attribute_instance __________________________
|
||||||
|
@ -356,8 +356,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
|
|
||||||
> assert Foo().b == 2
|
> assert Foo().b == 2
|
||||||
E AssertionError: assert 1 == 2
|
E AssertionError: assert 1 == 2
|
||||||
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0018>.b
|
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0019>.b
|
||||||
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0018> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0019> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
||||||
|
|
||||||
failure_demo.py:135: AssertionError
|
failure_demo.py:135: AssertionError
|
||||||
__________________________ test_attribute_failure __________________________
|
__________________________ test_attribute_failure __________________________
|
||||||
|
@ -375,7 +375,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:146:
|
failure_demo.py:146:
|
||||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||||
|
|
||||||
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef0019>
|
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef001a>
|
||||||
|
|
||||||
def _get_b(self):
|
def _get_b(self):
|
||||||
> raise Exception("Failed to get attrib")
|
> raise Exception("Failed to get attrib")
|
||||||
|
@ -393,15 +393,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
|
|
||||||
> assert Foo().b == Bar().b
|
> assert Foo().b == Bar().b
|
||||||
E AssertionError: assert 1 == 2
|
E AssertionError: assert 1 == 2
|
||||||
E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001a>.b
|
E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001b>.b
|
||||||
E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001a> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
|
E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
|
||||||
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001b>.b
|
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001c>.b
|
||||||
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001c> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
||||||
|
|
||||||
failure_demo.py:156: AssertionError
|
failure_demo.py:156: AssertionError
|
||||||
__________________________ TestRaises.test_raises __________________________
|
__________________________ TestRaises.test_raises __________________________
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef001c>
|
self = <failure_demo.TestRaises object at 0xdeadbeef001d>
|
||||||
|
|
||||||
def test_raises(self):
|
def test_raises(self):
|
||||||
s = "qwe"
|
s = "qwe"
|
||||||
|
@ -411,7 +411,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:166: ValueError
|
failure_demo.py:166: ValueError
|
||||||
______________________ TestRaises.test_raises_doesnt _______________________
|
______________________ TestRaises.test_raises_doesnt _______________________
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef001d>
|
self = <failure_demo.TestRaises object at 0xdeadbeef001e>
|
||||||
|
|
||||||
def test_raises_doesnt(self):
|
def test_raises_doesnt(self):
|
||||||
> raises(OSError, int, "3")
|
> raises(OSError, int, "3")
|
||||||
|
@ -420,7 +420,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:169: Failed
|
failure_demo.py:169: Failed
|
||||||
__________________________ TestRaises.test_raise ___________________________
|
__________________________ TestRaises.test_raise ___________________________
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef001e>
|
self = <failure_demo.TestRaises object at 0xdeadbeef001f>
|
||||||
|
|
||||||
def test_raise(self):
|
def test_raise(self):
|
||||||
> raise ValueError("demo error")
|
> raise ValueError("demo error")
|
||||||
|
@ -429,7 +429,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:172: ValueError
|
failure_demo.py:172: ValueError
|
||||||
________________________ TestRaises.test_tupleerror ________________________
|
________________________ TestRaises.test_tupleerror ________________________
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef001f>
|
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
||||||
|
|
||||||
def test_tupleerror(self):
|
def test_tupleerror(self):
|
||||||
> a, b = [1] # NOQA
|
> a, b = [1] # NOQA
|
||||||
|
@ -438,7 +438,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:175: ValueError
|
failure_demo.py:175: ValueError
|
||||||
______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
|
______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
self = <failure_demo.TestRaises object at 0xdeadbeef0021>
|
||||||
|
|
||||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||||
items = [1, 2, 3]
|
items = [1, 2, 3]
|
||||||
|
@ -451,7 +451,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
items is [1, 2, 3]
|
items is [1, 2, 3]
|
||||||
________________________ TestRaises.test_some_error ________________________
|
________________________ TestRaises.test_some_error ________________________
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef0021>
|
self = <failure_demo.TestRaises object at 0xdeadbeef0022>
|
||||||
|
|
||||||
def test_some_error(self):
|
def test_some_error(self):
|
||||||
> if namenotexi: # NOQA
|
> if namenotexi: # NOQA
|
||||||
|
@ -482,7 +482,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
abc-123:2: AssertionError
|
abc-123:2: AssertionError
|
||||||
____________________ TestMoreErrors.test_complex_error _____________________
|
____________________ TestMoreErrors.test_complex_error _____________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0022>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0023>
|
||||||
|
|
||||||
def test_complex_error(self):
|
def test_complex_error(self):
|
||||||
def f():
|
def f():
|
||||||
|
@ -508,7 +508,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:6: AssertionError
|
failure_demo.py:6: AssertionError
|
||||||
___________________ TestMoreErrors.test_z1_unpack_error ____________________
|
___________________ TestMoreErrors.test_z1_unpack_error ____________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0023>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0024>
|
||||||
|
|
||||||
def test_z1_unpack_error(self):
|
def test_z1_unpack_error(self):
|
||||||
items = []
|
items = []
|
||||||
|
@ -518,7 +518,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:217: ValueError
|
failure_demo.py:217: ValueError
|
||||||
____________________ TestMoreErrors.test_z2_type_error _____________________
|
____________________ TestMoreErrors.test_z2_type_error _____________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0024>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0025>
|
||||||
|
|
||||||
def test_z2_type_error(self):
|
def test_z2_type_error(self):
|
||||||
items = 3
|
items = 3
|
||||||
|
@ -528,20 +528,20 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:221: TypeError
|
failure_demo.py:221: TypeError
|
||||||
______________________ TestMoreErrors.test_startswith ______________________
|
______________________ TestMoreErrors.test_startswith ______________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0025>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0026>
|
||||||
|
|
||||||
def test_startswith(self):
|
def test_startswith(self):
|
||||||
s = "123"
|
s = "123"
|
||||||
g = "456"
|
g = "456"
|
||||||
> assert s.startswith(g)
|
> assert s.startswith(g)
|
||||||
E AssertionError: assert False
|
E AssertionError: assert False
|
||||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0026>('456')
|
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
|
||||||
E + where <built-in method startswith of str object at 0xdeadbeef0026> = '123'.startswith
|
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
|
||||||
|
|
||||||
failure_demo.py:226: AssertionError
|
failure_demo.py:226: AssertionError
|
||||||
__________________ TestMoreErrors.test_startswith_nested ___________________
|
__________________ TestMoreErrors.test_startswith_nested ___________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0027>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0028>
|
||||||
|
|
||||||
def test_startswith_nested(self):
|
def test_startswith_nested(self):
|
||||||
def f():
|
def f():
|
||||||
|
@ -552,15 +552,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
|
|
||||||
> assert f().startswith(g())
|
> assert f().startswith(g())
|
||||||
E AssertionError: assert False
|
E AssertionError: assert False
|
||||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0026>('456')
|
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
|
||||||
E + where <built-in method startswith of str object at 0xdeadbeef0026> = '123'.startswith
|
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
|
||||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0028>()
|
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
|
||||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
|
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
|
||||||
|
|
||||||
failure_demo.py:235: AssertionError
|
failure_demo.py:235: AssertionError
|
||||||
_____________________ TestMoreErrors.test_global_func ______________________
|
_____________________ TestMoreErrors.test_global_func ______________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
||||||
|
|
||||||
def test_global_func(self):
|
def test_global_func(self):
|
||||||
> assert isinstance(globf(42), float)
|
> assert isinstance(globf(42), float)
|
||||||
|
@ -571,18 +571,18 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:238: AssertionError
|
failure_demo.py:238: AssertionError
|
||||||
_______________________ TestMoreErrors.test_instance _______________________
|
_______________________ TestMoreErrors.test_instance _______________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||||
|
|
||||||
def test_instance(self):
|
def test_instance(self):
|
||||||
self.x = 6 * 7
|
self.x = 6 * 7
|
||||||
> assert self.x != 42
|
> assert self.x != 42
|
||||||
E assert 42 != 42
|
E assert 42 != 42
|
||||||
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>.x
|
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>.x
|
||||||
|
|
||||||
failure_demo.py:242: AssertionError
|
failure_demo.py:242: AssertionError
|
||||||
_______________________ TestMoreErrors.test_compare ________________________
|
_______________________ TestMoreErrors.test_compare ________________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
||||||
|
|
||||||
def test_compare(self):
|
def test_compare(self):
|
||||||
> assert globf(10) < 5
|
> assert globf(10) < 5
|
||||||
|
@ -592,7 +592,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:245: AssertionError
|
failure_demo.py:245: AssertionError
|
||||||
_____________________ TestMoreErrors.test_try_finally ______________________
|
_____________________ TestMoreErrors.test_try_finally ______________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
|
||||||
|
|
||||||
def test_try_finally(self):
|
def test_try_finally(self):
|
||||||
x = 1
|
x = 1
|
||||||
|
@ -603,7 +603,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:250: AssertionError
|
failure_demo.py:250: AssertionError
|
||||||
___________________ TestCustomAssertMsg.test_single_line ___________________
|
___________________ TestCustomAssertMsg.test_single_line ___________________
|
||||||
|
|
||||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002e>
|
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
||||||
|
|
||||||
def test_single_line(self):
|
def test_single_line(self):
|
||||||
class A:
|
class A:
|
||||||
|
@ -618,7 +618,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:261: AssertionError
|
failure_demo.py:261: AssertionError
|
||||||
____________________ TestCustomAssertMsg.test_multiline ____________________
|
____________________ TestCustomAssertMsg.test_multiline ____________________
|
||||||
|
|
||||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
||||||
|
|
||||||
def test_multiline(self):
|
def test_multiline(self):
|
||||||
class A:
|
class A:
|
||||||
|
@ -637,7 +637,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:268: AssertionError
|
failure_demo.py:268: AssertionError
|
||||||
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
||||||
|
|
||||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
|
||||||
|
|
||||||
def test_custom_repr(self):
|
def test_custom_repr(self):
|
||||||
class JSON:
|
class JSON:
|
||||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 7.2.0.dev534+ga2c84caaa.d20230317
|
pytest 7.3.2
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
:orphan:
|
: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
|
- `pytest tips and tricks for a better testsuite <https://ep2023.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2023 <https://ep2023.europython.eu/>`_, July 18th (3h), Prague/Remote
|
||||||
|
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote
|
||||||
|
|
||||||
Also see :doc:`previous talks and blogposts <talks>`.
|
Also see :doc:`previous talks and blogposts <talks>`.
|
||||||
|
|
||||||
.. _features:
|
.. _features:
|
||||||
|
|
||||||
|
|
|
@ -1049,11 +1049,11 @@ Environment variables that can be used to change pytest's behavior.
|
||||||
|
|
||||||
.. envvar:: CI
|
.. envvar:: CI
|
||||||
|
|
||||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to ``BUILD_NUMBER`` variable.
|
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
|
||||||
|
|
||||||
.. envvar:: BUILD_NUMBER
|
.. envvar:: BUILD_NUMBER
|
||||||
|
|
||||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to CI variable.
|
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
|
||||||
|
|
||||||
.. envvar:: PYTEST_ADDOPTS
|
.. envvar:: PYTEST_ADDOPTS
|
||||||
|
|
||||||
|
@ -1713,13 +1713,12 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
|
|
||||||
.. confval:: testpaths
|
.. confval:: testpaths
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Sets list of directories that should be searched for tests when
|
Sets list of directories that should be searched for tests when
|
||||||
no specific directories, files or test ids are given in the command line when
|
no specific directories, files or test ids are given in the command line when
|
||||||
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
||||||
File system paths may use shell-style wildcards, including the recursive
|
File system paths may use shell-style wildcards, including the recursive
|
||||||
``**`` pattern.
|
``**`` pattern.
|
||||||
|
|
||||||
Useful when all project tests are in a known location to speed up
|
Useful when all project tests are in a known location to speed up
|
||||||
test collection and to avoid picking up undesired tests by accident.
|
test collection and to avoid picking up undesired tests by accident.
|
||||||
|
|
||||||
|
@ -1728,8 +1727,17 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
[pytest]
|
[pytest]
|
||||||
testpaths = testing doc
|
testpaths = testing doc
|
||||||
|
|
||||||
This tells pytest to only look for tests in ``testing`` and ``doc``
|
This configuration means that executing:
|
||||||
directories when executing from the root directory.
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pytest
|
||||||
|
|
||||||
|
has the same practical effects as executing:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pytest testing doc
|
||||||
|
|
||||||
|
|
||||||
.. confval:: tmp_path_retention_count
|
.. confval:: tmp_path_retention_count
|
||||||
|
@ -1744,7 +1752,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
[pytest]
|
[pytest]
|
||||||
tmp_path_retention_count = 3
|
tmp_path_retention_count = 3
|
||||||
|
|
||||||
Default: 3
|
Default: ``3``
|
||||||
|
|
||||||
|
|
||||||
.. confval:: tmp_path_retention_policy
|
.. confval:: tmp_path_retention_policy
|
||||||
|
@ -1763,7 +1771,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
[pytest]
|
[pytest]
|
||||||
tmp_path_retention_policy = "all"
|
tmp_path_retention_policy = "all"
|
||||||
|
|
||||||
Default: all
|
Default: ``all``
|
||||||
|
|
||||||
|
|
||||||
.. confval:: usefixtures
|
.. confval:: usefixtures
|
||||||
|
@ -1996,7 +2004,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
Auto-indent multiline messages passed to the logging
|
Auto-indent multiline messages passed to the logging
|
||||||
module. Accepts true|on, false|off or an integer.
|
module. Accepts true|on, false|off or an integer.
|
||||||
--log-disable=LOGGER_DISABLE
|
--log-disable=LOGGER_DISABLE
|
||||||
Disable a logger by name. Can be passed multipe
|
Disable a logger by name. Can be passed multiple
|
||||||
times.
|
times.
|
||||||
|
|
||||||
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
||||||
|
|
|
@ -22,6 +22,7 @@ classifiers =
|
||||||
Programming Language :: Python :: 3.9
|
Programming Language :: Python :: 3.9
|
||||||
Programming Language :: Python :: 3.10
|
Programming Language :: Python :: 3.10
|
||||||
Programming Language :: Python :: 3.11
|
Programming Language :: Python :: 3.11
|
||||||
|
Programming Language :: Python :: 3.12
|
||||||
Topic :: Software Development :: Libraries
|
Topic :: Software Development :: Libraries
|
||||||
Topic :: Software Development :: Testing
|
Topic :: Software Development :: Testing
|
||||||
Topic :: Utilities
|
Topic :: Utilities
|
||||||
|
@ -73,6 +74,7 @@ testing =
|
||||||
nose
|
nose
|
||||||
pygments>=2.7.2
|
pygments>=2.7.2
|
||||||
requests
|
requests
|
||||||
|
setuptools
|
||||||
xmlschema
|
xmlschema
|
||||||
|
|
||||||
[options.package_data]
|
[options.package_data]
|
||||||
|
|
|
@ -411,13 +411,13 @@ class Traceback(List[TracebackEntry]):
|
||||||
"""
|
"""
|
||||||
return Traceback(filter(fn, self), self._excinfo)
|
return Traceback(filter(fn, self), self._excinfo)
|
||||||
|
|
||||||
def getcrashentry(self) -> Optional[TracebackEntry]:
|
def getcrashentry(self) -> TracebackEntry:
|
||||||
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
|
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
|
||||||
for i in range(-1, -len(self) - 1, -1):
|
for i in range(-1, -len(self) - 1, -1):
|
||||||
entry = self[i]
|
entry = self[i]
|
||||||
if not entry.ishidden():
|
if not entry.ishidden():
|
||||||
return entry
|
return entry
|
||||||
return None
|
return self[-1]
|
||||||
|
|
||||||
def recursionindex(self) -> Optional[int]:
|
def recursionindex(self) -> Optional[int]:
|
||||||
"""Return the index of the frame/TracebackEntry where recursion originates if
|
"""Return the index of the frame/TracebackEntry where recursion originates if
|
||||||
|
@ -602,13 +602,11 @@ class ExceptionInfo(Generic[E]):
|
||||||
"""
|
"""
|
||||||
return isinstance(self.value, exc)
|
return isinstance(self.value, exc)
|
||||||
|
|
||||||
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
def _getreprcrash(self) -> "ReprFileLocation":
|
||||||
exconly = self.exconly(tryshort=True)
|
exconly = self.exconly(tryshort=True)
|
||||||
entry = self.traceback.getcrashentry()
|
entry = self.traceback.getcrashentry()
|
||||||
if entry:
|
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
return ReprFileLocation(path, lineno + 1, exconly)
|
||||||
return ReprFileLocation(path, lineno + 1, exconly)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getrepr(
|
def getrepr(
|
||||||
self,
|
self,
|
||||||
|
@ -946,14 +944,9 @@ class FormattedExcinfo:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
reprtraceback = self.repr_traceback(excinfo_)
|
reprtraceback = self.repr_traceback(excinfo_)
|
||||||
|
reprcrash: Optional[ReprFileLocation] = (
|
||||||
# will be None if all traceback entries are hidden
|
excinfo_._getreprcrash() if self.style != "value" else None
|
||||||
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
|
)
|
||||||
if reprcrash:
|
|
||||||
if self.style == "value":
|
|
||||||
repr_chain += [(reprtraceback, None, descr)]
|
|
||||||
else:
|
|
||||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
|
||||||
else:
|
else:
|
||||||
# Fallback to native repr if the exception doesn't have a traceback:
|
# Fallback to native repr if the exception doesn't have a traceback:
|
||||||
# ExceptionInfo objects require a full traceback to work.
|
# ExceptionInfo objects require a full traceback to work.
|
||||||
|
@ -961,8 +954,8 @@ class FormattedExcinfo:
|
||||||
traceback.format_exception(type(e), e, None)
|
traceback.format_exception(type(e), e, None)
|
||||||
)
|
)
|
||||||
reprcrash = None
|
reprcrash = None
|
||||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
|
||||||
|
|
||||||
|
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||||
if e.__cause__ is not None and self.chain:
|
if e.__cause__ is not None and self.chain:
|
||||||
e = e.__cause__
|
e = e.__cause__
|
||||||
excinfo_ = (
|
excinfo_ = (
|
||||||
|
@ -1053,7 +1046,7 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||||
@dataclasses.dataclass(eq=False)
|
@dataclasses.dataclass(eq=False)
|
||||||
class ReprExceptionInfo(ExceptionRepr):
|
class ReprExceptionInfo(ExceptionRepr):
|
||||||
reprtraceback: "ReprTraceback"
|
reprtraceback: "ReprTraceback"
|
||||||
reprcrash: Optional["ReprFileLocation"]
|
reprcrash: "ReprFileLocation"
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
self.reprtraceback.toterminal(tw)
|
self.reprtraceback.toterminal(tw)
|
||||||
|
|
|
@ -953,7 +953,7 @@ class LocalPath:
|
||||||
else:
|
else:
|
||||||
p.dirpath()._ensuredirs()
|
p.dirpath()._ensuredirs()
|
||||||
if not p.check(file=1):
|
if not p.check(file=1):
|
||||||
p.open("w").close()
|
p.open("wb").close()
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
|
|
@ -46,8 +46,14 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
if sys.version_info >= (3, 8):
|
if sys.version_info >= (3, 8):
|
||||||
namedExpr = ast.NamedExpr
|
namedExpr = ast.NamedExpr
|
||||||
|
astNameConstant = ast.Constant
|
||||||
|
astStr = ast.Constant
|
||||||
|
astNum = ast.Constant
|
||||||
else:
|
else:
|
||||||
namedExpr = ast.Expr
|
namedExpr = ast.Expr
|
||||||
|
astNameConstant = ast.NameConstant
|
||||||
|
astStr = ast.Str
|
||||||
|
astNum = ast.Num
|
||||||
|
|
||||||
|
|
||||||
assertstate_key = StashKey["AssertionState"]()
|
assertstate_key = StashKey["AssertionState"]()
|
||||||
|
@ -680,9 +686,12 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
if (
|
if (
|
||||||
expect_docstring
|
expect_docstring
|
||||||
and isinstance(item, ast.Expr)
|
and isinstance(item, ast.Expr)
|
||||||
and isinstance(item.value, ast.Str)
|
and isinstance(item.value, astStr)
|
||||||
):
|
):
|
||||||
doc = item.value.s
|
if sys.version_info >= (3, 8):
|
||||||
|
doc = item.value.value
|
||||||
|
else:
|
||||||
|
doc = item.value.s
|
||||||
if self.is_rewrite_disabled(doc):
|
if self.is_rewrite_disabled(doc):
|
||||||
return
|
return
|
||||||
expect_docstring = False
|
expect_docstring = False
|
||||||
|
@ -814,7 +823,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
current = self.stack.pop()
|
current = self.stack.pop()
|
||||||
if self.stack:
|
if self.stack:
|
||||||
self.explanation_specifiers = self.stack[-1]
|
self.explanation_specifiers = self.stack[-1]
|
||||||
keys = [ast.Str(key) for key in current.keys()]
|
keys = [astStr(key) for key in current.keys()]
|
||||||
format_dict = ast.Dict(keys, list(current.values()))
|
format_dict = ast.Dict(keys, list(current.values()))
|
||||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||||
name = "@py_format" + str(next(self.variable_counter))
|
name = "@py_format" + str(next(self.variable_counter))
|
||||||
|
@ -868,16 +877,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||||
|
|
||||||
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
||||||
msg = self.pop_format_context(ast.Str(explanation))
|
msg = self.pop_format_context(astStr(explanation))
|
||||||
|
|
||||||
# Failed
|
# Failed
|
||||||
if assert_.msg:
|
if assert_.msg:
|
||||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||||
gluestr = "\n>assert "
|
gluestr = "\n>assert "
|
||||||
else:
|
else:
|
||||||
assertmsg = ast.Str("")
|
assertmsg = astStr("")
|
||||||
gluestr = "assert "
|
gluestr = "assert "
|
||||||
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
|
err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
|
||||||
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
||||||
err_name = ast.Name("AssertionError", ast.Load())
|
err_name = ast.Name("AssertionError", ast.Load())
|
||||||
fmt = self.helper("_format_explanation", err_msg)
|
fmt = self.helper("_format_explanation", err_msg)
|
||||||
|
@ -893,8 +902,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
hook_call_pass = ast.Expr(
|
hook_call_pass = ast.Expr(
|
||||||
self.helper(
|
self.helper(
|
||||||
"_call_assertion_pass",
|
"_call_assertion_pass",
|
||||||
ast.Num(assert_.lineno),
|
astNum(assert_.lineno),
|
||||||
ast.Str(orig),
|
astStr(orig),
|
||||||
fmt_pass,
|
fmt_pass,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -913,7 +922,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
variables = [
|
variables = [
|
||||||
ast.Name(name, ast.Store()) for name in self.format_variables
|
ast.Name(name, ast.Store()) for name in self.format_variables
|
||||||
]
|
]
|
||||||
clear_format = ast.Assign(variables, ast.NameConstant(None))
|
clear_format = ast.Assign(variables, astNameConstant(None))
|
||||||
self.statements.append(clear_format)
|
self.statements.append(clear_format)
|
||||||
|
|
||||||
else: # Original assertion rewriting
|
else: # Original assertion rewriting
|
||||||
|
@ -924,9 +933,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||||
explanation = "\n>assert " + explanation
|
explanation = "\n>assert " + explanation
|
||||||
else:
|
else:
|
||||||
assertmsg = ast.Str("")
|
assertmsg = astStr("")
|
||||||
explanation = "assert " + explanation
|
explanation = "assert " + explanation
|
||||||
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
|
template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
|
||||||
msg = self.pop_format_context(template)
|
msg = self.pop_format_context(template)
|
||||||
fmt = self.helper("_format_explanation", msg)
|
fmt = self.helper("_format_explanation", msg)
|
||||||
err_name = ast.Name("AssertionError", ast.Load())
|
err_name = ast.Name("AssertionError", ast.Load())
|
||||||
|
@ -938,7 +947,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
# Clear temporary variables by setting them to None.
|
# Clear temporary variables by setting them to None.
|
||||||
if self.variables:
|
if self.variables:
|
||||||
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
||||||
clear = ast.Assign(variables, ast.NameConstant(None))
|
clear = ast.Assign(variables, astNameConstant(None))
|
||||||
self.statements.append(clear)
|
self.statements.append(clear)
|
||||||
# Fix locations (line numbers/column offsets).
|
# Fix locations (line numbers/column offsets).
|
||||||
for stmt in self.statements:
|
for stmt in self.statements:
|
||||||
|
@ -952,20 +961,20 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
# thinks it's acceptable.
|
# thinks it's acceptable.
|
||||||
locs = ast.Call(self.builtin("locals"), [], [])
|
locs = ast.Call(self.builtin("locals"), [], [])
|
||||||
target_id = name.target.id # type: ignore[attr-defined]
|
target_id = name.target.id # type: ignore[attr-defined]
|
||||||
inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs])
|
inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])
|
||||||
dorepr = self.helper("_should_repr_global_name", name)
|
dorepr = self.helper("_should_repr_global_name", name)
|
||||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||||
expr = ast.IfExp(test, self.display(name), ast.Str(target_id))
|
expr = ast.IfExp(test, self.display(name), astStr(target_id))
|
||||||
return name, self.explanation_param(expr)
|
return name, self.explanation_param(expr)
|
||||||
|
|
||||||
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
||||||
# Display the repr of the name if it's a local variable or
|
# Display the repr of the name if it's a local variable or
|
||||||
# _should_repr_global_name() thinks it's acceptable.
|
# _should_repr_global_name() thinks it's acceptable.
|
||||||
locs = ast.Call(self.builtin("locals"), [], [])
|
locs = ast.Call(self.builtin("locals"), [], [])
|
||||||
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
|
inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
|
||||||
dorepr = self.helper("_should_repr_global_name", name)
|
dorepr = self.helper("_should_repr_global_name", name)
|
||||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||||
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
|
expr = ast.IfExp(test, self.display(name), astStr(name.id))
|
||||||
return name, self.explanation_param(expr)
|
return name, self.explanation_param(expr)
|
||||||
|
|
||||||
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
||||||
|
@ -996,12 +1005,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
]
|
]
|
||||||
):
|
):
|
||||||
pytest_temp = self.variable()
|
pytest_temp = self.variable()
|
||||||
self.variables_overwrite[v.left.target.id] = pytest_temp
|
self.variables_overwrite[
|
||||||
|
v.left.target.id
|
||||||
|
] = v.left # type:ignore[assignment]
|
||||||
v.left.target.id = pytest_temp
|
v.left.target.id = pytest_temp
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
res, expl = self.visit(v)
|
res, expl = self.visit(v)
|
||||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||||
expl_format = self.pop_format_context(ast.Str(expl))
|
expl_format = self.pop_format_context(astStr(expl))
|
||||||
call = ast.Call(app, [expl_format], [])
|
call = ast.Call(app, [expl_format], [])
|
||||||
self.expl_stmts.append(ast.Expr(call))
|
self.expl_stmts.append(ast.Expr(call))
|
||||||
if i < levels:
|
if i < levels:
|
||||||
|
@ -1013,7 +1024,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
self.statements = body = inner
|
self.statements = body = inner
|
||||||
self.statements = save
|
self.statements = save
|
||||||
self.expl_stmts = fail_save
|
self.expl_stmts = fail_save
|
||||||
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
|
expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
|
||||||
expl = self.pop_format_context(expl_template)
|
expl = self.pop_format_context(expl_template)
|
||||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||||
|
|
||||||
|
@ -1037,10 +1048,19 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
new_args = []
|
new_args = []
|
||||||
new_kwargs = []
|
new_kwargs = []
|
||||||
for arg in call.args:
|
for arg in call.args:
|
||||||
|
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
|
||||||
|
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
|
||||||
res, expl = self.visit(arg)
|
res, expl = self.visit(arg)
|
||||||
arg_expls.append(expl)
|
arg_expls.append(expl)
|
||||||
new_args.append(res)
|
new_args.append(res)
|
||||||
for keyword in call.keywords:
|
for keyword in call.keywords:
|
||||||
|
if (
|
||||||
|
isinstance(keyword.value, ast.Name)
|
||||||
|
and keyword.value.id in self.variables_overwrite
|
||||||
|
):
|
||||||
|
keyword.value = self.variables_overwrite[
|
||||||
|
keyword.value.id
|
||||||
|
] # type:ignore[assignment]
|
||||||
res, expl = self.visit(keyword.value)
|
res, expl = self.visit(keyword.value)
|
||||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||||
if keyword.arg:
|
if keyword.arg:
|
||||||
|
@ -1075,7 +1095,13 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
self.push_format_context()
|
self.push_format_context()
|
||||||
# We first check if we have overwritten a variable in the previous assert
|
# We first check if we have overwritten a variable in the previous assert
|
||||||
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
||||||
comp.left.id = self.variables_overwrite[comp.left.id]
|
comp.left = self.variables_overwrite[
|
||||||
|
comp.left.id
|
||||||
|
] # type:ignore[assignment]
|
||||||
|
if isinstance(comp.left, namedExpr):
|
||||||
|
self.variables_overwrite[
|
||||||
|
comp.left.target.id
|
||||||
|
] = comp.left # type:ignore[assignment]
|
||||||
left_res, left_expl = self.visit(comp.left)
|
left_res, left_expl = self.visit(comp.left)
|
||||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||||
left_expl = f"({left_expl})"
|
left_expl = f"({left_expl})"
|
||||||
|
@ -1093,15 +1119,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||||
and next_operand.target.id == left_res.id
|
and next_operand.target.id == left_res.id
|
||||||
):
|
):
|
||||||
next_operand.target.id = self.variable()
|
next_operand.target.id = self.variable()
|
||||||
self.variables_overwrite[left_res.id] = next_operand.target.id
|
self.variables_overwrite[
|
||||||
|
left_res.id
|
||||||
|
] = next_operand # type:ignore[assignment]
|
||||||
next_res, next_expl = self.visit(next_operand)
|
next_res, next_expl = self.visit(next_operand)
|
||||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||||
next_expl = f"({next_expl})"
|
next_expl = f"({next_expl})"
|
||||||
results.append(next_res)
|
results.append(next_res)
|
||||||
sym = BINOP_MAP[op.__class__]
|
sym = BINOP_MAP[op.__class__]
|
||||||
syms.append(ast.Str(sym))
|
syms.append(astStr(sym))
|
||||||
expl = f"{left_expl} {sym} {next_expl}"
|
expl = f"{left_expl} {sym} {next_expl}"
|
||||||
expls.append(ast.Str(expl))
|
expls.append(astStr(expl))
|
||||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||||
left_res, left_expl = next_res, next_expl
|
left_res, left_expl = next_res, next_expl
|
||||||
|
|
|
@ -213,7 +213,7 @@ class LFPluginCollWrapper:
|
||||||
|
|
||||||
@hookimpl(hookwrapper=True)
|
@hookimpl(hookwrapper=True)
|
||||||
def pytest_make_collect_report(self, collector: nodes.Collector):
|
def pytest_make_collect_report(self, collector: nodes.Collector):
|
||||||
if isinstance(collector, Session):
|
if isinstance(collector, (Session, Package)):
|
||||||
out = yield
|
out = yield
|
||||||
res: CollectReport = out.get_result()
|
res: CollectReport = out.get_result()
|
||||||
|
|
||||||
|
|
|
@ -241,7 +241,7 @@ class DontReadFromInput(TextIO):
|
||||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
||||||
|
|
||||||
def truncate(self, size: Optional[int] = None) -> int:
|
def truncate(self, size: Optional[int] = None) -> int:
|
||||||
raise UnsupportedOperation("cannont truncate stdin")
|
raise UnsupportedOperation("cannot truncate stdin")
|
||||||
|
|
||||||
def write(self, data: str) -> int:
|
def write(self, data: str) -> int:
|
||||||
raise UnsupportedOperation("cannot write to stdin")
|
raise UnsupportedOperation("cannot write to stdin")
|
||||||
|
|
|
@ -526,7 +526,10 @@ class PytestPluginManager(PluginManager):
|
||||||
# Internal API for local conftest plugin handling.
|
# Internal API for local conftest plugin handling.
|
||||||
#
|
#
|
||||||
def _set_initial_conftests(
|
def _set_initial_conftests(
|
||||||
self, namespace: argparse.Namespace, rootpath: Path
|
self,
|
||||||
|
namespace: argparse.Namespace,
|
||||||
|
rootpath: Path,
|
||||||
|
testpaths_ini: Sequence[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Load initial conftest files given a preparsed "namespace".
|
"""Load initial conftest files given a preparsed "namespace".
|
||||||
|
|
||||||
|
@ -543,7 +546,7 @@ class PytestPluginManager(PluginManager):
|
||||||
)
|
)
|
||||||
self._noconftest = namespace.noconftest
|
self._noconftest = namespace.noconftest
|
||||||
self._using_pyargs = namespace.pyargs
|
self._using_pyargs = namespace.pyargs
|
||||||
testpaths = namespace.file_or_dir
|
testpaths = namespace.file_or_dir + testpaths_ini
|
||||||
foundanchor = False
|
foundanchor = False
|
||||||
for testpath in testpaths:
|
for testpath in testpaths:
|
||||||
path = str(testpath)
|
path = str(testpath)
|
||||||
|
@ -552,7 +555,14 @@ class PytestPluginManager(PluginManager):
|
||||||
if i != -1:
|
if i != -1:
|
||||||
path = path[:i]
|
path = path[:i]
|
||||||
anchor = absolutepath(current / path)
|
anchor = absolutepath(current / path)
|
||||||
if anchor.exists(): # we found some file object
|
|
||||||
|
# Ensure we do not break if what appears to be an anchor
|
||||||
|
# is in fact a very long option (#10169).
|
||||||
|
try:
|
||||||
|
anchor_exists = anchor.exists()
|
||||||
|
except OSError: # pragma: no cover
|
||||||
|
anchor_exists = False
|
||||||
|
if anchor_exists:
|
||||||
self._try_load_conftest(anchor, namespace.importmode, rootpath)
|
self._try_load_conftest(anchor, namespace.importmode, rootpath)
|
||||||
foundanchor = True
|
foundanchor = True
|
||||||
if not foundanchor:
|
if not foundanchor:
|
||||||
|
@ -1131,7 +1141,9 @@ class Config:
|
||||||
@hookimpl(trylast=True)
|
@hookimpl(trylast=True)
|
||||||
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
||||||
self.pluginmanager._set_initial_conftests(
|
self.pluginmanager._set_initial_conftests(
|
||||||
early_config.known_args_namespace, rootpath=early_config.rootpath
|
early_config.known_args_namespace,
|
||||||
|
rootpath=early_config.rootpath,
|
||||||
|
testpaths_ini=self.getini("testpaths"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _initini(self, args: Sequence[str]) -> None:
|
def _initini(self, args: Sequence[str]) -> None:
|
||||||
|
|
|
@ -2,7 +2,6 @@ import io
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import TextIO
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
@ -11,7 +10,7 @@ from _pytest.nodes import Item
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
|
||||||
|
|
||||||
fault_handler_stderr_key = StashKey[TextIO]()
|
fault_handler_stderr_fd_key = StashKey[int]()
|
||||||
fault_handler_originally_enabled_key = StashKey[bool]()
|
fault_handler_originally_enabled_key = StashKey[bool]()
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,10 +25,9 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
def pytest_configure(config: Config) -> None:
|
def pytest_configure(config: Config) -> None:
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
stderr_fd_copy = os.dup(get_stderr_fileno())
|
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
|
||||||
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
|
|
||||||
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
||||||
faulthandler.enable(file=config.stash[fault_handler_stderr_key])
|
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
|
||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config: Config) -> None:
|
def pytest_unconfigure(config: Config) -> None:
|
||||||
|
@ -37,9 +35,9 @@ def pytest_unconfigure(config: Config) -> None:
|
||||||
|
|
||||||
faulthandler.disable()
|
faulthandler.disable()
|
||||||
# Close the dup file installed during pytest_configure.
|
# Close the dup file installed during pytest_configure.
|
||||||
if fault_handler_stderr_key in config.stash:
|
if fault_handler_stderr_fd_key in config.stash:
|
||||||
config.stash[fault_handler_stderr_key].close()
|
os.close(config.stash[fault_handler_stderr_fd_key])
|
||||||
del config.stash[fault_handler_stderr_key]
|
del config.stash[fault_handler_stderr_fd_key]
|
||||||
if config.stash.get(fault_handler_originally_enabled_key, False):
|
if config.stash.get(fault_handler_originally_enabled_key, False):
|
||||||
# Re-enable the faulthandler if it was originally enabled.
|
# Re-enable the faulthandler if it was originally enabled.
|
||||||
faulthandler.enable(file=get_stderr_fileno())
|
faulthandler.enable(file=get_stderr_fileno())
|
||||||
|
@ -67,10 +65,10 @@ def get_timeout_config_value(config: Config) -> float:
|
||||||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
||||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||||
timeout = get_timeout_config_value(item.config)
|
timeout = get_timeout_config_value(item.config)
|
||||||
stderr = item.config.stash[fault_handler_stderr_key]
|
if timeout > 0:
|
||||||
if timeout > 0 and stderr is not None:
|
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
|
stderr = item.config.stash[fault_handler_stderr_fd_key]
|
||||||
faulthandler.dump_traceback_later(timeout, file=stderr)
|
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
from _pytest._code.code import ExceptionRepr
|
from _pytest._code.code import ExceptionRepr
|
||||||
from _pytest.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import PytestPluginManager
|
from _pytest.config import PytestPluginManager
|
||||||
|
|
|
@ -302,7 +302,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
action="append",
|
action="append",
|
||||||
default=[],
|
default=[],
|
||||||
dest="logger_disable",
|
dest="logger_disable",
|
||||||
help="Disable a logger by name. Can be passed multipe times.",
|
help="Disable a logger by name. Can be passed multiple times.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import ast
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import enum
|
import enum
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import types
|
import types
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
|
@ -26,6 +27,11 @@ from typing import NoReturn
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
|
if sys.version_info >= (3, 8):
|
||||||
|
astNameConstant = ast.Constant
|
||||||
|
else:
|
||||||
|
astNameConstant = ast.NameConstant
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Expression",
|
"Expression",
|
||||||
|
@ -132,7 +138,7 @@ IDENT_PREFIX = "$"
|
||||||
|
|
||||||
def expression(s: Scanner) -> ast.Expression:
|
def expression(s: Scanner) -> ast.Expression:
|
||||||
if s.accept(TokenType.EOF):
|
if s.accept(TokenType.EOF):
|
||||||
ret: ast.expr = ast.NameConstant(False)
|
ret: ast.expr = astNameConstant(False)
|
||||||
else:
|
else:
|
||||||
ret = expr(s)
|
ret = expr(s)
|
||||||
s.accept(TokenType.EOF, reject=True)
|
s.accept(TokenType.EOF, reject=True)
|
||||||
|
|
|
@ -7,6 +7,7 @@ from contextlib import contextmanager
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Mapping
|
||||||
from typing import MutableMapping
|
from typing import MutableMapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
|
@ -129,7 +130,7 @@ class MonkeyPatch:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._setattr: List[Tuple[object, str, object]] = []
|
self._setattr: List[Tuple[object, str, object]] = []
|
||||||
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = []
|
self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
|
||||||
self._cwd: Optional[str] = None
|
self._cwd: Optional[str] = None
|
||||||
self._savesyspath: Optional[List[str]] = None
|
self._savesyspath: Optional[List[str]] = None
|
||||||
|
|
||||||
|
@ -290,12 +291,13 @@ class MonkeyPatch:
|
||||||
self._setattr.append((target, name, oldval))
|
self._setattr.append((target, name, oldval))
|
||||||
delattr(target, name)
|
delattr(target, name)
|
||||||
|
|
||||||
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
|
def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
|
||||||
"""Set dictionary entry ``name`` to value."""
|
"""Set dictionary entry ``name`` to value."""
|
||||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||||
dic[name] = value
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
dic[name] = value # type: ignore[index]
|
||||||
|
|
||||||
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
|
def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
|
||||||
"""Delete ``name`` from dict.
|
"""Delete ``name`` from dict.
|
||||||
|
|
||||||
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
||||||
|
@ -306,7 +308,8 @@ class MonkeyPatch:
|
||||||
raise KeyError(name)
|
raise KeyError(name)
|
||||||
else:
|
else:
|
||||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||||
del dic[name]
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
del dic[name] # type: ignore[attr-defined]
|
||||||
|
|
||||||
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
||||||
"""Set environment variable ``name`` to ``value``.
|
"""Set environment variable ``name`` to ``value``.
|
||||||
|
@ -401,11 +404,13 @@ class MonkeyPatch:
|
||||||
for dictionary, key, value in reversed(self._setitem):
|
for dictionary, key, value in reversed(self._setitem):
|
||||||
if value is notset:
|
if value is notset:
|
||||||
try:
|
try:
|
||||||
del dictionary[key]
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
del dictionary[key] # type: ignore[attr-defined]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass # Was already deleted, so we have the desired state.
|
pass # Was already deleted, so we have the desired state.
|
||||||
else:
|
else:
|
||||||
dictionary[key] = value
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
dictionary[key] = value # type: ignore[index]
|
||||||
self._setitem[:] = []
|
self._setitem[:] = []
|
||||||
if self._savesyspath is not None:
|
if self._savesyspath is not None:
|
||||||
sys.path[:] = self._savesyspath
|
sys.path[:] = self._savesyspath
|
||||||
|
|
|
@ -6,6 +6,7 @@ import itertools
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import types
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
@ -28,6 +29,8 @@ from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Type
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
@ -63,21 +66,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||||
return path.joinpath(".lock")
|
return path.joinpath(".lock")
|
||||||
|
|
||||||
|
|
||||||
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
def on_rm_rf_error(
|
||||||
|
func,
|
||||||
|
path: str,
|
||||||
|
excinfo: Union[
|
||||||
|
BaseException,
|
||||||
|
Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
|
||||||
|
],
|
||||||
|
*,
|
||||||
|
start_path: Path,
|
||||||
|
) -> bool:
|
||||||
"""Handle known read-only errors during rmtree.
|
"""Handle known read-only errors during rmtree.
|
||||||
|
|
||||||
The returned value is used only by our own tests.
|
The returned value is used only by our own tests.
|
||||||
"""
|
"""
|
||||||
exctype, excvalue = exc[:2]
|
if isinstance(excinfo, BaseException):
|
||||||
|
exc = excinfo
|
||||||
|
else:
|
||||||
|
exc = excinfo[1]
|
||||||
|
|
||||||
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
|
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
|
||||||
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
||||||
if isinstance(excvalue, FileNotFoundError):
|
if isinstance(exc, FileNotFoundError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not isinstance(excvalue, PermissionError):
|
if not isinstance(exc, PermissionError):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
|
PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -86,7 +101,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
PytestWarning(
|
PytestWarning(
|
||||||
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
||||||
func, path, exctype, excvalue
|
func, path, type(exc), exc
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -149,7 +164,10 @@ def rm_rf(path: Path) -> None:
|
||||||
are read-only."""
|
are read-only."""
|
||||||
path = ensure_extended_length_path(path)
|
path = ensure_extended_length_path(path)
|
||||||
onerror = partial(on_rm_rf_error, start_path=path)
|
onerror = partial(on_rm_rf_error, start_path=path)
|
||||||
shutil.rmtree(str(path), onerror=onerror)
|
if sys.version_info >= (3, 12):
|
||||||
|
shutil.rmtree(str(path), onexc=onerror)
|
||||||
|
else:
|
||||||
|
shutil.rmtree(str(path), onerror=onerror)
|
||||||
|
|
||||||
|
|
||||||
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
||||||
|
@ -335,7 +353,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||||
yield path
|
yield path
|
||||||
|
|
||||||
|
|
||||||
def cleanup_dead_symlink(root: Path):
|
def cleanup_dead_symlinks(root: Path):
|
||||||
for left_dir in root.iterdir():
|
for left_dir in root.iterdir():
|
||||||
if left_dir.is_symlink():
|
if left_dir.is_symlink():
|
||||||
if not left_dir.resolve().exists():
|
if not left_dir.resolve().exists():
|
||||||
|
@ -353,7 +371,7 @@ def cleanup_numbered_dir(
|
||||||
for path in root.glob("garbage-*"):
|
for path in root.glob("garbage-*"):
|
||||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||||
|
|
||||||
cleanup_dead_symlink(root)
|
cleanup_dead_symlinks(root)
|
||||||
|
|
||||||
|
|
||||||
def make_numbered_dir_with_cleanup(
|
def make_numbered_dir_with_cleanup(
|
||||||
|
|
|
@ -347,10 +347,6 @@ class TestReport(BaseReport):
|
||||||
elif isinstance(excinfo.value, skip.Exception):
|
elif isinstance(excinfo.value, skip.Exception):
|
||||||
outcome = "skipped"
|
outcome = "skipped"
|
||||||
r = excinfo._getreprcrash()
|
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:
|
if excinfo.value._use_item_location:
|
||||||
path, line = item.reportinfo()[:2]
|
path, line = item.reportinfo()[:2]
|
||||||
assert line is not None
|
assert line is not None
|
||||||
|
|
|
@ -28,7 +28,7 @@ from .pathlib import LOCK_TIMEOUT
|
||||||
from .pathlib import make_numbered_dir
|
from .pathlib import make_numbered_dir
|
||||||
from .pathlib import make_numbered_dir_with_cleanup
|
from .pathlib import make_numbered_dir_with_cleanup
|
||||||
from .pathlib import rm_rf
|
from .pathlib import rm_rf
|
||||||
from .pathlib import cleanup_dead_symlink
|
from .pathlib import cleanup_dead_symlinks
|
||||||
from _pytest.compat import final, get_user_id
|
from _pytest.compat import final, get_user_id
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
@ -100,7 +100,7 @@ class TempPathFactory:
|
||||||
policy = config.getini("tmp_path_retention_policy")
|
policy = config.getini("tmp_path_retention_policy")
|
||||||
if policy not in ("all", "failed", "none"):
|
if policy not in ("all", "failed", "none"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"tmp_path_retention_policy must be either all, failed, none. Current intput: {policy}."
|
f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
|
@ -289,31 +289,30 @@ def tmp_path(
|
||||||
|
|
||||||
del request.node.stash[tmppath_result_key]
|
del request.node.stash[tmppath_result_key]
|
||||||
|
|
||||||
# remove dead symlink
|
|
||||||
basetemp = tmp_path_factory._basetemp
|
|
||||||
if basetemp is None:
|
|
||||||
return
|
|
||||||
cleanup_dead_symlink(basetemp)
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
|
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
|
||||||
"""After each session, remove base directory if all the tests passed,
|
"""After each session, remove base directory if all the tests passed,
|
||||||
the policy is "failed", and the basetemp is not specified by a user.
|
the policy is "failed", and the basetemp is not specified by a user.
|
||||||
"""
|
"""
|
||||||
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
||||||
if tmp_path_factory._basetemp is None:
|
basetemp = tmp_path_factory._basetemp
|
||||||
|
if basetemp is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
policy = tmp_path_factory._retention_policy
|
policy = tmp_path_factory._retention_policy
|
||||||
if (
|
if (
|
||||||
exitstatus == 0
|
exitstatus == 0
|
||||||
and policy == "failed"
|
and policy == "failed"
|
||||||
and tmp_path_factory._given_basetemp is None
|
and tmp_path_factory._given_basetemp is None
|
||||||
):
|
):
|
||||||
passed_dir = tmp_path_factory._basetemp
|
if basetemp.is_dir():
|
||||||
if passed_dir.exists():
|
|
||||||
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
||||||
# permissions, etc, in which case we ignore it.
|
# permissions, etc, in which case we ignore it.
|
||||||
rmtree(passed_dir, ignore_errors=True)
|
rmtree(basetemp, ignore_errors=True)
|
||||||
|
|
||||||
|
# Remove dead symlinks.
|
||||||
|
if basetemp.is_dir():
|
||||||
|
cleanup_dead_symlinks(basetemp)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
|
|
|
@ -298,6 +298,9 @@ class TestCaseFunction(Function):
|
||||||
def stopTest(self, testcase: "unittest.TestCase") -> None:
|
def stopTest(self, testcase: "unittest.TestCase") -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
||||||
|
|
||||||
|
|
|
@ -149,7 +149,7 @@ def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
|
||||||
"""
|
"""
|
||||||
Issue the warning :param:`message` for the definition of the given :param:`method`
|
Issue the warning :param:`message` for the definition of the given :param:`method`
|
||||||
|
|
||||||
this helps to log warnigns for functions defined prior to finding an issue with them
|
this helps to log warnings for functions defined prior to finding an issue with them
|
||||||
(like hook wrappers being marked in a legacy mechanism)
|
(like hook wrappers being marked in a legacy mechanism)
|
||||||
"""
|
"""
|
||||||
lineno = method.__code__.co_firstlineno
|
lineno = method.__code__.co_firstlineno
|
||||||
|
|
|
@ -695,11 +695,15 @@ class TestInvocationVariants:
|
||||||
monkeypatch.chdir("world")
|
monkeypatch.chdir("world")
|
||||||
|
|
||||||
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
|
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
|
||||||
|
# pgk_resources has been deprecated entirely.
|
||||||
# While we could change the test to use implicit namespace packages, seems better
|
# While we could change the test to use implicit namespace packages, seems better
|
||||||
# to still ensure the old declaration via declare_namespace still works.
|
# to still ensure the old declaration via declare_namespace still works.
|
||||||
ignore_w = r"-Wignore:Deprecated call to `pkg_resources.declare_namespace"
|
ignore_w = (
|
||||||
|
r"-Wignore:Deprecated call to `pkg_resources.declare_namespace",
|
||||||
|
r"-Wignore:pkg_resources is deprecated",
|
||||||
|
)
|
||||||
result = pytester.runpytest(
|
result = pytester.runpytest(
|
||||||
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", ignore_w
|
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", *ignore_w
|
||||||
)
|
)
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
|
|
|
@ -294,7 +294,6 @@ class TestTraceback_f_g_h:
|
||||||
excinfo = pytest.raises(ValueError, f)
|
excinfo = pytest.raises(ValueError, f)
|
||||||
tb = excinfo.traceback
|
tb = excinfo.traceback
|
||||||
entry = tb.getcrashentry()
|
entry = tb.getcrashentry()
|
||||||
assert entry is not None
|
|
||||||
co = _pytest._code.Code.from_function(h)
|
co = _pytest._code.Code.from_function(h)
|
||||||
assert entry.frame.code.path == co.path
|
assert entry.frame.code.path == co.path
|
||||||
assert entry.lineno == co.firstlineno + 1
|
assert entry.lineno == co.firstlineno + 1
|
||||||
|
@ -312,7 +311,10 @@ class TestTraceback_f_g_h:
|
||||||
excinfo = pytest.raises(ValueError, f)
|
excinfo = pytest.raises(ValueError, f)
|
||||||
tb = excinfo.traceback
|
tb = excinfo.traceback
|
||||||
entry = tb.getcrashentry()
|
entry = tb.getcrashentry()
|
||||||
assert entry is None
|
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"
|
||||||
|
|
||||||
|
|
||||||
def test_excinfo_exconly():
|
def test_excinfo_exconly():
|
||||||
|
@ -1573,3 +1575,20 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
|
||||||
# with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it
|
# with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it
|
||||||
pytest.importorskip("exceptiongroup")
|
pytest.importorskip("exceptiongroup")
|
||||||
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
|
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_entries_hidden_doesnt_crash(pytester: Pytester) -> None:
|
||||||
|
"""Regression test for #10903.
|
||||||
|
|
||||||
|
We're not really sure what should be *displayed* here, so this test
|
||||||
|
just verified that at least it doesn't crash.
|
||||||
|
"""
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def test():
|
||||||
|
__tracebackhide__ = True
|
||||||
|
1 / 0
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 1
|
||||||
|
|
|
@ -897,25 +897,29 @@ class TestConftestCustomization:
|
||||||
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
|
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
|
||||||
"""Ensure we can collect files with weird file extensions as Python
|
"""Ensure we can collect files with weird file extensions as Python
|
||||||
modules (#2369)"""
|
modules (#2369)"""
|
||||||
# We'll implement a little finder and loader to import files containing
|
# Implement a little meta path finder to import files containing
|
||||||
# Python source code whose file extension is ".narf".
|
# Python source code whose file extension is ".narf".
|
||||||
pytester.makeconftest(
|
pytester.makeconftest(
|
||||||
"""
|
"""
|
||||||
import sys, os, imp
|
import sys
|
||||||
|
import os.path
|
||||||
|
from importlib.util import spec_from_loader
|
||||||
|
from importlib.machinery import SourceFileLoader
|
||||||
from _pytest.python import Module
|
from _pytest.python import Module
|
||||||
|
|
||||||
class Loader(object):
|
class MetaPathFinder:
|
||||||
def load_module(self, name):
|
def find_spec(self, fullname, path, target=None):
|
||||||
return imp.load_source(name, name + ".narf")
|
if os.path.exists(fullname + ".narf"):
|
||||||
class Finder(object):
|
return spec_from_loader(
|
||||||
def find_module(self, name, path=None):
|
fullname,
|
||||||
if os.path.exists(name + ".narf"):
|
SourceFileLoader(fullname, fullname + ".narf"),
|
||||||
return Loader()
|
)
|
||||||
sys.meta_path.append(Finder())
|
sys.meta_path.append(MetaPathFinder())
|
||||||
|
|
||||||
def pytest_collect_file(file_path, parent):
|
def pytest_collect_file(file_path, parent):
|
||||||
if file_path.suffix == ".narf":
|
if file_path.suffix == ".narf":
|
||||||
return Module.from_parent(path=file_path, parent=parent)"""
|
return Module.from_parent(path=file_path, parent=parent)
|
||||||
|
"""
|
||||||
)
|
)
|
||||||
pytester.makefile(
|
pytester.makefile(
|
||||||
".narf",
|
".narf",
|
||||||
|
|
|
@ -1436,6 +1436,96 @@ class TestIssue10743:
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
sys.version_info < (3, 8), reason="walrus operator not available in py<38"
|
||||||
|
)
|
||||||
|
class TestIssue11028:
|
||||||
|
def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def test_in_string():
|
||||||
|
assert (obj := "foo") in obj
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_assertion_walrus_operator_in_operand_json_dumps(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
|
||||||
|
def test_json_encoder():
|
||||||
|
assert (obj := "foo") in json.dumps(obj)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_assertion_walrus_operator_equals_operand_function(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def f(a):
|
||||||
|
return a
|
||||||
|
|
||||||
|
def test_call_other_function_arg():
|
||||||
|
assert (obj := "foo") == f(obj)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_assertion_walrus_operator_equals_operand_function_keyword_arg(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def f(a='test'):
|
||||||
|
return a
|
||||||
|
|
||||||
|
def test_call_other_function_k_arg():
|
||||||
|
assert (obj := "foo") == f(a=obj)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_assertion_walrus_operator_equals_operand_function_arg_as_function(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def f(a='test'):
|
||||||
|
return a
|
||||||
|
|
||||||
|
def test_function_of_function():
|
||||||
|
assert (obj := "foo") == f(f(obj))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
def test_assertion_walrus_operator_gt_operand_function(
|
||||||
|
self, pytester: Pytester
|
||||||
|
) -> None:
|
||||||
|
pytester.makepyfile(
|
||||||
|
"""
|
||||||
|
def add_one(a):
|
||||||
|
return a + 1
|
||||||
|
|
||||||
|
def test_gt():
|
||||||
|
assert (obj := 4) > add_one(obj)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
assert result.ret == 1
|
||||||
|
result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||||
)
|
)
|
||||||
|
|
|
@ -420,7 +420,13 @@ class TestLastFailed:
|
||||||
result = pytester.runpytest()
|
result = pytester.runpytest()
|
||||||
result.stdout.fnmatch_lines(["*1 failed in*"])
|
result.stdout.fnmatch_lines(["*1 failed in*"])
|
||||||
|
|
||||||
def test_terminal_report_lastfailed(self, pytester: Pytester) -> None:
|
@pytest.mark.parametrize("parent", ("session", "package"))
|
||||||
|
def test_terminal_report_lastfailed(self, pytester: Pytester, parent: str) -> None:
|
||||||
|
if parent == "package":
|
||||||
|
pytester.makepyfile(
|
||||||
|
__init__="",
|
||||||
|
)
|
||||||
|
|
||||||
test_a = pytester.makepyfile(
|
test_a = pytester.makepyfile(
|
||||||
test_a="""
|
test_a="""
|
||||||
def test_a1(): pass
|
def test_a1(): pass
|
||||||
|
|
|
@ -1247,6 +1247,48 @@ def test_collect_pyargs_with_testpaths(
|
||||||
result.stdout.fnmatch_lines(["*1 passed in*"])
|
result.stdout.fnmatch_lines(["*1 passed in*"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_initial_conftests_with_testpaths(pytester: Pytester) -> None:
|
||||||
|
"""The testpaths ini option should load conftests in those paths as 'initial' (#10987)."""
|
||||||
|
p = pytester.mkdir("some_path")
|
||||||
|
p.joinpath("conftest.py").write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""
|
||||||
|
def pytest_sessionstart(session):
|
||||||
|
raise Exception("pytest_sessionstart hook successfully run")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
testpaths = some_path
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
"INTERNALERROR* Exception: pytest_sessionstart hook successfully run"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_large_option_breaks_initial_conftests(pytester: Pytester) -> None:
|
||||||
|
"""Long option values do not break initial conftests handling (#10169)."""
|
||||||
|
option_value = "x" * 1024 * 1000
|
||||||
|
pytester.makeconftest(
|
||||||
|
"""
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption("--xx", default=None)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makepyfile(
|
||||||
|
f"""
|
||||||
|
def test_foo(request):
|
||||||
|
assert request.config.getoption("xx") == {option_value!r}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest(f"--xx={option_value}")
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
def test_collect_symlink_file_arg(pytester: Pytester) -> None:
|
def test_collect_symlink_file_arg(pytester: Pytester) -> None:
|
||||||
"""Collect a direct symlink works even if it does not match python_files (#4325)."""
|
"""Collect a direct symlink works even if it does not match python_files (#4325)."""
|
||||||
real = pytester.makepyfile(
|
real = pytester.makepyfile(
|
||||||
|
|
|
@ -35,7 +35,7 @@ def conftest_setinitial(
|
||||||
self.importmode = "prepend"
|
self.importmode = "prepend"
|
||||||
|
|
||||||
namespace = cast(argparse.Namespace, Namespace())
|
namespace = cast(argparse.Namespace, Namespace())
|
||||||
conftest._set_initial_conftests(namespace, rootpath=Path(args[0]))
|
conftest._set_initial_conftests(namespace, rootpath=Path(args[0]), testpaths_ini=[])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("_sys_snapshot")
|
@pytest.mark.usefixtures("_sys_snapshot")
|
||||||
|
|
|
@ -425,9 +425,7 @@ def test_context_classmethod() -> None:
|
||||||
assert A.x == 1
|
assert A.x == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings(
|
@pytest.mark.filterwarnings(r"ignore:.*\bpkg_resources\b:DeprecationWarning")
|
||||||
"ignore:Deprecated call to `pkg_resources.declare_namespace"
|
|
||||||
)
|
|
||||||
def test_syspath_prepend_with_namespace_packages(
|
def test_syspath_prepend_with_namespace_packages(
|
||||||
pytester: Pytester, monkeypatch: MonkeyPatch
|
pytester: Pytester, monkeypatch: MonkeyPatch
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -82,7 +82,7 @@ def test_no_ini(pytester: Pytester, file_structure) -> None:
|
||||||
|
|
||||||
def test_clean_up(pytester: Pytester) -> None:
|
def test_clean_up(pytester: Pytester) -> None:
|
||||||
"""Test that the plugin cleans up after itself."""
|
"""Test that the plugin cleans up after itself."""
|
||||||
# This is tough to test behaviorly because the cleanup really runs last.
|
# This is tough to test behaviorally because the cleanup really runs last.
|
||||||
# So the test make several implementation assumptions:
|
# So the test make several implementation assumptions:
|
||||||
# - Cleanup is done in pytest_unconfigure().
|
# - Cleanup is done in pytest_unconfigure().
|
||||||
# - Not a hookwrapper.
|
# - Not a hookwrapper.
|
||||||
|
|
|
@ -512,20 +512,20 @@ class TestRmRf:
|
||||||
|
|
||||||
# unknown exception
|
# unknown exception
|
||||||
with pytest.warns(pytest.PytestWarning):
|
with pytest.warns(pytest.PytestWarning):
|
||||||
exc_info1 = (None, RuntimeError(), None)
|
exc_info1 = (RuntimeError, RuntimeError(), None)
|
||||||
on_rm_rf_error(os.unlink, str(fn), exc_info1, start_path=tmp_path)
|
on_rm_rf_error(os.unlink, str(fn), exc_info1, start_path=tmp_path)
|
||||||
assert fn.is_file()
|
assert fn.is_file()
|
||||||
|
|
||||||
# we ignore FileNotFoundError
|
# we ignore FileNotFoundError
|
||||||
exc_info2 = (None, FileNotFoundError(), None)
|
exc_info2 = (FileNotFoundError, FileNotFoundError(), None)
|
||||||
assert not on_rm_rf_error(None, str(fn), exc_info2, start_path=tmp_path)
|
assert not on_rm_rf_error(None, str(fn), exc_info2, start_path=tmp_path)
|
||||||
|
|
||||||
# unknown function
|
# unknown function
|
||||||
with pytest.warns(
|
with pytest.warns(
|
||||||
pytest.PytestWarning,
|
pytest.PytestWarning,
|
||||||
match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\nNone: ",
|
match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\n<class 'PermissionError'>: ",
|
||||||
):
|
):
|
||||||
exc_info3 = (None, PermissionError(), None)
|
exc_info3 = (PermissionError, PermissionError(), None)
|
||||||
on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path)
|
on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path)
|
||||||
assert fn.is_file()
|
assert fn.is_file()
|
||||||
|
|
||||||
|
@ -533,12 +533,12 @@ class TestRmRf:
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter("ignore")
|
warnings.simplefilter("ignore")
|
||||||
with pytest.warns(None) as warninfo: # type: ignore[call-overload]
|
with pytest.warns(None) as warninfo: # type: ignore[call-overload]
|
||||||
exc_info4 = (None, PermissionError(), None)
|
exc_info4 = PermissionError()
|
||||||
on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path)
|
on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path)
|
||||||
assert fn.is_file()
|
assert fn.is_file()
|
||||||
assert not [x.message for x in warninfo]
|
assert not [x.message for x in warninfo]
|
||||||
|
|
||||||
exc_info5 = (None, PermissionError(), None)
|
exc_info5 = PermissionError()
|
||||||
on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path)
|
on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path)
|
||||||
assert not fn.is_file()
|
assert not fn.is_file()
|
||||||
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
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
|
|
|
@ -9,6 +9,7 @@ from typing import Optional
|
||||||
from typing_extensions import assert_type
|
from typing_extensions import assert_type
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pytest import MonkeyPatch
|
||||||
|
|
||||||
|
|
||||||
# Issue #7488.
|
# Issue #7488.
|
||||||
|
@ -29,6 +30,19 @@ def check_parametrize_ids_callable(func) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Issue #10999.
|
||||||
|
def check_monkeypatch_typeddict(monkeypatch: MonkeyPatch) -> None:
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Foo(TypedDict):
|
||||||
|
x: int
|
||||||
|
y: float
|
||||||
|
|
||||||
|
a: Foo = {"x": 1, "y": 3.14}
|
||||||
|
monkeypatch.setitem(a, "x", 2)
|
||||||
|
monkeypatch.delitem(a, "y")
|
||||||
|
|
||||||
|
|
||||||
def check_raises_is_a_context_manager(val: bool) -> None:
|
def check_raises_is_a_context_manager(val: bool) -> None:
|
||||||
with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo:
|
with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo:
|
||||||
pass
|
pass
|
||||||
|
|
Loading…
Reference in New Issue