Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf3bcb2bf1 | |||
| ecb23106d8 | |||
| 8174a30164 | |||
| 0142bb6687 | |||
| 52cf700f1b | |||
| 6b3ece5bdb | |||
| 4059000834 | |||
| 7d5207a736 | |||
| 3e65a461c7 | |||
| fc3b5b2610 | |||
| 9335a0b445 | |||
| 0ded3297a9 | |||
| fc9cbbd4c4 | |||
| 1a17539065 | |||
| 32de8e2893 | |||
| 5d4a342a7a | |||
| 1790f17228 | |||
| ee8baa2676 | |||
| ae38b076da | |||
| 85c5bd26b6 | |||
| e1204e1e23 | |||
| 313b61471f | |||
| 1daa8129c6 | |||
| b5ff089d2c | |||
| fda8024622 | |||
| 4b823a42ce | |||
| 6c9b277ce4 | |||
| 3de43e5102 | |||
| c76ae74bd7 | |||
| 24534cdd29 | |||
| 99c78aa93a | |||
| 3a6bdcd76b | |||
| 4a1bba25b9 | |||
| 9e1add75f7 | |||
| 4da9026766 | |||
| 7c231baa64 | |||
| fc538c5766 | |||
| fbfd4b5005 | |||
| ec752537ea | |||
| dd667336ce | |||
| 29d16d2939 | |||
| 5313d50e18 | |||
| 4f3f36c396 | |||
| af124c7f21 | |||
| 4e6d53fef5 | |||
| 751d726d21 | |||
| 9e491f430e | |||
| 63f258f432 | |||
| baaa67dfb9 | |||
| 519f351b4f | |||
| 5d53447a73 | |||
| 1716d3c9bf | |||
| ac699e7b25 | |||
| a5f37199a9 | |||
| 9fa82598a9 | |||
| ba32a3bd87 | |||
| c8641f879f | |||
| 6041511fb4 | |||
| 739408b958 | |||
| 1636322995 | |||
| b8edacb8f1 | |||
| 612489e2bd | |||
| 383774db10 | |||
| 3b5b3cf50e | |||
| 23e343af60 | |||
| f9a995b56d | |||
| d9d78a8aef | |||
| 76d15231f5 | |||
| 4cc05e7bee | |||
| 2d57d5c32f | |||
| faeb16146b | |||
| b241c0b479 | |||
| 78403237cf | |||
| f84fea0888 | |||
| 271bdf6c23 | |||
| fd56968f2b | |||
| 5b75b0d03f | |||
| aac5d5d08b | |||
| b1460f3261 | |||
| a88ae8289c | |||
| 62320e4ff7 | |||
| 6514041a35 | |||
| 7d548c38e2 | |||
| 07eeeb8dfc | |||
| be774667c2 | |||
| 762bb61562 | |||
| 725de3a0d3 | |||
| fcada1ea47 | |||
| 6f7f89f3c4 | |||
| 0a20452f78 | |||
| cc23ec91d0 | |||
| a15f544962 | |||
| 3823ce60dd | |||
| e03f82c359 | |||
| 158f41fdf8 | |||
| fd6a4507ac | |||
| 15156757b6 | |||
| 0860f4e916 | |||
| 11965d1c27 | |||
| 14be71b234 | |||
| 2d6206b89a | |||
| 41f57ef95d | |||
| 4eca6063c8 | |||
| 819f5abd73 | |||
| adf891ab1d | |||
| f08184ba20 | |||
| 28783e5d23 | |||
| 7834b3b07f | |||
| ece756fcb4 | |||
| d380771065 | |||
| b893d2a0fe | |||
| e3b1799766 | |||
| 5d1385320f | |||
| eff54aece1 | |||
| bc1fc3f0fc | |||
| 784ffa0fad | |||
| 90412827c3 | |||
| 424c3eebde | |||
| 9c2247ec1b | |||
| 61f7c27ec0 | |||
| 1b81d636e2 | |||
| 1b196fbeaf | |||
| 22524046cf | |||
| 6dcd652d4a | |||
| be9faa68d8 | |||
| e4e13dd913 | |||
| 5a399030c1 | |||
| 19c8ef63a4 | |||
| 22951bba67 | |||
| ec8e23951d | |||
| bf47357511 | |||
| 2d2dc4a2a8 | |||
| f1c7585184 |
@@ -0,0 +1,23 @@
|
||||
name: close needs-information issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
debug-only: false
|
||||
days-before-issue-stale: 14
|
||||
days-before-issue-close: 7
|
||||
only-labels: "status: needs information"
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
@@ -43,6 +43,7 @@ jobs:
|
||||
"windows-py39",
|
||||
"windows-py310",
|
||||
"windows-py311",
|
||||
"windows-py312",
|
||||
|
||||
"ubuntu-py37",
|
||||
"ubuntu-py37-pluggy",
|
||||
@@ -51,12 +52,13 @@ jobs:
|
||||
"ubuntu-py39",
|
||||
"ubuntu-py310",
|
||||
"ubuntu-py311",
|
||||
"ubuntu-py312",
|
||||
"ubuntu-pypy3",
|
||||
|
||||
"macos-py37",
|
||||
"macos-py38",
|
||||
"macos-py39",
|
||||
"macos-py310",
|
||||
"macos-py312",
|
||||
|
||||
"docs",
|
||||
"doctesting",
|
||||
@@ -86,9 +88,13 @@ jobs:
|
||||
os: windows-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "windows-py311"
|
||||
python: "3.11-dev"
|
||||
python: "3.11"
|
||||
os: windows-latest
|
||||
tox_env: "py311"
|
||||
- name: "windows-py312"
|
||||
python: "3.12-dev"
|
||||
os: windows-latest
|
||||
tox_env: "py312"
|
||||
|
||||
- name: "ubuntu-py37"
|
||||
python: "3.7"
|
||||
@@ -116,10 +122,15 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "ubuntu-py311"
|
||||
python: "3.11-dev"
|
||||
python: "3.11"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py311"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py312"
|
||||
python: "3.12-dev"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py312"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-pypy3"
|
||||
python: "pypy-3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -129,19 +140,19 @@ jobs:
|
||||
python: "3.7"
|
||||
os: macos-latest
|
||||
tox_env: "py37-xdist"
|
||||
- name: "macos-py38"
|
||||
python: "3.8"
|
||||
os: macos-latest
|
||||
tox_env: "py38-xdist"
|
||||
use_coverage: true
|
||||
- name: "macos-py39"
|
||||
python: "3.9"
|
||||
os: macos-latest
|
||||
tox_env: "py39-xdist"
|
||||
use_coverage: true
|
||||
- name: "macos-py310"
|
||||
python: "3.10"
|
||||
os: macos-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "macos-py312"
|
||||
python: "3.12-dev"
|
||||
os: macos-latest
|
||||
tox_env: "py312-xdist"
|
||||
|
||||
- name: "plugins"
|
||||
python: "3.9"
|
||||
@@ -168,6 +179,7 @@ jobs:
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54
|
||||
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
default_language_version:
|
||||
python: "3.10"
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
@@ -23,7 +21,7 @@ repos:
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.0.2
|
||||
rev: v2.1.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
@@ -38,27 +36,27 @@ repos:
|
||||
additional_dependencies:
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.9.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py37-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
rev: v3.4.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.2.0
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- 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
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.1.1
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
||||
@@ -8,11 +8,14 @@ Abdeali JK
|
||||
Abdelrahman Elbehery
|
||||
Abhijeet Kasurde
|
||||
Adam Johnson
|
||||
Adam Stewart
|
||||
Adam Uhlir
|
||||
Ahn Ki-Wook
|
||||
Akiomi Kamakura
|
||||
Alan Velasco
|
||||
Alessio Izzo
|
||||
Alex Jones
|
||||
Alex Lambson
|
||||
Alexander Johnson
|
||||
Alexander King
|
||||
Alexei Kozlenok
|
||||
@@ -55,6 +58,7 @@ Benjamin Peterson
|
||||
Bernard Pratz
|
||||
Bob Ippolito
|
||||
Brian Dorsey
|
||||
Brian Larsen
|
||||
Brian Maissy
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
@@ -68,6 +72,7 @@ Charles Cloud
|
||||
Charles Machalow
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Cheuk Ting Ho
|
||||
Chris Mahoney
|
||||
Chris Lamb
|
||||
Chris NeJame
|
||||
Chris Rose
|
||||
@@ -126,6 +131,7 @@ Eric Siegerman
|
||||
Erik Aronesty
|
||||
Erik M. Bray
|
||||
Evan Kepner
|
||||
Evgeny Seliverstov
|
||||
Fabien Zarifian
|
||||
Fabio Zadrozny
|
||||
Felix Hofstätter
|
||||
@@ -192,6 +198,7 @@ Justice Ndou
|
||||
Justyna Janczyszyn
|
||||
Kale Kundert
|
||||
Kamran Ahmad
|
||||
Kenny Y
|
||||
Karl O. Pinc
|
||||
Karthikeyan Singaravelan
|
||||
Katarzyna Jachim
|
||||
@@ -304,6 +311,7 @@ Rafal Semik
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
Robert Holt
|
||||
Roberto Aldera
|
||||
Roberto Polli
|
||||
Roland Puntaier
|
||||
Romain Dorgueil
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
|
||||
@@ -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 @@
|
||||
Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
|
||||
@@ -0,0 +1,2 @@
|
||||
Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
|
||||
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
|
||||
@@ -0,0 +1,5 @@
|
||||
When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
|
||||
|
||||
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
|
||||
|
||||
Previously, the last frame of the traceback was shown, even though it was hidden.
|
||||
@@ -0,0 +1,3 @@
|
||||
Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
|
||||
|
||||
Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>` as a helper for that.
|
||||
@@ -0,0 +1 @@
|
||||
Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
|
||||
@@ -0,0 +1 @@
|
||||
Added underlying exception to cache provider path creation and write warning messages.
|
||||
@@ -0,0 +1 @@
|
||||
Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
|
||||
@@ -0,0 +1 @@
|
||||
Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
|
||||
@@ -0,0 +1,3 @@
|
||||
When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir`.
|
||||
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
|
||||
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
|
||||
@@ -0,0 +1,7 @@
|
||||
The :confval:`norecursedir` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
|
||||
|
||||
If after updating to this version you see that your `norecursedir` setting is not being respected,
|
||||
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
|
||||
Most likely, your hook returns `False` for paths it does not want to ignore,
|
||||
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
|
||||
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
|
||||
@@ -1 +1 @@
|
||||
Correctly handle ``__tracebackhide__`` for chained exceptions.
|
||||
Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,3 @@
|
||||
:func:`_pytest.logging.LogCaptureFixture.set_level` and :func:`_pytest.logging.LogCaptureFixture.at_level`
|
||||
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
|
||||
``logging.disable(LEVEL)``.
|
||||
@@ -6,6 +6,9 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.3.2
|
||||
release-7.3.1
|
||||
release-7.3.0
|
||||
release-7.2.2
|
||||
release-7.2.1
|
||||
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+
|
||||
3.3 - 4.6 2.7, 3.4+
|
||||
============== ===================
|
||||
|
||||
`Status of Python Versions <https://devguide.python.org/versions/>`__.
|
||||
|
||||
+1
-1
@@ -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.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.
|
||||
|
||||
The fixture provides these methods to modify objects, dictionaries, or
|
||||
|
||||
+125
-2
@@ -28,6 +28,129 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. 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)
|
||||
=========================
|
||||
|
||||
@@ -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`.
|
||||
|
||||
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)`.
|
||||
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.
|
||||
|
||||
|
||||
- :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.
|
||||
|
||||
|
||||
+1
-1
@@ -341,7 +341,7 @@ epub_copyright = "2013, holger krekel et alii"
|
||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
||||
# 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.
|
||||
# epub_identifier = ''
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
|
||||
" no further details known at this point.",
|
||||
]
|
||||
)
|
||||
return super().repr_failure(excinfo)
|
||||
|
||||
def reportinfo(self):
|
||||
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
|
||||
|
||||
. $ pytest -rs -q multipython.py
|
||||
........................... [100%]
|
||||
27 passed in 0.12s
|
||||
sssssssssssssssssssssssssss [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [9] multipython.py:69: 'python3.5' not found
|
||||
SKIPPED [9] multipython.py:69: 'python3.6' not found
|
||||
SKIPPED [9] multipython.py:69: 'python3.7' not found
|
||||
27 skipped in 0.12s
|
||||
|
||||
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()
|
||||
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
|
||||
_________________ TestSpecialisedExplanations.test_eq_text _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0006>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0007>
|
||||
|
||||
def test_eq_text(self):
|
||||
> 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
|
||||
_____________ 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):
|
||||
> 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
|
||||
____________ 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):
|
||||
> 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
|
||||
______________ 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):
|
||||
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
|
||||
_________ 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):
|
||||
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
|
||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000b>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000c>
|
||||
|
||||
def test_eq_list(self):
|
||||
> 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
|
||||
______________ 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):
|
||||
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
|
||||
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000d>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
|
||||
|
||||
def test_eq_dict(self):
|
||||
> 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
|
||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000f>
|
||||
|
||||
def test_eq_set(self):
|
||||
> 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
|
||||
_____________ 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):
|
||||
> 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
|
||||
_________________ TestSpecialisedExplanations.test_in_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0010>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0011>
|
||||
|
||||
def test_in_list(self):
|
||||
> 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
|
||||
__________ 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):
|
||||
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
|
||||
___________ 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):
|
||||
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
|
||||
_________ 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):
|
||||
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
|
||||
______ 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):
|
||||
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
|
||||
______________ TestSpecialisedExplanations.test_eq_dataclass _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0015>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
|
||||
|
||||
def test_eq_dataclass(self):
|
||||
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
|
||||
________________ TestSpecialisedExplanations.test_eq_attrs _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0017>
|
||||
|
||||
def test_eq_attrs(self):
|
||||
import attr
|
||||
@@ -345,7 +345,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
i = Foo()
|
||||
> assert i.b == 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
|
||||
_________________________ test_attribute_instance __________________________
|
||||
@@ -356,8 +356,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
|
||||
> assert Foo().b == 2
|
||||
E AssertionError: assert 1 == 2
|
||||
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0018>.b
|
||||
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0018> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
||||
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 0xdeadbeef0019> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
||||
|
||||
failure_demo.py:135: AssertionError
|
||||
__________________________ test_attribute_failure __________________________
|
||||
@@ -375,7 +375,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
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):
|
||||
> 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
|
||||
E AssertionError: assert 1 == 2
|
||||
E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001a>.b
|
||||
E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001a> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
|
||||
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001b>.b
|
||||
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
||||
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 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
|
||||
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 0xdeadbeef001c> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
||||
|
||||
failure_demo.py:156: AssertionError
|
||||
__________________________ TestRaises.test_raises __________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001c>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001d>
|
||||
|
||||
def test_raises(self):
|
||||
s = "qwe"
|
||||
@@ -411,7 +411,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:166: ValueError
|
||||
______________________ TestRaises.test_raises_doesnt _______________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001d>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001e>
|
||||
|
||||
def test_raises_doesnt(self):
|
||||
> 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
|
||||
__________________________ TestRaises.test_raise ___________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001e>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001f>
|
||||
|
||||
def test_raise(self):
|
||||
> 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
|
||||
________________________ TestRaises.test_tupleerror ________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef001f>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
||||
|
||||
def test_tupleerror(self):
|
||||
> 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
|
||||
______ 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):
|
||||
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]
|
||||
________________________ TestRaises.test_some_error ________________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0021>
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef0022>
|
||||
|
||||
def test_some_error(self):
|
||||
> if namenotexi: # NOQA
|
||||
@@ -482,7 +482,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
abc-123:2: AssertionError
|
||||
____________________ TestMoreErrors.test_complex_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0022>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0023>
|
||||
|
||||
def test_complex_error(self):
|
||||
def f():
|
||||
@@ -508,7 +508,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:6: AssertionError
|
||||
___________________ 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):
|
||||
items = []
|
||||
@@ -518,7 +518,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:217: ValueError
|
||||
____________________ 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):
|
||||
items = 3
|
||||
@@ -528,20 +528,20 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:221: TypeError
|
||||
______________________ TestMoreErrors.test_startswith ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0025>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0026>
|
||||
|
||||
def test_startswith(self):
|
||||
s = "123"
|
||||
g = "456"
|
||||
> assert s.startswith(g)
|
||||
E AssertionError: assert False
|
||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0026>('456')
|
||||
E + where <built-in method startswith of str object at 0xdeadbeef0026> = '123'.startswith
|
||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
|
||||
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
|
||||
|
||||
failure_demo.py:226: AssertionError
|
||||
__________________ TestMoreErrors.test_startswith_nested ___________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0027>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0028>
|
||||
|
||||
def test_startswith_nested(self):
|
||||
def f():
|
||||
@@ -552,15 +552,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
|
||||
> assert f().startswith(g())
|
||||
E AssertionError: assert False
|
||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0026>('456')
|
||||
E + where <built-in method startswith of str object at 0xdeadbeef0026> = '123'.startswith
|
||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0028>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
|
||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
|
||||
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
|
||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
|
||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
|
||||
|
||||
failure_demo.py:235: AssertionError
|
||||
_____________________ TestMoreErrors.test_global_func ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
||||
|
||||
def test_global_func(self):
|
||||
> 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
|
||||
_______________________ TestMoreErrors.test_instance _______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||
|
||||
def test_instance(self):
|
||||
self.x = 6 * 7
|
||||
> assert self.x != 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
|
||||
_______________________ TestMoreErrors.test_compare ________________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
||||
|
||||
def test_compare(self):
|
||||
> 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
|
||||
_____________________ TestMoreErrors.test_try_finally ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
|
||||
|
||||
def test_try_finally(self):
|
||||
x = 1
|
||||
@@ -603,7 +603,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:250: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_single_line ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002e>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
||||
|
||||
def test_single_line(self):
|
||||
class A:
|
||||
@@ -618,7 +618,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:261: AssertionError
|
||||
____________________ TestCustomAssertMsg.test_multiline ____________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
||||
|
||||
def test_multiline(self):
|
||||
class A:
|
||||
@@ -637,7 +637,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
failure_demo.py:268: AssertionError
|
||||
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
||||
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
|
||||
|
||||
def test_custom_repr(self):
|
||||
class JSON:
|
||||
|
||||
@@ -691,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@pytest.fixture(scope="package")
|
||||
def db():
|
||||
return DB()
|
||||
|
||||
|
||||
@@ -294,3 +294,20 @@ See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`
|
||||
|
||||
setuptools intends to
|
||||
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
|
||||
|
||||
Checking with flake8-pytest-style
|
||||
---------------------------------
|
||||
|
||||
In order to ensure that pytest is being used correctly in your project,
|
||||
it can be helpful to use the `flake8-pytest-style <https://github.com/m-burst/flake8-pytest-style>`_ flake8 plugin.
|
||||
|
||||
flake8-pytest-style checks for common mistakes and coding style violations in pytest code,
|
||||
such as incorrect use of fixtures, test function names, and markers.
|
||||
By using this plugin, you can catch these errors early in the development process
|
||||
and ensure that your pytest code is consistent and easy to maintain.
|
||||
|
||||
A list of the lints detected by flake8-pytest-style can be found on its `PyPI page <https://pypi.org/project/flake8-pytest-style/>`_.
|
||||
|
||||
.. note::
|
||||
|
||||
flake8-pytest-style is not an official pytest project. Some of the rules enforce certain style choices, such as using `@pytest.fixture()` over `@pytest.fixture`, but you can configure the plugin to fit your preferred style.
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.2.0.dev534+ga2c84caaa.d20230317
|
||||
pytest 7.3.2
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -35,11 +35,12 @@ Pytest supports several ways to run and select tests from the command-line.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest -k "MyClass and not method"
|
||||
pytest -k 'MyClass and not method'
|
||||
|
||||
This will run tests which contain names that match the given *string expression* (case-insensitive),
|
||||
which can include Python operators that use filenames, class names and function names as variables.
|
||||
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
||||
Use ``""`` instead of ``''`` in expression when running this on Windows
|
||||
|
||||
.. _nodeids:
|
||||
|
||||
|
||||
+3
-4
@@ -1,11 +1,10 @@
|
||||
: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
|
||||
- `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:
|
||||
|
||||
|
||||
+497
-239
File diff suppressed because it is too large
Load Diff
@@ -956,6 +956,12 @@ TestReport
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
TestShortLogReport
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.TestShortLogReport()
|
||||
:members:
|
||||
|
||||
_Result
|
||||
~~~~~~~
|
||||
|
||||
@@ -1049,11 +1055,11 @@ Environment variables that can be used to change pytest's behavior.
|
||||
|
||||
.. envvar:: CI
|
||||
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to ``BUILD_NUMBER`` variable.
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
|
||||
|
||||
.. envvar:: BUILD_NUMBER
|
||||
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. 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
|
||||
|
||||
@@ -1713,13 +1719,12 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
.. confval:: testpaths
|
||||
|
||||
|
||||
|
||||
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
|
||||
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
||||
File system paths may use shell-style wildcards, including the recursive
|
||||
``**`` pattern.
|
||||
|
||||
Useful when all project tests are in a known location to speed up
|
||||
test collection and to avoid picking up undesired tests by accident.
|
||||
|
||||
@@ -1728,8 +1733,17 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
testpaths = testing doc
|
||||
|
||||
This tells pytest to only look for tests in ``testing`` and ``doc``
|
||||
directories when executing from the root directory.
|
||||
This configuration means that executing:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pytest
|
||||
|
||||
has the same practical effects as executing:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pytest testing doc
|
||||
|
||||
|
||||
.. confval:: tmp_path_retention_count
|
||||
@@ -1744,7 +1758,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
tmp_path_retention_count = 3
|
||||
|
||||
Default: 3
|
||||
Default: ``3``
|
||||
|
||||
|
||||
.. confval:: tmp_path_retention_policy
|
||||
@@ -1763,7 +1777,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
tmp_path_retention_policy = "all"
|
||||
|
||||
Default: all
|
||||
Default: ``all``
|
||||
|
||||
|
||||
.. confval:: usefixtures
|
||||
@@ -1904,7 +1918,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
--strict-markers Markers not registered in the `markers` section of
|
||||
the configuration file raise errors
|
||||
--strict (Deprecated) alias to --strict-markers
|
||||
-c file Load configuration from `file` instead of trying to
|
||||
-c, --config-file FILE
|
||||
Load configuration from `FILE` instead of trying to
|
||||
locate one of the implicit configuration files
|
||||
--continue-on-collection-errors
|
||||
Force test execution even if collection errors occur
|
||||
@@ -1996,7 +2011,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
Auto-indent multiline messages passed to the logging
|
||||
module. Accepts true|on, false|off or an integer.
|
||||
--log-disable=LOGGER_DISABLE
|
||||
Disable a logger by name. Can be passed multipe
|
||||
Disable a logger by name. Can be passed multiple
|
||||
times.
|
||||
|
||||
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
||||
|
||||
@@ -17,7 +17,9 @@ Plugin List
|
||||
===========
|
||||
|
||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||
automatically. Packages classified as inactive are excluded.
|
||||
automatically together with a manually-maintained list in `the source
|
||||
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||
Packages classified as inactive are excluded.
|
||||
|
||||
.. The following conditional uses a different format for this list when
|
||||
creating a PDF, because otherwise the table gets far too wide for the
|
||||
@@ -33,6 +35,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
||||
"Development Status :: 6 - Mature",
|
||||
"Development Status :: 7 - Inactive",
|
||||
)
|
||||
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||
"logassert",
|
||||
}
|
||||
|
||||
|
||||
def escape_rst(text: str) -> str:
|
||||
@@ -52,18 +57,18 @@ def iter_plugins():
|
||||
regex = r">([\d\w-]*)</a>"
|
||||
response = requests.get("https://pypi.org/simple")
|
||||
|
||||
matches = list(
|
||||
match
|
||||
for match in re.finditer(regex, response.text)
|
||||
if match.groups()[0].startswith("pytest-")
|
||||
)
|
||||
match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
|
||||
plugin_names = [
|
||||
name
|
||||
for name in match_names
|
||||
if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||
]
|
||||
|
||||
for match in tqdm(matches, smoothing=0):
|
||||
name = match.groups()[0]
|
||||
for name in tqdm(plugin_names, smoothing=0):
|
||||
response = requests.get(f"https://pypi.org/pypi/{name}/json")
|
||||
if response.status_code == 404:
|
||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
|
||||
# return 404 on the JSON API. Skip.
|
||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
|
||||
# but return 404 on the JSON API. Skip.
|
||||
continue
|
||||
response.raise_for_status()
|
||||
info = response.json()["info"]
|
||||
|
||||
@@ -6,7 +6,7 @@ long_description_content_type = text/x-rst
|
||||
url = https://docs.pytest.org/en/latest/
|
||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||
license = MIT
|
||||
license_file = LICENSE
|
||||
license_files = LICENSE
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
classifiers =
|
||||
Development Status :: 6 - Mature
|
||||
@@ -22,6 +22,7 @@ classifiers =
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Software Development :: Testing
|
||||
Topic :: Utilities
|
||||
@@ -73,6 +74,7 @@ testing =
|
||||
nose
|
||||
pygments>=2.7.2
|
||||
requests
|
||||
setuptools
|
||||
xmlschema
|
||||
|
||||
[options.package_data]
|
||||
|
||||
+111
-85
@@ -31,7 +31,6 @@ from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
from weakref import ref
|
||||
|
||||
import pluggy
|
||||
|
||||
@@ -50,9 +49,9 @@ from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
from typing_extensions import Literal
|
||||
from typing_extensions import SupportsIndex
|
||||
from weakref import ReferenceType
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||
|
||||
@@ -194,25 +193,25 @@ class Frame:
|
||||
class TracebackEntry:
|
||||
"""A single entry in a Traceback."""
|
||||
|
||||
__slots__ = ("_rawentry", "_excinfo", "_repr_style")
|
||||
__slots__ = ("_rawentry", "_repr_style")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rawentry: TracebackType,
|
||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
||||
repr_style: Optional['Literal["short", "long"]'] = None,
|
||||
) -> None:
|
||||
self._rawentry = rawentry
|
||||
self._excinfo = excinfo
|
||||
self._repr_style: Optional['Literal["short", "long"]'] = None
|
||||
self._rawentry: "Final" = rawentry
|
||||
self._repr_style: "Final" = repr_style
|
||||
|
||||
def with_repr_style(
|
||||
self, repr_style: Optional['Literal["short", "long"]']
|
||||
) -> "TracebackEntry":
|
||||
return TracebackEntry(self._rawentry, repr_style)
|
||||
|
||||
@property
|
||||
def lineno(self) -> int:
|
||||
return self._rawentry.tb_lineno - 1
|
||||
|
||||
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
|
||||
assert mode in ("short", "long")
|
||||
self._repr_style = mode
|
||||
|
||||
@property
|
||||
def frame(self) -> Frame:
|
||||
return Frame(self._rawentry.tb_frame)
|
||||
@@ -272,7 +271,7 @@ class TracebackEntry:
|
||||
|
||||
source = property(getsource)
|
||||
|
||||
def ishidden(self) -> bool:
|
||||
def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
|
||||
"""Return True if the current frame has a var __tracebackhide__
|
||||
resolving to True.
|
||||
|
||||
@@ -296,7 +295,7 @@ class TracebackEntry:
|
||||
else:
|
||||
break
|
||||
if tbh and callable(tbh):
|
||||
return tbh(None if self._excinfo is None else self._excinfo())
|
||||
return tbh(excinfo)
|
||||
return tbh
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -329,16 +328,14 @@ class Traceback(List[TracebackEntry]):
|
||||
def __init__(
|
||||
self,
|
||||
tb: Union[TracebackType, Iterable[TracebackEntry]],
|
||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
||||
) -> None:
|
||||
"""Initialize from given python traceback object and ExceptionInfo."""
|
||||
self._excinfo = excinfo
|
||||
if isinstance(tb, TracebackType):
|
||||
|
||||
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
|
||||
cur_: Optional[TracebackType] = cur
|
||||
while cur_ is not None:
|
||||
yield TracebackEntry(cur_, excinfo=excinfo)
|
||||
yield TracebackEntry(cur_)
|
||||
cur_ = cur_.tb_next
|
||||
|
||||
super().__init__(f(tb))
|
||||
@@ -378,7 +375,7 @@ class Traceback(List[TracebackEntry]):
|
||||
continue
|
||||
if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
|
||||
continue
|
||||
return Traceback(x._rawentry, self._excinfo)
|
||||
return Traceback(x._rawentry)
|
||||
return self
|
||||
|
||||
@overload
|
||||
@@ -398,26 +395,27 @@ class Traceback(List[TracebackEntry]):
|
||||
return super().__getitem__(key)
|
||||
|
||||
def filter(
|
||||
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
|
||||
self,
|
||||
# TODO(py38): change to positional only.
|
||||
_excinfo_or_fn: Union[
|
||||
"ExceptionInfo[BaseException]",
|
||||
Callable[[TracebackEntry], bool],
|
||||
],
|
||||
) -> "Traceback":
|
||||
"""Return a Traceback instance with certain items removed
|
||||
"""Return a Traceback instance with certain items removed.
|
||||
|
||||
fn is a function that gets a single argument, a TracebackEntry
|
||||
instance, and should return True when the item should be added
|
||||
to the Traceback, False when not.
|
||||
If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
|
||||
which are hidden (see ishidden() above).
|
||||
|
||||
By default this removes all the TracebackEntries which are hidden
|
||||
(see ishidden() above).
|
||||
Otherwise, the filter is a function that gets a single argument, a
|
||||
``TracebackEntry`` instance, and should return True when the item should
|
||||
be added to the ``Traceback``, False when not.
|
||||
"""
|
||||
return Traceback(filter(fn, self), self._excinfo)
|
||||
|
||||
def getcrashentry(self) -> Optional[TracebackEntry]:
|
||||
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
|
||||
for i in range(-1, -len(self) - 1, -1):
|
||||
entry = self[i]
|
||||
if not entry.ishidden():
|
||||
return entry
|
||||
return None
|
||||
if isinstance(_excinfo_or_fn, ExceptionInfo):
|
||||
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
|
||||
else:
|
||||
fn = _excinfo_or_fn
|
||||
return Traceback(filter(fn, self))
|
||||
|
||||
def recursionindex(self) -> Optional[int]:
|
||||
"""Return the index of the frame/TracebackEntry where recursion originates if
|
||||
@@ -469,22 +467,41 @@ class ExceptionInfo(Generic[E]):
|
||||
self._traceback = traceback
|
||||
|
||||
@classmethod
|
||||
def from_exc_info(
|
||||
def from_exception(
|
||||
cls,
|
||||
exc_info: Tuple[Type[E], E, TracebackType],
|
||||
# Ignoring error: "Cannot use a covariant type variable as a parameter".
|
||||
# This is OK to ignore because this class is (conceptually) readonly.
|
||||
# See https://github.com/python/mypy/issues/7049.
|
||||
exception: E, # type: ignore[misc]
|
||||
exprinfo: Optional[str] = None,
|
||||
) -> "ExceptionInfo[E]":
|
||||
"""Return an ExceptionInfo for an existing exc_info tuple.
|
||||
"""Return an ExceptionInfo for an existing exception.
|
||||
|
||||
.. warning::
|
||||
|
||||
Experimental API
|
||||
The exception must have a non-``None`` ``__traceback__`` attribute,
|
||||
otherwise this function fails with an assertion error. This means that
|
||||
the exception must have been raised, or added a traceback with the
|
||||
:py:meth:`~BaseException.with_traceback()` method.
|
||||
|
||||
:param exprinfo:
|
||||
A text string helping to determine if we should strip
|
||||
``AssertionError`` from the output. Defaults to the exception
|
||||
message/``__str__()``.
|
||||
|
||||
.. versionadded:: 7.4
|
||||
"""
|
||||
assert (
|
||||
exception.__traceback__
|
||||
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
|
||||
exc_info = (type(exception), exception, exception.__traceback__)
|
||||
return cls.from_exc_info(exc_info, exprinfo)
|
||||
|
||||
@classmethod
|
||||
def from_exc_info(
|
||||
cls,
|
||||
exc_info: Tuple[Type[E], E, TracebackType],
|
||||
exprinfo: Optional[str] = None,
|
||||
) -> "ExceptionInfo[E]":
|
||||
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
|
||||
_striptext = ""
|
||||
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
||||
exprinfo = getattr(exc_info[1], "msg", None)
|
||||
@@ -563,7 +580,7 @@ class ExceptionInfo(Generic[E]):
|
||||
def traceback(self) -> Traceback:
|
||||
"""The traceback."""
|
||||
if self._traceback is None:
|
||||
self._traceback = Traceback(self.tb, excinfo=ref(self))
|
||||
self._traceback = Traceback(self.tb)
|
||||
return self._traceback
|
||||
|
||||
@traceback.setter
|
||||
@@ -603,11 +620,14 @@ class ExceptionInfo(Generic[E]):
|
||||
return isinstance(self.value, exc)
|
||||
|
||||
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
||||
exconly = self.exconly(tryshort=True)
|
||||
entry = self.traceback.getcrashentry()
|
||||
if entry:
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
# Find last non-hidden traceback entry that led to the exception of the
|
||||
# traceback, or None if all hidden.
|
||||
for i in range(-1, -len(self.traceback) - 1, -1):
|
||||
entry = self.traceback[i]
|
||||
if not entry.ishidden(self):
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
exconly = self.exconly(tryshort=True)
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
return None
|
||||
|
||||
def getrepr(
|
||||
@@ -615,7 +635,9 @@ class ExceptionInfo(Generic[E]):
|
||||
showlocals: bool = False,
|
||||
style: "_TracebackStyle" = "long",
|
||||
abspath: bool = False,
|
||||
tbfilter: bool = True,
|
||||
tbfilter: Union[
|
||||
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
||||
] = True,
|
||||
funcargs: bool = False,
|
||||
truncate_locals: bool = True,
|
||||
chain: bool = True,
|
||||
@@ -627,14 +649,20 @@ class ExceptionInfo(Generic[E]):
|
||||
Ignored if ``style=="native"``.
|
||||
|
||||
:param str style:
|
||||
long|short|no|native|value traceback style.
|
||||
long|short|line|no|native|value traceback style.
|
||||
|
||||
:param bool abspath:
|
||||
If paths should be changed to absolute or left unchanged.
|
||||
|
||||
:param bool tbfilter:
|
||||
Hide entries that contain a local variable ``__tracebackhide__==True``.
|
||||
Ignored if ``style=="native"``.
|
||||
:param tbfilter:
|
||||
A filter for traceback entries.
|
||||
|
||||
* If false, don't hide any entries.
|
||||
* If true, hide internal entries and entries that contain a local
|
||||
variable ``__tracebackhide__ = True``.
|
||||
* If a callable, delegates the filtering to the callable.
|
||||
|
||||
Ignored if ``style`` is ``"native"``.
|
||||
|
||||
:param bool funcargs:
|
||||
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
|
||||
@@ -653,7 +681,9 @@ class ExceptionInfo(Generic[E]):
|
||||
return ReprExceptionInfo(
|
||||
reprtraceback=ReprTracebackNative(
|
||||
traceback.format_exception(
|
||||
self.type, self.value, self.traceback[0]._rawentry
|
||||
self.type,
|
||||
self.value,
|
||||
self.traceback[0]._rawentry if self.traceback else None,
|
||||
)
|
||||
),
|
||||
reprcrash=self._getreprcrash(),
|
||||
@@ -697,7 +727,7 @@ class FormattedExcinfo:
|
||||
showlocals: bool = False
|
||||
style: "_TracebackStyle" = "long"
|
||||
abspath: bool = True
|
||||
tbfilter: bool = True
|
||||
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
||||
funcargs: bool = False
|
||||
truncate_locals: bool = True
|
||||
chain: bool = True
|
||||
@@ -809,12 +839,16 @@ class FormattedExcinfo:
|
||||
|
||||
def repr_traceback_entry(
|
||||
self,
|
||||
entry: TracebackEntry,
|
||||
entry: Optional[TracebackEntry],
|
||||
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
||||
) -> "ReprEntry":
|
||||
lines: List[str] = []
|
||||
style = entry._repr_style if entry._repr_style is not None else self.style
|
||||
if style in ("short", "long"):
|
||||
style = (
|
||||
entry._repr_style
|
||||
if entry is not None and entry._repr_style is not None
|
||||
else self.style
|
||||
)
|
||||
if style in ("short", "long") and entry is not None:
|
||||
source = self._getentrysource(entry)
|
||||
if source is None:
|
||||
source = Source("???")
|
||||
@@ -855,25 +889,31 @@ class FormattedExcinfo:
|
||||
|
||||
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
|
||||
traceback = excinfo.traceback
|
||||
if self.tbfilter:
|
||||
traceback = traceback.filter()
|
||||
if callable(self.tbfilter):
|
||||
traceback = self.tbfilter(excinfo)
|
||||
elif self.tbfilter:
|
||||
traceback = traceback.filter(excinfo)
|
||||
|
||||
if isinstance(excinfo.value, RecursionError):
|
||||
traceback, extraline = self._truncate_recursive_traceback(traceback)
|
||||
else:
|
||||
extraline = None
|
||||
|
||||
if not traceback:
|
||||
if extraline is None:
|
||||
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
|
||||
entries = [self.repr_traceback_entry(None, excinfo)]
|
||||
return ReprTraceback(entries, extraline, style=self.style)
|
||||
|
||||
last = traceback[-1]
|
||||
entries = []
|
||||
if self.style == "value":
|
||||
reprentry = self.repr_traceback_entry(last, excinfo)
|
||||
entries.append(reprentry)
|
||||
entries = [self.repr_traceback_entry(last, excinfo)]
|
||||
return ReprTraceback(entries, None, style=self.style)
|
||||
|
||||
for index, entry in enumerate(traceback):
|
||||
einfo = (last == entry) and excinfo or None
|
||||
reprentry = self.repr_traceback_entry(entry, einfo)
|
||||
entries.append(reprentry)
|
||||
entries = [
|
||||
self.repr_traceback_entry(entry, excinfo if last == entry else None)
|
||||
for entry in traceback
|
||||
]
|
||||
return ReprTraceback(entries, extraline, style=self.style)
|
||||
|
||||
def _truncate_recursive_traceback(
|
||||
@@ -930,6 +970,7 @@ class FormattedExcinfo:
|
||||
seen: Set[int] = set()
|
||||
while e is not None and id(e) not in seen:
|
||||
seen.add(id(e))
|
||||
|
||||
if excinfo_:
|
||||
# Fall back to native traceback as a temporary workaround until
|
||||
# full support for exception groups added to ExceptionInfo.
|
||||
@@ -946,14 +987,7 @@ class FormattedExcinfo:
|
||||
)
|
||||
else:
|
||||
reprtraceback = self.repr_traceback(excinfo_)
|
||||
|
||||
# will be None if all traceback entries are hidden
|
||||
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
|
||||
if reprcrash:
|
||||
if self.style == "value":
|
||||
repr_chain += [(reprtraceback, None, descr)]
|
||||
else:
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
reprcrash = excinfo_._getreprcrash()
|
||||
else:
|
||||
# Fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work.
|
||||
@@ -961,25 +995,17 @@ class FormattedExcinfo:
|
||||
traceback.format_exception(type(e), e, None)
|
||||
)
|
||||
reprcrash = None
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
|
||||
if e.__cause__ is not None and self.chain:
|
||||
e = e.__cause__
|
||||
excinfo_ = (
|
||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
||||
if e.__traceback__
|
||||
else None
|
||||
)
|
||||
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||
descr = "The above exception was the direct cause of the following exception:"
|
||||
elif (
|
||||
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
||||
):
|
||||
e = e.__context__
|
||||
excinfo_ = (
|
||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
||||
if e.__traceback__
|
||||
else None
|
||||
)
|
||||
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||
descr = "During handling of the above exception, another exception occurred:"
|
||||
else:
|
||||
e = None
|
||||
@@ -1158,8 +1184,8 @@ class ReprEntry(TerminalRepr):
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
if self.style == "short":
|
||||
assert self.reprfileloc is not None
|
||||
self.reprfileloc.toterminal(tw)
|
||||
if self.reprfileloc:
|
||||
self.reprfileloc.toterminal(tw)
|
||||
self._write_entry_lines(tw)
|
||||
if self.reprlocals:
|
||||
self.reprlocals.toterminal(tw, indent=" " * 8)
|
||||
|
||||
@@ -953,7 +953,7 @@ class LocalPath:
|
||||
else:
|
||||
p.dirpath()._ensuredirs()
|
||||
if not p.check(file=1):
|
||||
p.open("w").close()
|
||||
p.open("wb").close()
|
||||
return p
|
||||
|
||||
@overload
|
||||
|
||||
@@ -46,8 +46,14 @@ if TYPE_CHECKING:
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
namedExpr = ast.NamedExpr
|
||||
astNameConstant = ast.Constant
|
||||
astStr = ast.Constant
|
||||
astNum = ast.Constant
|
||||
else:
|
||||
namedExpr = ast.Expr
|
||||
astNameConstant = ast.NameConstant
|
||||
astStr = ast.Str
|
||||
astNum = ast.Num
|
||||
|
||||
|
||||
assertstate_key = StashKey["AssertionState"]()
|
||||
@@ -680,9 +686,12 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if (
|
||||
expect_docstring
|
||||
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):
|
||||
return
|
||||
expect_docstring = False
|
||||
@@ -814,7 +823,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
current = self.stack.pop()
|
||||
if self.stack:
|
||||
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()))
|
||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||
name = "@py_format" + str(next(self.variable_counter))
|
||||
@@ -868,16 +877,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||
|
||||
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
|
||||
if assert_.msg:
|
||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||
gluestr = "\n>assert "
|
||||
else:
|
||||
assertmsg = ast.Str("")
|
||||
assertmsg = astStr("")
|
||||
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_name = ast.Name("AssertionError", ast.Load())
|
||||
fmt = self.helper("_format_explanation", err_msg)
|
||||
@@ -893,8 +902,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
hook_call_pass = ast.Expr(
|
||||
self.helper(
|
||||
"_call_assertion_pass",
|
||||
ast.Num(assert_.lineno),
|
||||
ast.Str(orig),
|
||||
astNum(assert_.lineno),
|
||||
astStr(orig),
|
||||
fmt_pass,
|
||||
)
|
||||
)
|
||||
@@ -913,7 +922,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
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)
|
||||
|
||||
else: # Original assertion rewriting
|
||||
@@ -924,9 +933,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||
explanation = "\n>assert " + explanation
|
||||
else:
|
||||
assertmsg = ast.Str("")
|
||||
assertmsg = astStr("")
|
||||
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)
|
||||
fmt = self.helper("_format_explanation", msg)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
@@ -938,7 +947,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# Clear temporary variables by setting them to None.
|
||||
if 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)
|
||||
# Fix locations (line numbers/column offsets).
|
||||
for stmt in self.statements:
|
||||
@@ -952,20 +961,20 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# thinks it's acceptable.
|
||||
locs = ast.Call(self.builtin("locals"), [], [])
|
||||
target_id = name.target.id # type: ignore[attr-defined]
|
||||
inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs])
|
||||
inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])
|
||||
dorepr = self.helper("_should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
expr = ast.IfExp(test, self.display(name), ast.Str(target_id))
|
||||
expr = ast.IfExp(test, self.display(name), astStr(target_id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
||||
# Display the repr of the name if it's a local variable or
|
||||
# _should_repr_global_name() thinks it's acceptable.
|
||||
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)
|
||||
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)
|
||||
|
||||
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
||||
@@ -996,12 +1005,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
]
|
||||
):
|
||||
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
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
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], [])
|
||||
self.expl_stmts.append(ast.Expr(call))
|
||||
if i < levels:
|
||||
@@ -1013,7 +1024,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
self.statements = body = inner
|
||||
self.statements = 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)
|
||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||
|
||||
@@ -1037,10 +1048,19 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
new_args = []
|
||||
new_kwargs = []
|
||||
for arg in call.args:
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
|
||||
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
for keyword in call.keywords:
|
||||
if (
|
||||
isinstance(keyword.value, ast.Name)
|
||||
and keyword.value.id in self.variables_overwrite
|
||||
):
|
||||
keyword.value = self.variables_overwrite[
|
||||
keyword.value.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
if keyword.arg:
|
||||
@@ -1075,7 +1095,13 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
self.push_format_context()
|
||||
# We first check if we have overwritten a variable in the previous assert
|
||||
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
||||
comp.left.id = self.variables_overwrite[comp.left.id]
|
||||
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)
|
||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||
left_expl = f"({left_expl})"
|
||||
@@ -1093,15 +1119,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
and next_operand.target.id == left_res.id
|
||||
):
|
||||
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)
|
||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||
next_expl = f"({next_expl})"
|
||||
results.append(next_res)
|
||||
sym = BINOP_MAP[op.__class__]
|
||||
syms.append(ast.Str(sym))
|
||||
syms.append(astStr(sym))
|
||||
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])
|
||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||
left_res, left_expl = next_res, next_expl
|
||||
|
||||
@@ -27,7 +27,7 @@ from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import Module
|
||||
from _pytest.nodes import File
|
||||
from _pytest.python import Package
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
@@ -179,16 +179,22 @@ class Cache:
|
||||
else:
|
||||
cache_dir_exists_already = self._cachedir.exists()
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
except OSError:
|
||||
self.warn("could not create cache path {path}", path=path, _ispytest=True)
|
||||
except OSError as exc:
|
||||
self.warn(
|
||||
f"could not create cache path {path}: {exc}",
|
||||
_ispytest=True,
|
||||
)
|
||||
return
|
||||
if not cache_dir_exists_already:
|
||||
self._ensure_supporting_files()
|
||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||
try:
|
||||
f = path.open("w", encoding="UTF-8")
|
||||
except OSError:
|
||||
self.warn("cache could not write path {path}", path=path, _ispytest=True)
|
||||
except OSError as exc:
|
||||
self.warn(
|
||||
f"cache could not write path {path}: {exc}",
|
||||
_ispytest=True,
|
||||
)
|
||||
else:
|
||||
with f:
|
||||
f.write(data)
|
||||
@@ -213,22 +219,30 @@ class LFPluginCollWrapper:
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector: nodes.Collector):
|
||||
if isinstance(collector, Session):
|
||||
if isinstance(collector, (Session, Package)):
|
||||
out = yield
|
||||
res: CollectReport = out.get_result()
|
||||
|
||||
# Sort any lf-paths to the beginning.
|
||||
lf_paths = self.lfplugin._last_failed_paths
|
||||
|
||||
# Use stable sort to priorize last failed.
|
||||
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
|
||||
# Package.path is the __init__.py file, we need the directory.
|
||||
if isinstance(node, Package):
|
||||
path = node.path.parent
|
||||
else:
|
||||
path = node.path
|
||||
return path in lf_paths
|
||||
|
||||
res.result = sorted(
|
||||
res.result,
|
||||
# use stable sort to priorize last failed
|
||||
key=lambda x: x.path in lf_paths,
|
||||
key=sort_key,
|
||||
reverse=True,
|
||||
)
|
||||
return
|
||||
|
||||
elif isinstance(collector, Module):
|
||||
elif isinstance(collector, File):
|
||||
if collector.path in self.lfplugin._last_failed_paths:
|
||||
out = yield
|
||||
res = out.get_result()
|
||||
@@ -266,10 +280,9 @@ class LFPluginCollSkipfiles:
|
||||
def pytest_make_collect_report(
|
||||
self, collector: nodes.Collector
|
||||
) -> Optional[CollectReport]:
|
||||
# Packages are Modules, but _last_failed_paths only contains
|
||||
# test-bearing paths and doesn't try to include the paths of their
|
||||
# packages, so don't filter them.
|
||||
if isinstance(collector, Module) and not isinstance(collector, Package):
|
||||
# Packages are Files, but we only want to skip test-bearing Files,
|
||||
# so don't filter Packages.
|
||||
if isinstance(collector, File) and not isinstance(collector, Package):
|
||||
if collector.path not in self.lfplugin._last_failed_paths:
|
||||
self.lfplugin._skipped_files += 1
|
||||
|
||||
@@ -299,9 +312,14 @@ class LFPlugin:
|
||||
)
|
||||
|
||||
def get_last_failed_paths(self) -> Set[Path]:
|
||||
"""Return a set with all Paths()s of the previously failed nodeids."""
|
||||
"""Return a set with all Paths of the previously failed nodeids and
|
||||
their parents."""
|
||||
rootpath = self.config.rootpath
|
||||
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
|
||||
result = set()
|
||||
for nodeid in self.lastfailed:
|
||||
path = rootpath / nodeid.split("::")[0]
|
||||
result.add(path)
|
||||
result.update(path.parents)
|
||||
return {x for x in result if x.exists()}
|
||||
|
||||
def pytest_report_collectionfinish(self) -> Optional[str]:
|
||||
|
||||
@@ -241,7 +241,7 @@ class DontReadFromInput(TextIO):
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
||||
|
||||
def truncate(self, size: Optional[int] = None) -> int:
|
||||
raise UnsupportedOperation("cannont truncate stdin")
|
||||
raise UnsupportedOperation("cannot truncate stdin")
|
||||
|
||||
def write(self, data: str) -> int:
|
||||
raise UnsupportedOperation("cannot write to stdin")
|
||||
|
||||
@@ -49,7 +49,7 @@ from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import importlib_metadata
|
||||
from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.pathlib import absolutepath
|
||||
@@ -526,7 +526,10 @@ class PytestPluginManager(PluginManager):
|
||||
# Internal API for local conftest plugin handling.
|
||||
#
|
||||
def _set_initial_conftests(
|
||||
self, namespace: argparse.Namespace, rootpath: Path
|
||||
self,
|
||||
namespace: argparse.Namespace,
|
||||
rootpath: Path,
|
||||
testpaths_ini: Sequence[str],
|
||||
) -> None:
|
||||
"""Load initial conftest files given a preparsed "namespace".
|
||||
|
||||
@@ -543,7 +546,7 @@ class PytestPluginManager(PluginManager):
|
||||
)
|
||||
self._noconftest = namespace.noconftest
|
||||
self._using_pyargs = namespace.pyargs
|
||||
testpaths = namespace.file_or_dir
|
||||
testpaths = namespace.file_or_dir + testpaths_ini
|
||||
foundanchor = False
|
||||
for testpath in testpaths:
|
||||
path = str(testpath)
|
||||
@@ -552,7 +555,14 @@ class PytestPluginManager(PluginManager):
|
||||
if i != -1:
|
||||
path = path[:i]
|
||||
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)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
@@ -1131,7 +1141,9 @@ class Config:
|
||||
@hookimpl(trylast=True)
|
||||
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
||||
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:
|
||||
@@ -1249,8 +1261,11 @@ class Config:
|
||||
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
||||
)
|
||||
|
||||
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
if self.known_args_namespace.confcutdir is None:
|
||||
if self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
else:
|
||||
confcutdir = str(self.rootpath)
|
||||
self.known_args_namespace.confcutdir = confcutdir
|
||||
try:
|
||||
self.hook.pytest_load_initial_conftests(
|
||||
@@ -1370,6 +1385,15 @@ class Config:
|
||||
args = []
|
||||
for path in testpaths:
|
||||
args.extend(sorted(glob.iglob(path, recursive=True)))
|
||||
if testpaths and not args:
|
||||
warning_text = (
|
||||
"No files were found in testpaths; "
|
||||
"consider removing or adjusting your testpaths configuration. "
|
||||
"Searching recursively from the current directory instead."
|
||||
)
|
||||
self.issue_config_time_warning(
|
||||
PytestConfigWarning(warning_text), stacklevel=3
|
||||
)
|
||||
if not args:
|
||||
source = Config.ArgsSource.INCOVATION_DIR
|
||||
args = [str(self.invocation_params.dir)]
|
||||
|
||||
@@ -2,7 +2,6 @@ import io
|
||||
import os
|
||||
import sys
|
||||
from typing import Generator
|
||||
from typing import TextIO
|
||||
|
||||
import pytest
|
||||
from _pytest.config import Config
|
||||
@@ -11,7 +10,7 @@ from _pytest.nodes import Item
|
||||
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]()
|
||||
|
||||
|
||||
@@ -26,10 +25,9 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
def pytest_configure(config: Config) -> None:
|
||||
import faulthandler
|
||||
|
||||
stderr_fd_copy = os.dup(get_stderr_fileno())
|
||||
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
|
||||
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
|
||||
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:
|
||||
@@ -37,9 +35,9 @@ def pytest_unconfigure(config: Config) -> None:
|
||||
|
||||
faulthandler.disable()
|
||||
# Close the dup file installed during pytest_configure.
|
||||
if fault_handler_stderr_key in config.stash:
|
||||
config.stash[fault_handler_stderr_key].close()
|
||||
del config.stash[fault_handler_stderr_key]
|
||||
if fault_handler_stderr_fd_key in config.stash:
|
||||
os.close(config.stash[fault_handler_stderr_fd_key])
|
||||
del config.stash[fault_handler_stderr_fd_key]
|
||||
if config.stash.get(fault_handler_originally_enabled_key, False):
|
||||
# Re-enable the faulthandler if it was originally enabled.
|
||||
faulthandler.enable(file=get_stderr_fileno())
|
||||
@@ -67,10 +65,10 @@ def get_timeout_config_value(config: Config) -> float:
|
||||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||
timeout = get_timeout_config_value(item.config)
|
||||
stderr = item.config.stash[fault_handler_stderr_key]
|
||||
if timeout > 0 and stderr is not None:
|
||||
if timeout > 0:
|
||||
import faulthandler
|
||||
|
||||
stderr = item.config.stash[fault_handler_stderr_fd_key]
|
||||
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||
try:
|
||||
yield
|
||||
|
||||
+68
-36
@@ -46,6 +46,7 @@ from _pytest.compat import getimfunc
|
||||
from _pytest.compat import getlocation
|
||||
from _pytest.compat import is_generator
|
||||
from _pytest.compat import NOTSET
|
||||
from _pytest.compat import NotSetType
|
||||
from _pytest.compat import overload
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.config import _PluggyPlugin
|
||||
@@ -112,16 +113,18 @@ def pytest_sessionstart(session: "Session") -> None:
|
||||
session._fixturemanager = FixtureManager(session)
|
||||
|
||||
|
||||
def get_scope_package(node, fixturedef: "FixtureDef[object]"):
|
||||
import pytest
|
||||
def get_scope_package(
|
||||
node: nodes.Item,
|
||||
fixturedef: "FixtureDef[object]",
|
||||
) -> Optional[Union[nodes.Item, nodes.Collector]]:
|
||||
from _pytest.python import Package
|
||||
|
||||
cls = pytest.Package
|
||||
current = node
|
||||
current: Optional[Union[nodes.Item, nodes.Collector]] = node
|
||||
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
|
||||
while current and (
|
||||
type(current) is not cls or fixture_package_name != current.nodeid
|
||||
not isinstance(current, Package) or fixture_package_name != current.nodeid
|
||||
):
|
||||
current = current.parent
|
||||
current = current.parent # type: ignore[assignment]
|
||||
if current is None:
|
||||
return node.session
|
||||
return current
|
||||
@@ -434,7 +437,23 @@ class FixtureRequest:
|
||||
@property
|
||||
def node(self):
|
||||
"""Underlying collection node (depends on current request scope)."""
|
||||
return self._getscopeitem(self._scope)
|
||||
scope = self._scope
|
||||
if scope is Scope.Function:
|
||||
# This might also be a non-function Item despite its attribute name.
|
||||
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
||||
elif scope is Scope.Package:
|
||||
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
||||
# but on FixtureRequest (a subclass).
|
||||
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
||||
else:
|
||||
node = get_scope_node(self._pyfuncitem, scope)
|
||||
if node is None and scope is Scope.Class:
|
||||
# Fallback to function item itself.
|
||||
node = self._pyfuncitem
|
||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
||||
scope, self._pyfuncitem
|
||||
)
|
||||
return node
|
||||
|
||||
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
|
||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||
@@ -518,11 +537,7 @@ class FixtureRequest:
|
||||
"""Add finalizer/teardown function to be called without arguments after
|
||||
the last test within the requesting test context finished execution."""
|
||||
# XXX usually this method is shadowed by fixturedef specific ones.
|
||||
self._addfinalizer(finalizer, scope=self.scope)
|
||||
|
||||
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
|
||||
node = self._getscopeitem(scope)
|
||||
node.addfinalizer(finalizer)
|
||||
self.node.addfinalizer(finalizer)
|
||||
|
||||
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
|
||||
"""Apply a marker to a single test function invocation.
|
||||
@@ -717,28 +732,6 @@ class FixtureRequest:
|
||||
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
|
||||
return lines
|
||||
|
||||
def _getscopeitem(
|
||||
self, scope: Union[Scope, "_ScopeName"]
|
||||
) -> Union[nodes.Item, nodes.Collector]:
|
||||
if isinstance(scope, str):
|
||||
scope = Scope(scope)
|
||||
if scope is Scope.Function:
|
||||
# This might also be a non-function Item despite its attribute name.
|
||||
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
|
||||
elif scope is Scope.Package:
|
||||
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
|
||||
# but on FixtureRequest (a subclass).
|
||||
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
|
||||
else:
|
||||
node = get_scope_node(self._pyfuncitem, scope)
|
||||
if node is None and scope is Scope.Class:
|
||||
# Fallback to function item itself.
|
||||
node = self._pyfuncitem
|
||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
|
||||
scope, self._pyfuncitem
|
||||
)
|
||||
return node
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<FixtureRequest for %r>" % (self.node)
|
||||
|
||||
@@ -1593,13 +1586,52 @@ class FixtureManager:
|
||||
# Separate parametrized setups.
|
||||
items[:] = reorder_items(items)
|
||||
|
||||
@overload
|
||||
def parsefactories(
|
||||
self, node_or_obj, nodeid=NOTSET, unittest: bool = False
|
||||
self,
|
||||
node_or_obj: nodes.Node,
|
||||
*,
|
||||
unittest: bool = ...,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@overload
|
||||
def parsefactories( # noqa: F811
|
||||
self,
|
||||
node_or_obj: object,
|
||||
nodeid: Optional[str],
|
||||
*,
|
||||
unittest: bool = ...,
|
||||
) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def parsefactories( # noqa: F811
|
||||
self,
|
||||
node_or_obj: Union[nodes.Node, object],
|
||||
nodeid: Union[str, NotSetType, None] = NOTSET,
|
||||
*,
|
||||
unittest: bool = False,
|
||||
) -> None:
|
||||
"""Collect fixtures from a collection node or object.
|
||||
|
||||
Found fixtures are parsed into `FixtureDef`s and saved.
|
||||
|
||||
If `node_or_object` is a collection node (with an underlying Python
|
||||
object), the node's object is traversed and the node's nodeid is used to
|
||||
determine the fixtures' visibilty. `nodeid` must not be specified in
|
||||
this case.
|
||||
|
||||
If `node_or_object` is an object (e.g. a plugin), the object is
|
||||
traversed and the given `nodeid` is used to determine the fixtures'
|
||||
visibility. `nodeid` must be specified in this case; None and "" mean
|
||||
total visibility.
|
||||
"""
|
||||
if nodeid is not NOTSET:
|
||||
holderobj = node_or_obj
|
||||
else:
|
||||
holderobj = node_or_obj.obj
|
||||
assert isinstance(node_or_obj, nodes.Node)
|
||||
holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
|
||||
assert isinstance(node_or_obj.nodeid, str)
|
||||
nodeid = node_or_obj.nodeid
|
||||
if holderobj in self._holderobjseen:
|
||||
return
|
||||
|
||||
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
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 ExitCode
|
||||
from _pytest.config import PytestPluginManager
|
||||
@@ -41,6 +41,7 @@ if TYPE_CHECKING:
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.runner import CallInfo
|
||||
from _pytest.terminal import TerminalReporter
|
||||
from _pytest.terminal import TestShortLogReport
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
|
||||
|
||||
@@ -806,7 +807,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_report_teststatus( # type:ignore[empty-body]
|
||||
report: Union["CollectReport", "TestReport"], config: "Config"
|
||||
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
|
||||
) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]":
|
||||
"""Return result-category, shortletter and verbose word for status
|
||||
reporting.
|
||||
|
||||
|
||||
+75
-5
@@ -5,7 +5,11 @@ import os
|
||||
import re
|
||||
from contextlib import contextmanager
|
||||
from contextlib import nullcontext
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
from datetime import timezone
|
||||
from io import StringIO
|
||||
from logging import LogRecord
|
||||
from pathlib import Path
|
||||
from typing import AbstractSet
|
||||
from typing import Dict
|
||||
@@ -53,7 +57,25 @@ def _remove_ansi_escape_sequences(text: str) -> str:
|
||||
return _ANSI_ESCAPE_SEQ.sub("", text)
|
||||
|
||||
|
||||
class ColoredLevelFormatter(logging.Formatter):
|
||||
class DatetimeFormatter(logging.Formatter):
|
||||
"""A logging formatter which formats record with
|
||||
:func:`datetime.datetime.strftime` formatter instead of
|
||||
:func:`time.strftime` in case of microseconds in format string.
|
||||
"""
|
||||
|
||||
def formatTime(self, record: LogRecord, datefmt=None) -> str:
|
||||
if datefmt and "%f" in datefmt:
|
||||
ct = self.converter(record.created)
|
||||
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
|
||||
# Construct `datetime.datetime` object from `struct_time`
|
||||
# and msecs information from `record`
|
||||
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
|
||||
return dt.strftime(datefmt)
|
||||
# Use `logging.Formatter` for non-microsecond formats
|
||||
return super().formatTime(record, datefmt)
|
||||
|
||||
|
||||
class ColoredLevelFormatter(DatetimeFormatter):
|
||||
"""A logging formatter which colorizes the %(levelname)..s part of the
|
||||
log format passed to __init__."""
|
||||
|
||||
@@ -302,7 +324,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
action="append",
|
||||
default=[],
|
||||
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.",
|
||||
)
|
||||
|
||||
|
||||
@@ -376,11 +398,12 @@ class LogCaptureFixture:
|
||||
self._initial_handler_level: Optional[int] = None
|
||||
# Dict of log name -> log level.
|
||||
self._initial_logger_levels: Dict[Optional[str], int] = {}
|
||||
self._initial_disabled_logging_level: Optional[int] = None
|
||||
|
||||
def _finalize(self) -> None:
|
||||
"""Finalize the fixture.
|
||||
|
||||
This restores the log levels changed by :meth:`set_level`.
|
||||
This restores the log levels and the disabled logging levels changed by :meth:`set_level`.
|
||||
"""
|
||||
# Restore log levels.
|
||||
if self._initial_handler_level is not None:
|
||||
@@ -388,6 +411,10 @@ class LogCaptureFixture:
|
||||
for logger_name, level in self._initial_logger_levels.items():
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(level)
|
||||
# Disable logging at the original disabled logging level.
|
||||
if self._initial_disabled_logging_level is not None:
|
||||
logging.disable(self._initial_disabled_logging_level)
|
||||
self._initial_disabled_logging_level = None
|
||||
|
||||
@property
|
||||
def handler(self) -> LogCaptureHandler:
|
||||
@@ -453,6 +480,40 @@ class LogCaptureFixture:
|
||||
"""Reset the list of log records and the captured log text."""
|
||||
self.handler.clear()
|
||||
|
||||
def _force_enable_logging(
|
||||
self, level: Union[int, str], logger_obj: logging.Logger
|
||||
) -> int:
|
||||
"""Enable the desired logging level if the global level was disabled via ``logging.disabled``.
|
||||
|
||||
Only enables logging levels greater than or equal to the requested ``level``.
|
||||
|
||||
Does nothing if the desired ``level`` wasn't disabled.
|
||||
|
||||
:param level:
|
||||
The logger level caplog should capture.
|
||||
All logging is enabled if a non-standard logging level string is supplied.
|
||||
Valid level strings are in :data:`logging._nameToLevel`.
|
||||
:param logger_obj: The logger object to check.
|
||||
|
||||
:return: The original disabled logging level.
|
||||
"""
|
||||
original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
|
||||
|
||||
if isinstance(level, str):
|
||||
# Try to translate the level string to an int for `logging.disable()`
|
||||
level = logging.getLevelName(level)
|
||||
|
||||
if not isinstance(level, int):
|
||||
# The level provided was not valid, so just un-disable all logging.
|
||||
logging.disable(logging.NOTSET)
|
||||
elif not logger_obj.isEnabledFor(level):
|
||||
# Each level is `10` away from other levels.
|
||||
# https://docs.python.org/3/library/logging.html#logging-levels
|
||||
disable_level = max(level - 10, logging.NOTSET)
|
||||
logging.disable(disable_level)
|
||||
|
||||
return original_disable_level
|
||||
|
||||
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
|
||||
"""Set the level of a logger for the duration of a test.
|
||||
|
||||
@@ -460,6 +521,8 @@ class LogCaptureFixture:
|
||||
The levels of the loggers changed by this function will be
|
||||
restored to their initial values at the end of the test.
|
||||
|
||||
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
||||
|
||||
:param level: The level.
|
||||
:param logger: The logger to update. If not given, the root logger.
|
||||
"""
|
||||
@@ -470,6 +533,9 @@ class LogCaptureFixture:
|
||||
if self._initial_handler_level is None:
|
||||
self._initial_handler_level = self.handler.level
|
||||
self.handler.setLevel(level)
|
||||
initial_disabled_logging_level = self._force_enable_logging(level, logger_obj)
|
||||
if self._initial_disabled_logging_level is None:
|
||||
self._initial_disabled_logging_level = initial_disabled_logging_level
|
||||
|
||||
@contextmanager
|
||||
def at_level(
|
||||
@@ -479,6 +545,8 @@ class LogCaptureFixture:
|
||||
the end of the 'with' statement the level is restored to its original
|
||||
value.
|
||||
|
||||
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
||||
|
||||
:param level: The level.
|
||||
:param logger: The logger to update. If not given, the root logger.
|
||||
"""
|
||||
@@ -487,11 +555,13 @@ class LogCaptureFixture:
|
||||
logger_obj.setLevel(level)
|
||||
handler_orig_level = self.handler.level
|
||||
self.handler.setLevel(level)
|
||||
original_disable_level = self._force_enable_logging(level, logger_obj)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
logger_obj.setLevel(orig_level)
|
||||
self.handler.setLevel(handler_orig_level)
|
||||
logging.disable(original_disable_level)
|
||||
|
||||
|
||||
@fixture
|
||||
@@ -577,7 +647,7 @@ class LoggingPlugin:
|
||||
config, "log_file_date_format", "log_date_format"
|
||||
)
|
||||
|
||||
log_file_formatter = logging.Formatter(
|
||||
log_file_formatter = DatetimeFormatter(
|
||||
log_file_format, datefmt=log_file_date_format
|
||||
)
|
||||
self.log_file_handler.setFormatter(log_file_formatter)
|
||||
@@ -621,7 +691,7 @@ class LoggingPlugin:
|
||||
create_terminal_writer(self._config), log_format, log_date_format
|
||||
)
|
||||
else:
|
||||
formatter = logging.Formatter(log_format, log_date_format)
|
||||
formatter = DatetimeFormatter(log_format, log_date_format)
|
||||
|
||||
formatter._style = PercentStyleMultiline(
|
||||
formatter._style._fmt, auto_indent=auto_indent
|
||||
|
||||
+17
-13
@@ -122,11 +122,12 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
)
|
||||
group._addoption(
|
||||
"-c",
|
||||
metavar="file",
|
||||
"--config-file",
|
||||
metavar="FILE",
|
||||
type=str,
|
||||
dest="inifilename",
|
||||
help="Load configuration from `file` instead of trying to locate one of the "
|
||||
"implicit configuration files",
|
||||
help="Load configuration from `FILE` instead of trying to locate one of the "
|
||||
"implicit configuration files.",
|
||||
)
|
||||
group._addoption(
|
||||
"--continue-on-collection-errors",
|
||||
@@ -399,6 +400,12 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
|
||||
allow_in_venv = config.getoption("collect_in_virtualenv")
|
||||
if not allow_in_venv and _in_venv(collection_path):
|
||||
return True
|
||||
|
||||
if collection_path.is_dir():
|
||||
norecursepatterns = config.getini("norecursedirs")
|
||||
if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
|
||||
return True
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@@ -562,9 +569,6 @@ class Session(nodes.FSCollector):
|
||||
ihook = self.gethookproxy(fspath.parent)
|
||||
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
||||
return False
|
||||
norecursepatterns = self.config.getini("norecursedirs")
|
||||
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _collectfile(
|
||||
@@ -685,8 +689,8 @@ class Session(nodes.FSCollector):
|
||||
# are not collected more than once.
|
||||
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
|
||||
|
||||
# Dirnames of pkgs with dunder-init files.
|
||||
pkg_roots: Dict[str, Package] = {}
|
||||
# Directories of pkgs with dunder-init files.
|
||||
pkg_roots: Dict[Path, Package] = {}
|
||||
|
||||
for argpath, names in self._initial_parts:
|
||||
self.trace("processing argument", (argpath, names))
|
||||
@@ -707,7 +711,7 @@ class Session(nodes.FSCollector):
|
||||
col = self._collectfile(pkginit, handle_dupes=False)
|
||||
if col:
|
||||
if isinstance(col[0], Package):
|
||||
pkg_roots[str(parent)] = col[0]
|
||||
pkg_roots[parent] = col[0]
|
||||
node_cache1[col[0].path] = [col[0]]
|
||||
|
||||
# If it's a directory argument, recurse and look for any Subpackages.
|
||||
@@ -716,7 +720,7 @@ class Session(nodes.FSCollector):
|
||||
assert not names, f"invalid arg {(argpath, names)!r}"
|
||||
|
||||
seen_dirs: Set[Path] = set()
|
||||
for direntry in visit(str(argpath), self._recurse):
|
||||
for direntry in visit(argpath, self._recurse):
|
||||
if not direntry.is_file():
|
||||
continue
|
||||
|
||||
@@ -731,8 +735,8 @@ class Session(nodes.FSCollector):
|
||||
for x in self._collectfile(pkginit):
|
||||
yield x
|
||||
if isinstance(x, Package):
|
||||
pkg_roots[str(dirpath)] = x
|
||||
if str(dirpath) in pkg_roots:
|
||||
pkg_roots[dirpath] = x
|
||||
if dirpath in pkg_roots:
|
||||
# Do not collect packages here.
|
||||
continue
|
||||
|
||||
@@ -749,7 +753,7 @@ class Session(nodes.FSCollector):
|
||||
if argpath in node_cache1:
|
||||
col = node_cache1[argpath]
|
||||
else:
|
||||
collect_root = pkg_roots.get(str(argpath.parent), self)
|
||||
collect_root = pkg_roots.get(argpath.parent, self)
|
||||
col = collect_root._collectfile(argpath, handle_dupes=False)
|
||||
if col:
|
||||
node_cache1[argpath] = col
|
||||
|
||||
@@ -18,6 +18,7 @@ import ast
|
||||
import dataclasses
|
||||
import enum
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
from typing import Callable
|
||||
from typing import Iterator
|
||||
@@ -26,6 +27,11 @@ from typing import NoReturn
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
astNameConstant = ast.Constant
|
||||
else:
|
||||
astNameConstant = ast.NameConstant
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Expression",
|
||||
@@ -132,7 +138,7 @@ IDENT_PREFIX = "$"
|
||||
|
||||
def expression(s: Scanner) -> ast.Expression:
|
||||
if s.accept(TokenType.EOF):
|
||||
ret: ast.expr = ast.NameConstant(False)
|
||||
ret: ast.expr = astNameConstant(False)
|
||||
else:
|
||||
ret = expr(s)
|
||||
s.accept(TokenType.EOF, reject=True)
|
||||
|
||||
@@ -7,6 +7,7 @@ from contextlib import contextmanager
|
||||
from typing import Any
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import MutableMapping
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
@@ -129,7 +130,7 @@ class MonkeyPatch:
|
||||
|
||||
def __init__(self) -> None:
|
||||
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._savesyspath: Optional[List[str]] = None
|
||||
|
||||
@@ -290,12 +291,13 @@ class MonkeyPatch:
|
||||
self._setattr.append((target, name, oldval))
|
||||
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."""
|
||||
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.
|
||||
|
||||
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
||||
@@ -306,7 +308,8 @@ class MonkeyPatch:
|
||||
raise KeyError(name)
|
||||
else:
|
||||
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:
|
||||
"""Set environment variable ``name`` to ``value``.
|
||||
@@ -401,11 +404,13 @@ class MonkeyPatch:
|
||||
for dictionary, key, value in reversed(self._setitem):
|
||||
if value is notset:
|
||||
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:
|
||||
pass # Was already deleted, so we have the desired state.
|
||||
else:
|
||||
dictionary[key] = value
|
||||
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||
dictionary[key] = value # type: ignore[index]
|
||||
self._setitem[:] = []
|
||||
if self._savesyspath is not None:
|
||||
sys.path[:] = self._savesyspath
|
||||
|
||||
+11
-9
@@ -22,6 +22,7 @@ import _pytest._code
|
||||
from _pytest._code import getfslineno
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._code.code import Traceback
|
||||
from _pytest.compat import cached_property
|
||||
from _pytest.compat import LEGACY_PATH
|
||||
from _pytest.config import Config
|
||||
@@ -432,8 +433,8 @@ class Node(metaclass=NodeMeta):
|
||||
assert current is None or isinstance(current, cls)
|
||||
return current
|
||||
|
||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
||||
pass
|
||||
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||
return excinfo.traceback
|
||||
|
||||
def _repr_failure_py(
|
||||
self,
|
||||
@@ -449,13 +450,13 @@ class Node(metaclass=NodeMeta):
|
||||
style = "value"
|
||||
if isinstance(excinfo.value, FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
|
||||
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]]
|
||||
if self.config.getoption("fulltrace", False):
|
||||
style = "long"
|
||||
tbfilter = False
|
||||
else:
|
||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
||||
self._prunetraceback(excinfo)
|
||||
if len(excinfo.traceback) == 0:
|
||||
excinfo.traceback = tb
|
||||
tbfilter = self._traceback_filter
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||
@@ -486,7 +487,7 @@ class Node(metaclass=NodeMeta):
|
||||
abspath=abspath,
|
||||
showlocals=self.config.getoption("showlocals", False),
|
||||
style=style,
|
||||
tbfilter=False, # pruned already, or in --fulltrace mode.
|
||||
tbfilter=tbfilter,
|
||||
truncate_locals=truncate_locals,
|
||||
)
|
||||
|
||||
@@ -557,13 +558,14 @@ class Collector(Node):
|
||||
|
||||
return self._repr_failure_py(excinfo, style=tbstyle)
|
||||
|
||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
||||
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||
if hasattr(self, "path"):
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.path)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
return excinfo.traceback.filter(excinfo)
|
||||
return excinfo.traceback
|
||||
|
||||
|
||||
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
|
||||
|
||||
+52
-25
@@ -6,6 +6,7 @@ import itertools
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import types
|
||||
import uuid
|
||||
import warnings
|
||||
from enum import Enum
|
||||
@@ -26,8 +27,11 @@ from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
@@ -63,21 +67,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||
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.
|
||||
|
||||
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).
|
||||
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
||||
if isinstance(excvalue, FileNotFoundError):
|
||||
if isinstance(exc, FileNotFoundError):
|
||||
return False
|
||||
|
||||
if not isinstance(excvalue, PermissionError):
|
||||
if not isinstance(exc, PermissionError):
|
||||
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
|
||||
|
||||
@@ -86,7 +102,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
||||
warnings.warn(
|
||||
PytestWarning(
|
||||
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
||||
func, path, exctype, excvalue
|
||||
func, path, type(exc), exc
|
||||
)
|
||||
)
|
||||
)
|
||||
@@ -149,7 +165,10 @@ def rm_rf(path: Path) -> None:
|
||||
are read-only."""
|
||||
path = ensure_extended_length_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]:
|
||||
@@ -335,7 +354,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||
yield path
|
||||
|
||||
|
||||
def cleanup_dead_symlink(root: Path):
|
||||
def cleanup_dead_symlinks(root: Path):
|
||||
for left_dir in root.iterdir():
|
||||
if left_dir.is_symlink():
|
||||
if not left_dir.resolve().exists():
|
||||
@@ -353,7 +372,7 @@ def cleanup_numbered_dir(
|
||||
for path in root.glob("garbage-*"):
|
||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||
|
||||
cleanup_dead_symlink(root)
|
||||
cleanup_dead_symlinks(root)
|
||||
|
||||
|
||||
def make_numbered_dir_with_cleanup(
|
||||
@@ -651,30 +670,38 @@ def resolve_package_path(path: Path) -> Optional[Path]:
|
||||
return result
|
||||
|
||||
|
||||
def scandir(path: Union[str, "os.PathLike[str]"]) -> List["os.DirEntry[str]"]:
|
||||
"""Scan a directory recursively, in breadth-first order.
|
||||
|
||||
The returned entries are sorted.
|
||||
"""
|
||||
entries = []
|
||||
with os.scandir(path) as s:
|
||||
# Skip entries with symlink loops and other brokenness, so the caller
|
||||
# doesn't have to deal with it.
|
||||
for entry in s:
|
||||
try:
|
||||
entry.is_file()
|
||||
except OSError as err:
|
||||
if _ignore_error(err):
|
||||
continue
|
||||
raise
|
||||
entries.append(entry)
|
||||
entries.sort(key=lambda entry: entry.name)
|
||||
return entries
|
||||
|
||||
|
||||
def visit(
|
||||
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
|
||||
) -> Iterator["os.DirEntry[str]"]:
|
||||
"""Walk a directory recursively, in breadth-first order.
|
||||
|
||||
The `recurse` predicate determines whether a directory is recursed.
|
||||
|
||||
Entries at each directory level are sorted.
|
||||
"""
|
||||
|
||||
# Skip entries with symlink loops and other brokenness, so the caller doesn't
|
||||
# have to deal with it.
|
||||
entries = []
|
||||
for entry in os.scandir(path):
|
||||
try:
|
||||
entry.is_file()
|
||||
except OSError as err:
|
||||
if _ignore_error(err):
|
||||
continue
|
||||
raise
|
||||
entries.append(entry)
|
||||
|
||||
entries.sort(key=lambda entry: entry.name)
|
||||
|
||||
entries = scandir(path)
|
||||
yield from entries
|
||||
|
||||
for entry in entries:
|
||||
if entry.is_dir() and recurse(entry):
|
||||
yield from visit(entry.path, recurse)
|
||||
|
||||
+19
-23
@@ -35,6 +35,7 @@ from _pytest._code import filter_traceback
|
||||
from _pytest._code import getfslineno
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest._code.code import Traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import ascii_escaped
|
||||
@@ -56,7 +57,6 @@ from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
|
||||
from _pytest.deprecated import INSTANCE_COLLECTOR
|
||||
from _pytest.deprecated import NOSE_SUPPORT_METHOD
|
||||
from _pytest.fixtures import FuncFixtureInfo
|
||||
@@ -667,7 +667,7 @@ class Package(Module):
|
||||
config=None,
|
||||
session=None,
|
||||
nodeid=None,
|
||||
path=Optional[Path],
|
||||
path: Optional[Path] = None,
|
||||
) -> None:
|
||||
# NOTE: Could be just the following, but kept as-is for compat.
|
||||
# nodes.FSCollector.__init__(self, fspath, parent=parent)
|
||||
@@ -699,14 +699,6 @@ class Package(Module):
|
||||
func = partial(_call_with_optional_argument, teardown_module, self.obj)
|
||||
self.addfinalizer(func)
|
||||
|
||||
def gethookproxy(self, fspath: "os.PathLike[str]"):
|
||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
|
||||
return self.session.gethookproxy(fspath)
|
||||
|
||||
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
|
||||
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
|
||||
return self.session.isinitpath(path)
|
||||
|
||||
def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
|
||||
if direntry.name == "__pycache__":
|
||||
return False
|
||||
@@ -714,9 +706,6 @@ class Package(Module):
|
||||
ihook = self.session.gethookproxy(fspath.parent)
|
||||
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
|
||||
return False
|
||||
norecursepatterns = self.config.getini("norecursedirs")
|
||||
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _collectfile(
|
||||
@@ -745,11 +734,11 @@ class Package(Module):
|
||||
|
||||
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
|
||||
this_path = self.path.parent
|
||||
init_module = this_path / "__init__.py"
|
||||
if init_module.is_file() and path_matches_patterns(
|
||||
init_module, self.config.getini("python_files")
|
||||
):
|
||||
yield Module.from_parent(self, path=init_module)
|
||||
|
||||
# Always collect the __init__ first.
|
||||
if path_matches_patterns(self.path, self.config.getini("python_files")):
|
||||
yield Module.from_parent(self, path=self.path)
|
||||
|
||||
pkg_prefixes: Set[Path] = set()
|
||||
for direntry in visit(str(this_path), recurse=self._recurse):
|
||||
path = Path(direntry.path)
|
||||
@@ -1801,7 +1790,7 @@ class Function(PyobjMixin, nodes.Item):
|
||||
def setup(self) -> None:
|
||||
self._request._fillfixtures()
|
||||
|
||||
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
||||
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
|
||||
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
|
||||
code = _pytest._code.Code.from_function(get_real_func(self.obj))
|
||||
path, firstlineno = code.path, code.firstlineno
|
||||
@@ -1813,14 +1802,21 @@ class Function(PyobjMixin, nodes.Item):
|
||||
ntraceback = ntraceback.filter(filter_traceback)
|
||||
if not ntraceback:
|
||||
ntraceback = traceback
|
||||
ntraceback = ntraceback.filter(excinfo)
|
||||
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
# issue364: mark all but first and last frames to
|
||||
# only show a single-line message for each frame.
|
||||
if self.config.getoption("tbstyle", "auto") == "auto":
|
||||
if len(excinfo.traceback) > 2:
|
||||
for entry in excinfo.traceback[1:-1]:
|
||||
entry.set_repr_style("short")
|
||||
if len(ntraceback) > 2:
|
||||
ntraceback = Traceback(
|
||||
entry
|
||||
if i == 0 or i == len(ntraceback) - 1
|
||||
else entry.with_repr_style("short")
|
||||
for i, entry in enumerate(ntraceback)
|
||||
)
|
||||
|
||||
return ntraceback
|
||||
return excinfo.traceback
|
||||
|
||||
# TODO: Type ignored -- breaks Liskov Substitution.
|
||||
def repr_failure( # type: ignore[override]
|
||||
|
||||
@@ -950,11 +950,7 @@ def raises( # noqa: F811
|
||||
try:
|
||||
func(*args[1:], **kwargs)
|
||||
except expected_exception as e:
|
||||
# We just caught the exception - there is a traceback.
|
||||
assert e.__traceback__ is not None
|
||||
return _pytest._code.ExceptionInfo.from_exc_info(
|
||||
(type(e), e, e.__traceback__)
|
||||
)
|
||||
return _pytest._code.ExceptionInfo.from_exception(e)
|
||||
fail(message)
|
||||
|
||||
|
||||
|
||||
@@ -347,10 +347,9 @@ class TestReport(BaseReport):
|
||||
elif isinstance(excinfo.value, skip.Exception):
|
||||
outcome = "skipped"
|
||||
r = excinfo._getreprcrash()
|
||||
if r is None:
|
||||
raise ValueError(
|
||||
"There should always be a traceback entry for skipping a test."
|
||||
)
|
||||
assert (
|
||||
r is not None
|
||||
), "There should always be a traceback entry for skipping a test."
|
||||
if excinfo.value._use_item_location:
|
||||
path, line = item.reportinfo()[:2]
|
||||
assert line is not None
|
||||
|
||||
+50
-5
@@ -8,6 +8,7 @@ import datetime
|
||||
import inspect
|
||||
import platform
|
||||
import sys
|
||||
import textwrap
|
||||
import warnings
|
||||
from collections import Counter
|
||||
from functools import partial
|
||||
@@ -20,6 +21,7 @@ from typing import Dict
|
||||
from typing import Generator
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
@@ -111,6 +113,26 @@ class MoreQuietAction(argparse.Action):
|
||||
namespace.quiet = getattr(namespace, "quiet", 0) + 1
|
||||
|
||||
|
||||
class TestShortLogReport(NamedTuple):
|
||||
"""Used to store the test status result category, shortletter and verbose word.
|
||||
For example ``"rerun", "R", ("RERUN", {"yellow": True})``.
|
||||
|
||||
:ivar category:
|
||||
The class of result, for example ``“passed”``, ``“skipped”``, ``“error”``, or the empty string.
|
||||
|
||||
:ivar letter:
|
||||
The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string.
|
||||
|
||||
:ivar word:
|
||||
Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``,
|
||||
``"ERROR"``, or the empty string.
|
||||
"""
|
||||
|
||||
category: str
|
||||
letter: str
|
||||
word: Union[str, Tuple[str, Mapping[str, bool]]]
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
group = parser.getgroup("terminal reporting", "Reporting", after="general")
|
||||
group._addoption(
|
||||
@@ -426,6 +448,28 @@ class TerminalReporter:
|
||||
self._tw.line()
|
||||
self.currentfspath = None
|
||||
|
||||
def wrap_write(
|
||||
self,
|
||||
content: str,
|
||||
*,
|
||||
flush: bool = False,
|
||||
margin: int = 8,
|
||||
line_sep: str = "\n",
|
||||
**markup: bool,
|
||||
) -> None:
|
||||
"""Wrap message with margin for progress info."""
|
||||
width_of_current_line = self._tw.width_of_current_line
|
||||
wrapped = line_sep.join(
|
||||
textwrap.wrap(
|
||||
" " * width_of_current_line + content,
|
||||
width=self._screen_width - margin,
|
||||
drop_whitespace=True,
|
||||
replace_whitespace=False,
|
||||
),
|
||||
)
|
||||
wrapped = wrapped[width_of_current_line:]
|
||||
self._tw.write(wrapped, flush=flush, **markup)
|
||||
|
||||
def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
|
||||
self._tw.write(content, flush=flush, **markup)
|
||||
|
||||
@@ -525,10 +569,11 @@ class TerminalReporter:
|
||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||
self._tests_ran = True
|
||||
rep = report
|
||||
res: Tuple[
|
||||
str, str, Union[str, Tuple[str, Mapping[str, bool]]]
|
||||
] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
||||
category, letter, word = res
|
||||
|
||||
res = TestShortLogReport(
|
||||
*self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
||||
)
|
||||
category, letter, word = res.category, res.letter, res.word
|
||||
if not isinstance(word, tuple):
|
||||
markup = None
|
||||
else:
|
||||
@@ -572,7 +617,7 @@ class TerminalReporter:
|
||||
formatted_reason = f" ({reason})"
|
||||
|
||||
if reason and formatted_reason is not None:
|
||||
self._tw.write(formatted_reason)
|
||||
self.wrap_write(formatted_reason)
|
||||
if self._show_progress_info:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
|
||||
+11
-12
@@ -28,7 +28,7 @@ from .pathlib import LOCK_TIMEOUT
|
||||
from .pathlib import make_numbered_dir
|
||||
from .pathlib import make_numbered_dir_with_cleanup
|
||||
from .pathlib import rm_rf
|
||||
from .pathlib import cleanup_dead_symlink
|
||||
from .pathlib import cleanup_dead_symlinks
|
||||
from _pytest.compat import final, get_user_id
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
@@ -100,7 +100,7 @@ class TempPathFactory:
|
||||
policy = config.getini("tmp_path_retention_policy")
|
||||
if policy not in ("all", "failed", "none"):
|
||||
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(
|
||||
@@ -289,31 +289,30 @@ def tmp_path(
|
||||
|
||||
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]):
|
||||
"""After each session, remove base directory if all the tests passed,
|
||||
the policy is "failed", and the basetemp is not specified by a user.
|
||||
"""
|
||||
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
|
||||
|
||||
policy = tmp_path_factory._retention_policy
|
||||
if (
|
||||
exitstatus == 0
|
||||
and policy == "failed"
|
||||
and tmp_path_factory._given_basetemp is None
|
||||
):
|
||||
passed_dir = tmp_path_factory._basetemp
|
||||
if passed_dir.exists():
|
||||
if basetemp.is_dir():
|
||||
# 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.
|
||||
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)
|
||||
|
||||
+11
-7
@@ -298,6 +298,9 @@ class TestCaseFunction(Function):
|
||||
def stopTest(self, testcase: "unittest.TestCase") -> None:
|
||||
pass
|
||||
|
||||
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
|
||||
pass
|
||||
|
||||
def runtest(self) -> None:
|
||||
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
||||
|
||||
@@ -331,15 +334,16 @@ class TestCaseFunction(Function):
|
||||
finally:
|
||||
delattr(self._testcase, self.name)
|
||||
|
||||
def _prunetraceback(
|
||||
def _traceback_filter(
|
||||
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
|
||||
) -> None:
|
||||
super()._prunetraceback(excinfo)
|
||||
traceback = excinfo.traceback.filter(
|
||||
lambda x: not x.frame.f_globals.get("__unittest")
|
||||
) -> _pytest._code.Traceback:
|
||||
traceback = super()._traceback_filter(excinfo)
|
||||
ntraceback = traceback.filter(
|
||||
lambda x: not x.frame.f_globals.get("__unittest"),
|
||||
)
|
||||
if traceback:
|
||||
excinfo.traceback = traceback
|
||||
if not ntraceback:
|
||||
ntraceback = traceback
|
||||
return ntraceback
|
||||
|
||||
|
||||
@hookimpl(tryfirst=True)
|
||||
|
||||
@@ -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`
|
||||
|
||||
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)
|
||||
"""
|
||||
lineno = method.__code__.co_firstlineno
|
||||
|
||||
@@ -62,6 +62,7 @@ from _pytest.reports import TestReport
|
||||
from _pytest.runner import CallInfo
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.stash import StashKey
|
||||
from _pytest.terminal import TestShortLogReport
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||
from _pytest.warning_types import PytestCacheWarning
|
||||
@@ -152,6 +153,7 @@ __all__ = [
|
||||
"TempPathFactory",
|
||||
"Testdir",
|
||||
"TestReport",
|
||||
"TestShortLogReport",
|
||||
"UsageError",
|
||||
"WarningsRecorder",
|
||||
"warns",
|
||||
|
||||
@@ -695,11 +695,15 @@ class TestInvocationVariants:
|
||||
monkeypatch.chdir("world")
|
||||
|
||||
# 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
|
||||
# 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(
|
||||
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", ignore_w
|
||||
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", *ignore_w
|
||||
)
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(
|
||||
@@ -1299,12 +1303,12 @@ def test_no_brokenpipeerror_message(pytester: Pytester) -> None:
|
||||
popen.stderr.close()
|
||||
|
||||
|
||||
def test_function_return_non_none_warning(testdir) -> None:
|
||||
testdir.makepyfile(
|
||||
def test_function_return_non_none_warning(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test_stuff():
|
||||
return "something"
|
||||
"""
|
||||
)
|
||||
res = testdir.runpytest()
|
||||
res = pytester.runpytest()
|
||||
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
|
||||
|
||||
@@ -11,7 +11,7 @@ from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import _pytest
|
||||
import _pytest._code
|
||||
import pytest
|
||||
from _pytest._code.code import ExceptionChainRepr
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
@@ -53,6 +53,20 @@ def test_excinfo_from_exc_info_simple() -> None:
|
||||
assert info.type == ValueError
|
||||
|
||||
|
||||
def test_excinfo_from_exception_simple() -> None:
|
||||
try:
|
||||
raise ValueError
|
||||
except ValueError as e:
|
||||
assert e.__traceback__ is not None
|
||||
info = _pytest._code.ExceptionInfo.from_exception(e)
|
||||
assert info.type == ValueError
|
||||
|
||||
|
||||
def test_excinfo_from_exception_missing_traceback_assertion() -> None:
|
||||
with pytest.raises(AssertionError, match=r"must have.*__traceback__"):
|
||||
_pytest._code.ExceptionInfo.from_exception(ValueError())
|
||||
|
||||
|
||||
def test_excinfo_getstatement():
|
||||
def g():
|
||||
raise ValueError
|
||||
@@ -172,7 +186,7 @@ class TestTraceback_f_g_h:
|
||||
|
||||
def test_traceback_filter(self):
|
||||
traceback = self.excinfo.traceback
|
||||
ntraceback = traceback.filter()
|
||||
ntraceback = traceback.filter(self.excinfo)
|
||||
assert len(ntraceback) == len(traceback) - 1
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -203,7 +217,7 @@ class TestTraceback_f_g_h:
|
||||
|
||||
excinfo = pytest.raises(ValueError, h)
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.filter()
|
||||
ntraceback = traceback.filter(excinfo)
|
||||
print(f"old: {traceback!r}")
|
||||
print(f"new: {ntraceback!r}")
|
||||
|
||||
@@ -276,7 +290,7 @@ class TestTraceback_f_g_h:
|
||||
excinfo = pytest.raises(ValueError, fail)
|
||||
assert excinfo.traceback.recursionindex() is None
|
||||
|
||||
def test_traceback_getcrashentry(self):
|
||||
def test_getreprcrash(self):
|
||||
def i():
|
||||
__tracebackhide__ = True
|
||||
raise ValueError
|
||||
@@ -292,15 +306,13 @@ class TestTraceback_f_g_h:
|
||||
g()
|
||||
|
||||
excinfo = pytest.raises(ValueError, f)
|
||||
tb = excinfo.traceback
|
||||
entry = tb.getcrashentry()
|
||||
assert entry is not None
|
||||
reprcrash = excinfo._getreprcrash()
|
||||
assert reprcrash is not None
|
||||
co = _pytest._code.Code.from_function(h)
|
||||
assert entry.frame.code.path == co.path
|
||||
assert entry.lineno == co.firstlineno + 1
|
||||
assert entry.frame.code.name == "h"
|
||||
assert reprcrash.path == str(co.path)
|
||||
assert reprcrash.lineno == co.firstlineno + 1 + 1
|
||||
|
||||
def test_traceback_getcrashentry_empty(self):
|
||||
def test_getreprcrash_empty(self):
|
||||
def g():
|
||||
__tracebackhide__ = True
|
||||
raise ValueError
|
||||
@@ -310,9 +322,7 @@ class TestTraceback_f_g_h:
|
||||
g()
|
||||
|
||||
excinfo = pytest.raises(ValueError, f)
|
||||
tb = excinfo.traceback
|
||||
entry = tb.getcrashentry()
|
||||
assert entry is None
|
||||
assert excinfo._getreprcrash() is None
|
||||
|
||||
|
||||
def test_excinfo_exconly():
|
||||
@@ -614,7 +624,7 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.func1)
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
p = FormattedExcinfo()
|
||||
reprtb = p.repr_traceback_entry(excinfo.traceback[-1])
|
||||
|
||||
@@ -647,7 +657,7 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120)
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
entry = excinfo.traceback[-1]
|
||||
p = FormattedExcinfo(funcargs=True)
|
||||
reprfuncargs = p.repr_args(entry)
|
||||
@@ -674,7 +684,7 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d")
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
entry = excinfo.traceback[-1]
|
||||
p = FormattedExcinfo(funcargs=True)
|
||||
reprfuncargs = p.repr_args(entry)
|
||||
@@ -948,7 +958,7 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.f)
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
repr = excinfo.getrepr()
|
||||
repr.toterminal(tw_mock)
|
||||
assert tw_mock.lines[0] == ""
|
||||
@@ -982,7 +992,7 @@ raise ValueError()
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.f)
|
||||
tmp_path.joinpath("mod.py").unlink()
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
repr = excinfo.getrepr()
|
||||
repr.toterminal(tw_mock)
|
||||
assert tw_mock.lines[0] == ""
|
||||
@@ -1014,7 +1024,7 @@ raise ValueError()
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.f)
|
||||
tmp_path.joinpath("mod.py").write_text("asdf")
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
repr = excinfo.getrepr()
|
||||
repr.toterminal(tw_mock)
|
||||
assert tw_mock.lines[0] == ""
|
||||
@@ -1111,9 +1121,11 @@ raise ValueError()
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ValueError, mod.f)
|
||||
excinfo.traceback = excinfo.traceback.filter()
|
||||
excinfo.traceback[1].set_repr_style("short")
|
||||
excinfo.traceback[2].set_repr_style("short")
|
||||
excinfo.traceback = excinfo.traceback.filter(excinfo)
|
||||
excinfo.traceback = _pytest._code.Traceback(
|
||||
entry if i not in (1, 2) else entry.with_repr_style("short")
|
||||
for i, entry in enumerate(excinfo.traceback)
|
||||
)
|
||||
r = excinfo.getrepr(style="long")
|
||||
r.toterminal(tw_mock)
|
||||
for line in tw_mock.lines:
|
||||
@@ -1379,7 +1391,7 @@ raise ValueError()
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
mod.f()
|
||||
# previously crashed with `AttributeError: list has no attribute get`
|
||||
excinfo.traceback.filter()
|
||||
excinfo.traceback.filter(excinfo)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("style", ["short", "long"])
|
||||
@@ -1573,3 +1585,66 @@ 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
|
||||
pytest.importorskip("exceptiongroup")
|
||||
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native"))
|
||||
def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None:
|
||||
"""Regression test for #10903."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
def test():
|
||||
__tracebackhide__ = True
|
||||
1 / 0
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("--tb", tbstyle)
|
||||
assert result.ret == 1
|
||||
if tbstyle != "line":
|
||||
result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"])
|
||||
if tbstyle not in ("line", "native"):
|
||||
result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])
|
||||
|
||||
|
||||
def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester) -> None:
|
||||
"""Hidden entries of chained exceptions are not shown (#1904)."""
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
def g1():
|
||||
__tracebackhide__ = True
|
||||
str.does_not_exist
|
||||
|
||||
def f3():
|
||||
__tracebackhide__ = True
|
||||
1 / 0
|
||||
|
||||
def f2():
|
||||
try:
|
||||
f3()
|
||||
except Exception:
|
||||
g1()
|
||||
|
||||
def f1():
|
||||
__tracebackhide__ = True
|
||||
f2()
|
||||
|
||||
def test():
|
||||
f1()
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest(str(p), "--tb=short")
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*.py:11: in f2",
|
||||
" f3()",
|
||||
"E ZeroDivisionError: division by zero",
|
||||
"",
|
||||
"During handling of the above exception, another exception occurred:",
|
||||
"*.py:20: in test",
|
||||
" f1()",
|
||||
"*.py:13: in f2",
|
||||
" g1()",
|
||||
"E AttributeError:*'does_not_exist'",
|
||||
],
|
||||
consecutive=True,
|
||||
)
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ def tw_mock():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dummy_yaml_custom_test(pytester: Pytester):
|
||||
def dummy_yaml_custom_test(pytester: Pytester) -> None:
|
||||
"""Writes a conftest file that collects and executes a dummy yaml test.
|
||||
|
||||
Taken from the docs, but stripped down to the bare minimum, useful for
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# mypy: disable-error-code="attr-defined"
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
@@ -8,6 +9,19 @@ logger = logging.getLogger(__name__)
|
||||
sublogger = logging.getLogger(__name__ + ".baz")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cleanup_disabled_logging():
|
||||
"""Simple fixture that ensures that a test doesn't disable logging.
|
||||
|
||||
This is necessary because ``logging.disable()`` is global, so a test disabling logging
|
||||
and not cleaning up after will break every test that runs after it.
|
||||
|
||||
This behavior was moved to a fixture so that logging will be un-disabled even if the test fails an assertion.
|
||||
"""
|
||||
yield
|
||||
logging.disable(logging.NOTSET)
|
||||
|
||||
|
||||
def test_fixture_help(pytester: Pytester) -> None:
|
||||
result = pytester.runpytest("--fixtures")
|
||||
result.stdout.fnmatch_lines(["*caplog*"])
|
||||
@@ -28,10 +42,27 @@ def test_change_level(caplog):
|
||||
assert "CRITICAL" in caplog.text
|
||||
|
||||
|
||||
def test_change_level_logging_disabled(caplog, cleanup_disabled_logging):
|
||||
logging.disable(logging.CRITICAL)
|
||||
assert logging.root.manager.disable == logging.CRITICAL
|
||||
caplog.set_level(logging.WARNING)
|
||||
logger.info("handler INFO level")
|
||||
logger.warning("handler WARNING level")
|
||||
|
||||
caplog.set_level(logging.CRITICAL, logger=sublogger.name)
|
||||
sublogger.warning("logger SUB_WARNING level")
|
||||
sublogger.critical("logger SUB_CRITICAL level")
|
||||
|
||||
assert "INFO" not in caplog.text
|
||||
assert "WARNING" in caplog.text
|
||||
assert "SUB_WARNING" not in caplog.text
|
||||
assert "SUB_CRITICAL" in caplog.text
|
||||
|
||||
|
||||
def test_change_level_undo(pytester: Pytester) -> None:
|
||||
"""Ensure that 'set_level' is undone after the end of the test.
|
||||
|
||||
Tests the logging output themselves (affacted both by logger and handler levels).
|
||||
Tests the logging output themselves (affected both by logger and handler levels).
|
||||
"""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
@@ -54,6 +85,37 @@ def test_change_level_undo(pytester: Pytester) -> None:
|
||||
result.stdout.no_fnmatch_line("*log from test2*")
|
||||
|
||||
|
||||
def test_change_disabled_level_undo(
|
||||
pytester: Pytester, cleanup_disabled_logging
|
||||
) -> None:
|
||||
"""Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
|
||||
|
||||
Tests the logging output themselves (affected by disabled logging level).
|
||||
"""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
def test1(caplog):
|
||||
logging.disable(logging.CRITICAL)
|
||||
caplog.set_level(logging.INFO)
|
||||
# using + operator here so fnmatch_lines doesn't match the code in the traceback
|
||||
logging.info('log from ' + 'test1')
|
||||
assert 0
|
||||
|
||||
def test2(caplog):
|
||||
# using + operator here so fnmatch_lines doesn't match the code in the traceback
|
||||
# use logging.warning because we need a level that will show up if logging.disabled
|
||||
# isn't reset to ``CRITICAL`` after test1.
|
||||
logging.warning('log from ' + 'test2')
|
||||
assert 0
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"])
|
||||
result.stdout.no_fnmatch_line("*log from test2*")
|
||||
|
||||
|
||||
def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
||||
"""Ensure that 'set_level' is undone after the end of the test (handler).
|
||||
|
||||
@@ -97,6 +159,65 @@ def test_with_statement(caplog):
|
||||
assert "CRITICAL" in caplog.text
|
||||
|
||||
|
||||
def test_with_statement_logging_disabled(caplog, cleanup_disabled_logging):
|
||||
logging.disable(logging.CRITICAL)
|
||||
assert logging.root.manager.disable == logging.CRITICAL
|
||||
with caplog.at_level(logging.WARNING):
|
||||
logger.debug("handler DEBUG level")
|
||||
logger.info("handler INFO level")
|
||||
logger.warning("handler WARNING level")
|
||||
logger.error("handler ERROR level")
|
||||
logger.critical("handler CRITICAL level")
|
||||
|
||||
assert logging.root.manager.disable == logging.INFO
|
||||
|
||||
with caplog.at_level(logging.CRITICAL, logger=sublogger.name):
|
||||
sublogger.warning("logger SUB_WARNING level")
|
||||
sublogger.critical("logger SUB_CRITICAL level")
|
||||
|
||||
assert "DEBUG" not in caplog.text
|
||||
assert "INFO" not in caplog.text
|
||||
assert "WARNING" in caplog.text
|
||||
assert "ERROR" in caplog.text
|
||||
assert " CRITICAL" in caplog.text
|
||||
assert "SUB_WARNING" not in caplog.text
|
||||
assert "SUB_CRITICAL" in caplog.text
|
||||
assert logging.root.manager.disable == logging.CRITICAL
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"level_str,expected_disable_level",
|
||||
[
|
||||
("CRITICAL", logging.ERROR),
|
||||
("ERROR", logging.WARNING),
|
||||
("WARNING", logging.INFO),
|
||||
("INFO", logging.DEBUG),
|
||||
("DEBUG", logging.NOTSET),
|
||||
("NOTSET", logging.NOTSET),
|
||||
("NOTVALIDLEVEL", logging.NOTSET),
|
||||
],
|
||||
)
|
||||
def test_force_enable_logging_level_string(
|
||||
caplog, cleanup_disabled_logging, level_str, expected_disable_level
|
||||
):
|
||||
"""Test _force_enable_logging using a level string.
|
||||
|
||||
``expected_disable_level`` is one level below ``level_str`` because the disabled log level
|
||||
always needs to be *at least* one level lower than the level that caplog is trying to capture.
|
||||
"""
|
||||
test_logger = logging.getLogger("test_str_level_force_enable")
|
||||
# Emulate a testing environment where all logging is disabled.
|
||||
logging.disable(logging.CRITICAL)
|
||||
# Make sure all logging is disabled.
|
||||
assert not test_logger.isEnabledFor(logging.CRITICAL)
|
||||
# Un-disable logging for `level_str`.
|
||||
caplog._force_enable_logging(level_str, test_logger)
|
||||
# Make sure that the disabled level is now one below the requested logging level.
|
||||
# We don't use `isEnabledFor` here because that also checks the level set by
|
||||
# `logging.setLevel()` which is irrelevant to `logging.disable()`.
|
||||
assert test_logger.manager.disable == expected_disable_level
|
||||
|
||||
|
||||
def test_log_access(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
logger.info("boo %s", "arg")
|
||||
|
||||
@@ -1167,8 +1167,8 @@ def test_log_file_cli_subdirectories_are_successfully_created(
|
||||
assert result.ret == ExitCode.OK
|
||||
|
||||
|
||||
def test_disable_loggers(testdir):
|
||||
testdir.makepyfile(
|
||||
def test_disable_loggers(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
@@ -1181,13 +1181,13 @@ def test_disable_loggers(testdir):
|
||||
assert caplog.record_tuples == [('test', 10, 'Visible text!')]
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("--log-disable=disabled", "-s")
|
||||
result = pytester.runpytest("--log-disable=disabled", "-s")
|
||||
assert result.ret == ExitCode.OK
|
||||
assert not result.stderr.lines
|
||||
|
||||
|
||||
def test_disable_loggers_does_not_propagate(testdir):
|
||||
testdir.makepyfile(
|
||||
def test_disable_loggers_does_not_propagate(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
@@ -1205,13 +1205,13 @@ def test_disable_loggers_does_not_propagate(testdir):
|
||||
"""
|
||||
)
|
||||
|
||||
result = testdir.runpytest("--log-disable=parent.child", "-s")
|
||||
result = pytester.runpytest("--log-disable=parent.child", "-s")
|
||||
assert result.ret == ExitCode.OK
|
||||
assert not result.stderr.lines
|
||||
|
||||
|
||||
def test_log_disabling_works_with_log_cli(testdir):
|
||||
testdir.makepyfile(
|
||||
def test_log_disabling_works_with_log_cli(pytester: Pytester) -> None:
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
disabled_log = logging.getLogger('disabled')
|
||||
@@ -1222,7 +1222,7 @@ def test_log_disabling_works_with_log_cli(testdir):
|
||||
disabled_log.warning("This string will be suppressed.")
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest(
|
||||
result = pytester.runpytest(
|
||||
"--log-cli-level=DEBUG",
|
||||
"--log-disable=disabled",
|
||||
)
|
||||
@@ -1234,3 +1234,100 @@ def test_log_disabling_works_with_log_cli(testdir):
|
||||
"WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed."
|
||||
)
|
||||
assert not result.stderr.lines
|
||||
|
||||
|
||||
def test_without_date_format_log(pytester: Pytester) -> None:
|
||||
"""Check that date is not printed by default."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_foo():
|
||||
logger.warning('text')
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.fnmatch_lines(
|
||||
["WARNING test_without_date_format_log:test_without_date_format_log.py:6 text"]
|
||||
)
|
||||
|
||||
|
||||
def test_date_format_log(pytester: Pytester) -> None:
|
||||
"""Check that log_date_format affects output."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_foo():
|
||||
logger.warning('text')
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
log_format=%(asctime)s; %(levelname)s; %(message)s
|
||||
log_date_format=%Y-%m-%d %H:%M:%S
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}; WARNING; text"])
|
||||
|
||||
|
||||
def test_date_format_percentf_log(pytester: Pytester) -> None:
|
||||
"""Make sure that microseconds are printed in log."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_foo():
|
||||
logger.warning('text')
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
log_format=%(asctime)s; %(levelname)s; %(message)s
|
||||
log_date_format=%Y-%m-%d %H:%M:%S.%f
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}; WARNING; text"])
|
||||
|
||||
|
||||
def test_date_format_percentf_tz_log(pytester: Pytester) -> None:
|
||||
"""Make sure that timezone and microseconds are properly formatted together."""
|
||||
pytester.makepyfile(
|
||||
"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def test_foo():
|
||||
logger.warning('text')
|
||||
assert False
|
||||
"""
|
||||
)
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
log_format=%(asctime)s; %(levelname)s; %(message)s
|
||||
log_date_format=%Y-%m-%d %H:%M:%S.%f%z
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
assert result.ret == 1
|
||||
result.stdout.re_match_lines(
|
||||
[r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}[+-][0-9\.]+; WARNING; text"]
|
||||
)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
anyio[curio,trio]==3.6.2
|
||||
django==4.1.7
|
||||
anyio[curio,trio]==3.7.0
|
||||
django==4.2.1
|
||||
pytest-asyncio==0.21.0
|
||||
pytest-bdd==6.1.1
|
||||
pytest-cov==4.0.0
|
||||
pytest-cov==4.1.0
|
||||
pytest-django==4.5.2
|
||||
pytest-flakes==4.0.5
|
||||
pytest-html==3.2.0
|
||||
pytest-mock==3.10.0
|
||||
pytest-rerunfailures==11.1.2
|
||||
pytest-sugar==0.9.5
|
||||
pytest-sugar==0.9.7
|
||||
pytest-trio==0.7.0
|
||||
pytest-twisted==1.14.0
|
||||
twisted==22.8.0
|
||||
pytest-xvfb==2.0.0
|
||||
pytest-xvfb==3.0.0
|
||||
|
||||
+17
-13
@@ -897,25 +897,29 @@ class TestConftestCustomization:
|
||||
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
|
||||
"""Ensure we can collect files with weird file extensions as Python
|
||||
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".
|
||||
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
|
||||
|
||||
class Loader(object):
|
||||
def load_module(self, name):
|
||||
return imp.load_source(name, name + ".narf")
|
||||
class Finder(object):
|
||||
def find_module(self, name, path=None):
|
||||
if os.path.exists(name + ".narf"):
|
||||
return Loader()
|
||||
sys.meta_path.append(Finder())
|
||||
class MetaPathFinder:
|
||||
def find_spec(self, fullname, path, target=None):
|
||||
if os.path.exists(fullname + ".narf"):
|
||||
return spec_from_loader(
|
||||
fullname,
|
||||
SourceFileLoader(fullname, fullname + ".narf"),
|
||||
)
|
||||
sys.meta_path.append(MetaPathFinder())
|
||||
|
||||
def pytest_collect_file(file_path, parent):
|
||||
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(
|
||||
".narf",
|
||||
@@ -1003,9 +1007,9 @@ class TestTracebackCutting:
|
||||
with pytest.raises(pytest.skip.Exception) as excinfo:
|
||||
pytest.skip("xxx")
|
||||
assert excinfo.traceback[-1].frame.code.name == "skip"
|
||||
assert excinfo.traceback[-1].ishidden()
|
||||
assert excinfo.traceback[-1].ishidden(excinfo)
|
||||
assert excinfo.traceback[-2].frame.code.name == "test_skip_simple"
|
||||
assert not excinfo.traceback[-2].ishidden()
|
||||
assert not excinfo.traceback[-2].ishidden(excinfo)
|
||||
|
||||
def test_traceback_argsetup(self, pytester: Pytester) -> None:
|
||||
pytester.makeconftest(
|
||||
|
||||
@@ -1436,6 +1436,96 @@ class TestIssue10743:
|
||||
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(
|
||||
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
|
||||
)
|
||||
|
||||
@@ -87,7 +87,7 @@ class TestNewAPI:
|
||||
"*= warnings summary =*",
|
||||
"*/cacheprovider.py:*",
|
||||
" */cacheprovider.py:*: PytestCacheWarning: could not create cache path "
|
||||
f"{unwritable_cache_dir}/v/cache/nodeids",
|
||||
f"{unwritable_cache_dir}/v/cache/nodeids: *",
|
||||
' config.cache.set("cache/nodeids", sorted(self.cached_nodeids))',
|
||||
"*1 failed, 3 warnings in*",
|
||||
]
|
||||
@@ -420,7 +420,13 @@ class TestLastFailed:
|
||||
result = pytester.runpytest()
|
||||
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="""
|
||||
def test_a1(): pass
|
||||
@@ -848,6 +854,33 @@ class TestLastFailed:
|
||||
]
|
||||
)
|
||||
|
||||
def test_lastfailed_skip_collection_with_nesting(self, pytester: Pytester) -> None:
|
||||
"""Check that file skipping works even when the file with failures is
|
||||
nested at a different level of the collection tree."""
|
||||
pytester.makepyfile(
|
||||
**{
|
||||
"test_1.py": """
|
||||
def test_1(): pass
|
||||
""",
|
||||
"pkg/__init__.py": "",
|
||||
"pkg/test_2.py": """
|
||||
def test_2(): assert False
|
||||
""",
|
||||
}
|
||||
)
|
||||
# first run
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["collected 2 items", "*1 failed*1 passed*"])
|
||||
# second run - test_1.py is skipped.
|
||||
result = pytester.runpytest("--lf")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"collected 1 item",
|
||||
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
|
||||
"*= 1 failed in *",
|
||||
]
|
||||
)
|
||||
|
||||
def test_lastfailed_with_known_failures_not_being_selected(
|
||||
self, pytester: Pytester
|
||||
) -> None:
|
||||
@@ -1052,6 +1085,28 @@ class TestLastFailed:
|
||||
result = pytester.runpytest("--lf")
|
||||
result.assert_outcomes(failed=3)
|
||||
|
||||
def test_non_python_file_skipped(
|
||||
self,
|
||||
pytester: Pytester,
|
||||
dummy_yaml_custom_test: None,
|
||||
) -> None:
|
||||
pytester.makepyfile(
|
||||
**{
|
||||
"test_bad.py": """def test_bad(): assert False""",
|
||||
},
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"])
|
||||
|
||||
result = pytester.runpytest("--lf")
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"collected 1 item",
|
||||
"run-last-failure: rerun previous 1 failure (skipped 1 file)",
|
||||
"* 1 failed in *",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class TestNewFirst:
|
||||
def test_newfirst_usecase(self, pytester: Pytester) -> None:
|
||||
|
||||
@@ -1247,6 +1247,48 @@ def test_collect_pyargs_with_testpaths(
|
||||
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:
|
||||
"""Collect a direct symlink works even if it does not match python_files (#4325)."""
|
||||
real = pytester.makepyfile(
|
||||
|
||||
@@ -179,6 +179,23 @@ class TestParseIni:
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines("ERROR: *pyproject.toml: Invalid statement*")
|
||||
|
||||
def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None:
|
||||
# If --confcutdir is not specified, and there is no configfile, default
|
||||
# to the roothpath.
|
||||
sub = pytester.mkdir("sub")
|
||||
os.chdir(sub)
|
||||
config = pytester.parseconfigure()
|
||||
assert config.pluginmanager._confcutdir == sub
|
||||
|
||||
def test_confcutdir_default_with_configfile(self, pytester: Pytester) -> None:
|
||||
# If --confcutdir is not specified, and there is a configfile, default
|
||||
# to the configfile's directory.
|
||||
pytester.makeini("[pytest]")
|
||||
sub = pytester.mkdir("sub")
|
||||
os.chdir(sub)
|
||||
config = pytester.parseconfigure()
|
||||
assert config.pluginmanager._confcutdir == pytester.path
|
||||
|
||||
@pytest.mark.xfail(reason="probably not needed")
|
||||
def test_confcutdir(self, pytester: Pytester) -> None:
|
||||
sub = pytester.mkdir("sub")
|
||||
@@ -514,6 +531,8 @@ class TestConfigCmdlineParsing:
|
||||
)
|
||||
config = pytester.parseconfig("-c", "custom.ini")
|
||||
assert config.getini("custom") == "1"
|
||||
config = pytester.parseconfig("--config-file", "custom.ini")
|
||||
assert config.getini("custom") == "1"
|
||||
|
||||
pytester.makefile(
|
||||
".cfg",
|
||||
@@ -524,6 +543,8 @@ class TestConfigCmdlineParsing:
|
||||
)
|
||||
config = pytester.parseconfig("-c", "custom_tool_pytest_section.cfg")
|
||||
assert config.getini("custom") == "1"
|
||||
config = pytester.parseconfig("--config-file", "custom_tool_pytest_section.cfg")
|
||||
assert config.getini("custom") == "1"
|
||||
|
||||
pytester.makefile(
|
||||
".toml",
|
||||
@@ -536,6 +557,8 @@ class TestConfigCmdlineParsing:
|
||||
)
|
||||
config = pytester.parseconfig("-c", "custom.toml")
|
||||
assert config.getini("custom") == "1"
|
||||
config = pytester.parseconfig("--config-file", "custom.toml")
|
||||
assert config.getini("custom") == "1"
|
||||
|
||||
def test_absolute_win32_path(self, pytester: Pytester) -> None:
|
||||
temp_ini_file = pytester.makefile(
|
||||
@@ -550,6 +573,8 @@ class TestConfigCmdlineParsing:
|
||||
temp_ini_file_norm = normpath(str(temp_ini_file))
|
||||
ret = pytest.main(["-c", temp_ini_file_norm])
|
||||
assert ret == ExitCode.OK
|
||||
ret = pytest.main(["--config-file", temp_ini_file_norm])
|
||||
assert ret == ExitCode.OK
|
||||
|
||||
|
||||
class TestConfigAPI:
|
||||
@@ -1907,6 +1932,9 @@ class TestSetupCfg:
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
pytester.runpytest("-c", "custom.cfg")
|
||||
|
||||
with pytest.raises(pytest.fail.Exception):
|
||||
pytester.runpytest("--config-file", "custom.cfg")
|
||||
|
||||
|
||||
class TestPytestPluginsVariable:
|
||||
def test_pytest_plugins_in_non_top_level_conftest_unsupported(
|
||||
|
||||
@@ -35,7 +35,7 @@ def conftest_setinitial(
|
||||
self.importmode = "prepend"
|
||||
|
||||
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")
|
||||
@@ -594,7 +594,13 @@ class TestConftestVisibility:
|
||||
print("pytestarg : %s" % testarg)
|
||||
print("expected pass : %s" % expect_ntests_passed)
|
||||
os.chdir(dirs[chdir])
|
||||
reprec = pytester.inline_run(testarg, "-q", "--traceconfig")
|
||||
reprec = pytester.inline_run(
|
||||
testarg,
|
||||
"-q",
|
||||
"--traceconfig",
|
||||
"--confcutdir",
|
||||
pytester.path,
|
||||
)
|
||||
reprec.assertoutcome(passed=expect_ntests_passed)
|
||||
|
||||
|
||||
|
||||
@@ -425,9 +425,7 @@ def test_context_classmethod() -> None:
|
||||
assert A.x == 1
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings(
|
||||
"ignore:Deprecated call to `pkg_resources.declare_namespace"
|
||||
)
|
||||
@pytest.mark.filterwarnings(r"ignore:.*\bpkg_resources\b:DeprecationWarning")
|
||||
def test_syspath_prepend_with_namespace_packages(
|
||||
pytester: Pytester, monkeypatch: MonkeyPatch
|
||||
) -> None:
|
||||
|
||||
@@ -82,7 +82,7 @@ def test_no_ini(pytester: Pytester, file_structure) -> None:
|
||||
|
||||
def test_clean_up(pytester: Pytester) -> None:
|
||||
"""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:
|
||||
# - Cleanup is done in pytest_unconfigure().
|
||||
# - Not a hookwrapper.
|
||||
|
||||
@@ -387,13 +387,13 @@ class TestTerminal:
|
||||
pytest.xfail("It's 🕙 o'clock")
|
||||
|
||||
@pytest.mark.skip(
|
||||
reason="cannot do foobar because baz is missing due to I don't know what"
|
||||
reason="1 cannot do foobar because baz is missing due to I don't know what"
|
||||
)
|
||||
def test_long_skip():
|
||||
pass
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason="cannot do foobar because baz is missing due to I don't know what"
|
||||
reason="2 cannot do foobar because baz is missing due to I don't know what"
|
||||
)
|
||||
def test_long_xfail():
|
||||
print(1 / 0)
|
||||
@@ -417,8 +417,8 @@ class TestTerminal:
|
||||
result.stdout.fnmatch_lines(
|
||||
common_output
|
||||
+ [
|
||||
"test_verbose_skip_reason.py::test_long_skip SKIPPED (cannot *...) *",
|
||||
"test_verbose_skip_reason.py::test_long_xfail XFAIL (cannot *...) *",
|
||||
"test_verbose_skip_reason.py::test_long_skip SKIPPED (1 cannot *...) *",
|
||||
"test_verbose_skip_reason.py::test_long_xfail XFAIL (2 cannot *...) *",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -428,12 +428,14 @@ class TestTerminal:
|
||||
+ [
|
||||
(
|
||||
"test_verbose_skip_reason.py::test_long_skip SKIPPED"
|
||||
" (cannot do foobar because baz is missing due to I don't know what) *"
|
||||
" (1 cannot do foobar"
|
||||
),
|
||||
"because baz is missing due to I don't know what) *",
|
||||
(
|
||||
"test_verbose_skip_reason.py::test_long_xfail XFAIL"
|
||||
" (cannot do foobar because baz is missing due to I don't know what) *"
|
||||
" (2 cannot do foobar"
|
||||
),
|
||||
"because baz is missing due to I don't know what) *",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -1539,6 +1541,19 @@ class TestGenericReporting:
|
||||
s = result.stdout.str()
|
||||
assert "def test_func2" not in s
|
||||
|
||||
def test_tb_crashline_pytrace_false(self, pytester: Pytester, option) -> None:
|
||||
p = pytester.makepyfile(
|
||||
"""
|
||||
import pytest
|
||||
def test_func1():
|
||||
pytest.fail('test_func1', pytrace=False)
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest("--tb=line")
|
||||
result.stdout.str()
|
||||
bn = p.name
|
||||
result.stdout.fnmatch_lines(["*%s:3: Failed: test_func1" % bn])
|
||||
|
||||
def test_pytest_report_header(self, pytester: Pytester, option) -> None:
|
||||
pytester.makeconftest(
|
||||
"""
|
||||
|
||||
@@ -512,20 +512,20 @@ class TestRmRf:
|
||||
|
||||
# unknown exception
|
||||
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)
|
||||
assert fn.is_file()
|
||||
|
||||
# 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)
|
||||
|
||||
# unknown function
|
||||
with pytest.warns(
|
||||
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)
|
||||
assert fn.is_file()
|
||||
|
||||
@@ -533,12 +533,12 @@ class TestRmRf:
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("ignore")
|
||||
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)
|
||||
assert fn.is_file()
|
||||
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)
|
||||
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
|
||||
@@ -777,6 +777,20 @@ class TestStackLevel:
|
||||
)
|
||||
|
||||
|
||||
def test_warning_on_testpaths_not_found(pytester: Pytester) -> None:
|
||||
# Check for warning when testpaths set, but not found by glob
|
||||
pytester.makeini(
|
||||
"""
|
||||
[pytest]
|
||||
testpaths = absent
|
||||
"""
|
||||
)
|
||||
result = pytester.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
["*ConfigWarning: No files were found in testpaths*", "*1 warning*"]
|
||||
)
|
||||
|
||||
|
||||
def test_resource_warning(pytester: Pytester, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Some platforms (notably PyPy) don't have tracemalloc.
|
||||
# We choose to explicitly not skip this in case tracemalloc is not
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Optional
|
||||
from typing_extensions import assert_type
|
||||
|
||||
import pytest
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
|
||||
# Issue #7488.
|
||||
@@ -29,6 +30,19 @@ def check_parametrize_ids_callable(func) -> None:
|
||||
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:
|
||||
with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user