Merge branch 'main' into improve-high-scope-fixtures-teardown-issue-3806
This commit is contained in:
commit
eba76ce663
|
@ -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
|
|
@ -38,7 +38,7 @@ jobs:
|
||||||
run: python scripts/update-plugin-list.py
|
run: python scripts/update-plugin-list.py
|
||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54
|
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5
|
||||||
with:
|
with:
|
||||||
commit-message: '[automated] Update plugin list'
|
commit-message: '[automated] Update plugin list'
|
||||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
default_language_version:
|
|
||||||
python: "3.10"
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.1.0
|
rev: 23.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
args: [--safe, --quiet]
|
args: [--safe, --quiet]
|
||||||
|
@ -23,7 +21,7 @@ repos:
|
||||||
exclude: _pytest/(debugging|hookspec).py
|
exclude: _pytest/(debugging|hookspec).py
|
||||||
language_version: python3
|
language_version: python3
|
||||||
- repo: https://github.com/PyCQA/autoflake
|
- repo: https://github.com/PyCQA/autoflake
|
||||||
rev: v2.0.2
|
rev: v2.1.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: autoflake
|
- id: autoflake
|
||||||
name: autoflake
|
name: autoflake
|
||||||
|
@ -38,13 +36,13 @@ repos:
|
||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- flake8-typing-imports==1.12.0
|
- flake8-typing-imports==1.12.0
|
||||||
- flake8-docstrings==1.5.0
|
- flake8-docstrings==1.5.0
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
- repo: https://github.com/asottile/reorder-python-imports
|
||||||
rev: v3.9.0
|
rev: v3.9.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
args: ['--application-directories=.:src', --py37-plus]
|
args: ['--application-directories=.:src', --py37-plus]
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v3.3.1
|
rev: v3.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py37-plus]
|
args: [--py37-plus]
|
||||||
|
@ -58,7 +56,7 @@ repos:
|
||||||
hooks:
|
hooks:
|
||||||
- id: python-use-type-annotations
|
- id: python-use-type-annotations
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.1.1
|
rev: v1.3.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
files: ^(src/|testing/)
|
files: ^(src/|testing/)
|
||||||
|
|
6
AUTHORS
6
AUTHORS
|
@ -8,11 +8,14 @@ Abdeali JK
|
||||||
Abdelrahman Elbehery
|
Abdelrahman Elbehery
|
||||||
Abhijeet Kasurde
|
Abhijeet Kasurde
|
||||||
Adam Johnson
|
Adam Johnson
|
||||||
|
Adam Stewart
|
||||||
Adam Uhlir
|
Adam Uhlir
|
||||||
Ahn Ki-Wook
|
Ahn Ki-Wook
|
||||||
Akiomi Kamakura
|
Akiomi Kamakura
|
||||||
Alan Velasco
|
Alan Velasco
|
||||||
Alessio Izzo
|
Alessio Izzo
|
||||||
|
Alex Jones
|
||||||
|
Alex Lambson
|
||||||
Alexander Johnson
|
Alexander Johnson
|
||||||
Alexander King
|
Alexander King
|
||||||
Alexei Kozlenok
|
Alexei Kozlenok
|
||||||
|
@ -55,6 +58,7 @@ Benjamin Peterson
|
||||||
Bernard Pratz
|
Bernard Pratz
|
||||||
Bob Ippolito
|
Bob Ippolito
|
||||||
Brian Dorsey
|
Brian Dorsey
|
||||||
|
Brian Larsen
|
||||||
Brian Maissy
|
Brian Maissy
|
||||||
Brian Okken
|
Brian Okken
|
||||||
Brianna Laugher
|
Brianna Laugher
|
||||||
|
@ -163,6 +167,7 @@ Ionuț Turturică
|
||||||
Itxaso Aizpurua
|
Itxaso Aizpurua
|
||||||
Iwan Briquemont
|
Iwan Briquemont
|
||||||
Jaap Broekhuizen
|
Jaap Broekhuizen
|
||||||
|
Jake VanderPlas
|
||||||
Jakob van Santen
|
Jakob van Santen
|
||||||
Jakub Mitoraj
|
Jakub Mitoraj
|
||||||
James Bourbeau
|
James Bourbeau
|
||||||
|
@ -303,6 +308,7 @@ Rafal Semik
|
||||||
Raquel Alegre
|
Raquel Alegre
|
||||||
Ravi Chandra
|
Ravi Chandra
|
||||||
Robert Holt
|
Robert Holt
|
||||||
|
Roberto Aldera
|
||||||
Roberto Polli
|
Roberto Polli
|
||||||
Roland Puntaier
|
Roland Puntaier
|
||||||
Romain Dorgueil
|
Romain Dorgueil
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
|
|
@ -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``.
|
|
@ -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 @@
|
||||||
|
:confval:`testpaths` is now honored to load root ``conftests``.
|
|
@ -0,0 +1 @@
|
||||||
|
The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
|
|
@ -0,0 +1 @@
|
||||||
|
Added underlying exception to cache provider path creation and write warning messages.
|
|
@ -1 +0,0 @@
|
||||||
Correctly handle ``__tracebackhide__`` for chained exceptions.
|
|
|
@ -1,2 +0,0 @@
|
||||||
The full output of a test is no longer truncated if the truncation message would be longer than
|
|
||||||
the hidden text. The line number shown has also been fixed.
|
|
|
@ -1 +0,0 @@
|
||||||
``--log-disable`` CLI option added to disable individual loggers.
|
|
|
@ -1 +0,0 @@
|
||||||
Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
|
|
|
@ -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,8 @@ Release announcements
|
||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
|
|
||||||
|
release-7.3.1
|
||||||
|
release-7.3.0
|
||||||
release-7.2.2
|
release-7.2.2
|
||||||
release-7.2.1
|
release-7.2.1
|
||||||
release-7.2.0
|
release-7.2.0
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
pytest-7.3.0
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
The pytest team is proud to announce the 7.3.0 release!
|
||||||
|
|
||||||
|
This release contains new features, improvements, and bug fixes,
|
||||||
|
the full list of changes is available in the changelog:
|
||||||
|
|
||||||
|
https://docs.pytest.org/en/stable/changelog.html
|
||||||
|
|
||||||
|
For complete documentation, please visit:
|
||||||
|
|
||||||
|
https://docs.pytest.org/en/stable/
|
||||||
|
|
||||||
|
As usual, you can upgrade from PyPI via:
|
||||||
|
|
||||||
|
pip install -U pytest
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Aaron Berdy
|
||||||
|
* Adam Turner
|
||||||
|
* Albert Villanova del Moral
|
||||||
|
* Alessio Izzo
|
||||||
|
* Alex Hadley
|
||||||
|
* Alice Purcell
|
||||||
|
* Anthony Sottile
|
||||||
|
* Anton Yakutovich
|
||||||
|
* Ashish Kurmi
|
||||||
|
* Babak Keyvani
|
||||||
|
* Billy
|
||||||
|
* Brandon Chinn
|
||||||
|
* Bruno Oliveira
|
||||||
|
* Cal Jacobson
|
||||||
|
* Chanvin Xiao
|
||||||
|
* Cheuk Ting Ho
|
||||||
|
* Chris Wheeler
|
||||||
|
* Daniel Garcia Moreno
|
||||||
|
* Daniel Scheffler
|
||||||
|
* Daniel Valenzuela
|
||||||
|
* EmptyRabbit
|
||||||
|
* Ezio Melotti
|
||||||
|
* Felix Hofstätter
|
||||||
|
* Florian Best
|
||||||
|
* Florian Bruhin
|
||||||
|
* Fredrik Berndtsson
|
||||||
|
* Gabriel Landau
|
||||||
|
* Garvit Shubham
|
||||||
|
* Gergely Kalmár
|
||||||
|
* HTRafal
|
||||||
|
* Hugo van Kemenade
|
||||||
|
* Ilya Konstantinov
|
||||||
|
* Itxaso Aizpurua
|
||||||
|
* James Gerity
|
||||||
|
* Jay
|
||||||
|
* John Litborn
|
||||||
|
* Jon Parise
|
||||||
|
* Jouke Witteveen
|
||||||
|
* Kadino
|
||||||
|
* Kevin C
|
||||||
|
* Kian Eliasi
|
||||||
|
* Klaus Rettinghaus
|
||||||
|
* Kodi Arfer
|
||||||
|
* Mahesh Vashishtha
|
||||||
|
* Manuel Jacob
|
||||||
|
* Marko Pacak
|
||||||
|
* MatthewFlamm
|
||||||
|
* Miro Hrončok
|
||||||
|
* Nate Meyvis
|
||||||
|
* Neil Girdhar
|
||||||
|
* Nhieuvu1802
|
||||||
|
* Nipunn Koorapati
|
||||||
|
* Ofek Lev
|
||||||
|
* Paul Kehrer
|
||||||
|
* Paul Müller
|
||||||
|
* Paul Reece
|
||||||
|
* Pax
|
||||||
|
* Pete Baughman
|
||||||
|
* Peyman Salehi
|
||||||
|
* Philipp A
|
||||||
|
* Pierre Sassoulas
|
||||||
|
* Prerak Patel
|
||||||
|
* Ramsey
|
||||||
|
* Ran Benita
|
||||||
|
* Robert O'Shea
|
||||||
|
* Ronny Pfannschmidt
|
||||||
|
* Rowin
|
||||||
|
* Ruth Comer
|
||||||
|
* Samuel Colvin
|
||||||
|
* Samuel Gaist
|
||||||
|
* Sandro Tosi
|
||||||
|
* Santiago Castro
|
||||||
|
* Shantanu
|
||||||
|
* Simon K
|
||||||
|
* Stefanie Molin
|
||||||
|
* Stephen Rosen
|
||||||
|
* Sviatoslav Sydorenko
|
||||||
|
* Tatiana Ovary
|
||||||
|
* Teejay
|
||||||
|
* Thierry Moisan
|
||||||
|
* Thomas Grainger
|
||||||
|
* Tim Hoffmann
|
||||||
|
* Tobias Diez
|
||||||
|
* Tony Narlock
|
||||||
|
* Vivaan Verma
|
||||||
|
* Wolfremium
|
||||||
|
* Yannick PÉROUX
|
||||||
|
* Yusuke Kadowaki
|
||||||
|
* Zac Hatfield-Dodds
|
||||||
|
* Zach OBrien
|
||||||
|
* aizpurua23a
|
||||||
|
* bitzge
|
||||||
|
* bluthej
|
||||||
|
* gresm
|
||||||
|
* holesch
|
||||||
|
* itxasos23
|
||||||
|
* johnkangw
|
||||||
|
* q0w
|
||||||
|
* rdb
|
||||||
|
* s-padmanaban
|
||||||
|
* skhomuti
|
||||||
|
* sommersoft
|
||||||
|
* vin01
|
||||||
|
* wim glenn
|
||||||
|
* wodny
|
||||||
|
* zx.qiu
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -0,0 +1,18 @@
|
||||||
|
pytest-7.3.1
|
||||||
|
=======================================
|
||||||
|
|
||||||
|
pytest 7.3.1 has just been released to PyPI.
|
||||||
|
|
||||||
|
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||||
|
|
||||||
|
pip install --upgrade pytest
|
||||||
|
|
||||||
|
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||||
|
|
||||||
|
Thanks to all of the contributors to this release:
|
||||||
|
|
||||||
|
* Ran Benita
|
||||||
|
|
||||||
|
|
||||||
|
Happy testing,
|
||||||
|
The pytest Development Team
|
|
@ -92,3 +92,5 @@ pytest version min. Python version
|
||||||
5.0 - 6.1 3.5+
|
5.0 - 6.1 3.5+
|
||||||
3.3 - 4.6 2.7, 3.4+
|
3.3 - 4.6 2.7, 3.4+
|
||||||
============== ===================
|
============== ===================
|
||||||
|
|
||||||
|
`Status of Python Versions <https://devguide.python.org/versions/>`__.
|
||||||
|
|
|
@ -28,6 +28,105 @@ with advance notice in the **Deprecations** section of releases.
|
||||||
|
|
||||||
.. towncrier release notes start
|
.. towncrier release notes start
|
||||||
|
|
||||||
|
pytest 7.3.1 (2023-04-14)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#10875 <https://github.com/pytest-dev/pytest/issues/10875>`_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10890 <https://github.com/pytest-dev/pytest/issues/10890>`_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10896 <https://github.com/pytest-dev/pytest/issues/10896>`_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10903 <https://github.com/pytest-dev/pytest/issues/10903>`_: Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden.
|
||||||
|
This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0.
|
||||||
|
|
||||||
|
|
||||||
|
pytest 7.3.0 (2023-04-08)
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
- `#10525 <https://github.com/pytest-dev/pytest/issues/10525>`_: Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10755 <https://github.com/pytest-dev/pytest/issues/10755>`_: :confval:`console_output_style` now supports ``progress-even-when-capture-no`` to force the use of the progress output even when capture is disabled. This is useful in large test suites where capture may have significant performance impact.
|
||||||
|
|
||||||
|
|
||||||
|
- `#7431 <https://github.com/pytest-dev/pytest/issues/7431>`_: ``--log-disable`` CLI option added to disable individual loggers.
|
||||||
|
|
||||||
|
|
||||||
|
- `#8141 <https://github.com/pytest-dev/pytest/issues/8141>`_: Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improvements
|
||||||
|
------------
|
||||||
|
|
||||||
|
- `#10226 <https://github.com/pytest-dev/pytest/issues/10226>`_: If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10658 <https://github.com/pytest-dev/pytest/issues/10658>`_: Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of
|
||||||
|
``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration
|
||||||
|
file.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10710 <https://github.com/pytest-dev/pytest/issues/10710>`_: Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10727 <https://github.com/pytest-dev/pytest/issues/10727>`_: Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10840 <https://github.com/pytest-dev/pytest/issues/10840>`_: pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#6267 <https://github.com/pytest-dev/pytest/issues/6267>`_: The full output of a test is no longer truncated if the truncation message would be longer than
|
||||||
|
the hidden text. The line number shown has also been fixed.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
---------
|
||||||
|
|
||||||
|
- `#10743 <https://github.com/pytest-dev/pytest/issues/10743>`_: The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.
|
||||||
|
|
||||||
|
|
||||||
|
- `#10765 <https://github.com/pytest-dev/pytest/issues/10765>`_: Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.
|
||||||
|
|
||||||
|
|
||||||
|
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions.
|
||||||
|
NOTE: This change was reverted in version 7.3.1.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Improved Documentation
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
- `#10782 <https://github.com/pytest-dev/pytest/issues/10782>`_: Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Trivial/Internal Changes
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer directly depends on the `attrs <https://www.attrs.org/en/stable/>`__ package. While
|
||||||
|
we at pytest all love the package dearly and would like to thank the ``attrs`` team for many years of cooperation and support,
|
||||||
|
it makes sense for ``pytest`` to have as little external dependencies as possible, as this helps downstream projects.
|
||||||
|
With that in mind, we have replaced the pytest's limited internal usage to use the standard library's ``dataclasses`` instead.
|
||||||
|
|
||||||
|
Nice diffs for ``attrs`` classes are still supported though.
|
||||||
|
|
||||||
|
|
||||||
pytest 7.2.2 (2023-03-03)
|
pytest 7.2.2 (2023-03-03)
|
||||||
=========================
|
=========================
|
||||||
|
|
||||||
|
@ -468,7 +567,7 @@ Breaking Changes
|
||||||
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
||||||
|
|
||||||
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
||||||
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
|
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
|
||||||
|
|
||||||
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
||||||
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
||||||
|
@ -3968,7 +4067,7 @@ Removals
|
||||||
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
||||||
|
|
||||||
|
|
||||||
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
|
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
|
||||||
|
|
||||||
Use ``Node.get_closest_marker(name)`` as a replacement.
|
Use ``Node.get_closest_marker(name)`` as a replacement.
|
||||||
|
|
||||||
|
|
|
@ -341,7 +341,7 @@ epub_copyright = "2013, holger krekel et alii"
|
||||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
||||||
# epub_scheme = ''
|
# epub_scheme = ''
|
||||||
|
|
||||||
# The unique identifier of the text. This can be a ISBN number
|
# The unique identifier of the text. This can be an ISBN number
|
||||||
# or the project homepage.
|
# or the project homepage.
|
||||||
# epub_identifier = ''
|
# epub_identifier = ''
|
||||||
|
|
||||||
|
|
|
@ -502,8 +502,12 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||||
.. code-block:: pytest
|
.. code-block:: pytest
|
||||||
|
|
||||||
. $ pytest -rs -q multipython.py
|
. $ pytest -rs -q multipython.py
|
||||||
........................... [100%]
|
sssssssssssssssssssssssssss [100%]
|
||||||
27 passed in 0.12s
|
========================= short test summary info ==========================
|
||||||
|
SKIPPED [9] multipython.py:69: 'python3.5' not found
|
||||||
|
SKIPPED [9] multipython.py:69: 'python3.6' not found
|
||||||
|
SKIPPED [9] multipython.py:69: 'python3.7' not found
|
||||||
|
27 skipped in 0.12s
|
||||||
|
|
||||||
Indirect parametrization of optional implementations/imports
|
Indirect parametrization of optional implementations/imports
|
||||||
--------------------------------------------------------------------
|
--------------------------------------------------------------------
|
||||||
|
|
|
@ -70,12 +70,12 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
|
|
||||||
> assert not f()
|
> assert not f()
|
||||||
E assert not 42
|
E assert not 42
|
||||||
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef0002>()
|
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef0006>()
|
||||||
|
|
||||||
failure_demo.py:39: AssertionError
|
failure_demo.py:39: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_eq_text _________________
|
_________________ TestSpecialisedExplanations.test_eq_text _________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0006>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0007>
|
||||||
|
|
||||||
def test_eq_text(self):
|
def test_eq_text(self):
|
||||||
> assert "spam" == "eggs"
|
> assert "spam" == "eggs"
|
||||||
|
@ -86,7 +86,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:44: AssertionError
|
failure_demo.py:44: AssertionError
|
||||||
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0007>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0008>
|
||||||
|
|
||||||
def test_eq_similar_text(self):
|
def test_eq_similar_text(self):
|
||||||
> assert "foo 1 bar" == "foo 2 bar"
|
> assert "foo 1 bar" == "foo 2 bar"
|
||||||
|
@ -99,7 +99,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:47: AssertionError
|
failure_demo.py:47: AssertionError
|
||||||
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
|
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0008>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0009>
|
||||||
|
|
||||||
def test_eq_multiline_text(self):
|
def test_eq_multiline_text(self):
|
||||||
> assert "foo\nspam\nbar" == "foo\neggs\nbar"
|
> assert "foo\nspam\nbar" == "foo\neggs\nbar"
|
||||||
|
@ -112,7 +112,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:50: AssertionError
|
failure_demo.py:50: AssertionError
|
||||||
______________ TestSpecialisedExplanations.test_eq_long_text _______________
|
______________ TestSpecialisedExplanations.test_eq_long_text _______________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0009>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000a>
|
||||||
|
|
||||||
def test_eq_long_text(self):
|
def test_eq_long_text(self):
|
||||||
a = "1" * 100 + "a" + "2" * 100
|
a = "1" * 100 + "a" + "2" * 100
|
||||||
|
@ -129,7 +129,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:55: AssertionError
|
failure_demo.py:55: AssertionError
|
||||||
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
|
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000a>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000b>
|
||||||
|
|
||||||
def test_eq_long_text_multiline(self):
|
def test_eq_long_text_multiline(self):
|
||||||
a = "1\n" * 100 + "a" + "2\n" * 100
|
a = "1\n" * 100 + "a" + "2\n" * 100
|
||||||
|
@ -149,7 +149,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:60: AssertionError
|
failure_demo.py:60: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000b>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000c>
|
||||||
|
|
||||||
def test_eq_list(self):
|
def test_eq_list(self):
|
||||||
> assert [0, 1, 2] == [0, 1, 3]
|
> assert [0, 1, 2] == [0, 1, 3]
|
||||||
|
@ -160,7 +160,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:63: AssertionError
|
failure_demo.py:63: AssertionError
|
||||||
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000c>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000d>
|
||||||
|
|
||||||
def test_eq_list_long(self):
|
def test_eq_list_long(self):
|
||||||
a = [0] * 100 + [1] + [3] * 100
|
a = [0] * 100 + [1] + [3] * 100
|
||||||
|
@ -173,7 +173,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:68: AssertionError
|
failure_demo.py:68: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000d>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
|
||||||
|
|
||||||
def test_eq_dict(self):
|
def test_eq_dict(self):
|
||||||
> assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}
|
> assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}
|
||||||
|
@ -190,7 +190,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:71: AssertionError
|
failure_demo.py:71: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000f>
|
||||||
|
|
||||||
def test_eq_set(self):
|
def test_eq_set(self):
|
||||||
> assert {0, 10, 11, 12} == {0, 20, 21}
|
> assert {0, 10, 11, 12} == {0, 20, 21}
|
||||||
|
@ -207,7 +207,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:74: AssertionError
|
failure_demo.py:74: AssertionError
|
||||||
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000f>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0010>
|
||||||
|
|
||||||
def test_eq_longer_list(self):
|
def test_eq_longer_list(self):
|
||||||
> assert [1, 2] == [1, 2, 3]
|
> assert [1, 2] == [1, 2, 3]
|
||||||
|
@ -218,7 +218,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:77: AssertionError
|
failure_demo.py:77: AssertionError
|
||||||
_________________ TestSpecialisedExplanations.test_in_list _________________
|
_________________ TestSpecialisedExplanations.test_in_list _________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0010>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0011>
|
||||||
|
|
||||||
def test_in_list(self):
|
def test_in_list(self):
|
||||||
> assert 1 in [0, 2, 3, 4, 5]
|
> assert 1 in [0, 2, 3, 4, 5]
|
||||||
|
@ -227,7 +227,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:80: AssertionError
|
failure_demo.py:80: AssertionError
|
||||||
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________
|
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0011>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0012>
|
||||||
|
|
||||||
def test_not_in_text_multiline(self):
|
def test_not_in_text_multiline(self):
|
||||||
text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail"
|
text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail"
|
||||||
|
@ -245,7 +245,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:84: AssertionError
|
failure_demo.py:84: AssertionError
|
||||||
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0012>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0013>
|
||||||
|
|
||||||
def test_not_in_text_single(self):
|
def test_not_in_text_single(self):
|
||||||
text = "single foo line"
|
text = "single foo line"
|
||||||
|
@ -258,7 +258,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:88: AssertionError
|
failure_demo.py:88: AssertionError
|
||||||
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
|
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0013>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0014>
|
||||||
|
|
||||||
def test_not_in_text_single_long(self):
|
def test_not_in_text_single_long(self):
|
||||||
text = "head " * 50 + "foo " + "tail " * 20
|
text = "head " * 50 + "foo " + "tail " * 20
|
||||||
|
@ -271,7 +271,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:92: AssertionError
|
failure_demo.py:92: AssertionError
|
||||||
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
|
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0014>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0015>
|
||||||
|
|
||||||
def test_not_in_text_single_long_term(self):
|
def test_not_in_text_single_long_term(self):
|
||||||
text = "head " * 50 + "f" * 70 + "tail " * 20
|
text = "head " * 50 + "f" * 70 + "tail " * 20
|
||||||
|
@ -284,7 +284,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:96: AssertionError
|
failure_demo.py:96: AssertionError
|
||||||
______________ TestSpecialisedExplanations.test_eq_dataclass _______________
|
______________ TestSpecialisedExplanations.test_eq_dataclass _______________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0015>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
|
||||||
|
|
||||||
def test_eq_dataclass(self):
|
def test_eq_dataclass(self):
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
@ -311,7 +311,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:108: AssertionError
|
failure_demo.py:108: AssertionError
|
||||||
________________ TestSpecialisedExplanations.test_eq_attrs _________________
|
________________ TestSpecialisedExplanations.test_eq_attrs _________________
|
||||||
|
|
||||||
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
|
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0017>
|
||||||
|
|
||||||
def test_eq_attrs(self):
|
def test_eq_attrs(self):
|
||||||
import attr
|
import attr
|
||||||
|
@ -345,7 +345,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
i = Foo()
|
i = Foo()
|
||||||
> assert i.b == 2
|
> assert i.b == 2
|
||||||
E assert 1 == 2
|
E assert 1 == 2
|
||||||
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef0017>.b
|
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef0018>.b
|
||||||
|
|
||||||
failure_demo.py:128: AssertionError
|
failure_demo.py:128: AssertionError
|
||||||
_________________________ test_attribute_instance __________________________
|
_________________________ test_attribute_instance __________________________
|
||||||
|
@ -356,8 +356,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
|
|
||||||
> assert Foo().b == 2
|
> assert Foo().b == 2
|
||||||
E AssertionError: assert 1 == 2
|
E AssertionError: assert 1 == 2
|
||||||
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0018>.b
|
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0019>.b
|
||||||
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0018> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0019> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
|
||||||
|
|
||||||
failure_demo.py:135: AssertionError
|
failure_demo.py:135: AssertionError
|
||||||
__________________________ test_attribute_failure __________________________
|
__________________________ test_attribute_failure __________________________
|
||||||
|
@ -375,7 +375,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:146:
|
failure_demo.py:146:
|
||||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||||
|
|
||||||
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef0019>
|
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef001a>
|
||||||
|
|
||||||
def _get_b(self):
|
def _get_b(self):
|
||||||
> raise Exception("Failed to get attrib")
|
> raise Exception("Failed to get attrib")
|
||||||
|
@ -393,15 +393,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
|
|
||||||
> assert Foo().b == Bar().b
|
> assert Foo().b == Bar().b
|
||||||
E AssertionError: assert 1 == 2
|
E AssertionError: assert 1 == 2
|
||||||
E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001a>.b
|
E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001b>.b
|
||||||
E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001a> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
|
E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
|
||||||
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001b>.b
|
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001c>.b
|
||||||
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001c> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
|
||||||
|
|
||||||
failure_demo.py:156: AssertionError
|
failure_demo.py:156: AssertionError
|
||||||
__________________________ TestRaises.test_raises __________________________
|
__________________________ TestRaises.test_raises __________________________
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef001c>
|
self = <failure_demo.TestRaises object at 0xdeadbeef001d>
|
||||||
|
|
||||||
def test_raises(self):
|
def test_raises(self):
|
||||||
s = "qwe"
|
s = "qwe"
|
||||||
|
@ -411,7 +411,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:166: ValueError
|
failure_demo.py:166: ValueError
|
||||||
______________________ TestRaises.test_raises_doesnt _______________________
|
______________________ TestRaises.test_raises_doesnt _______________________
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef001d>
|
self = <failure_demo.TestRaises object at 0xdeadbeef001e>
|
||||||
|
|
||||||
def test_raises_doesnt(self):
|
def test_raises_doesnt(self):
|
||||||
> raises(OSError, int, "3")
|
> raises(OSError, int, "3")
|
||||||
|
@ -420,7 +420,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:169: Failed
|
failure_demo.py:169: Failed
|
||||||
__________________________ TestRaises.test_raise ___________________________
|
__________________________ TestRaises.test_raise ___________________________
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef001e>
|
self = <failure_demo.TestRaises object at 0xdeadbeef001f>
|
||||||
|
|
||||||
def test_raise(self):
|
def test_raise(self):
|
||||||
> raise ValueError("demo error")
|
> raise ValueError("demo error")
|
||||||
|
@ -429,7 +429,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:172: ValueError
|
failure_demo.py:172: ValueError
|
||||||
________________________ TestRaises.test_tupleerror ________________________
|
________________________ TestRaises.test_tupleerror ________________________
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef001f>
|
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
||||||
|
|
||||||
def test_tupleerror(self):
|
def test_tupleerror(self):
|
||||||
> a, b = [1] # NOQA
|
> a, b = [1] # NOQA
|
||||||
|
@ -438,7 +438,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:175: ValueError
|
failure_demo.py:175: ValueError
|
||||||
______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
|
______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
|
self = <failure_demo.TestRaises object at 0xdeadbeef0021>
|
||||||
|
|
||||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||||
items = [1, 2, 3]
|
items = [1, 2, 3]
|
||||||
|
@ -451,7 +451,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
items is [1, 2, 3]
|
items is [1, 2, 3]
|
||||||
________________________ TestRaises.test_some_error ________________________
|
________________________ TestRaises.test_some_error ________________________
|
||||||
|
|
||||||
self = <failure_demo.TestRaises object at 0xdeadbeef0021>
|
self = <failure_demo.TestRaises object at 0xdeadbeef0022>
|
||||||
|
|
||||||
def test_some_error(self):
|
def test_some_error(self):
|
||||||
> if namenotexi: # NOQA
|
> if namenotexi: # NOQA
|
||||||
|
@ -482,7 +482,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
abc-123:2: AssertionError
|
abc-123:2: AssertionError
|
||||||
____________________ TestMoreErrors.test_complex_error _____________________
|
____________________ TestMoreErrors.test_complex_error _____________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0022>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0023>
|
||||||
|
|
||||||
def test_complex_error(self):
|
def test_complex_error(self):
|
||||||
def f():
|
def f():
|
||||||
|
@ -508,7 +508,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:6: AssertionError
|
failure_demo.py:6: AssertionError
|
||||||
___________________ TestMoreErrors.test_z1_unpack_error ____________________
|
___________________ TestMoreErrors.test_z1_unpack_error ____________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0023>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0024>
|
||||||
|
|
||||||
def test_z1_unpack_error(self):
|
def test_z1_unpack_error(self):
|
||||||
items = []
|
items = []
|
||||||
|
@ -518,7 +518,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:217: ValueError
|
failure_demo.py:217: ValueError
|
||||||
____________________ TestMoreErrors.test_z2_type_error _____________________
|
____________________ TestMoreErrors.test_z2_type_error _____________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0024>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0025>
|
||||||
|
|
||||||
def test_z2_type_error(self):
|
def test_z2_type_error(self):
|
||||||
items = 3
|
items = 3
|
||||||
|
@ -528,20 +528,20 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:221: TypeError
|
failure_demo.py:221: TypeError
|
||||||
______________________ TestMoreErrors.test_startswith ______________________
|
______________________ TestMoreErrors.test_startswith ______________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0025>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0026>
|
||||||
|
|
||||||
def test_startswith(self):
|
def test_startswith(self):
|
||||||
s = "123"
|
s = "123"
|
||||||
g = "456"
|
g = "456"
|
||||||
> assert s.startswith(g)
|
> assert s.startswith(g)
|
||||||
E AssertionError: assert False
|
E AssertionError: assert False
|
||||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0026>('456')
|
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
|
||||||
E + where <built-in method startswith of str object at 0xdeadbeef0026> = '123'.startswith
|
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
|
||||||
|
|
||||||
failure_demo.py:226: AssertionError
|
failure_demo.py:226: AssertionError
|
||||||
__________________ TestMoreErrors.test_startswith_nested ___________________
|
__________________ TestMoreErrors.test_startswith_nested ___________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0027>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0028>
|
||||||
|
|
||||||
def test_startswith_nested(self):
|
def test_startswith_nested(self):
|
||||||
def f():
|
def f():
|
||||||
|
@ -552,15 +552,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
|
|
||||||
> assert f().startswith(g())
|
> assert f().startswith(g())
|
||||||
E AssertionError: assert False
|
E AssertionError: assert False
|
||||||
E + where False = <built-in method startswith of str object at 0xdeadbeef0026>('456')
|
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
|
||||||
E + where <built-in method startswith of str object at 0xdeadbeef0026> = '123'.startswith
|
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
|
||||||
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0028>()
|
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
|
||||||
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
|
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
|
||||||
|
|
||||||
failure_demo.py:235: AssertionError
|
failure_demo.py:235: AssertionError
|
||||||
_____________________ TestMoreErrors.test_global_func ______________________
|
_____________________ TestMoreErrors.test_global_func ______________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
||||||
|
|
||||||
def test_global_func(self):
|
def test_global_func(self):
|
||||||
> assert isinstance(globf(42), float)
|
> assert isinstance(globf(42), float)
|
||||||
|
@ -571,18 +571,18 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:238: AssertionError
|
failure_demo.py:238: AssertionError
|
||||||
_______________________ TestMoreErrors.test_instance _______________________
|
_______________________ TestMoreErrors.test_instance _______________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
||||||
|
|
||||||
def test_instance(self):
|
def test_instance(self):
|
||||||
self.x = 6 * 7
|
self.x = 6 * 7
|
||||||
> assert self.x != 42
|
> assert self.x != 42
|
||||||
E assert 42 != 42
|
E assert 42 != 42
|
||||||
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>.x
|
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>.x
|
||||||
|
|
||||||
failure_demo.py:242: AssertionError
|
failure_demo.py:242: AssertionError
|
||||||
_______________________ TestMoreErrors.test_compare ________________________
|
_______________________ TestMoreErrors.test_compare ________________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
||||||
|
|
||||||
def test_compare(self):
|
def test_compare(self):
|
||||||
> assert globf(10) < 5
|
> assert globf(10) < 5
|
||||||
|
@ -592,7 +592,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:245: AssertionError
|
failure_demo.py:245: AssertionError
|
||||||
_____________________ TestMoreErrors.test_try_finally ______________________
|
_____________________ TestMoreErrors.test_try_finally ______________________
|
||||||
|
|
||||||
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
|
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
|
||||||
|
|
||||||
def test_try_finally(self):
|
def test_try_finally(self):
|
||||||
x = 1
|
x = 1
|
||||||
|
@ -603,7 +603,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:250: AssertionError
|
failure_demo.py:250: AssertionError
|
||||||
___________________ TestCustomAssertMsg.test_single_line ___________________
|
___________________ TestCustomAssertMsg.test_single_line ___________________
|
||||||
|
|
||||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002e>
|
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
||||||
|
|
||||||
def test_single_line(self):
|
def test_single_line(self):
|
||||||
class A:
|
class A:
|
||||||
|
@ -618,7 +618,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:261: AssertionError
|
failure_demo.py:261: AssertionError
|
||||||
____________________ TestCustomAssertMsg.test_multiline ____________________
|
____________________ TestCustomAssertMsg.test_multiline ____________________
|
||||||
|
|
||||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
|
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
||||||
|
|
||||||
def test_multiline(self):
|
def test_multiline(self):
|
||||||
class A:
|
class A:
|
||||||
|
@ -637,7 +637,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||||
failure_demo.py:268: AssertionError
|
failure_demo.py:268: AssertionError
|
||||||
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
___________________ TestCustomAssertMsg.test_custom_repr ___________________
|
||||||
|
|
||||||
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
|
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
|
||||||
|
|
||||||
def test_custom_repr(self):
|
def test_custom_repr(self):
|
||||||
class JSON:
|
class JSON:
|
||||||
|
|
|
@ -294,3 +294,20 @@ See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`
|
||||||
|
|
||||||
setuptools intends to
|
setuptools intends to
|
||||||
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
|
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
|
||||||
|
|
||||||
|
Checking with flake8-pytest-style
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
In order to ensure that pytest is being used correctly in your project,
|
||||||
|
it can be helpful to use the `flake8-pytest-style <https://github.com/m-burst/flake8-pytest-style>`_ flake8 plugin.
|
||||||
|
|
||||||
|
flake8-pytest-style checks for common mistakes and coding style violations in pytest code,
|
||||||
|
such as incorrect use of fixtures, test function names, and markers.
|
||||||
|
By using this plugin, you can catch these errors early in the development process
|
||||||
|
and ensure that your pytest code is consistent and easy to maintain.
|
||||||
|
|
||||||
|
A list of the lints detected by flake8-pytest-style can be found on its `PyPI page <https://pypi.org/project/flake8-pytest-style/>`_.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
flake8-pytest-style is not an official pytest project. Some of the rules enforce certain style choices, such as using `@pytest.fixture()` over `@pytest.fixture`, but you can configure the plugin to fit your preferred style.
|
||||||
|
|
|
@ -22,7 +22,7 @@ Install ``pytest``
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pytest --version
|
$ pytest --version
|
||||||
pytest 7.2.0.dev534+ga2c84caaa.d20230317
|
pytest 7.3.1
|
||||||
|
|
||||||
.. _`simpletest`:
|
.. _`simpletest`:
|
||||||
|
|
||||||
|
|
|
@ -35,11 +35,12 @@ Pytest supports several ways to run and select tests from the command-line.
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pytest -k "MyClass and not method"
|
pytest -k 'MyClass and not method'
|
||||||
|
|
||||||
This will run tests which contain names that match the given *string expression* (case-insensitive),
|
This will run tests which contain names that match the given *string expression* (case-insensitive),
|
||||||
which can include Python operators that use filenames, class names and function names as variables.
|
which can include Python operators that use filenames, class names and function names as variables.
|
||||||
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
||||||
|
Use ``""`` instead of ``''`` in expression when running this on Windows
|
||||||
|
|
||||||
.. _nodeids:
|
.. _nodeids:
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
:orphan:
|
:orphan:
|
||||||
|
|
||||||
..
|
.. sidebar:: Next Open Trainings
|
||||||
.. sidebar:: Next Open Trainings
|
|
||||||
|
|
||||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote
|
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote
|
||||||
|
|
||||||
Also see :doc:`previous talks and blogposts <talks>`.
|
Also see :doc:`previous talks and blogposts <talks>`.
|
||||||
|
|
||||||
.. _features:
|
.. _features:
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -956,6 +956,12 @@ TestReport
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:inherited-members:
|
:inherited-members:
|
||||||
|
|
||||||
|
TestShortLogReport
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. autoclass:: pytest.TestShortLogReport()
|
||||||
|
:members:
|
||||||
|
|
||||||
_Result
|
_Result
|
||||||
~~~~~~~
|
~~~~~~~
|
||||||
|
|
||||||
|
@ -1049,11 +1055,11 @@ Environment variables that can be used to change pytest's behavior.
|
||||||
|
|
||||||
.. envvar:: CI
|
.. envvar:: CI
|
||||||
|
|
||||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to ``BUILD_NUMBER`` variable.
|
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
|
||||||
|
|
||||||
.. envvar:: BUILD_NUMBER
|
.. envvar:: BUILD_NUMBER
|
||||||
|
|
||||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to CI variable.
|
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
|
||||||
|
|
||||||
.. envvar:: PYTEST_ADDOPTS
|
.. envvar:: PYTEST_ADDOPTS
|
||||||
|
|
||||||
|
@ -1713,13 +1719,12 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
|
|
||||||
.. confval:: testpaths
|
.. confval:: testpaths
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Sets list of directories that should be searched for tests when
|
Sets list of directories that should be searched for tests when
|
||||||
no specific directories, files or test ids are given in the command line when
|
no specific directories, files or test ids are given in the command line when
|
||||||
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
||||||
File system paths may use shell-style wildcards, including the recursive
|
File system paths may use shell-style wildcards, including the recursive
|
||||||
``**`` pattern.
|
``**`` pattern.
|
||||||
|
|
||||||
Useful when all project tests are in a known location to speed up
|
Useful when all project tests are in a known location to speed up
|
||||||
test collection and to avoid picking up undesired tests by accident.
|
test collection and to avoid picking up undesired tests by accident.
|
||||||
|
|
||||||
|
@ -1728,8 +1733,17 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
[pytest]
|
[pytest]
|
||||||
testpaths = testing doc
|
testpaths = testing doc
|
||||||
|
|
||||||
This tells pytest to only look for tests in ``testing`` and ``doc``
|
This configuration means that executing:
|
||||||
directories when executing from the root directory.
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pytest
|
||||||
|
|
||||||
|
has the same practical effects as executing:
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
pytest testing doc
|
||||||
|
|
||||||
|
|
||||||
.. confval:: tmp_path_retention_count
|
.. confval:: tmp_path_retention_count
|
||||||
|
@ -1744,7 +1758,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
[pytest]
|
[pytest]
|
||||||
tmp_path_retention_count = 3
|
tmp_path_retention_count = 3
|
||||||
|
|
||||||
Default: 3
|
Default: ``3``
|
||||||
|
|
||||||
|
|
||||||
.. confval:: tmp_path_retention_policy
|
.. confval:: tmp_path_retention_policy
|
||||||
|
@ -1763,7 +1777,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||||
[pytest]
|
[pytest]
|
||||||
tmp_path_retention_policy = "all"
|
tmp_path_retention_policy = "all"
|
||||||
|
|
||||||
Default: all
|
Default: ``all``
|
||||||
|
|
||||||
|
|
||||||
.. confval:: usefixtures
|
.. confval:: usefixtures
|
||||||
|
@ -1996,7 +2010,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||||
Auto-indent multiline messages passed to the logging
|
Auto-indent multiline messages passed to the logging
|
||||||
module. Accepts true|on, false|off or an integer.
|
module. Accepts true|on, false|off or an integer.
|
||||||
--log-disable=LOGGER_DISABLE
|
--log-disable=LOGGER_DISABLE
|
||||||
Disable a logger by name. Can be passed multipe
|
Disable a logger by name. Can be passed multiple
|
||||||
times.
|
times.
|
||||||
|
|
||||||
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
||||||
|
|
|
@ -412,7 +412,8 @@ class Traceback(List[TracebackEntry]):
|
||||||
return Traceback(filter(fn, self), self._excinfo)
|
return Traceback(filter(fn, self), self._excinfo)
|
||||||
|
|
||||||
def getcrashentry(self) -> Optional[TracebackEntry]:
|
def getcrashentry(self) -> Optional[TracebackEntry]:
|
||||||
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
|
"""Return last non-hidden traceback entry that lead to the exception of
|
||||||
|
a traceback, or None if all hidden."""
|
||||||
for i in range(-1, -len(self) - 1, -1):
|
for i in range(-1, -len(self) - 1, -1):
|
||||||
entry = self[i]
|
entry = self[i]
|
||||||
if not entry.ishidden():
|
if not entry.ishidden():
|
||||||
|
@ -469,22 +470,41 @@ class ExceptionInfo(Generic[E]):
|
||||||
self._traceback = traceback
|
self._traceback = traceback
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_exc_info(
|
def from_exception(
|
||||||
cls,
|
cls,
|
||||||
exc_info: Tuple[Type[E], E, TracebackType],
|
# Ignoring error: "Cannot use a covariant type variable as a parameter".
|
||||||
|
# This is OK to ignore because this class is (conceptually) readonly.
|
||||||
|
# See https://github.com/python/mypy/issues/7049.
|
||||||
|
exception: E, # type: ignore[misc]
|
||||||
exprinfo: Optional[str] = None,
|
exprinfo: Optional[str] = None,
|
||||||
) -> "ExceptionInfo[E]":
|
) -> "ExceptionInfo[E]":
|
||||||
"""Return an ExceptionInfo for an existing exc_info tuple.
|
"""Return an ExceptionInfo for an existing exception.
|
||||||
|
|
||||||
.. warning::
|
The exception must have a non-``None`` ``__traceback__`` attribute,
|
||||||
|
otherwise this function fails with an assertion error. This means that
|
||||||
Experimental API
|
the exception must have been raised, or added a traceback with the
|
||||||
|
:py:meth:`~BaseException.with_traceback()` method.
|
||||||
|
|
||||||
:param exprinfo:
|
:param exprinfo:
|
||||||
A text string helping to determine if we should strip
|
A text string helping to determine if we should strip
|
||||||
``AssertionError`` from the output. Defaults to the exception
|
``AssertionError`` from the output. Defaults to the exception
|
||||||
message/``__str__()``.
|
message/``__str__()``.
|
||||||
|
|
||||||
|
.. versionadded:: 7.4
|
||||||
"""
|
"""
|
||||||
|
assert (
|
||||||
|
exception.__traceback__
|
||||||
|
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
|
||||||
|
exc_info = (type(exception), exception, exception.__traceback__)
|
||||||
|
return cls.from_exc_info(exc_info, exprinfo)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_exc_info(
|
||||||
|
cls,
|
||||||
|
exc_info: Tuple[Type[E], E, TracebackType],
|
||||||
|
exprinfo: Optional[str] = None,
|
||||||
|
) -> "ExceptionInfo[E]":
|
||||||
|
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
|
||||||
_striptext = ""
|
_striptext = ""
|
||||||
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
||||||
exprinfo = getattr(exc_info[1], "msg", None)
|
exprinfo = getattr(exc_info[1], "msg", None)
|
||||||
|
@ -605,10 +625,10 @@ class ExceptionInfo(Generic[E]):
|
||||||
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
||||||
exconly = self.exconly(tryshort=True)
|
exconly = self.exconly(tryshort=True)
|
||||||
entry = self.traceback.getcrashentry()
|
entry = self.traceback.getcrashentry()
|
||||||
if entry:
|
if entry is None:
|
||||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
return None
|
||||||
return ReprFileLocation(path, lineno + 1, exconly)
|
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||||
return None
|
return ReprFileLocation(path, lineno + 1, exconly)
|
||||||
|
|
||||||
def getrepr(
|
def getrepr(
|
||||||
self,
|
self,
|
||||||
|
@ -627,7 +647,7 @@ class ExceptionInfo(Generic[E]):
|
||||||
Ignored if ``style=="native"``.
|
Ignored if ``style=="native"``.
|
||||||
|
|
||||||
:param str style:
|
:param str style:
|
||||||
long|short|no|native|value traceback style.
|
long|short|line|no|native|value traceback style.
|
||||||
|
|
||||||
:param bool abspath:
|
:param bool abspath:
|
||||||
If paths should be changed to absolute or left unchanged.
|
If paths should be changed to absolute or left unchanged.
|
||||||
|
@ -653,7 +673,9 @@ class ExceptionInfo(Generic[E]):
|
||||||
return ReprExceptionInfo(
|
return ReprExceptionInfo(
|
||||||
reprtraceback=ReprTracebackNative(
|
reprtraceback=ReprTracebackNative(
|
||||||
traceback.format_exception(
|
traceback.format_exception(
|
||||||
self.type, self.value, self.traceback[0]._rawentry
|
self.type,
|
||||||
|
self.value,
|
||||||
|
self.traceback[0]._rawentry if self.traceback else None,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
reprcrash=self._getreprcrash(),
|
reprcrash=self._getreprcrash(),
|
||||||
|
@ -743,11 +765,13 @@ class FormattedExcinfo:
|
||||||
) -> List[str]:
|
) -> List[str]:
|
||||||
"""Return formatted and marked up source lines."""
|
"""Return formatted and marked up source lines."""
|
||||||
lines = []
|
lines = []
|
||||||
if source is None or line_index >= len(source.lines):
|
if source is not None and line_index < 0:
|
||||||
|
line_index += len(source)
|
||||||
|
if source is None or line_index >= len(source.lines) or line_index < 0:
|
||||||
|
# `line_index` could still be outside `range(len(source.lines))` if
|
||||||
|
# we're processing AST with pathological position attributes.
|
||||||
source = Source("???")
|
source = Source("???")
|
||||||
line_index = 0
|
line_index = 0
|
||||||
if line_index < 0:
|
|
||||||
line_index += len(source)
|
|
||||||
space_prefix = " "
|
space_prefix = " "
|
||||||
if short:
|
if short:
|
||||||
lines.append(space_prefix + source.lines[line_index].strip())
|
lines.append(space_prefix + source.lines[line_index].strip())
|
||||||
|
@ -807,12 +831,16 @@ class FormattedExcinfo:
|
||||||
|
|
||||||
def repr_traceback_entry(
|
def repr_traceback_entry(
|
||||||
self,
|
self,
|
||||||
entry: TracebackEntry,
|
entry: Optional[TracebackEntry],
|
||||||
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
||||||
) -> "ReprEntry":
|
) -> "ReprEntry":
|
||||||
lines: List[str] = []
|
lines: List[str] = []
|
||||||
style = entry._repr_style if entry._repr_style is not None else self.style
|
style = (
|
||||||
if style in ("short", "long"):
|
entry._repr_style
|
||||||
|
if entry is not None and entry._repr_style is not None
|
||||||
|
else self.style
|
||||||
|
)
|
||||||
|
if style in ("short", "long") and entry is not None:
|
||||||
source = self._getentrysource(entry)
|
source = self._getentrysource(entry)
|
||||||
if source is None:
|
if source is None:
|
||||||
source = Source("???")
|
source = Source("???")
|
||||||
|
@ -861,17 +889,21 @@ class FormattedExcinfo:
|
||||||
else:
|
else:
|
||||||
extraline = None
|
extraline = None
|
||||||
|
|
||||||
|
if not traceback:
|
||||||
|
if extraline is None:
|
||||||
|
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
|
||||||
|
entries = [self.repr_traceback_entry(None, excinfo)]
|
||||||
|
return ReprTraceback(entries, extraline, style=self.style)
|
||||||
|
|
||||||
last = traceback[-1]
|
last = traceback[-1]
|
||||||
entries = []
|
|
||||||
if self.style == "value":
|
if self.style == "value":
|
||||||
reprentry = self.repr_traceback_entry(last, excinfo)
|
entries = [self.repr_traceback_entry(last, excinfo)]
|
||||||
entries.append(reprentry)
|
|
||||||
return ReprTraceback(entries, None, style=self.style)
|
return ReprTraceback(entries, None, style=self.style)
|
||||||
|
|
||||||
for index, entry in enumerate(traceback):
|
entries = [
|
||||||
einfo = (last == entry) and excinfo or None
|
self.repr_traceback_entry(entry, excinfo if last == entry else None)
|
||||||
reprentry = self.repr_traceback_entry(entry, einfo)
|
for entry in traceback
|
||||||
entries.append(reprentry)
|
]
|
||||||
return ReprTraceback(entries, extraline, style=self.style)
|
return ReprTraceback(entries, extraline, style=self.style)
|
||||||
|
|
||||||
def _truncate_recursive_traceback(
|
def _truncate_recursive_traceback(
|
||||||
|
@ -928,6 +960,7 @@ class FormattedExcinfo:
|
||||||
seen: Set[int] = set()
|
seen: Set[int] = set()
|
||||||
while e is not None and id(e) not in seen:
|
while e is not None and id(e) not in seen:
|
||||||
seen.add(id(e))
|
seen.add(id(e))
|
||||||
|
|
||||||
if excinfo_:
|
if excinfo_:
|
||||||
# Fall back to native traceback as a temporary workaround until
|
# Fall back to native traceback as a temporary workaround until
|
||||||
# full support for exception groups added to ExceptionInfo.
|
# full support for exception groups added to ExceptionInfo.
|
||||||
|
@ -944,14 +977,7 @@ class FormattedExcinfo:
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
reprtraceback = self.repr_traceback(excinfo_)
|
reprtraceback = self.repr_traceback(excinfo_)
|
||||||
|
reprcrash = excinfo_._getreprcrash()
|
||||||
# will be None if all traceback entries are hidden
|
|
||||||
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
|
|
||||||
if reprcrash:
|
|
||||||
if self.style == "value":
|
|
||||||
repr_chain += [(reprtraceback, None, descr)]
|
|
||||||
else:
|
|
||||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
|
||||||
else:
|
else:
|
||||||
# Fallback to native repr if the exception doesn't have a traceback:
|
# Fallback to native repr if the exception doesn't have a traceback:
|
||||||
# ExceptionInfo objects require a full traceback to work.
|
# ExceptionInfo objects require a full traceback to work.
|
||||||
|
@ -959,25 +985,17 @@ class FormattedExcinfo:
|
||||||
traceback.format_exception(type(e), e, None)
|
traceback.format_exception(type(e), e, None)
|
||||||
)
|
)
|
||||||
reprcrash = None
|
reprcrash = None
|
||||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||||
|
|
||||||
if e.__cause__ is not None and self.chain:
|
if e.__cause__ is not None and self.chain:
|
||||||
e = e.__cause__
|
e = e.__cause__
|
||||||
excinfo_ = (
|
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
|
||||||
if e.__traceback__
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
descr = "The above exception was the direct cause of the following exception:"
|
descr = "The above exception was the direct cause of the following exception:"
|
||||||
elif (
|
elif (
|
||||||
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
||||||
):
|
):
|
||||||
e = e.__context__
|
e = e.__context__
|
||||||
excinfo_ = (
|
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
|
||||||
if e.__traceback__
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
descr = "During handling of the above exception, another exception occurred:"
|
descr = "During handling of the above exception, another exception occurred:"
|
||||||
else:
|
else:
|
||||||
e = None
|
e = None
|
||||||
|
@ -1156,8 +1174,8 @@ class ReprEntry(TerminalRepr):
|
||||||
|
|
||||||
def toterminal(self, tw: TerminalWriter) -> None:
|
def toterminal(self, tw: TerminalWriter) -> None:
|
||||||
if self.style == "short":
|
if self.style == "short":
|
||||||
assert self.reprfileloc is not None
|
if self.reprfileloc:
|
||||||
self.reprfileloc.toterminal(tw)
|
self.reprfileloc.toterminal(tw)
|
||||||
self._write_entry_lines(tw)
|
self._write_entry_lines(tw)
|
||||||
if self.reprlocals:
|
if self.reprlocals:
|
||||||
self.reprlocals.toterminal(tw, indent=" " * 8)
|
self.reprlocals.toterminal(tw, indent=" " * 8)
|
||||||
|
|
|
@ -953,7 +953,7 @@ class LocalPath:
|
||||||
else:
|
else:
|
||||||
p.dirpath()._ensuredirs()
|
p.dirpath()._ensuredirs()
|
||||||
if not p.check(file=1):
|
if not p.check(file=1):
|
||||||
p.open("w").close()
|
p.open("wb").close()
|
||||||
return p
|
return p
|
||||||
|
|
||||||
@overload
|
@overload
|
||||||
|
|
|
@ -179,16 +179,22 @@ class Cache:
|
||||||
else:
|
else:
|
||||||
cache_dir_exists_already = self._cachedir.exists()
|
cache_dir_exists_already = self._cachedir.exists()
|
||||||
path.parent.mkdir(exist_ok=True, parents=True)
|
path.parent.mkdir(exist_ok=True, parents=True)
|
||||||
except OSError:
|
except OSError as exc:
|
||||||
self.warn("could not create cache path {path}", path=path, _ispytest=True)
|
self.warn(
|
||||||
|
f"could not create cache path {path}: {exc}",
|
||||||
|
_ispytest=True,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
if not cache_dir_exists_already:
|
if not cache_dir_exists_already:
|
||||||
self._ensure_supporting_files()
|
self._ensure_supporting_files()
|
||||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||||
try:
|
try:
|
||||||
f = path.open("w", encoding="UTF-8")
|
f = path.open("w", encoding="UTF-8")
|
||||||
except OSError:
|
except OSError as exc:
|
||||||
self.warn("cache could not write path {path}", path=path, _ispytest=True)
|
self.warn(
|
||||||
|
f"cache could not write path {path}: {exc}",
|
||||||
|
_ispytest=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
with f:
|
with f:
|
||||||
f.write(data)
|
f.write(data)
|
||||||
|
|
|
@ -241,7 +241,7 @@ class DontReadFromInput(TextIO):
|
||||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
||||||
|
|
||||||
def truncate(self, size: Optional[int] = None) -> int:
|
def truncate(self, size: Optional[int] = None) -> int:
|
||||||
raise UnsupportedOperation("cannont truncate stdin")
|
raise UnsupportedOperation("cannot truncate stdin")
|
||||||
|
|
||||||
def write(self, data: str) -> int:
|
def write(self, data: str) -> int:
|
||||||
raise UnsupportedOperation("cannot write to stdin")
|
raise UnsupportedOperation("cannot write to stdin")
|
||||||
|
|
|
@ -49,7 +49,7 @@ from _pytest._code import ExceptionInfo
|
||||||
from _pytest._code import filter_traceback
|
from _pytest._code import filter_traceback
|
||||||
from _pytest._io import TerminalWriter
|
from _pytest._io import TerminalWriter
|
||||||
from _pytest.compat import final
|
from _pytest.compat import final
|
||||||
from _pytest.compat import importlib_metadata
|
from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
|
||||||
from _pytest.outcomes import fail
|
from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import Skipped
|
from _pytest.outcomes import Skipped
|
||||||
from _pytest.pathlib import absolutepath
|
from _pytest.pathlib import absolutepath
|
||||||
|
@ -526,7 +526,10 @@ class PytestPluginManager(PluginManager):
|
||||||
# Internal API for local conftest plugin handling.
|
# Internal API for local conftest plugin handling.
|
||||||
#
|
#
|
||||||
def _set_initial_conftests(
|
def _set_initial_conftests(
|
||||||
self, namespace: argparse.Namespace, rootpath: Path
|
self,
|
||||||
|
namespace: argparse.Namespace,
|
||||||
|
rootpath: Path,
|
||||||
|
testpaths_ini: Sequence[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Load initial conftest files given a preparsed "namespace".
|
"""Load initial conftest files given a preparsed "namespace".
|
||||||
|
|
||||||
|
@ -543,7 +546,7 @@ class PytestPluginManager(PluginManager):
|
||||||
)
|
)
|
||||||
self._noconftest = namespace.noconftest
|
self._noconftest = namespace.noconftest
|
||||||
self._using_pyargs = namespace.pyargs
|
self._using_pyargs = namespace.pyargs
|
||||||
testpaths = namespace.file_or_dir
|
testpaths = namespace.file_or_dir + testpaths_ini
|
||||||
foundanchor = False
|
foundanchor = False
|
||||||
for testpath in testpaths:
|
for testpath in testpaths:
|
||||||
path = str(testpath)
|
path = str(testpath)
|
||||||
|
@ -552,7 +555,14 @@ class PytestPluginManager(PluginManager):
|
||||||
if i != -1:
|
if i != -1:
|
||||||
path = path[:i]
|
path = path[:i]
|
||||||
anchor = absolutepath(current / path)
|
anchor = absolutepath(current / path)
|
||||||
if anchor.exists(): # we found some file object
|
|
||||||
|
# Ensure we do not break if what appears to be an anchor
|
||||||
|
# is in fact a very long option (#10169).
|
||||||
|
try:
|
||||||
|
anchor_exists = anchor.exists()
|
||||||
|
except OSError: # pragma: no cover
|
||||||
|
anchor_exists = False
|
||||||
|
if anchor_exists:
|
||||||
self._try_load_conftest(anchor, namespace.importmode, rootpath)
|
self._try_load_conftest(anchor, namespace.importmode, rootpath)
|
||||||
foundanchor = True
|
foundanchor = True
|
||||||
if not foundanchor:
|
if not foundanchor:
|
||||||
|
@ -1131,7 +1141,9 @@ class Config:
|
||||||
@hookimpl(trylast=True)
|
@hookimpl(trylast=True)
|
||||||
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
||||||
self.pluginmanager._set_initial_conftests(
|
self.pluginmanager._set_initial_conftests(
|
||||||
early_config.known_args_namespace, rootpath=early_config.rootpath
|
early_config.known_args_namespace,
|
||||||
|
rootpath=early_config.rootpath,
|
||||||
|
testpaths_ini=self.getini("testpaths"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _initini(self, args: Sequence[str]) -> None:
|
def _initini(self, args: Sequence[str]) -> None:
|
||||||
|
|
|
@ -2,7 +2,6 @@ import io
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import TextIO
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
|
@ -11,7 +10,7 @@ from _pytest.nodes import Item
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
|
||||||
|
|
||||||
fault_handler_stderr_key = StashKey[TextIO]()
|
fault_handler_stderr_fd_key = StashKey[int]()
|
||||||
fault_handler_originally_enabled_key = StashKey[bool]()
|
fault_handler_originally_enabled_key = StashKey[bool]()
|
||||||
|
|
||||||
|
|
||||||
|
@ -26,10 +25,9 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
def pytest_configure(config: Config) -> None:
|
def pytest_configure(config: Config) -> None:
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
stderr_fd_copy = os.dup(get_stderr_fileno())
|
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
|
||||||
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
|
|
||||||
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
|
||||||
faulthandler.enable(file=config.stash[fault_handler_stderr_key])
|
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
|
||||||
|
|
||||||
|
|
||||||
def pytest_unconfigure(config: Config) -> None:
|
def pytest_unconfigure(config: Config) -> None:
|
||||||
|
@ -37,9 +35,9 @@ def pytest_unconfigure(config: Config) -> None:
|
||||||
|
|
||||||
faulthandler.disable()
|
faulthandler.disable()
|
||||||
# Close the dup file installed during pytest_configure.
|
# Close the dup file installed during pytest_configure.
|
||||||
if fault_handler_stderr_key in config.stash:
|
if fault_handler_stderr_fd_key in config.stash:
|
||||||
config.stash[fault_handler_stderr_key].close()
|
os.close(config.stash[fault_handler_stderr_fd_key])
|
||||||
del config.stash[fault_handler_stderr_key]
|
del config.stash[fault_handler_stderr_fd_key]
|
||||||
if config.stash.get(fault_handler_originally_enabled_key, False):
|
if config.stash.get(fault_handler_originally_enabled_key, False):
|
||||||
# Re-enable the faulthandler if it was originally enabled.
|
# Re-enable the faulthandler if it was originally enabled.
|
||||||
faulthandler.enable(file=get_stderr_fileno())
|
faulthandler.enable(file=get_stderr_fileno())
|
||||||
|
@ -67,10 +65,10 @@ def get_timeout_config_value(config: Config) -> float:
|
||||||
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
@pytest.hookimpl(hookwrapper=True, trylast=True)
|
||||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||||
timeout = get_timeout_config_value(item.config)
|
timeout = get_timeout_config_value(item.config)
|
||||||
stderr = item.config.stash[fault_handler_stderr_key]
|
if timeout > 0:
|
||||||
if timeout > 0 and stderr is not None:
|
|
||||||
import faulthandler
|
import faulthandler
|
||||||
|
|
||||||
|
stderr = item.config.stash[fault_handler_stderr_fd_key]
|
||||||
faulthandler.dump_traceback_later(timeout, file=stderr)
|
faulthandler.dump_traceback_later(timeout, file=stderr)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -21,7 +21,7 @@ if TYPE_CHECKING:
|
||||||
from typing_extensions import Literal
|
from typing_extensions import Literal
|
||||||
|
|
||||||
from _pytest._code.code import ExceptionRepr
|
from _pytest._code.code import ExceptionRepr
|
||||||
from _pytest.code import ExceptionInfo
|
from _pytest._code.code import ExceptionInfo
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
from _pytest.config import PytestPluginManager
|
from _pytest.config import PytestPluginManager
|
||||||
|
@ -41,6 +41,7 @@ if TYPE_CHECKING:
|
||||||
from _pytest.reports import TestReport
|
from _pytest.reports import TestReport
|
||||||
from _pytest.runner import CallInfo
|
from _pytest.runner import CallInfo
|
||||||
from _pytest.terminal import TerminalReporter
|
from _pytest.terminal import TerminalReporter
|
||||||
|
from _pytest.terminal import TestShortLogReport
|
||||||
from _pytest.compat import LEGACY_PATH
|
from _pytest.compat import LEGACY_PATH
|
||||||
|
|
||||||
|
|
||||||
|
@ -806,7 +807,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
|
||||||
@hookspec(firstresult=True)
|
@hookspec(firstresult=True)
|
||||||
def pytest_report_teststatus( # type:ignore[empty-body]
|
def pytest_report_teststatus( # type:ignore[empty-body]
|
||||||
report: Union["CollectReport", "TestReport"], config: "Config"
|
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
|
"""Return result-category, shortletter and verbose word for status
|
||||||
reporting.
|
reporting.
|
||||||
|
|
||||||
|
|
|
@ -302,7 +302,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||||
action="append",
|
action="append",
|
||||||
default=[],
|
default=[],
|
||||||
dest="logger_disable",
|
dest="logger_disable",
|
||||||
help="Disable a logger by name. Can be passed multipe times.",
|
help="Disable a logger by name. Can be passed multiple times.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -376,11 +376,12 @@ class LogCaptureFixture:
|
||||||
self._initial_handler_level: Optional[int] = None
|
self._initial_handler_level: Optional[int] = None
|
||||||
# Dict of log name -> log level.
|
# Dict of log name -> log level.
|
||||||
self._initial_logger_levels: Dict[Optional[str], int] = {}
|
self._initial_logger_levels: Dict[Optional[str], int] = {}
|
||||||
|
self._initial_disabled_logging_level: Optional[int] = None
|
||||||
|
|
||||||
def _finalize(self) -> None:
|
def _finalize(self) -> None:
|
||||||
"""Finalize the fixture.
|
"""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.
|
# Restore log levels.
|
||||||
if self._initial_handler_level is not None:
|
if self._initial_handler_level is not None:
|
||||||
|
@ -388,6 +389,10 @@ class LogCaptureFixture:
|
||||||
for logger_name, level in self._initial_logger_levels.items():
|
for logger_name, level in self._initial_logger_levels.items():
|
||||||
logger = logging.getLogger(logger_name)
|
logger = logging.getLogger(logger_name)
|
||||||
logger.setLevel(level)
|
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
|
@property
|
||||||
def handler(self) -> LogCaptureHandler:
|
def handler(self) -> LogCaptureHandler:
|
||||||
|
@ -453,6 +458,40 @@ class LogCaptureFixture:
|
||||||
"""Reset the list of log records and the captured log text."""
|
"""Reset the list of log records and the captured log text."""
|
||||||
self.handler.clear()
|
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:
|
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.
|
"""Set the level of a logger for the duration of a test.
|
||||||
|
|
||||||
|
@ -460,6 +499,8 @@ class LogCaptureFixture:
|
||||||
The levels of the loggers changed by this function will be
|
The levels of the loggers changed by this function will be
|
||||||
restored to their initial values at the end of the test.
|
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 level: The level.
|
||||||
:param logger: The logger to update. If not given, the root logger.
|
:param logger: The logger to update. If not given, the root logger.
|
||||||
"""
|
"""
|
||||||
|
@ -470,6 +511,9 @@ class LogCaptureFixture:
|
||||||
if self._initial_handler_level is None:
|
if self._initial_handler_level is None:
|
||||||
self._initial_handler_level = self.handler.level
|
self._initial_handler_level = self.handler.level
|
||||||
self.handler.setLevel(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
|
@contextmanager
|
||||||
def at_level(
|
def at_level(
|
||||||
|
@ -479,6 +523,8 @@ class LogCaptureFixture:
|
||||||
the end of the 'with' statement the level is restored to its original
|
the end of the 'with' statement the level is restored to its original
|
||||||
value.
|
value.
|
||||||
|
|
||||||
|
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
|
||||||
|
|
||||||
:param level: The level.
|
:param level: The level.
|
||||||
:param logger: The logger to update. If not given, the root logger.
|
:param logger: The logger to update. If not given, the root logger.
|
||||||
"""
|
"""
|
||||||
|
@ -487,11 +533,13 @@ class LogCaptureFixture:
|
||||||
logger_obj.setLevel(level)
|
logger_obj.setLevel(level)
|
||||||
handler_orig_level = self.handler.level
|
handler_orig_level = self.handler.level
|
||||||
self.handler.setLevel(level)
|
self.handler.setLevel(level)
|
||||||
|
original_disable_level = self._force_enable_logging(level, logger_obj)
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
logger_obj.setLevel(orig_level)
|
logger_obj.setLevel(orig_level)
|
||||||
self.handler.setLevel(handler_orig_level)
|
self.handler.setLevel(handler_orig_level)
|
||||||
|
logging.disable(original_disable_level)
|
||||||
|
|
||||||
|
|
||||||
@fixture
|
@fixture
|
||||||
|
|
|
@ -7,6 +7,7 @@ from contextlib import contextmanager
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
|
from typing import Mapping
|
||||||
from typing import MutableMapping
|
from typing import MutableMapping
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import overload
|
from typing import overload
|
||||||
|
@ -129,7 +130,7 @@ class MonkeyPatch:
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._setattr: List[Tuple[object, str, object]] = []
|
self._setattr: List[Tuple[object, str, object]] = []
|
||||||
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = []
|
self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
|
||||||
self._cwd: Optional[str] = None
|
self._cwd: Optional[str] = None
|
||||||
self._savesyspath: Optional[List[str]] = None
|
self._savesyspath: Optional[List[str]] = None
|
||||||
|
|
||||||
|
@ -290,12 +291,13 @@ class MonkeyPatch:
|
||||||
self._setattr.append((target, name, oldval))
|
self._setattr.append((target, name, oldval))
|
||||||
delattr(target, name)
|
delattr(target, name)
|
||||||
|
|
||||||
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
|
def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
|
||||||
"""Set dictionary entry ``name`` to value."""
|
"""Set dictionary entry ``name`` to value."""
|
||||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||||
dic[name] = value
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
dic[name] = value # type: ignore[index]
|
||||||
|
|
||||||
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
|
def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
|
||||||
"""Delete ``name`` from dict.
|
"""Delete ``name`` from dict.
|
||||||
|
|
||||||
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
|
||||||
|
@ -306,7 +308,8 @@ class MonkeyPatch:
|
||||||
raise KeyError(name)
|
raise KeyError(name)
|
||||||
else:
|
else:
|
||||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||||
del dic[name]
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
del dic[name] # type: ignore[attr-defined]
|
||||||
|
|
||||||
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
|
||||||
"""Set environment variable ``name`` to ``value``.
|
"""Set environment variable ``name`` to ``value``.
|
||||||
|
@ -401,11 +404,13 @@ class MonkeyPatch:
|
||||||
for dictionary, key, value in reversed(self._setitem):
|
for dictionary, key, value in reversed(self._setitem):
|
||||||
if value is notset:
|
if value is notset:
|
||||||
try:
|
try:
|
||||||
del dictionary[key]
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
del dictionary[key] # type: ignore[attr-defined]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
pass # Was already deleted, so we have the desired state.
|
pass # Was already deleted, so we have the desired state.
|
||||||
else:
|
else:
|
||||||
dictionary[key] = value
|
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
|
||||||
|
dictionary[key] = value # type: ignore[index]
|
||||||
self._setitem[:] = []
|
self._setitem[:] = []
|
||||||
if self._savesyspath is not None:
|
if self._savesyspath is not None:
|
||||||
sys.path[:] = self._savesyspath
|
sys.path[:] = self._savesyspath
|
||||||
|
|
|
@ -452,10 +452,7 @@ class Node(metaclass=NodeMeta):
|
||||||
if self.config.getoption("fulltrace", False):
|
if self.config.getoption("fulltrace", False):
|
||||||
style = "long"
|
style = "long"
|
||||||
else:
|
else:
|
||||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
|
||||||
self._prunetraceback(excinfo)
|
self._prunetraceback(excinfo)
|
||||||
if len(excinfo.traceback) == 0:
|
|
||||||
excinfo.traceback = tb
|
|
||||||
if style == "auto":
|
if style == "auto":
|
||||||
style = "long"
|
style = "long"
|
||||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||||
|
|
|
@ -6,6 +6,7 @@ import itertools
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
|
import types
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
@ -28,6 +29,8 @@ from typing import Iterable
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
from typing import Tuple
|
||||||
|
from typing import Type
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
@ -63,21 +66,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
|
||||||
return path.joinpath(".lock")
|
return path.joinpath(".lock")
|
||||||
|
|
||||||
|
|
||||||
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
def on_rm_rf_error(
|
||||||
|
func,
|
||||||
|
path: str,
|
||||||
|
excinfo: Union[
|
||||||
|
BaseException,
|
||||||
|
Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
|
||||||
|
],
|
||||||
|
*,
|
||||||
|
start_path: Path,
|
||||||
|
) -> bool:
|
||||||
"""Handle known read-only errors during rmtree.
|
"""Handle known read-only errors during rmtree.
|
||||||
|
|
||||||
The returned value is used only by our own tests.
|
The returned value is used only by our own tests.
|
||||||
"""
|
"""
|
||||||
exctype, excvalue = exc[:2]
|
if isinstance(excinfo, BaseException):
|
||||||
|
exc = excinfo
|
||||||
|
else:
|
||||||
|
exc = excinfo[1]
|
||||||
|
|
||||||
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
|
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
|
||||||
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
|
||||||
if isinstance(excvalue, FileNotFoundError):
|
if isinstance(exc, FileNotFoundError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not isinstance(excvalue, PermissionError):
|
if not isinstance(exc, PermissionError):
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
|
PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -86,7 +101,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
PytestWarning(
|
PytestWarning(
|
||||||
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
|
||||||
func, path, exctype, excvalue
|
func, path, type(exc), exc
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -149,7 +164,10 @@ def rm_rf(path: Path) -> None:
|
||||||
are read-only."""
|
are read-only."""
|
||||||
path = ensure_extended_length_path(path)
|
path = ensure_extended_length_path(path)
|
||||||
onerror = partial(on_rm_rf_error, start_path=path)
|
onerror = partial(on_rm_rf_error, start_path=path)
|
||||||
shutil.rmtree(str(path), onerror=onerror)
|
if sys.version_info >= (3, 12):
|
||||||
|
shutil.rmtree(str(path), onexc=onerror)
|
||||||
|
else:
|
||||||
|
shutil.rmtree(str(path), onerror=onerror)
|
||||||
|
|
||||||
|
|
||||||
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
|
||||||
|
@ -335,7 +353,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
|
||||||
yield path
|
yield path
|
||||||
|
|
||||||
|
|
||||||
def cleanup_dead_symlink(root: Path):
|
def cleanup_dead_symlinks(root: Path):
|
||||||
for left_dir in root.iterdir():
|
for left_dir in root.iterdir():
|
||||||
if left_dir.is_symlink():
|
if left_dir.is_symlink():
|
||||||
if not left_dir.resolve().exists():
|
if not left_dir.resolve().exists():
|
||||||
|
@ -353,7 +371,7 @@ def cleanup_numbered_dir(
|
||||||
for path in root.glob("garbage-*"):
|
for path in root.glob("garbage-*"):
|
||||||
try_cleanup(path, consider_lock_dead_if_created_before)
|
try_cleanup(path, consider_lock_dead_if_created_before)
|
||||||
|
|
||||||
cleanup_dead_symlink(root)
|
cleanup_dead_symlinks(root)
|
||||||
|
|
||||||
|
|
||||||
def make_numbered_dir_with_cleanup(
|
def make_numbered_dir_with_cleanup(
|
||||||
|
|
|
@ -950,11 +950,7 @@ def raises( # noqa: F811
|
||||||
try:
|
try:
|
||||||
func(*args[1:], **kwargs)
|
func(*args[1:], **kwargs)
|
||||||
except expected_exception as e:
|
except expected_exception as e:
|
||||||
# We just caught the exception - there is a traceback.
|
return _pytest._code.ExceptionInfo.from_exception(e)
|
||||||
assert e.__traceback__ is not None
|
|
||||||
return _pytest._code.ExceptionInfo.from_exc_info(
|
|
||||||
(type(e), e, e.__traceback__)
|
|
||||||
)
|
|
||||||
fail(message)
|
fail(message)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -347,10 +347,9 @@ class TestReport(BaseReport):
|
||||||
elif isinstance(excinfo.value, skip.Exception):
|
elif isinstance(excinfo.value, skip.Exception):
|
||||||
outcome = "skipped"
|
outcome = "skipped"
|
||||||
r = excinfo._getreprcrash()
|
r = excinfo._getreprcrash()
|
||||||
if r is None:
|
assert (
|
||||||
raise ValueError(
|
r is not None
|
||||||
"There should always be a traceback entry for skipping a test."
|
), "There should always be a traceback entry for skipping a test."
|
||||||
)
|
|
||||||
if excinfo.value._use_item_location:
|
if excinfo.value._use_item_location:
|
||||||
path, line = item.reportinfo()[:2]
|
path, line = item.reportinfo()[:2]
|
||||||
assert line is not None
|
assert line is not None
|
||||||
|
|
|
@ -8,6 +8,7 @@ import datetime
|
||||||
import inspect
|
import inspect
|
||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
import textwrap
|
||||||
import warnings
|
import warnings
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
@ -20,6 +21,7 @@ from typing import Dict
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
from typing import List
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Set
|
from typing import Set
|
||||||
|
@ -111,6 +113,26 @@ class MoreQuietAction(argparse.Action):
|
||||||
namespace.quiet = getattr(namespace, "quiet", 0) + 1
|
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:
|
def pytest_addoption(parser: Parser) -> None:
|
||||||
group = parser.getgroup("terminal reporting", "Reporting", after="general")
|
group = parser.getgroup("terminal reporting", "Reporting", after="general")
|
||||||
group._addoption(
|
group._addoption(
|
||||||
|
@ -426,6 +448,28 @@ class TerminalReporter:
|
||||||
self._tw.line()
|
self._tw.line()
|
||||||
self.currentfspath = None
|
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:
|
def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
|
||||||
self._tw.write(content, flush=flush, **markup)
|
self._tw.write(content, flush=flush, **markup)
|
||||||
|
|
||||||
|
@ -525,10 +569,11 @@ class TerminalReporter:
|
||||||
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
def pytest_runtest_logreport(self, report: TestReport) -> None:
|
||||||
self._tests_ran = True
|
self._tests_ran = True
|
||||||
rep = report
|
rep = report
|
||||||
res: Tuple[
|
|
||||||
str, str, Union[str, Tuple[str, Mapping[str, bool]]]
|
res = TestShortLogReport(
|
||||||
] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
*self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
|
||||||
category, letter, word = res
|
)
|
||||||
|
category, letter, word = res.category, res.letter, res.word
|
||||||
if not isinstance(word, tuple):
|
if not isinstance(word, tuple):
|
||||||
markup = None
|
markup = None
|
||||||
else:
|
else:
|
||||||
|
@ -572,7 +617,7 @@ class TerminalReporter:
|
||||||
formatted_reason = f" ({reason})"
|
formatted_reason = f" ({reason})"
|
||||||
|
|
||||||
if reason and formatted_reason is not None:
|
if reason and formatted_reason is not None:
|
||||||
self._tw.write(formatted_reason)
|
self.wrap_write(formatted_reason)
|
||||||
if self._show_progress_info:
|
if self._show_progress_info:
|
||||||
self._write_progress_information_filling_space()
|
self._write_progress_information_filling_space()
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -28,7 +28,7 @@ from .pathlib import LOCK_TIMEOUT
|
||||||
from .pathlib import make_numbered_dir
|
from .pathlib import make_numbered_dir
|
||||||
from .pathlib import make_numbered_dir_with_cleanup
|
from .pathlib import make_numbered_dir_with_cleanup
|
||||||
from .pathlib import rm_rf
|
from .pathlib import rm_rf
|
||||||
from .pathlib import cleanup_dead_symlink
|
from .pathlib import cleanup_dead_symlinks
|
||||||
from _pytest.compat import final, get_user_id
|
from _pytest.compat import final, get_user_id
|
||||||
from _pytest.config import Config
|
from _pytest.config import Config
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
@ -100,7 +100,7 @@ class TempPathFactory:
|
||||||
policy = config.getini("tmp_path_retention_policy")
|
policy = config.getini("tmp_path_retention_policy")
|
||||||
if policy not in ("all", "failed", "none"):
|
if policy not in ("all", "failed", "none"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"tmp_path_retention_policy must be either all, failed, none. Current intput: {policy}."
|
f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
|
||||||
)
|
)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
|
@ -289,31 +289,30 @@ def tmp_path(
|
||||||
|
|
||||||
del request.node.stash[tmppath_result_key]
|
del request.node.stash[tmppath_result_key]
|
||||||
|
|
||||||
# remove dead symlink
|
|
||||||
basetemp = tmp_path_factory._basetemp
|
|
||||||
if basetemp is None:
|
|
||||||
return
|
|
||||||
cleanup_dead_symlink(basetemp)
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
|
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
|
||||||
"""After each session, remove base directory if all the tests passed,
|
"""After each session, remove base directory if all the tests passed,
|
||||||
the policy is "failed", and the basetemp is not specified by a user.
|
the policy is "failed", and the basetemp is not specified by a user.
|
||||||
"""
|
"""
|
||||||
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
|
||||||
if tmp_path_factory._basetemp is None:
|
basetemp = tmp_path_factory._basetemp
|
||||||
|
if basetemp is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
policy = tmp_path_factory._retention_policy
|
policy = tmp_path_factory._retention_policy
|
||||||
if (
|
if (
|
||||||
exitstatus == 0
|
exitstatus == 0
|
||||||
and policy == "failed"
|
and policy == "failed"
|
||||||
and tmp_path_factory._given_basetemp is None
|
and tmp_path_factory._given_basetemp is None
|
||||||
):
|
):
|
||||||
passed_dir = tmp_path_factory._basetemp
|
if basetemp.is_dir():
|
||||||
if passed_dir.exists():
|
|
||||||
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
|
||||||
# permissions, etc, in which case we ignore it.
|
# permissions, etc, in which case we ignore it.
|
||||||
rmtree(passed_dir, ignore_errors=True)
|
rmtree(basetemp, ignore_errors=True)
|
||||||
|
|
||||||
|
# Remove dead symlinks.
|
||||||
|
if basetemp.is_dir():
|
||||||
|
cleanup_dead_symlinks(basetemp)
|
||||||
|
|
||||||
|
|
||||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||||
|
|
|
@ -298,6 +298,9 @@ class TestCaseFunction(Function):
|
||||||
def stopTest(self, testcase: "unittest.TestCase") -> None:
|
def stopTest(self, testcase: "unittest.TestCase") -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
def runtest(self) -> None:
|
def runtest(self) -> None:
|
||||||
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
|
||||||
|
|
||||||
|
|
|
@ -149,7 +149,7 @@ def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
|
||||||
"""
|
"""
|
||||||
Issue the warning :param:`message` for the definition of the given :param:`method`
|
Issue the warning :param:`message` for the definition of the given :param:`method`
|
||||||
|
|
||||||
this helps to log warnigns for functions defined prior to finding an issue with them
|
this helps to log warnings for functions defined prior to finding an issue with them
|
||||||
(like hook wrappers being marked in a legacy mechanism)
|
(like hook wrappers being marked in a legacy mechanism)
|
||||||
"""
|
"""
|
||||||
lineno = method.__code__.co_firstlineno
|
lineno = method.__code__.co_firstlineno
|
||||||
|
|
|
@ -62,6 +62,7 @@ from _pytest.reports import TestReport
|
||||||
from _pytest.runner import CallInfo
|
from _pytest.runner import CallInfo
|
||||||
from _pytest.stash import Stash
|
from _pytest.stash import Stash
|
||||||
from _pytest.stash import StashKey
|
from _pytest.stash import StashKey
|
||||||
|
from _pytest.terminal import TestShortLogReport
|
||||||
from _pytest.tmpdir import TempPathFactory
|
from _pytest.tmpdir import TempPathFactory
|
||||||
from _pytest.warning_types import PytestAssertRewriteWarning
|
from _pytest.warning_types import PytestAssertRewriteWarning
|
||||||
from _pytest.warning_types import PytestCacheWarning
|
from _pytest.warning_types import PytestCacheWarning
|
||||||
|
@ -152,6 +153,7 @@ __all__ = [
|
||||||
"TempPathFactory",
|
"TempPathFactory",
|
||||||
"Testdir",
|
"Testdir",
|
||||||
"TestReport",
|
"TestReport",
|
||||||
|
"TestShortLogReport",
|
||||||
"UsageError",
|
"UsageError",
|
||||||
"WarningsRecorder",
|
"WarningsRecorder",
|
||||||
"warns",
|
"warns",
|
||||||
|
|
|
@ -695,11 +695,15 @@ class TestInvocationVariants:
|
||||||
monkeypatch.chdir("world")
|
monkeypatch.chdir("world")
|
||||||
|
|
||||||
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
|
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
|
||||||
|
# pgk_resources has been deprecated entirely.
|
||||||
# While we could change the test to use implicit namespace packages, seems better
|
# While we could change the test to use implicit namespace packages, seems better
|
||||||
# to still ensure the old declaration via declare_namespace still works.
|
# to still ensure the old declaration via declare_namespace still works.
|
||||||
ignore_w = r"-Wignore:Deprecated call to `pkg_resources.declare_namespace"
|
ignore_w = (
|
||||||
|
r"-Wignore:Deprecated call to `pkg_resources.declare_namespace",
|
||||||
|
r"-Wignore:pkg_resources is deprecated",
|
||||||
|
)
|
||||||
result = pytester.runpytest(
|
result = pytester.runpytest(
|
||||||
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", ignore_w
|
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", *ignore_w
|
||||||
)
|
)
|
||||||
assert result.ret == 0
|
assert result.ret == 0
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
|
@ -1299,12 +1303,12 @@ def test_no_brokenpipeerror_message(pytester: Pytester) -> None:
|
||||||
popen.stderr.close()
|
popen.stderr.close()
|
||||||
|
|
||||||
|
|
||||||
def test_function_return_non_none_warning(testdir) -> None:
|
def test_function_return_non_none_warning(pytester: Pytester) -> None:
|
||||||
testdir.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
def test_stuff():
|
def test_stuff():
|
||||||
return "something"
|
return "something"
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
res = testdir.runpytest()
|
res = pytester.runpytest()
|
||||||
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
|
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
|
||||||
|
|
|
@ -53,6 +53,20 @@ def test_excinfo_from_exc_info_simple() -> None:
|
||||||
assert info.type == ValueError
|
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 test_excinfo_getstatement():
|
||||||
def g():
|
def g():
|
||||||
raise ValueError
|
raise ValueError
|
||||||
|
@ -310,9 +324,7 @@ class TestTraceback_f_g_h:
|
||||||
g()
|
g()
|
||||||
|
|
||||||
excinfo = pytest.raises(ValueError, f)
|
excinfo = pytest.raises(ValueError, f)
|
||||||
tb = excinfo.traceback
|
assert excinfo.traceback.getcrashentry() is None
|
||||||
entry = tb.getcrashentry()
|
|
||||||
assert entry is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_excinfo_exconly():
|
def test_excinfo_exconly():
|
||||||
|
@ -461,6 +473,24 @@ class TestFormattedExcinfo:
|
||||||
assert lines[0] == "| def f(x):"
|
assert lines[0] == "| def f(x):"
|
||||||
assert lines[1] == " pass"
|
assert lines[1] == " pass"
|
||||||
|
|
||||||
|
def test_repr_source_out_of_bounds(self):
|
||||||
|
pr = FormattedExcinfo()
|
||||||
|
source = _pytest._code.Source(
|
||||||
|
"""\
|
||||||
|
def f(x):
|
||||||
|
pass
|
||||||
|
"""
|
||||||
|
).strip()
|
||||||
|
pr.flow_marker = "|" # type: ignore[misc]
|
||||||
|
|
||||||
|
lines = pr.get_source(source, 100)
|
||||||
|
assert len(lines) == 1
|
||||||
|
assert lines[0] == "| ???"
|
||||||
|
|
||||||
|
lines = pr.get_source(source, -100)
|
||||||
|
assert len(lines) == 1
|
||||||
|
assert lines[0] == "| ???"
|
||||||
|
|
||||||
def test_repr_source_excinfo(self) -> None:
|
def test_repr_source_excinfo(self) -> None:
|
||||||
"""Check if indentation is right."""
|
"""Check if indentation is right."""
|
||||||
try:
|
try:
|
||||||
|
@ -1555,3 +1585,21 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
|
||||||
# with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it
|
# with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it
|
||||||
pytest.importorskip("exceptiongroup")
|
pytest.importorskip("exceptiongroup")
|
||||||
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
|
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
|
||||||
|
|
||||||
|
|
||||||
|
@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.*"])
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
# mypy: disable-error-code="attr-defined"
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -8,6 +9,19 @@ logger = logging.getLogger(__name__)
|
||||||
sublogger = logging.getLogger(__name__ + ".baz")
|
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:
|
def test_fixture_help(pytester: Pytester) -> None:
|
||||||
result = pytester.runpytest("--fixtures")
|
result = pytester.runpytest("--fixtures")
|
||||||
result.stdout.fnmatch_lines(["*caplog*"])
|
result.stdout.fnmatch_lines(["*caplog*"])
|
||||||
|
@ -28,10 +42,27 @@ def test_change_level(caplog):
|
||||||
assert "CRITICAL" in caplog.text
|
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:
|
def test_change_level_undo(pytester: Pytester) -> None:
|
||||||
"""Ensure that 'set_level' is undone after the end of the test.
|
"""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(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
|
@ -54,6 +85,37 @@ def test_change_level_undo(pytester: Pytester) -> None:
|
||||||
result.stdout.no_fnmatch_line("*log from test2*")
|
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:
|
def test_change_level_undos_handler_level(pytester: Pytester) -> None:
|
||||||
"""Ensure that 'set_level' is undone after the end of the test (handler).
|
"""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
|
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):
|
def test_log_access(caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
logger.info("boo %s", "arg")
|
logger.info("boo %s", "arg")
|
||||||
|
|
|
@ -1167,8 +1167,8 @@ def test_log_file_cli_subdirectories_are_successfully_created(
|
||||||
assert result.ret == ExitCode.OK
|
assert result.ret == ExitCode.OK
|
||||||
|
|
||||||
|
|
||||||
def test_disable_loggers(testdir):
|
def test_disable_loggers(pytester: Pytester) -> None:
|
||||||
testdir.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
@ -1181,13 +1181,13 @@ def test_disable_loggers(testdir):
|
||||||
assert caplog.record_tuples == [('test', 10, 'Visible text!')]
|
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 result.ret == ExitCode.OK
|
||||||
assert not result.stderr.lines
|
assert not result.stderr.lines
|
||||||
|
|
||||||
|
|
||||||
def test_disable_loggers_does_not_propagate(testdir):
|
def test_disable_loggers_does_not_propagate(pytester: Pytester) -> None:
|
||||||
testdir.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
import os
|
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 result.ret == ExitCode.OK
|
||||||
assert not result.stderr.lines
|
assert not result.stderr.lines
|
||||||
|
|
||||||
|
|
||||||
def test_log_disabling_works_with_log_cli(testdir):
|
def test_log_disabling_works_with_log_cli(pytester: Pytester) -> None:
|
||||||
testdir.makepyfile(
|
pytester.makepyfile(
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
disabled_log = logging.getLogger('disabled')
|
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.")
|
disabled_log.warning("This string will be suppressed.")
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
result = testdir.runpytest(
|
result = pytester.runpytest(
|
||||||
"--log-cli-level=DEBUG",
|
"--log-cli-level=DEBUG",
|
||||||
"--log-disable=disabled",
|
"--log-disable=disabled",
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
anyio[curio,trio]==3.6.2
|
anyio[curio,trio]==3.6.2
|
||||||
django==4.1.7
|
django==4.2.1
|
||||||
pytest-asyncio==0.21.0
|
pytest-asyncio==0.21.0
|
||||||
pytest-bdd==6.1.1
|
pytest-bdd==6.1.1
|
||||||
pytest-cov==4.0.0
|
pytest-cov==4.0.0
|
||||||
|
@ -8,7 +8,7 @@ pytest-flakes==4.0.5
|
||||||
pytest-html==3.2.0
|
pytest-html==3.2.0
|
||||||
pytest-mock==3.10.0
|
pytest-mock==3.10.0
|
||||||
pytest-rerunfailures==11.1.2
|
pytest-rerunfailures==11.1.2
|
||||||
pytest-sugar==0.9.5
|
pytest-sugar==0.9.7
|
||||||
pytest-trio==0.7.0
|
pytest-trio==0.7.0
|
||||||
pytest-twisted==1.14.0
|
pytest-twisted==1.14.0
|
||||||
twisted==22.8.0
|
twisted==22.8.0
|
||||||
|
|
|
@ -87,7 +87,7 @@ class TestNewAPI:
|
||||||
"*= warnings summary =*",
|
"*= warnings summary =*",
|
||||||
"*/cacheprovider.py:*",
|
"*/cacheprovider.py:*",
|
||||||
" */cacheprovider.py:*: PytestCacheWarning: could not create cache path "
|
" */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))',
|
' config.cache.set("cache/nodeids", sorted(self.cached_nodeids))',
|
||||||
"*1 failed, 3 warnings in*",
|
"*1 failed, 3 warnings in*",
|
||||||
]
|
]
|
||||||
|
|
|
@ -1247,6 +1247,48 @@ def test_collect_pyargs_with_testpaths(
|
||||||
result.stdout.fnmatch_lines(["*1 passed in*"])
|
result.stdout.fnmatch_lines(["*1 passed in*"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_initial_conftests_with_testpaths(pytester: Pytester) -> None:
|
||||||
|
"""The testpaths ini option should load conftests in those paths as 'initial' (#10987)."""
|
||||||
|
p = pytester.mkdir("some_path")
|
||||||
|
p.joinpath("conftest.py").write_text(
|
||||||
|
textwrap.dedent(
|
||||||
|
"""
|
||||||
|
def pytest_sessionstart(session):
|
||||||
|
raise Exception("pytest_sessionstart hook successfully run")
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pytester.makeini(
|
||||||
|
"""
|
||||||
|
[pytest]
|
||||||
|
testpaths = some_path
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest()
|
||||||
|
result.stdout.fnmatch_lines(
|
||||||
|
"INTERNALERROR* Exception: pytest_sessionstart hook successfully run"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_large_option_breaks_initial_conftests(pytester: Pytester) -> None:
|
||||||
|
"""Long option values do not break initial conftests handling (#10169)."""
|
||||||
|
option_value = "x" * 1024 * 1000
|
||||||
|
pytester.makeconftest(
|
||||||
|
"""
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption("--xx", default=None)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
pytester.makepyfile(
|
||||||
|
f"""
|
||||||
|
def test_foo(request):
|
||||||
|
assert request.config.getoption("xx") == {option_value!r}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = pytester.runpytest(f"--xx={option_value}")
|
||||||
|
assert result.ret == 0
|
||||||
|
|
||||||
|
|
||||||
def test_collect_symlink_file_arg(pytester: Pytester) -> None:
|
def test_collect_symlink_file_arg(pytester: Pytester) -> None:
|
||||||
"""Collect a direct symlink works even if it does not match python_files (#4325)."""
|
"""Collect a direct symlink works even if it does not match python_files (#4325)."""
|
||||||
real = pytester.makepyfile(
|
real = pytester.makepyfile(
|
||||||
|
|
|
@ -35,7 +35,7 @@ def conftest_setinitial(
|
||||||
self.importmode = "prepend"
|
self.importmode = "prepend"
|
||||||
|
|
||||||
namespace = cast(argparse.Namespace, Namespace())
|
namespace = cast(argparse.Namespace, Namespace())
|
||||||
conftest._set_initial_conftests(namespace, rootpath=Path(args[0]))
|
conftest._set_initial_conftests(namespace, rootpath=Path(args[0]), testpaths_ini=[])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("_sys_snapshot")
|
@pytest.mark.usefixtures("_sys_snapshot")
|
||||||
|
|
|
@ -425,9 +425,7 @@ def test_context_classmethod() -> None:
|
||||||
assert A.x == 1
|
assert A.x == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.filterwarnings(
|
@pytest.mark.filterwarnings(r"ignore:.*\bpkg_resources\b:DeprecationWarning")
|
||||||
"ignore:Deprecated call to `pkg_resources.declare_namespace"
|
|
||||||
)
|
|
||||||
def test_syspath_prepend_with_namespace_packages(
|
def test_syspath_prepend_with_namespace_packages(
|
||||||
pytester: Pytester, monkeypatch: MonkeyPatch
|
pytester: Pytester, monkeypatch: MonkeyPatch
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
|
@ -82,7 +82,7 @@ def test_no_ini(pytester: Pytester, file_structure) -> None:
|
||||||
|
|
||||||
def test_clean_up(pytester: Pytester) -> None:
|
def test_clean_up(pytester: Pytester) -> None:
|
||||||
"""Test that the plugin cleans up after itself."""
|
"""Test that the plugin cleans up after itself."""
|
||||||
# This is tough to test behaviorly because the cleanup really runs last.
|
# This is tough to test behaviorally because the cleanup really runs last.
|
||||||
# So the test make several implementation assumptions:
|
# So the test make several implementation assumptions:
|
||||||
# - Cleanup is done in pytest_unconfigure().
|
# - Cleanup is done in pytest_unconfigure().
|
||||||
# - Not a hookwrapper.
|
# - Not a hookwrapper.
|
||||||
|
|
|
@ -387,13 +387,13 @@ class TestTerminal:
|
||||||
pytest.xfail("It's 🕙 o'clock")
|
pytest.xfail("It's 🕙 o'clock")
|
||||||
|
|
||||||
@pytest.mark.skip(
|
@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():
|
def test_long_skip():
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@pytest.mark.xfail(
|
@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():
|
def test_long_xfail():
|
||||||
print(1 / 0)
|
print(1 / 0)
|
||||||
|
@ -417,8 +417,8 @@ class TestTerminal:
|
||||||
result.stdout.fnmatch_lines(
|
result.stdout.fnmatch_lines(
|
||||||
common_output
|
common_output
|
||||||
+ [
|
+ [
|
||||||
"test_verbose_skip_reason.py::test_long_skip SKIPPED (cannot *...) *",
|
"test_verbose_skip_reason.py::test_long_skip SKIPPED (1 cannot *...) *",
|
||||||
"test_verbose_skip_reason.py::test_long_xfail XFAIL (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"
|
"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"
|
"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()
|
s = result.stdout.str()
|
||||||
assert "def test_func2" not in s
|
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:
|
def test_pytest_report_header(self, pytester: Pytester, option) -> None:
|
||||||
pytester.makeconftest(
|
pytester.makeconftest(
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -512,20 +512,20 @@ class TestRmRf:
|
||||||
|
|
||||||
# unknown exception
|
# unknown exception
|
||||||
with pytest.warns(pytest.PytestWarning):
|
with pytest.warns(pytest.PytestWarning):
|
||||||
exc_info1 = (None, RuntimeError(), None)
|
exc_info1 = (RuntimeError, RuntimeError(), None)
|
||||||
on_rm_rf_error(os.unlink, str(fn), exc_info1, start_path=tmp_path)
|
on_rm_rf_error(os.unlink, str(fn), exc_info1, start_path=tmp_path)
|
||||||
assert fn.is_file()
|
assert fn.is_file()
|
||||||
|
|
||||||
# we ignore FileNotFoundError
|
# we ignore FileNotFoundError
|
||||||
exc_info2 = (None, FileNotFoundError(), None)
|
exc_info2 = (FileNotFoundError, FileNotFoundError(), None)
|
||||||
assert not on_rm_rf_error(None, str(fn), exc_info2, start_path=tmp_path)
|
assert not on_rm_rf_error(None, str(fn), exc_info2, start_path=tmp_path)
|
||||||
|
|
||||||
# unknown function
|
# unknown function
|
||||||
with pytest.warns(
|
with pytest.warns(
|
||||||
pytest.PytestWarning,
|
pytest.PytestWarning,
|
||||||
match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\nNone: ",
|
match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\n<class 'PermissionError'>: ",
|
||||||
):
|
):
|
||||||
exc_info3 = (None, PermissionError(), None)
|
exc_info3 = (PermissionError, PermissionError(), None)
|
||||||
on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path)
|
on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path)
|
||||||
assert fn.is_file()
|
assert fn.is_file()
|
||||||
|
|
||||||
|
@ -533,12 +533,12 @@ class TestRmRf:
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter("ignore")
|
warnings.simplefilter("ignore")
|
||||||
with pytest.warns(None) as warninfo: # type: ignore[call-overload]
|
with pytest.warns(None) as warninfo: # type: ignore[call-overload]
|
||||||
exc_info4 = (None, PermissionError(), None)
|
exc_info4 = PermissionError()
|
||||||
on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path)
|
on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path)
|
||||||
assert fn.is_file()
|
assert fn.is_file()
|
||||||
assert not [x.message for x in warninfo]
|
assert not [x.message for x in warninfo]
|
||||||
|
|
||||||
exc_info5 = (None, PermissionError(), None)
|
exc_info5 = PermissionError()
|
||||||
on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path)
|
on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path)
|
||||||
assert not fn.is_file()
|
assert not fn.is_file()
|
||||||
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
def test_tbh_chained(testdir):
|
|
||||||
"""Ensure chained exceptions whose frames contain "__tracebackhide__" are not shown (#1904)."""
|
|
||||||
p = testdir.makepyfile(
|
|
||||||
"""
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
def f1():
|
|
||||||
__tracebackhide__ = True
|
|
||||||
try:
|
|
||||||
return f1.meh
|
|
||||||
except AttributeError:
|
|
||||||
pytest.fail("fail")
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def fix():
|
|
||||||
f1()
|
|
||||||
|
|
||||||
|
|
||||||
def test(fix):
|
|
||||||
pass
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
result = testdir.runpytest(str(p))
|
|
||||||
assert "'function' object has no attribute 'meh'" not in result.stdout.str()
|
|
||||||
assert result.ret == 1
|
|
|
@ -9,6 +9,7 @@ from typing import Optional
|
||||||
from typing_extensions import assert_type
|
from typing_extensions import assert_type
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from pytest import MonkeyPatch
|
||||||
|
|
||||||
|
|
||||||
# Issue #7488.
|
# Issue #7488.
|
||||||
|
@ -29,6 +30,19 @@ def check_parametrize_ids_callable(func) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Issue #10999.
|
||||||
|
def check_monkeypatch_typeddict(monkeypatch: MonkeyPatch) -> None:
|
||||||
|
from typing import TypedDict
|
||||||
|
|
||||||
|
class Foo(TypedDict):
|
||||||
|
x: int
|
||||||
|
y: float
|
||||||
|
|
||||||
|
a: Foo = {"x": 1, "y": 3.14}
|
||||||
|
monkeypatch.setitem(a, "x", 2)
|
||||||
|
monkeypatch.delitem(a, "y")
|
||||||
|
|
||||||
|
|
||||||
def check_raises_is_a_context_manager(val: bool) -> None:
|
def check_raises_is_a_context_manager(val: bool) -> None:
|
||||||
with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo:
|
with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo:
|
||||||
pass
|
pass
|
||||||
|
|
Loading…
Reference in New Issue