Merge branch 'main' into improve-high-scope-fixtures-teardown-issue-3806

This commit is contained in:
Sadra Barikbin 2023-05-20 17:22:17 +03:30
commit eba76ce663
75 changed files with 1354 additions and 452 deletions

23
.github/workflows/stale.yml vendored Normal file
View File

@ -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

View File

@ -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>'

View File

@ -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/)

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -1 +0,0 @@
pytest no longer depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported).

View File

@ -1 +0,0 @@
Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.

View File

@ -1 +0,0 @@
Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.

View File

@ -1 +0,0 @@
The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.

View File

@ -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.

View File

@ -1 +0,0 @@
Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.

View File

@ -1 +0,0 @@
Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.

View File

@ -0,0 +1 @@
Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.

View File

@ -0,0 +1 @@
Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1 @@
:confval:`testpaths` is now honored to load root ``conftests``.

View File

@ -0,0 +1 @@
The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.

View File

@ -0,0 +1 @@
Added underlying exception to cache provider path creation and write warning messages.

View File

@ -1 +0,0 @@
Correctly handle ``__tracebackhide__`` for chained exceptions.

View File

@ -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.

View File

@ -1 +0,0 @@
``--log-disable`` CLI option added to disable individual loggers.

View File

@ -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.

View File

@ -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)``.

View File

@ -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

View File

@ -0,0 +1,130 @@
pytest-7.3.0
=======================================
The pytest team is proud to announce the 7.3.0 release!
This release contains new features, improvements, and bug fixes,
the full list of changes is available in the changelog:
https://docs.pytest.org/en/stable/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/stable/
As usual, you can upgrade from PyPI via:
pip install -U pytest
Thanks to all of the contributors to this release:
* Aaron Berdy
* Adam Turner
* Albert Villanova del Moral
* Alessio Izzo
* Alex Hadley
* Alice Purcell
* Anthony Sottile
* Anton Yakutovich
* Ashish Kurmi
* Babak Keyvani
* Billy
* Brandon Chinn
* Bruno Oliveira
* Cal Jacobson
* Chanvin Xiao
* Cheuk Ting Ho
* Chris Wheeler
* Daniel Garcia Moreno
* Daniel Scheffler
* Daniel Valenzuela
* EmptyRabbit
* Ezio Melotti
* Felix Hofstätter
* Florian Best
* Florian Bruhin
* Fredrik Berndtsson
* Gabriel Landau
* Garvit Shubham
* Gergely Kalmár
* HTRafal
* Hugo van Kemenade
* Ilya Konstantinov
* Itxaso Aizpurua
* James Gerity
* Jay
* John Litborn
* Jon Parise
* Jouke Witteveen
* Kadino
* Kevin C
* Kian Eliasi
* Klaus Rettinghaus
* Kodi Arfer
* Mahesh Vashishtha
* Manuel Jacob
* Marko Pacak
* MatthewFlamm
* Miro Hrončok
* Nate Meyvis
* Neil Girdhar
* Nhieuvu1802
* Nipunn Koorapati
* Ofek Lev
* Paul Kehrer
* Paul Müller
* Paul Reece
* Pax
* Pete Baughman
* Peyman Salehi
* Philipp A
* Pierre Sassoulas
* Prerak Patel
* Ramsey
* Ran Benita
* Robert O'Shea
* Ronny Pfannschmidt
* Rowin
* Ruth Comer
* Samuel Colvin
* Samuel Gaist
* Sandro Tosi
* Santiago Castro
* Shantanu
* Simon K
* Stefanie Molin
* Stephen Rosen
* Sviatoslav Sydorenko
* Tatiana Ovary
* Teejay
* Thierry Moisan
* Thomas Grainger
* Tim Hoffmann
* Tobias Diez
* Tony Narlock
* Vivaan Verma
* Wolfremium
* Yannick PÉROUX
* Yusuke Kadowaki
* Zac Hatfield-Dodds
* Zach OBrien
* aizpurua23a
* bitzge
* bluthej
* gresm
* holesch
* itxasos23
* johnkangw
* q0w
* rdb
* s-padmanaban
* skhomuti
* sommersoft
* vin01
* wim glenn
* wodny
* zx.qiu
Happy testing,
The pytest Development Team

View File

@ -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

View File

@ -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/>`__.

View File

@ -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.

View File

@ -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 = ''

View File

@ -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
-------------------------------------------------------------------- --------------------------------------------------------------------

View File

@ -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:

View File

@ -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.

View File

@ -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`:

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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:

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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:

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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`?*"])

View File

@ -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.*"])

View File

@ -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")

View File

@ -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",
) )

View File

@ -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

View File

@ -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*",
] ]

View File

@ -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(

View File

@ -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")

View File

@ -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:

View File

@ -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.

View File

@ -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(
""" """

View File

@ -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()

View 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

View File

@ -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

View File

@ -100,7 +100,6 @@ basepython = python3
passenv = passenv =
SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST SETUPTOOLS_SCM_PRETEND_VERSION_FOR_PYTEST
deps = deps =
dataclasses
PyYAML PyYAML
regendoc>=0.8.1 regendoc>=0.8.1
sphinx sphinx