From bc3302850c52ec945eea1b2bbde7ec3b91fc3e44 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sat, 20 Jul 2024 15:20:56 +0000 Subject: [PATCH 01/78] Prepare release version 8.3.0 --- changelog/11706.bugfix.rst | 4 - changelog/11771.contrib.rst | 5 - changelog/11797.bugfix.rst | 1 - changelog/12153.doc.rst | 1 - changelog/12204.bugfix.rst | 11 -- changelog/12231.feature.rst | 11 -- changelog/12264.bugfix.rst | 1 - changelog/12275.bugfix.rst | 1 - changelog/12281.feature.rst | 8 - changelog/12328.bugfix.rst | 1 - changelog/12424.bugfix.rst | 1 - changelog/12467.improvement.rst | 3 - changelog/12469.doc.rst | 6 - changelog/12469.improvement.rst | 4 - changelog/12472.bugfix.rst | 1 - changelog/12493.contrib.rst | 13 -- changelog/12498.contrib.rst | 5 - changelog/12501.contrib.rst | 11 -- changelog/12502.contrib.rst | 7 - changelog/12505.bugfix.rst | 1 - changelog/12522.contrib.rst | 4 - changelog/12531.contrib.rst | 6 - changelog/12533.contrib.rst | 7 - changelog/12544.improvement.rst | 3 - changelog/12545.improvement.rst | 1 - changelog/12557.contrib.rst | 1 - changelog/12562.contrib.rst | 2 - changelog/12567.feature.rst | 7 - changelog/12577.doc.rst | 3 - changelog/12580.bugfix.rst | 1 - changelog/2871.improvement.rst | 1 - changelog/389.improvement.rst | 38 ---- changelog/6962.bugfix.rst | 2 - changelog/7166.bugfix.rst | 1 - changelog/7662.improvement.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-8.3.0.rst | 60 +++++++ doc/en/builtin.rst | 34 ++-- doc/en/changelog.rst | 257 ++++++++++++++++++++++++++++ doc/en/example/parametrize.rst | 10 +- doc/en/example/pythoncollection.rst | 4 +- doc/en/example/reportingdemo.rst | 96 +++++------ doc/en/example/simple.rst | 25 --- doc/en/getting-started.rst | 2 +- doc/en/how-to/fixtures.rst | 2 +- doc/en/how-to/output.rst | 8 - doc/en/reference/reference.rst | 199 ++++++++++----------- 47 files changed, 495 insertions(+), 377 deletions(-) delete mode 100644 changelog/11706.bugfix.rst delete mode 100644 changelog/11771.contrib.rst delete mode 100644 changelog/11797.bugfix.rst delete mode 100644 changelog/12153.doc.rst delete mode 100644 changelog/12204.bugfix.rst delete mode 100644 changelog/12231.feature.rst delete mode 120000 changelog/12264.bugfix.rst delete mode 100644 changelog/12275.bugfix.rst delete mode 100644 changelog/12281.feature.rst delete mode 100644 changelog/12328.bugfix.rst delete mode 100644 changelog/12424.bugfix.rst delete mode 100644 changelog/12467.improvement.rst delete mode 100644 changelog/12469.doc.rst delete mode 100644 changelog/12469.improvement.rst delete mode 100644 changelog/12472.bugfix.rst delete mode 100644 changelog/12493.contrib.rst delete mode 100644 changelog/12498.contrib.rst delete mode 100644 changelog/12501.contrib.rst delete mode 100644 changelog/12502.contrib.rst delete mode 100644 changelog/12505.bugfix.rst delete mode 100644 changelog/12522.contrib.rst delete mode 100644 changelog/12531.contrib.rst delete mode 100644 changelog/12533.contrib.rst delete mode 100644 changelog/12544.improvement.rst delete mode 120000 changelog/12545.improvement.rst delete mode 120000 changelog/12557.contrib.rst delete mode 100644 changelog/12562.contrib.rst delete mode 100644 changelog/12567.feature.rst delete mode 100644 changelog/12577.doc.rst delete mode 100644 changelog/12580.bugfix.rst delete mode 100644 changelog/2871.improvement.rst delete mode 100644 changelog/389.improvement.rst delete mode 100644 changelog/6962.bugfix.rst delete mode 100644 changelog/7166.bugfix.rst delete mode 100644 changelog/7662.improvement.rst create mode 100644 doc/en/announce/release-8.3.0.rst diff --git a/changelog/11706.bugfix.rst b/changelog/11706.bugfix.rst deleted file mode 100644 index a86db5ef66a..00000000000 --- a/changelog/11706.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`. - -Originally added in pytest 8.0.0, but reverted in 8.0.2 due to a regression in pytest-xdist. -This regression was fixed in pytest-xdist 3.6.1. diff --git a/changelog/11771.contrib.rst b/changelog/11771.contrib.rst deleted file mode 100644 index a3c1ed1099e..00000000000 --- a/changelog/11771.contrib.rst +++ /dev/null @@ -1,5 +0,0 @@ -The PyPy runtime version has been updated to 3.9 from 3.8 that introduced -a flaky bug at the garbage collector which was not expected to fix there -as the V3.8 is EoL. - --- by :user:`x612skm` diff --git a/changelog/11797.bugfix.rst b/changelog/11797.bugfix.rst deleted file mode 100644 index 94b72da00fd..00000000000 --- a/changelog/11797.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -:func:`pytest.approx` now correctly handles :class:`Sequence `-like objects. diff --git a/changelog/12153.doc.rst b/changelog/12153.doc.rst deleted file mode 100644 index ac36becf9a7..00000000000 --- a/changelog/12153.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Documented using :envvar:`PYTEST_VERSION` to detect if code is running from within a pytest run. diff --git a/changelog/12204.bugfix.rst b/changelog/12204.bugfix.rst deleted file mode 100644 index 099ad70610a..00000000000 --- a/changelog/12204.bugfix.rst +++ /dev/null @@ -1,11 +0,0 @@ -Fixed a regression in pytest 8.0 where tracebacks get longer and longer when multiple -tests fail due to a shared higher-scope fixture which raised -- by :user:`bluetech`. - -Also fixed a similar regression in pytest 5.4 for collectors which raise during setup. - -The fix necessitated internal changes which may affect some plugins: - -* ``FixtureDef.cached_result[2]`` is now a tuple ``(exc, tb)`` - instead of ``exc``. -* ``SetupState.stack`` failures are now a tuple ``(exc, tb)`` - instead of ``exc``. diff --git a/changelog/12231.feature.rst b/changelog/12231.feature.rst deleted file mode 100644 index dad04bc20c1..00000000000 --- a/changelog/12231.feature.rst +++ /dev/null @@ -1,11 +0,0 @@ -Added `--xfail-tb` flag, which turns on traceback output for XFAIL results. - -* If the `--xfail-tb` flag is not sent, tracebacks for XFAIL results are NOT shown. -* The style of traceback for XFAIL is set with `--tb`, and can be `auto|long|short|line|native|no`. -* Note: Even if you have `--xfail-tb` set, you won't see them if `--tb=no`. - -Some history: - -With pytest 8.0, `-rx` or `-ra` would not only turn on summary reports for xfail, but also report the tracebacks for xfail results. This caused issues with some projects that utilize xfail, but don't want to see all of the xfail tracebacks. - -This change detaches xfail tracebacks from `-rx`, and now we turn on xfail tracebacks with `--xfail-tb`. With this, the default `-rx`/ `-ra` behavior is identical to pre-8.0 with respect to xfail tracebacks. While this is a behavior change, it brings default behavior back to pre-8.0.0 behavior, which ultimately was considered the better course of action. diff --git a/changelog/12264.bugfix.rst b/changelog/12264.bugfix.rst deleted file mode 120000 index e5704e6e819..00000000000 --- a/changelog/12264.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -12204.bugfix.rst \ No newline at end of file diff --git a/changelog/12275.bugfix.rst b/changelog/12275.bugfix.rst deleted file mode 100644 index 2d040a3a063..00000000000 --- a/changelog/12275.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix collection error upon encountering an :mod:`abstract ` class, including abstract `unittest.TestCase` subclasses. diff --git a/changelog/12281.feature.rst b/changelog/12281.feature.rst deleted file mode 100644 index c6e8e3b3098..00000000000 --- a/changelog/12281.feature.rst +++ /dev/null @@ -1,8 +0,0 @@ -Added support for keyword matching in marker expressions. - -Now tests can be selected by marker keyword arguments. -Supported values are :class:`int`, (unescaped) :class:`str`, :class:`bool` & :data:`None`. - -See :ref:`marker examples ` for more information. - --- by :user:`lovetheguitar` diff --git a/changelog/12328.bugfix.rst b/changelog/12328.bugfix.rst deleted file mode 100644 index f334425850b..00000000000 --- a/changelog/12328.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix a regression in pytest 8.0.0 where package-scoped parameterized items were not correctly reordered to minimize setups/teardowns in some cases. diff --git a/changelog/12424.bugfix.rst b/changelog/12424.bugfix.rst deleted file mode 100644 index 7ad1126858b..00000000000 --- a/changelog/12424.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix crash with `assert testcase is not None` assertion failure when re-running unittest tests using plugins like pytest-rerunfailures. Regressed in 8.2.2. diff --git a/changelog/12467.improvement.rst b/changelog/12467.improvement.rst deleted file mode 100644 index b1e0581ed16..00000000000 --- a/changelog/12467.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -Migrated all internal type-annotations to the python3.10+ style by using the `annotations` future import. - --- by :user:`RonnyPfannschmidt` diff --git a/changelog/12469.doc.rst b/changelog/12469.doc.rst deleted file mode 100644 index 2340315353c..00000000000 --- a/changelog/12469.doc.rst +++ /dev/null @@ -1,6 +0,0 @@ -The external plugin mentions in the documentation now avoid mentioning -:std:doc:`setuptools entry-points ` as the concept is -much more generic nowadays. Instead, the terminology of "external", -"installed", or "third-party" plugins (or packages) replaces that. - --- by :user:`webknjaz` diff --git a/changelog/12469.improvement.rst b/changelog/12469.improvement.rst deleted file mode 100644 index a90fb1e6610..00000000000 --- a/changelog/12469.improvement.rst +++ /dev/null @@ -1,4 +0,0 @@ -The console output now uses the "third-party plugins" terminology, -replacing the previously established but confusing and outdated -reference to :std:doc:`setuptools ` --- by :user:`webknjaz`. diff --git a/changelog/12472.bugfix.rst b/changelog/12472.bugfix.rst deleted file mode 100644 index f08e9d1f90b..00000000000 --- a/changelog/12472.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a crash when returning category ``"error"`` or ``"failed"`` with a custom test status from :hook:`pytest_report_teststatus` hook -- :user:`pbrezina`. diff --git a/changelog/12493.contrib.rst b/changelog/12493.contrib.rst deleted file mode 100644 index db3d045697e..00000000000 --- a/changelog/12493.contrib.rst +++ /dev/null @@ -1,13 +0,0 @@ -The change log draft preview integration has been refactored to use a -third party extension ``sphinxcontib-towncrier``. The previous in-repo -script was putting the change log preview file at -:file:`doc/en/_changelog_towncrier_draft.rst`. Said file is no longer -ignored in Git and might show up among untracked files in the -development environments of the contributors. To address that, the -contributors can run the following command that will clean it up: - -.. code-block:: console - - $ git clean -x -i -- doc/en/_changelog_towncrier_draft.rst - --- by :user:`webknjaz` diff --git a/changelog/12498.contrib.rst b/changelog/12498.contrib.rst deleted file mode 100644 index 436c6f0e9ed..00000000000 --- a/changelog/12498.contrib.rst +++ /dev/null @@ -1,5 +0,0 @@ -All the undocumented ``tox`` environments now have descriptions. -They can be listed in one's development environment by invoking -``tox -av`` in a terminal. - --- by :user:`webknjaz` diff --git a/changelog/12501.contrib.rst b/changelog/12501.contrib.rst deleted file mode 100644 index 6f434c287b3..00000000000 --- a/changelog/12501.contrib.rst +++ /dev/null @@ -1,11 +0,0 @@ -The changelog configuration has been updated to introduce more accurate -audience-tailored categories. Previously, there was a ``trivial`` -change log fragment type with an unclear and broad meaning. It was -removed and we now have ``contrib``, ``misc`` and ``packaging`` in -place of it. - -The new change note types target the readers who are downstream -packagers and project contributors. Additionally, the miscellaneous -section is kept for unspecified updates that do not fit anywhere else. - --- by :user:`webknjaz` diff --git a/changelog/12502.contrib.rst b/changelog/12502.contrib.rst deleted file mode 100644 index 940a2d7a120..00000000000 --- a/changelog/12502.contrib.rst +++ /dev/null @@ -1,7 +0,0 @@ -The UX of the GitHub automation making pull requests to update the -plugin list has been updated. Previously, the maintainers had to close -the automatically created pull requests and re-open them to trigger the -CI runs. From now on, they only need to click the `Ready for review` -button instead. - --- by :user:`webknjaz` diff --git a/changelog/12505.bugfix.rst b/changelog/12505.bugfix.rst deleted file mode 100644 index f55a8a17e4b..00000000000 --- a/changelog/12505.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Improve handling of invalid regex patterns in :func:`pytest.raises(match=r'...') ` by providing a clear error message. diff --git a/changelog/12522.contrib.rst b/changelog/12522.contrib.rst deleted file mode 100644 index dd994317165..00000000000 --- a/changelog/12522.contrib.rst +++ /dev/null @@ -1,4 +0,0 @@ -The ``:pull:`` RST role has been replaced with a shorter -``:pr:`` due to starting to use the implementation from -the third-party :pypi:`sphinx-issues` Sphinx extension --- by :user:`webknjaz`. diff --git a/changelog/12531.contrib.rst b/changelog/12531.contrib.rst deleted file mode 100644 index 12083fc320e..00000000000 --- a/changelog/12531.contrib.rst +++ /dev/null @@ -1,6 +0,0 @@ -The coverage reporting configuration has been updated to exclude -pytest's own tests marked as expected to fail from the coverage -report. This has an effect of reducing the influence of flaky -tests on the resulting number. - --- by :user:`webknjaz` diff --git a/changelog/12533.contrib.rst b/changelog/12533.contrib.rst deleted file mode 100644 index 3da7007a0fd..00000000000 --- a/changelog/12533.contrib.rst +++ /dev/null @@ -1,7 +0,0 @@ -The ``extlinks`` Sphinx extension is no longer enabled. The ``:bpo:`` -role it used to declare has been removed with that. BPO itself has -migrated to GitHub some years ago and it is possible to link the -respective issues by using their GitHub issue numbers and the -``:issue:`` role that the ``sphinx-issues`` extension implements. - --- by :user:`webknjaz` diff --git a/changelog/12544.improvement.rst b/changelog/12544.improvement.rst deleted file mode 100644 index 41125f5d939..00000000000 --- a/changelog/12544.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -The ``_in_venv()`` function now detects Python virtual environments by -checking for a :file:`pyvenv.cfg` file, ensuring reliable detection on -various platforms -- by :user:`zachsnickers`. diff --git a/changelog/12545.improvement.rst b/changelog/12545.improvement.rst deleted file mode 120000 index 41a1e6bfa49..00000000000 --- a/changelog/12545.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -12544.improvement.rst \ No newline at end of file diff --git a/changelog/12557.contrib.rst b/changelog/12557.contrib.rst deleted file mode 120000 index c036c519093..00000000000 --- a/changelog/12557.contrib.rst +++ /dev/null @@ -1 +0,0 @@ -11771.contrib.rst \ No newline at end of file diff --git a/changelog/12562.contrib.rst b/changelog/12562.contrib.rst deleted file mode 100644 index 0d30495983a..00000000000 --- a/changelog/12562.contrib.rst +++ /dev/null @@ -1,2 +0,0 @@ -Possible typos in using the ``:user:`` RST role is now being linted -through the pre-commit tool integration -- by :user:`webknjaz`. diff --git a/changelog/12567.feature.rst b/changelog/12567.feature.rst deleted file mode 100644 index 3690d7aff68..00000000000 --- a/changelog/12567.feature.rst +++ /dev/null @@ -1,7 +0,0 @@ -Added ``--no-fold-skipped`` command line option - -If this option is set, then skipped tests in short summary are no longer grouped -by reason but all tests are printed individually with correct nodeid in the same -way as other statuses. - --- by :user:`pbrezina` diff --git a/changelog/12577.doc.rst b/changelog/12577.doc.rst deleted file mode 100644 index 0bd427e177d..00000000000 --- a/changelog/12577.doc.rst +++ /dev/null @@ -1,3 +0,0 @@ -`CI` and `BUILD_NUMBER` environment variables role is discribed in -the reference doc. They now also appears when doing `pytest -h` --- by :user:`MarcBresson`. diff --git a/changelog/12580.bugfix.rst b/changelog/12580.bugfix.rst deleted file mode 100644 index 9186ef1a4c9..00000000000 --- a/changelog/12580.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a crash when using the cache class on Windows and the cache directory was created concurrently. diff --git a/changelog/2871.improvement.rst b/changelog/2871.improvement.rst deleted file mode 100644 index 1ba399550c7..00000000000 --- a/changelog/2871.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Do not truncate arguments to functions in output when running with `-vvv`. diff --git a/changelog/389.improvement.rst b/changelog/389.improvement.rst deleted file mode 100644 index f8e2c19fde0..00000000000 --- a/changelog/389.improvement.rst +++ /dev/null @@ -1,38 +0,0 @@ -The readability of assertion introspection of bound methods has been enhanced --- by :user:`farbodahm`, :user:`webknjaz`, :user:`obestwalter`, :user:`flub` -and :user:`glyphack`. - -Earlier, it was like: - -.. code-block:: console - - =================================== FAILURES =================================== - _____________________________________ test _____________________________________ - - def test(): - > assert Help().fun() == 2 - E assert 1 == 2 - E + where 1 = >() - E + where > = .fun - E + where = Help() - - example.py:7: AssertionError - =========================== 1 failed in 0.03 seconds =========================== - - -And now it's like: - -.. code-block:: console - - =================================== FAILURES =================================== - _____________________________________ test _____________________________________ - - def test(): - > assert Help().fun() == 2 - E assert 1 == 2 - E + where 1 = fun() - E + where fun = .fun - E + where = Help() - - test_local.py:13: AssertionError - =========================== 1 failed in 0.03 seconds =========================== diff --git a/changelog/6962.bugfix.rst b/changelog/6962.bugfix.rst deleted file mode 100644 index 030b6e06392..00000000000 --- a/changelog/6962.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Parametrization parameters are now compared using `==` instead of `is` (`is` is still used as a fallback if the parameter does not support `==`). -This fixes use of parameters such as lists, which have a different `id` but compare equal, causing fixtures to be re-computed instead of being cached. diff --git a/changelog/7166.bugfix.rst b/changelog/7166.bugfix.rst deleted file mode 100644 index 98e6821f2ff..00000000000 --- a/changelog/7166.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed progress percentages (the ``[ 87%]`` at the edge of the screen) sometimes not aligning correctly when running with pytest-xdist ``-n``. diff --git a/changelog/7662.improvement.rst b/changelog/7662.improvement.rst deleted file mode 100644 index b6ae1ba7e4c..00000000000 --- a/changelog/7662.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Added timezone information to the testsuite timestamp in the JUnit XML report. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index c65eb5f3613..aa5dd8c87b4 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-8.3.0 release-8.2.2 release-8.2.1 release-8.2.0 diff --git a/doc/en/announce/release-8.3.0.rst b/doc/en/announce/release-8.3.0.rst new file mode 100644 index 00000000000..ec5cd3d0db9 --- /dev/null +++ b/doc/en/announce/release-8.3.0.rst @@ -0,0 +1,60 @@ +pytest-8.3.0 +======================================= + +The pytest team is proud to announce the 8.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: + +* Anita Hammer +* Ben Brown +* Brian Okken +* Bruno Oliveira +* Cornelius Riemenschneider +* Farbod Ahmadian +* Florian Bruhin +* Hynek Schlawack +* James Frost +* Jason R. Coombs +* Jelle Zijlstra +* Josh Soref +* Marc Bresson +* Michael Vogt +* Nathan Goldbaum +* Nicolas Simonds +* Oliver Bestwalter +* Pavel Březina +* Pierre Sassoulas +* Pradyun Gedam +* Ran Benita +* Ronny Pfannschmidt +* SOUBHIK KUMAR MITRA +* Sam Jirovec +* Stavros Ntentos +* Sviatoslav Sydorenko +* Sviatoslav Sydorenko (Святослав Сидоренко) +* Tomasz Kłoczko +* Virendra Patil +* Yutian Li +* Zach Snicker +* dj +* holger krekel +* joseph-sentry +* lovetheguitar +* neutraljump + + +Happy testing, +The pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 8dfffb0828a..b4c0f91f8e5 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a cachedir: .pytest_cache rootdir: /home/sweet/project collected 0 items - cache -- .../_pytest/cacheprovider.py:560 + cache -- .../_pytest/cacheprovider.py:558 Return a cache object that can persist state between testing sessions. cache.get(key, default) @@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Values can be any object handled by the json stdlib module. - capsysbinary -- .../_pytest/capture.py:1003 + capsysbinary -- .../_pytest/capture.py:1005 Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` @@ -43,6 +43,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Returns an instance of :class:`CaptureFixture[bytes] `. Example: + .. code-block:: python def test_output(capsysbinary): @@ -50,7 +51,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capsysbinary.readouterr() assert captured.out == b"hello\n" - capfd -- .../_pytest/capture.py:1030 + capfd -- .../_pytest/capture.py:1033 Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -60,6 +61,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Returns an instance of :class:`CaptureFixture[str] `. Example: + .. code-block:: python def test_system_echo(capfd): @@ -67,7 +69,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capfd.readouterr() assert captured.out == "hello\n" - capfdbinary -- .../_pytest/capture.py:1057 + capfdbinary -- .../_pytest/capture.py:1061 Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -77,6 +79,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Returns an instance of :class:`CaptureFixture[bytes] `. Example: + .. code-block:: python def test_system_echo(capfdbinary): @@ -84,7 +87,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capfdbinary.readouterr() assert captured.out == b"hello\n" - capsys -- .../_pytest/capture.py:976 + capsys -- .../_pytest/capture.py:977 Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method @@ -94,6 +97,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Returns an instance of :class:`CaptureFixture[str] `. Example: + .. code-block:: python def test_output(capsys): @@ -101,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capsys.readouterr() assert captured.out == "hello\n" - doctest_namespace [session scope] -- .../_pytest/doctest.py:738 + doctest_namespace [session scope] -- .../_pytest/doctest.py:740 Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. @@ -115,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a For more details: :ref:`doctest_namespace`. - pytestconfig [session scope] -- .../_pytest/fixtures.py:1338 + pytestconfig [session scope] -- .../_pytest/fixtures.py:1344 Session-scoped fixture that returns the session's :class:`pytest.Config` object. @@ -125,7 +129,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a if pytestconfig.getoption("verbose") > 0: ... - record_property -- .../_pytest/junitxml.py:284 + record_property -- .../_pytest/junitxml.py:280 Add extra properties to the calling test. User properties become part of the test report and are available to the @@ -139,13 +143,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a def test_function(record_property): record_property("example_key", 1) - record_xml_attribute -- .../_pytest/junitxml.py:307 + record_xml_attribute -- .../_pytest/junitxml.py:303 Add extra xml attributes to the tag for the calling test. The fixture is callable with ``name, value``. The value is automatically XML-encoded. - record_testsuite_property [session scope] -- .../_pytest/junitxml.py:345 + record_testsuite_property [session scope] -- .../_pytest/junitxml.py:341 Record a new ```` tag as child of the root ````. This is suitable to writing global information regarding the entire test @@ -170,10 +174,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a `pytest-xdist `__ plugin. See :issue:`7767` for details. - tmpdir_factory [session scope] -- .../_pytest/legacypath.py:303 + tmpdir_factory [session scope] -- .../_pytest/legacypath.py:298 Return a :class:`pytest.TempdirFactory` instance for the test session. - tmpdir -- .../_pytest/legacypath.py:310 + tmpdir -- .../_pytest/legacypath.py:305 Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. @@ -192,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a .. _legacy_path: https://py.readthedocs.io/en/latest/path.html - caplog -- .../_pytest/logging.py:602 + caplog -- .../_pytest/logging.py:600 Access and control log capturing. Captured logs are available through the following properties/methods:: @@ -203,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a * caplog.record_tuples -> list of (logger_name, level, message) tuples * caplog.clear() -> clear captured records and formatted log output string - monkeypatch -- .../_pytest/monkeypatch.py:33 + monkeypatch -- .../_pytest/monkeypatch.py:31 A convenient fixture for monkey-patching. The fixture provides these methods to modify objects, dictionaries, or @@ -227,7 +231,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a To undo modifications done by the fixture in a contained scope, use :meth:`context() `. - recwarn -- .../_pytest/recwarn.py:32 + recwarn -- .../_pytest/recwarn.py:35 Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 8e3efd0479b..12302ffe0e4 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -31,6 +31,263 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 8.3.0 (2024-07-20) +========================= + +New features +------------ + +- `#12231 `_: Added `--xfail-tb` flag, which turns on traceback output for XFAIL results. + + * If the `--xfail-tb` flag is not sent, tracebacks for XFAIL results are NOT shown. + * The style of traceback for XFAIL is set with `--tb`, and can be `auto|long|short|line|native|no`. + * Note: Even if you have `--xfail-tb` set, you won't see them if `--tb=no`. + + Some history: + + With pytest 8.0, `-rx` or `-ra` would not only turn on summary reports for xfail, but also report the tracebacks for xfail results. This caused issues with some projects that utilize xfail, but don't want to see all of the xfail tracebacks. + + This change detaches xfail tracebacks from `-rx`, and now we turn on xfail tracebacks with `--xfail-tb`. With this, the default `-rx`/ `-ra` behavior is identical to pre-8.0 with respect to xfail tracebacks. While this is a behavior change, it brings default behavior back to pre-8.0.0 behavior, which ultimately was considered the better course of action. + + +- `#12281 `_: Added support for keyword matching in marker expressions. + + Now tests can be selected by marker keyword arguments. + Supported values are :class:`int`, (unescaped) :class:`str`, :class:`bool` & :data:`None`. + + See :ref:`marker examples ` for more information. + + -- by :user:`lovetheguitar` + + +- `#12567 `_: Added ``--no-fold-skipped`` command line option + + If this option is set, then skipped tests in short summary are no longer grouped + by reason but all tests are printed individually with correct nodeid in the same + way as other statuses. + + -- by :user:`pbrezina` + + + +Improvements in existing functionality +-------------------------------------- + +- `#12467 `_: Migrated all internal type-annotations to the python3.10+ style by using the `annotations` future import. + + -- by :user:`RonnyPfannschmidt` + + +- `#12469 `_: The console output now uses the "third-party plugins" terminology, + replacing the previously established but confusing and outdated + reference to :std:doc:`setuptools ` + -- by :user:`webknjaz`. + + +- `#12544 `_, `#12545 `_: The ``_in_venv()`` function now detects Python virtual environments by + checking for a :file:`pyvenv.cfg` file, ensuring reliable detection on + various platforms -- by :user:`zachsnickers`. + + +- `#2871 `_: Do not truncate arguments to functions in output when running with `-vvv`. + + +- `#389 `_: The readability of assertion introspection of bound methods has been enhanced + -- by :user:`farbodahm`, :user:`webknjaz`, :user:`obestwalter`, :user:`flub` + and :user:`glyphack`. + + Earlier, it was like: + + .. code-block:: console + + =================================== FAILURES =================================== + _____________________________________ test _____________________________________ + + def test(): + > assert Help().fun() == 2 + E assert 1 == 2 + E + where 1 = >() + E + where > = .fun + E + where = Help() + + example.py:7: AssertionError + =========================== 1 failed in 0.03 seconds =========================== + + + And now it's like: + + .. code-block:: console + + =================================== FAILURES =================================== + _____________________________________ test _____________________________________ + + def test(): + > assert Help().fun() == 2 + E assert 1 == 2 + E + where 1 = fun() + E + where fun = .fun + E + where = Help() + + test_local.py:13: AssertionError + =========================== 1 failed in 0.03 seconds =========================== + + +- `#7662 `_: Added timezone information to the testsuite timestamp in the JUnit XML report. + + + +Bug fixes +--------- + +- `#11706 `_: Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`. + + Originally added in pytest 8.0.0, but reverted in 8.0.2 due to a regression in pytest-xdist. + This regression was fixed in pytest-xdist 3.6.1. + + +- `#11797 `_: :func:`pytest.approx` now correctly handles :class:`Sequence `-like objects. + + +- `#12204 `_, `#12264 `_: Fixed a regression in pytest 8.0 where tracebacks get longer and longer when multiple + tests fail due to a shared higher-scope fixture which raised -- by :user:`bluetech`. + + Also fixed a similar regression in pytest 5.4 for collectors which raise during setup. + + The fix necessitated internal changes which may affect some plugins: + + * ``FixtureDef.cached_result[2]`` is now a tuple ``(exc, tb)`` + instead of ``exc``. + * ``SetupState.stack`` failures are now a tuple ``(exc, tb)`` + instead of ``exc``. + + +- `#12275 `_: Fix collection error upon encountering an :mod:`abstract ` class, including abstract `unittest.TestCase` subclasses. + + +- `#12328 `_: Fix a regression in pytest 8.0.0 where package-scoped parameterized items were not correctly reordered to minimize setups/teardowns in some cases. + + +- `#12424 `_: Fix crash with `assert testcase is not None` assertion failure when re-running unittest tests using plugins like pytest-rerunfailures. Regressed in 8.2.2. + + +- `#12472 `_: Fixed a crash when returning category ``"error"`` or ``"failed"`` with a custom test status from :hook:`pytest_report_teststatus` hook -- :user:`pbrezina`. + + +- `#12505 `_: Improve handling of invalid regex patterns in :func:`pytest.raises(match=r'...') ` by providing a clear error message. + + +- `#12580 `_: Fixed a crash when using the cache class on Windows and the cache directory was created concurrently. + + +- `#6962 `_: Parametrization parameters are now compared using `==` instead of `is` (`is` is still used as a fallback if the parameter does not support `==`). + This fixes use of parameters such as lists, which have a different `id` but compare equal, causing fixtures to be re-computed instead of being cached. + + +- `#7166 `_: Fixed progress percentages (the ``[ 87%]`` at the edge of the screen) sometimes not aligning correctly when running with pytest-xdist ``-n``. + + + +Improved documentation +---------------------- + +- `#12153 `_: Documented using :envvar:`PYTEST_VERSION` to detect if code is running from within a pytest run. + + +- `#12469 `_: The external plugin mentions in the documentation now avoid mentioning + :std:doc:`setuptools entry-points ` as the concept is + much more generic nowadays. Instead, the terminology of "external", + "installed", or "third-party" plugins (or packages) replaces that. + + -- by :user:`webknjaz` + + +- `#12577 `_: `CI` and `BUILD_NUMBER` environment variables role is discribed in + the reference doc. They now also appears when doing `pytest -h` + -- by :user:`MarcBresson`. + + + +Contributor-facing changes +-------------------------- + +- `#11771 `_, `#12557 `_: The PyPy runtime version has been updated to 3.9 from 3.8 that introduced + a flaky bug at the garbage collector which was not expected to fix there + as the V3.8 is EoL. + + -- by :user:`x612skm` + + +- `#12493 `_: The change log draft preview integration has been refactored to use a + third party extension ``sphinxcontib-towncrier``. The previous in-repo + script was putting the change log preview file at + :file:`doc/en/_changelog_towncrier_draft.rst`. Said file is no longer + ignored in Git and might show up among untracked files in the + development environments of the contributors. To address that, the + contributors can run the following command that will clean it up: + + .. code-block:: console + + $ git clean -x -i -- doc/en/_changelog_towncrier_draft.rst + + -- by :user:`webknjaz` + + +- `#12498 `_: All the undocumented ``tox`` environments now have descriptions. + They can be listed in one's development environment by invoking + ``tox -av`` in a terminal. + + -- by :user:`webknjaz` + + +- `#12501 `_: The changelog configuration has been updated to introduce more accurate + audience-tailored categories. Previously, there was a ``trivial`` + change log fragment type with an unclear and broad meaning. It was + removed and we now have ``contrib``, ``misc`` and ``packaging`` in + place of it. + + The new change note types target the readers who are downstream + packagers and project contributors. Additionally, the miscellaneous + section is kept for unspecified updates that do not fit anywhere else. + + -- by :user:`webknjaz` + + +- `#12502 `_: The UX of the GitHub automation making pull requests to update the + plugin list has been updated. Previously, the maintainers had to close + the automatically created pull requests and re-open them to trigger the + CI runs. From now on, they only need to click the `Ready for review` + button instead. + + -- by :user:`webknjaz` + + +- `#12522 `_: The ``:pull:`` RST role has been replaced with a shorter + ``:pr:`` due to starting to use the implementation from + the third-party :pypi:`sphinx-issues` Sphinx extension + -- by :user:`webknjaz`. + + +- `#12531 `_: The coverage reporting configuration has been updated to exclude + pytest's own tests marked as expected to fail from the coverage + report. This has an effect of reducing the influence of flaky + tests on the resulting number. + + -- by :user:`webknjaz` + + +- `#12533 `_: The ``extlinks`` Sphinx extension is no longer enabled. The ``:bpo:`` + role it used to declare has been removed with that. BPO itself has + migrated to GitHub some years ago and it is possible to link the + respective issues by using their GitHub issue numbers and the + ``:issue:`` role that the ``sphinx-issues`` extension implements. + + -- by :user:`webknjaz` + + +- `#12562 `_: Possible typos in using the ``:user:`` RST role is now being linted + through the pre-commit tool integration -- by :user:`webknjaz`. + + pytest 8.2.2 (2024-06-04) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index d540bf08337..42dd1e2e416 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -162,7 +162,7 @@ objects, they are still using the default pytest representation: rootdir: /home/sweet/project collected 8 items - + @@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia rootdir: /home/sweet/project collected 4 items - + @@ -318,7 +318,7 @@ Let's first see how it looks like at collection time: rootdir: /home/sweet/project collected 2 items - + @@ -505,8 +505,8 @@ Running it results in some skips if we don't have all the python interpreters in . $ pytest -rs -q multipython.py ssssssssssss...ssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [12] multipython.py:65: 'python3.9' not found - SKIPPED [12] multipython.py:65: 'python3.11' not found + SKIPPED [12] multipython.py:67: 'python3.9' not found + SKIPPED [12] multipython.py:67: 'python3.11' not found 3 passed, 24 skipped in 0.12s Parametrization of optional implementations/imports diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 39b799ed934..2c8dbbb10d2 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -152,7 +152,7 @@ The test collection would look like this: configfile: pytest.ini collected 2 items - + @@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this: configfile: pytest.ini collected 3 items - + diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 2c34cc2b00d..0da58d0490e 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -25,7 +25,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert param1 * 2 < param2 E assert (3 * 2) < 6 - failure_demo.py:19: AssertionError + failure_demo.py:21: AssertionError _________________________ TestFailing.test_simple __________________________ self = @@ -42,7 +42,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 42 = .f at 0xdeadbeef0002>() E + and 43 = .g at 0xdeadbeef0003>() - failure_demo.py:30: AssertionError + failure_demo.py:32: AssertionError ____________________ TestFailing.test_simple_multiline _____________________ self = @@ -50,7 +50,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_simple_multiline(self): > otherfunc_multi(42, 6 * 9) - failure_demo.py:33: + failure_demo.py:35: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ a = 42, b = 54 @@ -59,7 +59,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert a == b E assert 42 == 54 - failure_demo.py:14: AssertionError + failure_demo.py:16: AssertionError ___________________________ TestFailing.test_not ___________________________ self = @@ -72,7 +72,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert not 42 E + where 42 = .f at 0xdeadbeef0006>() - failure_demo.py:39: AssertionError + failure_demo.py:41: AssertionError _________________ TestSpecialisedExplanations.test_eq_text _________________ self = @@ -84,7 +84,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E - eggs E + spam - failure_demo.py:44: AssertionError + failure_demo.py:46: AssertionError _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ self = @@ -98,7 +98,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + foo 1 bar E ? ^ - failure_demo.py:47: AssertionError + failure_demo.py:49: AssertionError ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ self = @@ -112,7 +112,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + spam E bar - failure_demo.py:50: AssertionError + failure_demo.py:52: AssertionError ______________ TestSpecialisedExplanations.test_eq_long_text _______________ self = @@ -130,7 +130,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + 1111111111a222222222 E ? ^ - failure_demo.py:55: AssertionError + failure_demo.py:57: AssertionError _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ self = @@ -150,7 +150,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (7 lines hidden), use '-vv' to show - failure_demo.py:60: AssertionError + failure_demo.py:62: AssertionError _________________ TestSpecialisedExplanations.test_eq_list _________________ self = @@ -162,7 +162,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E At index 2 diff: 2 != 3 E Use -v to get more diff - failure_demo.py:63: AssertionError + failure_demo.py:65: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ self = @@ -176,7 +176,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E At index 100 diff: 1 != 2 E Use -v to get more diff - failure_demo.py:68: AssertionError + failure_demo.py:70: AssertionError _________________ TestSpecialisedExplanations.test_eq_dict _________________ self = @@ -194,7 +194,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E {'d': 0} E Use -v to get more diff - failure_demo.py:71: AssertionError + failure_demo.py:73: AssertionError _________________ TestSpecialisedExplanations.test_eq_set __________________ self = @@ -212,7 +212,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E 21 E Use -v to get more diff - failure_demo.py:74: AssertionError + failure_demo.py:76: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ self = @@ -224,7 +224,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E Right contains one more item: 3 E Use -v to get more diff - failure_demo.py:77: AssertionError + failure_demo.py:79: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ self = @@ -233,7 +233,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert 1 in [0, 2, 3, 4, 5] E assert 1 in [0, 2, 3, 4, 5] - failure_demo.py:80: AssertionError + failure_demo.py:82: AssertionError __________ TestSpecialisedExplanations.test_not_in_text_multiline __________ self = @@ -252,7 +252,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E and a E tail - failure_demo.py:84: AssertionError + failure_demo.py:86: AssertionError ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ self = @@ -266,7 +266,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E single foo line E ? +++ - failure_demo.py:88: AssertionError + failure_demo.py:90: AssertionError _________ TestSpecialisedExplanations.test_not_in_text_single_long _________ self = @@ -280,7 +280,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? +++ - failure_demo.py:92: AssertionError + failure_demo.py:94: AssertionError ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______ self = @@ -294,7 +294,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - failure_demo.py:96: AssertionError + failure_demo.py:98: AssertionError ______________ TestSpecialisedExplanations.test_eq_dataclass _______________ self = @@ -321,7 +321,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E - c E + b - failure_demo.py:108: AssertionError + failure_demo.py:110: AssertionError ________________ TestSpecialisedExplanations.test_eq_attrs _________________ self = @@ -348,7 +348,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E - c E + b - failure_demo.py:120: AssertionError + failure_demo.py:122: AssertionError ______________________________ test_attribute ______________________________ def test_attribute(): @@ -360,7 +360,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .Foo object at 0xdeadbeef0018>.b - failure_demo.py:128: AssertionError + failure_demo.py:130: AssertionError _________________________ test_attribute_instance __________________________ def test_attribute_instance(): @@ -372,7 +372,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 1 = .Foo object at 0xdeadbeef0019>.b E + where .Foo object at 0xdeadbeef0019> = .Foo'>() - failure_demo.py:135: AssertionError + failure_demo.py:137: AssertionError __________________________ test_attribute_failure __________________________ def test_attribute_failure(): @@ -385,7 +385,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: i = Foo() > assert i.b == 2 - failure_demo.py:146: + failure_demo.py:148: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = .Foo object at 0xdeadbeef001a> @@ -394,7 +394,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raise Exception("Failed to get attrib") E Exception: Failed to get attrib - failure_demo.py:141: Exception + failure_demo.py:143: Exception _________________________ test_attribute_multiple __________________________ def test_attribute_multiple(): @@ -411,7 +411,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + and 2 = .Bar object at 0xdeadbeef001c>.b E + where .Bar object at 0xdeadbeef001c> = .Bar'>() - failure_demo.py:156: AssertionError + failure_demo.py:158: AssertionError __________________________ TestRaises.test_raises __________________________ self = @@ -421,7 +421,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raises(TypeError, int, s) E ValueError: invalid literal for int() with base 10: 'qwe' - failure_demo.py:166: ValueError + failure_demo.py:168: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ self = @@ -430,7 +430,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raises(OSError, int, "3") E Failed: DID NOT RAISE - failure_demo.py:169: Failed + failure_demo.py:171: Failed __________________________ TestRaises.test_raise ___________________________ self = @@ -439,7 +439,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raise ValueError("demo error") E ValueError: demo error - failure_demo.py:172: ValueError + failure_demo.py:174: ValueError ________________________ TestRaises.test_tupleerror ________________________ self = @@ -448,7 +448,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = [1] # noqa: F841 E ValueError: not enough values to unpack (expected 2, got 1) - failure_demo.py:175: ValueError + failure_demo.py:177: ValueError ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ self = @@ -459,7 +459,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items.pop() E TypeError: cannot unpack non-iterable int object - failure_demo.py:180: TypeError + failure_demo.py:182: TypeError --------------------------- Captured stdout call --------------------------- items is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ @@ -470,7 +470,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > if namenotexi: # noqa: F821 E NameError: name 'namenotexi' is not defined - failure_demo.py:183: NameError + failure_demo.py:185: NameError ____________________ test_dynamic_compile_shows_nicely _____________________ def test_dynamic_compile_shows_nicely(): @@ -486,7 +486,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: sys.modules[name] = module > module.foo() - failure_demo.py:202: + failure_demo.py:204: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ??? @@ -506,9 +506,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: > somefunc(f(), g()) - failure_demo.py:213: + failure_demo.py:215: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - failure_demo.py:10: in somefunc + failure_demo.py:12: in somefunc otherfunc(x, y) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @@ -518,7 +518,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert a == b E assert 44 == 43 - failure_demo.py:6: AssertionError + failure_demo.py:8: AssertionError ___________________ TestMoreErrors.test_z1_unpack_error ____________________ self = @@ -528,7 +528,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E ValueError: not enough values to unpack (expected 2, got 0) - failure_demo.py:217: ValueError + failure_demo.py:219: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ self = @@ -538,7 +538,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E TypeError: cannot unpack non-iterable int object - failure_demo.py:221: TypeError + failure_demo.py:223: TypeError ______________________ TestMoreErrors.test_startswith ______________________ self = @@ -551,7 +551,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = ('456') E + where = '123'.startswith - failure_demo.py:226: AssertionError + failure_demo.py:228: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ self = @@ -570,7 +570,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where '123' = .f at 0xdeadbeef0029>() E + and '456' = .g at 0xdeadbeef002a>() - failure_demo.py:235: AssertionError + failure_demo.py:237: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ self = @@ -581,7 +581,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = isinstance(43, float) E + where 43 = globf(42) - failure_demo.py:238: AssertionError + failure_demo.py:240: AssertionError _______________________ TestMoreErrors.test_instance _______________________ self = @@ -592,7 +592,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 42 != 42 E + where 42 = .x - failure_demo.py:242: AssertionError + failure_demo.py:244: AssertionError _______________________ TestMoreErrors.test_compare ________________________ self = @@ -602,7 +602,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 11 < 5 E + where 11 = globf(10) - failure_demo.py:245: AssertionError + failure_demo.py:247: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ self = @@ -613,7 +613,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert x == 0 E assert 1 == 0 - failure_demo.py:250: AssertionError + failure_demo.py:252: AssertionError ___________________ TestCustomAssertMsg.test_single_line ___________________ self = @@ -628,7 +628,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:261: AssertionError + failure_demo.py:263: AssertionError ____________________ TestCustomAssertMsg.test_multiline ____________________ self = @@ -647,7 +647,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:268: AssertionError + failure_demo.py:270: AssertionError ___________________ TestCustomAssertMsg.test_custom_repr ___________________ self = @@ -669,7 +669,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a - failure_demo.py:281: AssertionError + failure_demo.py:283: AssertionError ========================= short test summary info ========================== FAILED failure_demo.py::test_generative[3-6] - assert (3 * 2) < 6 FAILED failure_demo.py::TestFailing::test_simple - assert 42 == 43 diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index d4ace3f0413..a5e2e78c397 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -645,31 +645,6 @@ If we run this: E assert 0 test_step.py:11: AssertionError - ================================ XFAILURES ================================= - ______________________ TestUserHandling.test_deletion ______________________ - - item = - - def pytest_runtest_setup(item): - if "incremental" in item.keywords: - # retrieve the class name of the test - cls_name = str(item.cls) - # check if a previous test has failed for this class - if cls_name in _test_failed_incremental: - # retrieve the index of the test (if parametrize is used in combination with incremental) - parametrize_index = ( - tuple(item.callspec.indices.values()) - if hasattr(item, "callspec") - else () - ) - # retrieve the name of the first test function to fail for this class name and index - test_name = _test_failed_incremental[cls_name].get(parametrize_index, None) - # if name found, test has failed for the combination of class name & test name - if test_name is not None: - > pytest.xfail(f"previous test failed ({test_name})") - E _pytest.outcomes.XFailed: previous test failed (test_modification) - - conftest.py:47: XFailed ========================= short test summary info ========================== XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification) ================== 1 failed, 2 passed, 1 xfailed in 0.12s ================== diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 85bee729ba1..c8efdeaf745 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 8.2.2 + pytest 8.3.0 .. _`simpletest`: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index ecd297867c5..2dc37f7c061 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used: rootdir: /home/sweet/project collected 12 items - + diff --git a/doc/en/how-to/output.rst b/doc/en/how-to/output.rst index 4994ad1af69..8b15f95f0fd 100644 --- a/doc/en/how-to/output.rst +++ b/doc/en/how-to/output.rst @@ -444,14 +444,6 @@ Example: E assert 0 test_example.py:14: AssertionError - ================================ XFAILURES ================================= - ________________________________ test_xfail ________________________________ - - def test_xfail(): - > pytest.xfail("xfailing this test") - E _pytest.outcomes.XFailed: xfailing this test - - test_example.py:26: XFailed ================================= XPASSES ================================== ========================= short test summary info ========================== SKIPPED [1] test_example.py:22: skipping this test diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index d1222728e13..099c8a00260 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1939,18 +1939,19 @@ All the command-line flags can be obtained by running ``pytest --help``:: general: -k EXPRESSION Only run tests which match the given substring expression. An expression is a Python evaluable - expression where all names are substring-matched against - test names and their parent classes. Example: -k - 'test_method or test_other' matches all test functions - and classes whose name contains 'test_method' or - 'test_other', while -k 'not test_method' matches those - that don't contain 'test_method' in their names. -k 'not - test_method and not test_other' will eliminate the - matches. Additionally keywords are matched to classes - and functions containing extra names in their - 'extra_keyword_matches' set, as well as functions which - have names assigned directly to them. The matching is - case-insensitive. + expression where all names are substring-matched + against test names and their parent classes. + Example: -k 'test_method or test_other' matches all + test functions and classes whose name contains + 'test_method' or 'test_other', while -k 'not + test_method' matches those that don't contain + 'test_method' in their names. -k 'not test_method + and not test_other' will eliminate the matches. + Additionally keywords are matched to classes and + functions containing extra names in their + 'extra_keyword_matches' set, as well as functions + which have names assigned directly to them. The + matching is case-insensitive. -m MARKEXPR Only run tests matching given mark expression. For example: -m 'mark1 and not mark2'. --markers show markers (builtin, plugin and per-project ones). @@ -1968,28 +1969,28 @@ All the command-line flags can be obtained by running ``pytest --help``:: --trace Immediately break when running each test --capture=method Per-test capturing method: one of fd|sys|no|tee-sys -s Shortcut for --capture=no - --runxfail Report the results of xfail tests as if they were not - marked - --lf, --last-failed Rerun only the tests that failed at the last run (or all - if none failed) - --ff, --failed-first Run all tests, but run the last failures first. This may - re-order tests and thus lead to repeated fixture + --runxfail Report the results of xfail tests as if they were + not marked + --lf, --last-failed Rerun only the tests that failed at the last run (or + all if none failed) + --ff, --failed-first Run all tests, but run the last failures first. This + may re-order tests and thus lead to repeated fixture setup/teardown. --nf, --new-first Run tests from new files first, then the rest of the tests sorted by file mtime --cache-show=[CACHESHOW] - Show cache contents, don't perform collection or tests. - Optional argument: glob (default: '*'). + Show cache contents, don't perform collection or + tests. Optional argument: glob (default: '*'). --cache-clear Remove all cache contents at start of test run --lfnf={all,none}, --last-failed-no-failures={all,none} - With ``--lf``, determines whether to execute tests when - there are no previously (known) failures or when no - cached ``lastfailed`` data was found. ``all`` (the - default) runs the full test suite again. ``none`` just - emits a message about no known failures and exits - successfully. - --sw, --stepwise Exit on test failure and continue from last failing test - next time + With ``--lf``, determines whether to execute tests + when there are no previously (known) failures or + when no cached ``lastfailed`` data was found. + ``all`` (the default) runs the full test suite + again. ``none`` just emits a message about no known + failures and exits successfully. + --sw, --stepwise Exit on test failure and continue from last failing + test next time --sw-skip, --stepwise-skip Ignore the first failing test but stop on the next failing test. Implicitly enables --stepwise. @@ -2001,20 +2002,22 @@ All the command-line flags can be obtained by running ``pytest --help``:: -v, --verbose Increase verbosity --no-header Disable header --no-summary Disable summary + --no-fold-skipped Do not fold skipped tests in short summary. -q, --quiet Decrease verbosity --verbosity=VERBOSE Set verbosity. Default: 0. -r chars Show extra test summary info as specified by chars: (f)ailed, (E)rror, (s)kipped, (x)failed, (X)passed, (p)assed, (P)assed with output, (a)ll except passed - (p/P), or (A)ll. (w)arnings are enabled by default (see - --disable-warnings), 'N' can be used to reset the list. - (default: 'fE'). + (p/P), or (A)ll. (w)arnings are enabled by default + (see --disable-warnings), 'N' can be used to reset + the list. (default: 'fE'). --disable-warnings, --disable-pytest-warnings Disable warnings summary -l, --showlocals Show locals in tracebacks (disabled by default) - --no-showlocals Hide locals in tracebacks (negate --showlocals passed - through addopts) - --tb=style Traceback print mode (auto/long/short/line/native/no) + --no-showlocals Hide locals in tracebacks (negate --showlocals + passed through addopts) + --tb=style Traceback print mode + (auto/long/short/line/native/no) --xfail-tb Show tracebacks for xfail (as long as --tb != no) --show-capture={no,stdout,stderr,log,all} Controls how captured stdout/stderr/log is shown on @@ -2022,37 +2025,39 @@ All the command-line flags can be obtained by running ``pytest --help``:: --full-trace Don't cut any tracebacks (default is to cut) --color=color Color terminal output (yes/no/auto) --code-highlight={yes,no} - Whether code should be highlighted (only if --color is - also enabled). Default: yes. + Whether code should be highlighted (only if --color + is also enabled). Default: yes. --pastebin=mode Send failed|all info to bpaste.net pastebin service --junit-xml=path Create junit-xml style report file at given path --junit-prefix=str Prepend prefix to classnames in junit-xml output pytest-warnings: -W PYTHONWARNINGS, --pythonwarnings=PYTHONWARNINGS - Set which warnings to report, see -W option of Python - itself + Set which warnings to report, see -W option of + Python itself --maxfail=num Exit after first num failures or errors --strict-config Any warnings encountered while parsing the `pytest` section of the configuration file raise errors - --strict-markers Markers not registered in the `markers` section of the - configuration file raise errors + --strict-markers Markers not registered in the `markers` section of + the configuration file raise errors --strict (Deprecated) alias to --strict-markers -c FILE, --config-file=FILE Load configuration from `FILE` instead of trying to locate one of the implicit configuration files. --continue-on-collection-errors Force test execution even if collection errors occur - --rootdir=ROOTDIR Define root directory for tests. Can be relative path: - 'root_dir', './root_dir', 'root_dir/another_dir/'; - absolute path: '/home/user/root_dir'; path with - variables: '$HOME/root_dir'. + --rootdir=ROOTDIR Define root directory for tests. Can be relative + path: 'root_dir', './root_dir', + 'root_dir/another_dir/'; absolute path: + '/home/user/root_dir'; path with variables: + '$HOME/root_dir'. collection: --collect-only, --co Only collect tests, don't execute them --pyargs Try to interpret all arguments as Python packages --ignore=path Ignore path during collection (multi-allowed) - --ignore-glob=path Ignore path pattern during collection (multi-allowed) + --ignore-glob=path Ignore path pattern during collection (multi- + allowed) --deselect=nodeid_prefix Deselect item (via node id prefix) during collection (multi-allowed) @@ -2062,8 +2067,8 @@ All the command-line flags can be obtained by running ``pytest --help``:: --collect-in-virtualenv Don't ignore tests in a local virtualenv directory --import-mode={prepend,append,importlib} - Prepend/append to sys.path when importing test modules - and conftest files. Default: prepend. + Prepend/append to sys.path when importing test + modules and conftest files. Default: prepend. --doctest-modules Run doctests in all .py modules --doctest-report={none,cdiff,ndiff,udiff,only_first_failure} Choose another output format for diffs on doctest @@ -2076,37 +2081,38 @@ All the command-line flags can be obtained by running ``pytest --help``:: failure test session debugging and configuration: - --basetemp=dir Base temporary directory for this test run. (Warning: - this directory is removed if it exists.) - -V, --version Display pytest version and information about plugins. - When given twice, also display information about - plugins. + --basetemp=dir Base temporary directory for this test run. + (Warning: this directory is removed if it exists.) + -V, --version Display pytest version and information about + plugins. When given twice, also display information + about plugins. -h, --help Show help message and configuration info -p name Early-load given plugin module name or entry point - (multi-allowed). To avoid loading of plugins, use the - `no:` prefix, e.g. `no:doctest`. + (multi-allowed). To avoid loading of plugins, use + the `no:` prefix, e.g. `no:doctest`. --trace-config Trace considerations of conftest.py files --debug=[DEBUG_FILE_NAME] Store internal tracing debug information in this log - file. This file is opened with 'w' and truncated as a - result, care advised. Default: pytestdebug.log. + file. This file is opened with 'w' and truncated as + a result, care advised. Default: pytestdebug.log. -o OVERRIDE_INI, --override-ini=OVERRIDE_INI - Override ini option with "option=value" style, e.g. `-o - xfail_strict=True -o cache_dir=cache`. + Override ini option with "option=value" style, e.g. + `-o xfail_strict=True -o cache_dir=cache`. --assert=MODE Control assertion debugging tools. 'plain' performs no assertion debugging. - 'rewrite' (the default) rewrites assert statements in - test modules on import to provide assert expression - information. + 'rewrite' (the default) rewrites assert statements + in test modules on import to provide assert + expression information. --setup-only Only setup fixtures, do not execute tests --setup-show Show setup of fixtures while executing tests - --setup-plan Show what fixtures and tests would be executed but don't - execute anything + --setup-plan Show what fixtures and tests would be executed but + don't execute anything logging: - --log-level=LEVEL Level of messages to catch/display. Not set by default, - so it depends on the root/parent log handler's effective - level, where it is "WARNING" by default. + --log-level=LEVEL Level of messages to catch/display. Not set by + default, so it depends on the root/parent log + handler's effective level, where it is "WARNING" by + default. --log-format=LOG_FORMAT Log format used by the logging module --log-date-format=LOG_DATE_FORMAT @@ -2130,13 +2136,8 @@ All the command-line flags can be obtained by running ``pytest --help``:: Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer. --log-disable=LOGGER_DISABLE - Disable a logger by name. Can be passed multiple times. - - Custom options: - --lsof Run FD checks if lsof is available - --runpytest={inprocess,subprocess} - Run pytest sub runs in tests using an 'inprocess' or - 'subprocess' (python -m main) method + Disable a logger by name. Can be passed multiple + times. [pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found: @@ -2151,33 +2152,37 @@ All the command-line flags can be obtained by running ``pytest --help``:: warnings.filterwarnings. Processed after -W/--pythonwarnings. consider_namespace_packages (bool): - Consider namespace packages when resolving module names - during import - usefixtures (args): List of default fixtures to be used with this project + Consider namespace packages when resolving module + names during import + usefixtures (args): List of default fixtures to be used with this + project python_files (args): Glob-style file patterns for Python test module discovery python_classes (args): - Prefixes or glob names for Python test class discovery + Prefixes or glob names for Python test class + discovery python_functions (args): Prefixes or glob names for Python test function and method discovery disable_test_id_escaping_and_forfeit_all_rights_to_community_support (bool): - Disable string escape non-ASCII characters, might cause - unwanted side effects(use at your own risk) + Disable string escape non-ASCII characters, might + cause unwanted side effects(use at your own risk) console_output_style (string): - Console output: "classic", or with additional progress - information ("progress" (percentage) | "count" | - "progress-even-when-capture-no" (forces progress even - when capture=no) + Console output: "classic", or with additional + progress information ("progress" (percentage) | + "count" | "progress-even-when-capture-no" (forces + progress even when capture=no) verbosity_test_cases (string): Specify a verbosity level for test case execution, - overriding the main level. Higher levels will provide - more detailed information about each test case executed. - xfail_strict (bool): Default for the strict parameter of xfail markers when - not given explicitly (default: False) + overriding the main level. Higher levels will + provide more detailed information about each test + case executed. + xfail_strict (bool): Default for the strict parameter of xfail markers + when not given explicitly (default: False) tmp_path_retention_count (string): How many sessions should we keep the `tmp_path` - directories, according to `tmp_path_retention_policy`. + directories, according to + `tmp_path_retention_policy`. tmp_path_retention_policy (string): Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. @@ -2186,9 +2191,9 @@ All the command-line flags can be obtained by running ``pytest --help``:: Enables the pytest_assertion_pass hook. Make sure to delete any previously generated pyc cache files. verbosity_assertions (string): - Specify a verbosity level for assertions, overriding the - main level. Higher levels will provide more detailed - explanation when an assertion fails. + Specify a verbosity level for assertions, overriding + the main level. Higher levels will provide more + detailed explanation when an assertion fails. junit_suite_name (string): Test suite name for JUnit report junit_logging (string): @@ -2210,8 +2215,8 @@ All the command-line flags can be obtained by running ``pytest --help``:: log_format (string): Default value for --log-format log_date_format (string): Default value for --log-date-format - log_cli (bool): Enable log display during test run (also known as "live - logging") + log_cli (bool): Enable log display during test run (also known as + "live logging") log_cli_level (string): Default value for --log-cli-level log_cli_format (string): @@ -2231,18 +2236,16 @@ All the command-line flags can be obtained by running ``pytest --help``:: Default value for --log-auto-indent pythonpath (paths): Add paths to sys.path faulthandler_timeout (string): - Dump the traceback of all threads if a test takes more - than TIMEOUT seconds to finish + Dump the traceback of all threads if a test takes + more than TIMEOUT seconds to finish addopts (args): Extra command line options minversion (string): Minimally required pytest version required_plugins (args): Plugins that must be present for pytest to run - pytester_example_dir (string): - Directory to take the pytester example files from Environment variables: CI When set (regardless of value), pytest knows it is running in a CI process and does not truncate summary info - BUILD_NUMBER equivalent to CI + BUILD_NUMBER Equivalent to CI PYTEST_ADDOPTS Extra command line options PYTEST_PLUGINS Comma-separated plugins to load during startup PYTEST_DISABLE_PLUGIN_AUTOLOAD Set to disable plugin auto-loading From 664325bc9fd90217f51fe7cc0e9fff2f29a41a15 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 20 Jul 2024 18:47:40 +0300 Subject: [PATCH 02/78] doc/changelog: update 8.3.0 notes --- doc/en/changelog.rst | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 12302ffe0e4..e20de19ba31 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -39,7 +39,7 @@ New features - `#12231 `_: Added `--xfail-tb` flag, which turns on traceback output for XFAIL results. - * If the `--xfail-tb` flag is not sent, tracebacks for XFAIL results are NOT shown. + * If the `--xfail-tb` flag is not given, tracebacks for XFAIL results are NOT shown. * The style of traceback for XFAIL is set with `--tb`, and can be `auto|long|short|line|native|no`. * Note: Even if you have `--xfail-tb` set, you won't see them if `--tb=no`. @@ -60,10 +60,10 @@ New features -- by :user:`lovetheguitar` -- `#12567 `_: Added ``--no-fold-skipped`` command line option +- `#12567 `_: Added ``--no-fold-skipped`` command line option. If this option is set, then skipped tests in short summary are no longer grouped - by reason but all tests are printed individually with correct nodeid in the same + by reason but all tests are printed individually with their nodeid in the same way as other statuses. -- by :user:`pbrezina` @@ -73,18 +73,13 @@ New features Improvements in existing functionality -------------------------------------- -- `#12467 `_: Migrated all internal type-annotations to the python3.10+ style by using the `annotations` future import. - - -- by :user:`RonnyPfannschmidt` - - - `#12469 `_: The console output now uses the "third-party plugins" terminology, replacing the previously established but confusing and outdated reference to :std:doc:`setuptools ` -- by :user:`webknjaz`. -- `#12544 `_, `#12545 `_: The ``_in_venv()`` function now detects Python virtual environments by +- `#12544 `_, `#12545 `_: Python virtual environment detection was improved by checking for a :file:`pyvenv.cfg` file, ensuring reliable detection on various platforms -- by :user:`zachsnickers`. @@ -139,7 +134,7 @@ Improvements in existing functionality Bug fixes --------- -- `#11706 `_: Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`. +- `#11706 `_: Fixed reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`. Originally added in pytest 8.0.0, but reverted in 8.0.2 due to a regression in pytest-xdist. This regression was fixed in pytest-xdist 3.6.1. @@ -161,19 +156,19 @@ Bug fixes instead of ``exc``. -- `#12275 `_: Fix collection error upon encountering an :mod:`abstract ` class, including abstract `unittest.TestCase` subclasses. +- `#12275 `_: Fixed collection error upon encountering an :mod:`abstract ` class, including abstract `unittest.TestCase` subclasses. -- `#12328 `_: Fix a regression in pytest 8.0.0 where package-scoped parameterized items were not correctly reordered to minimize setups/teardowns in some cases. +- `#12328 `_: Fixed a regression in pytest 8.0.0 where package-scoped parameterized items were not correctly reordered to minimize setups/teardowns in some cases. -- `#12424 `_: Fix crash with `assert testcase is not None` assertion failure when re-running unittest tests using plugins like pytest-rerunfailures. Regressed in 8.2.2. +- `#12424 `_: Fixed crash with `assert testcase is not None` assertion failure when re-running unittest tests using plugins like pytest-rerunfailures. Regressed in 8.2.2. - `#12472 `_: Fixed a crash when returning category ``"error"`` or ``"failed"`` with a custom test status from :hook:`pytest_report_teststatus` hook -- :user:`pbrezina`. -- `#12505 `_: Improve handling of invalid regex patterns in :func:`pytest.raises(match=r'...') ` by providing a clear error message. +- `#12505 `_: Improved handling of invalid regex patterns in :func:`pytest.raises(match=r'...') ` by providing a clear error message. - `#12580 `_: Fixed a crash when using the cache class on Windows and the cache directory was created concurrently. @@ -202,7 +197,7 @@ Improved documentation - `#12577 `_: `CI` and `BUILD_NUMBER` environment variables role is discribed in - the reference doc. They now also appears when doing `pytest -h` + the reference doc. They now also appear when doing `pytest -h` -- by :user:`MarcBresson`. @@ -210,9 +205,14 @@ Improved documentation Contributor-facing changes -------------------------- +- `#12467 `_: Migrated all internal type-annotations to the python3.10+ style by using the `annotations` future import. + + -- by :user:`RonnyPfannschmidt` + + - `#11771 `_, `#12557 `_: The PyPy runtime version has been updated to 3.9 from 3.8 that introduced a flaky bug at the garbage collector which was not expected to fix there - as the V3.8 is EoL. + as the 3.8 is EoL. -- by :user:`x612skm` From de98446075cc07c768387cf64ba497dd75c205de Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sat, 20 Jul 2024 16:05:52 +0000 Subject: [PATCH 03/78] Prepare release version 8.3.1 --- doc/en/announce/index.rst | 1 + doc/en/announce/release-8.3.1.rst | 19 +++++++++++++++++++ doc/en/changelog.rst | 6 ++++++ doc/en/example/parametrize.rst | 6 +++--- doc/en/example/pythoncollection.rst | 4 ++-- doc/en/getting-started.rst | 2 +- doc/en/how-to/fixtures.rst | 2 +- 7 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 doc/en/announce/release-8.3.1.rst diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index aa5dd8c87b4..429cb8249a1 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-8.3.1 release-8.3.0 release-8.2.2 release-8.2.1 diff --git a/doc/en/announce/release-8.3.1.rst b/doc/en/announce/release-8.3.1.rst new file mode 100644 index 00000000000..0fb9b40d9c7 --- /dev/null +++ b/doc/en/announce/release-8.3.1.rst @@ -0,0 +1,19 @@ +pytest-8.3.1 +======================================= + +pytest 8.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: + +* Bruno Oliveira +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index e20de19ba31..f6aefbd3b13 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -31,6 +31,12 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 8.3.1 (2024-07-20) +========================= + +The 8.3.0 release failed to include the change notes and docs for the release. This patch release remedies this. There are no other changes. + + pytest 8.3.0 (2024-07-20) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 42dd1e2e416..b5679ff6305 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -162,7 +162,7 @@ objects, they are still using the default pytest representation: rootdir: /home/sweet/project collected 8 items - + @@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia rootdir: /home/sweet/project collected 4 items - + @@ -318,7 +318,7 @@ Let's first see how it looks like at collection time: rootdir: /home/sweet/project collected 2 items - + diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 2c8dbbb10d2..9705ccf94f8 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -152,7 +152,7 @@ The test collection would look like this: configfile: pytest.ini collected 2 items - + @@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this: configfile: pytest.ini collected 3 items - + diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index c8efdeaf745..b519656a182 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 8.3.0 + pytest 8.3.1 .. _`simpletest`: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index 2dc37f7c061..5c66c348eca 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used: rootdir: /home/sweet/project collected 12 items - + From 31337aba02a7698a87a6792eacf887fceff08af2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 21 Jul 2024 12:44:23 +0300 Subject: [PATCH 04/78] Merge pull request #12640 from pytest-dev/update-user doc/changelog: update user tag (cherry picked from commit c03989cee87a0c24ea61f2e322bea71433ee3ff4) --- doc/en/changelog.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index f6aefbd3b13..083d30abf86 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -55,6 +55,8 @@ New features This change detaches xfail tracebacks from `-rx`, and now we turn on xfail tracebacks with `--xfail-tb`. With this, the default `-rx`/ `-ra` behavior is identical to pre-8.0 with respect to xfail tracebacks. While this is a behavior change, it brings default behavior back to pre-8.0.0 behavior, which ultimately was considered the better course of action. + -- by :user:`okken` + - `#12281 `_: Added support for keyword matching in marker expressions. From 238bad2d2a1f1dbb47d740c84b5b4f5224f8a965 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Wed, 24 Jul 2024 08:09:28 +0200 Subject: [PATCH 05/78] Merge pull request #12656 from RonnyPfannschmidt/fix-12652-detect-conda-env (cherry picked from commit 6c806b499ddbb844753b5c8c4d70a8b98b9d1c3a) --- changelog/12652.bugfix.rst | 3 +++ src/_pytest/main.py | 15 +++++++++++++-- testing/test_collection.py | 28 ++++++++++++++++++++-------- 3 files changed, 36 insertions(+), 10 deletions(-) create mode 100644 changelog/12652.bugfix.rst diff --git a/changelog/12652.bugfix.rst b/changelog/12652.bugfix.rst new file mode 100644 index 00000000000..da7644df06d --- /dev/null +++ b/changelog/12652.bugfix.rst @@ -0,0 +1,3 @@ +Resolve regression `conda` environments where no longer being automatically detected. + +-- by :user:`RonnyPfannschmidt` diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 8ec26906003..befc7ccce6e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -370,9 +370,20 @@ def pytest_runtestloop(session: Session) -> bool: def _in_venv(path: Path) -> bool: """Attempt to detect if ``path`` is the root of a Virtual Environment by checking for the existence of the pyvenv.cfg file. - [https://peps.python.org/pep-0405/]""" + + [https://peps.python.org/pep-0405/] + + For regression protection we also check for conda environments that do not include pyenv.cfg yet -- + https://github.com/conda/conda/issues/13337 is the conda issue tracking adding pyenv.cfg. + + Checking for the `conda-meta/history` file per https://github.com/pytest-dev/pytest/issues/12652#issuecomment-2246336902. + + """ try: - return path.joinpath("pyvenv.cfg").is_file() + return ( + path.joinpath("pyvenv.cfg").is_file() + or path.joinpath("conda-meta", "history").is_file() + ) except OSError: return False diff --git a/testing/test_collection.py b/testing/test_collection.py index f5822240335..aba8f8ea48d 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -3,6 +3,7 @@ import os from pathlib import Path +from pathlib import PurePath import pprint import shutil import sys @@ -152,8 +153,17 @@ def test_ignored_certain_directories(self, pytester: Pytester) -> None: assert "test_notfound" not in s assert "test_found" in s - def test_ignored_virtualenvs(self, pytester: Pytester) -> None: - ensure_file(pytester.path / "virtual" / "pyvenv.cfg") + known_environment_types = pytest.mark.parametrize( + "env_path", + [ + pytest.param(PurePath("pyvenv.cfg"), id="venv"), + pytest.param(PurePath("conda-meta", "history"), id="conda"), + ], + ) + + @known_environment_types + def test_ignored_virtualenvs(self, pytester: Pytester, env_path: PurePath) -> None: + ensure_file(pytester.path / "virtual" / env_path) testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py") testfile.write_text("def test_hello(): pass", encoding="utf-8") @@ -167,11 +177,12 @@ def test_ignored_virtualenvs(self, pytester: Pytester) -> None: result = pytester.runpytest("virtual") assert "test_invenv" in result.stdout.str() + @known_environment_types def test_ignored_virtualenvs_norecursedirs_precedence( - self, pytester: Pytester + self, pytester: Pytester, env_path ) -> None: # norecursedirs takes priority - ensure_file(pytester.path / ".virtual" / "pyvenv.cfg") + ensure_file(pytester.path / ".virtual" / env_path) testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py") testfile.write_text("def test_hello(): pass", encoding="utf-8") result = pytester.runpytest("--collect-in-virtualenv") @@ -180,13 +191,14 @@ def test_ignored_virtualenvs_norecursedirs_precedence( result = pytester.runpytest("--collect-in-virtualenv", ".virtual") assert "test_invenv" in result.stdout.str() - def test__in_venv(self, pytester: Pytester) -> None: + @known_environment_types + def test__in_venv(self, pytester: Pytester, env_path: PurePath) -> None: """Directly test the virtual env detection function""" - # no pyvenv.cfg, not a virtualenv + # no env path, not a env base_path = pytester.mkdir("venv") assert _in_venv(base_path) is False - # with pyvenv.cfg, totally a virtualenv - base_path.joinpath("pyvenv.cfg").touch() + # with env path, totally a env + ensure_file(base_path.joinpath(env_path)) assert _in_venv(base_path) is True def test_custom_norecursedirs(self, pytester: Pytester) -> None: From bbcec9c46509c417ef58e4849847b4aa43f4591e Mon Sep 17 00:00:00 2001 From: pytest bot Date: Wed, 24 Jul 2024 21:30:07 +0000 Subject: [PATCH 06/78] Prepare release version 8.3.2 --- changelog/12652.bugfix.rst | 3 --- doc/en/announce/index.rst | 1 + doc/en/announce/release-8.3.2.rst | 19 +++++++++++++++++++ doc/en/changelog.rst | 11 +++++++++++ doc/en/example/parametrize.rst | 6 +++--- doc/en/example/pythoncollection.rst | 4 ++-- doc/en/getting-started.rst | 2 +- doc/en/how-to/fixtures.rst | 2 +- 8 files changed, 38 insertions(+), 10 deletions(-) delete mode 100644 changelog/12652.bugfix.rst create mode 100644 doc/en/announce/release-8.3.2.rst diff --git a/changelog/12652.bugfix.rst b/changelog/12652.bugfix.rst deleted file mode 100644 index da7644df06d..00000000000 --- a/changelog/12652.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Resolve regression `conda` environments where no longer being automatically detected. - --- by :user:`RonnyPfannschmidt` diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 429cb8249a1..09311a1a1ab 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-8.3.2 release-8.3.1 release-8.3.0 release-8.2.2 diff --git a/doc/en/announce/release-8.3.2.rst b/doc/en/announce/release-8.3.2.rst new file mode 100644 index 00000000000..1e4a071692c --- /dev/null +++ b/doc/en/announce/release-8.3.2.rst @@ -0,0 +1,19 @@ +pytest-8.3.2 +======================================= + +pytest 8.3.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Ran Benita +* Ronny Pfannschmidt + + +Happy testing, +The pytest Development Team diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 083d30abf86..3178f82044d 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -31,6 +31,17 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 8.3.2 (2024-07-24) +========================= + +Bug fixes +--------- + +- `#12652 `_: Resolve regression `conda` environments where no longer being automatically detected. + + -- by :user:`RonnyPfannschmidt` + + pytest 8.3.1 (2024-07-20) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index b5679ff6305..3e449b2eaa2 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -162,7 +162,7 @@ objects, they are still using the default pytest representation: rootdir: /home/sweet/project collected 8 items - + @@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia rootdir: /home/sweet/project collected 4 items - + @@ -318,7 +318,7 @@ Let's first see how it looks like at collection time: rootdir: /home/sweet/project collected 2 items - + diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 9705ccf94f8..5bd03035c14 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -152,7 +152,7 @@ The test collection would look like this: configfile: pytest.ini collected 2 items - + @@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this: configfile: pytest.ini collected 3 items - + diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index b519656a182..050fd2d80ec 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 8.3.1 + pytest 8.3.2 .. _`simpletest`: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index 5c66c348eca..f4d59ff93c0 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used: rootdir: /home/sweet/project collected 12 items - + From c77a96dc4e1eb629b9b65dd7f8439f5102e1dee1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 31 Jul 2024 22:34:24 -0300 Subject: [PATCH 07/78] Merge pull request #12675 from nicoddemus/pin-towncrier Pin towncrier due to incompatibility with sphinxcontrib-towncrier (cherry picked from commit f0a0436dd0b2c2fa2a3adea0f4a7cb388ec54af9) --- doc/en/broken-dep-constraints.txt | 6 ++++++ doc/en/requirements.txt | 5 +---- 2 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 doc/en/broken-dep-constraints.txt diff --git a/doc/en/broken-dep-constraints.txt b/doc/en/broken-dep-constraints.txt new file mode 100644 index 00000000000..f7c3211a9a0 --- /dev/null +++ b/doc/en/broken-dep-constraints.txt @@ -0,0 +1,6 @@ +# This file contains transitive dependencies that need to be pinned for some reason. +# Eventually this file will be empty, but in this case keep it around for future use. + +# Pin towncrier temporarily due to incompatibility with sphinxcontrib-towncrier: +# https://github.com/sphinx-contrib/sphinxcontrib-towncrier/issues/92 +towncrier!=24.7.0,!=24.7.1 diff --git a/doc/en/requirements.txt b/doc/en/requirements.txt index 0637c967b8a..ddcb7efb99b 100644 --- a/doc/en/requirements.txt +++ b/doc/en/requirements.txt @@ -1,13 +1,10 @@ +-c broken-dep-constraints.txt pluggy>=1.5.0 pygments-pytest>=2.3.0 sphinx-removed-in>=0.2.0 sphinx>=7 sphinxcontrib-trio sphinxcontrib-svg2pdfconverter -# Pin packaging because it no longer handles 'latest' version, which -# is the version that is assigned to the docs. -# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045. -packaging furo sphinxcontrib-towncrier sphinx-issues From 32f995188cf4c7e398bfa734985e7309cd0a8626 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 3 Aug 2024 14:15:52 +0000 Subject: [PATCH 08/78] Merge pull request #12679 from RFCArrow/tmp_path_retention_policy-example (#12683) Remove quotes from example tmp_path_retention_policy (cherry picked from commit dc756f4117f7cbabbe0866bab1fe562ac287cc03) Co-authored-by: Bruno Oliveira --- AUTHORS | 1 + changelog/12678.doc.rst | 1 + doc/en/reference/reference.rst | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog/12678.doc.rst diff --git a/AUTHORS b/AUTHORS index 9b6cb6a9d23..8103a1d52a5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -351,6 +351,7 @@ Rafal Semik Raquel Alegre Ravi Chandra Reagan Lee +Rob Arrow Robert Holt Roberto Aldera Roberto Polli diff --git a/changelog/12678.doc.rst b/changelog/12678.doc.rst new file mode 100644 index 00000000000..9d79ec41007 --- /dev/null +++ b/changelog/12678.doc.rst @@ -0,0 +1 @@ +Remove erroneous quotes from `tmp_path_retention_policy` example in docs. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 099c8a00260..dbb5cc36192 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1866,7 +1866,7 @@ passed multiple times. The expected format is ``name=value``. For example:: .. code-block:: ini [pytest] - tmp_path_retention_policy = "all" + tmp_path_retention_policy = all Default: ``all`` From dcff37ae9e9b6760e0863d156493372978ce6512 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:58:40 +0000 Subject: [PATCH 09/78] Obey verbosity level when printing 'msg' part of assertions (#12662) (#12687) Seems like we just missed that case when more fine-grained verbosity levels were added. Fixes #6682, #12307 (cherry picked from commit bc1e17e90c5324797b4ebc5fd229d2662ad15e6e) Co-authored-by: Bruno Oliveira --- changelog/6682.bugfix.rst | 1 + src/_pytest/assertion/rewrite.py | 2 +- testing/test_assertrewrite.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 changelog/6682.bugfix.rst diff --git a/changelog/6682.bugfix.rst b/changelog/6682.bugfix.rst new file mode 100644 index 00000000000..7f756cbd9c2 --- /dev/null +++ b/changelog/6682.bugfix.rst @@ -0,0 +1 @@ +Fixed bug where the verbosity levels where not being respected when printing the "msg" part of failed assertion (as in ``assert condition, msg``). diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index bfcbcbd3f8d..f7ff4f6f7a2 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -451,7 +451,7 @@ def _format_assertmsg(obj: object) -> str: # However in either case we want to preserve the newline. replaces = [("\n", "\n~"), ("%", "%%")] if not isinstance(obj, str): - obj = saferepr(obj) + obj = saferepr(obj, _get_maxsize_for_saferepr(util._config)) replaces.append(("\\n", "\n~")) for r1, r2 in replaces: diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 5ee40ee6568..11688acb6f5 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -341,6 +341,34 @@ def test_assertion_messages_bytes(self, pytester: Pytester) -> None: assert result.ret == 1 result.stdout.fnmatch_lines(["*AssertionError: b'ohai!'", "*assert False"]) + def test_assertion_message_verbosity(self, pytester: Pytester) -> None: + """ + Obey verbosity levels when printing the "message" part of assertions, when they are + non-strings (#6682). + """ + pytester.makepyfile( + """ + class LongRepr: + + def __repr__(self): + return "A" * 500 + + def test_assertion_verbosity(): + assert False, LongRepr() + """ + ) + # Normal verbosity: assertion message gets abbreviated. + result = pytester.runpytest() + assert result.ret == 1 + result.stdout.re_match_lines( + [r".*AssertionError: A+\.\.\.A+$", ".*assert False"] + ) + + # High-verbosity: do not abbreviate the assertion message. + result = pytester.runpytest("-vv") + assert result.ret == 1 + result.stdout.re_match_lines([r".*AssertionError: A+$", ".*assert False"]) + def test_boolop(self) -> None: def f1() -> None: f = g = False From de6824fa7417e580c890b057c544717e6b2a3835 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 11:01:33 +0000 Subject: [PATCH 10/78] Add merge/squash guidelines to CONTRIBUTING.rst (#12672) (#12692) As discussed in https://github.com/pytest-dev/pytest/discussions/12633. (cherry picked from commit 2b99703acace3194a7e28c05a097d06d29949aa9) Co-authored-by: Bruno Oliveira --- CONTRIBUTING.rst | 55 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 12e2b18bb52..6e96fd24c40 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -380,6 +380,57 @@ pull requests from other contributors yourself after having reviewed them. +Merge/squash guidelines +----------------------- + +When a PR is approved and ready to be integrated to the ``main`` branch, one has the option to *merge* the commits unchanged, or *squash* all the commits into a single commit. + +Here are some guidelines on how to proceed, based on examples of a single PR commit history: + +1. Miscellaneous commits: + + * ``Implement X`` + * ``Fix test_a`` + * ``Add myself to AUTHORS`` + * ``fixup! Fix test_a`` + * ``Update tests/test_integration.py`` + * ``Merge origin/main into PR branch`` + * ``Update tests/test_integration.py`` + + In this case, prefer to use the **Squash** merge strategy: the commit history is a bit messy (not in a derogatory way, often one just commits changes because they know the changes will eventually be squashed together), so squashing everything into a single commit is best. You must clean up the commit message, making sure it contains useful details. + +2. Separate commits related to the same topic: + + * ``Implement X`` + * ``Add myself to AUTHORS`` + * ``Update CHANGELOG for X`` + + In this case, prefer to use the **Squash** merge strategy: while the commit history is not "messy" as in the example above, the individual commits do not bring much value overall, specially when looking at the changes a few months/years down the line. + +3. Separate commits, each with their own topic (refactorings, renames, etc), but still have a larger topic/purpose. + + * ``Refactor class X in preparation for feature Y`` + * ``Remove unused method`` + * ``Implement feature Y`` + + In this case, prefer to use the **Merge** strategy: each commit is valuable on its own, even if they serve a common topic overall. Looking at the history later, it is useful to have the removal of the unused method separately on its own commit, along with more information (such as how it became unused in the first place). + +4. Separate commits, each with their own topic, but without a larger topic/purpose other than improve the code base (using more modern techniques, improve typing, removing clutter, etc). + + * ``Improve internal names in X`` + * ``Add type annotations to Y`` + * ``Remove unnecessary dict access`` + * ``Remove unreachable code due to EOL Python`` + + In this case, prefer to use the **Merge** strategy: each commit is valuable on its own, and the information on each is valuable in the long term. + + +As mentioned, those are overall guidelines, not rules cast in stone. This topic was discussed in `#12633 `_. + + +*Backport PRs* (as those created automatically from a ``backport`` label) should always be **squashed**, as they preserve the original PR author. + + Backporting bug fixes for the next patch release ------------------------------------------------ @@ -438,6 +489,8 @@ above? All the above are not rules, but merely some guidelines/suggestions on what we should expect about backports. +Backports should be **squashed** (rather than **merged**), as doing so preserves the original PR author correctly. + Handling stale issues/PRs ------------------------- @@ -485,7 +538,7 @@ When closing a Pull Request, it needs to be acknowledging the time, effort, and -Closing Issues +Closing issues -------------- When a pull request is submitted to fix an issue, add text like ``closes #XYZW`` to the PR description and/or commits (where ``XYZW`` is the issue number). See the `GitHub docs `_ for more information. From ae7959a2a72e43d6c97a47450eac3011e2519184 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:20:18 +0000 Subject: [PATCH 11/78] Add Pierre to official contacts (#12694) (#12695) As part of onboarding Pierre to the core team. Also add contacts in alphabetical order (cherry picked from commit a01dca5321c899dd01f01c6ff698af87d86b9fd3) Co-authored-by: Bruno Oliveira --- doc/en/contact.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/en/contact.rst b/doc/en/contact.rst index ef9d1e8edca..d650a7efbab 100644 --- a/doc/en/contact.rst +++ b/doc/en/contact.rst @@ -33,10 +33,11 @@ Mail discussed in public. Mails sent there will be distributed among the members in the pytest core team, who can also be contacted individually: - * Ronny Pfannschmidt (:user:`RonnyPfannschmidt`, `ronny@pytest.org `_) - * Florian Bruhin (:user:`The-Compiler`, `florian@pytest.org `_) * Bruno Oliveira (:user:`nicoddemus`, `bruno@pytest.org `_) + * Florian Bruhin (:user:`The-Compiler`, `florian@pytest.org `_) + * Pierre Sassoulas (:user:`Pierre-Sassoulas`, `pierre@pytest.org `_) * Ran Benita (:user:`bluetech`, `ran@pytest.org `_) + * Ronny Pfannschmidt (:user:`RonnyPfannschmidt`, `ronny@pytest.org `_) * Zac Hatfield-Dodds (:user:`Zac-HD`, `zac@pytest.org `_) Other From 74caad915d9fbf8f87ade5a1bd0d402f2567cc11 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 15:40:34 +0000 Subject: [PATCH 12/78] docs: Revise `cache_dir` option section (#12701) (#12703) Update the description for the `cache_dir` configuration options section to improve semantics and clarity for an unclear sentence in the file `reference.rst`. (cherry picked from commit 49eafce55bf31c1993db9f411bc2d669804d5bb3) Co-authored-by: Eugene Mwangi --- doc/en/reference/reference.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index dbb5cc36192..f1110097ecc 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1287,10 +1287,10 @@ passed multiple times. The expected format is ``name=value``. For example:: .. confval:: cache_dir - Sets a directory where stores content of cache plugin. Default directory is + Sets the directory where the cache plugin's content is stored. Default directory is ``.pytest_cache`` which is created in :ref:`rootdir `. Directory may be relative or absolute path. If setting relative path, then directory is created - relative to :ref:`rootdir `. Additionally path may contain environment + relative to :ref:`rootdir `. Additionally, a path may contain environment variables, that will be expanded. For more information about cache plugin please refer to :ref:`cache_provider`. From 8fb7faed48b1dcd804dd1a99dd7bd3dd0f180c68 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 15:43:07 +0000 Subject: [PATCH 13/78] Fix `errisinstance` typing (#12700) (#12704) Fixes #12667 (cherry picked from commit cb98538e9c2ba3c65baf07432229d97fd5a29657) Co-authored-by: Reagan Lee <96998476+reaganjlee@users.noreply.github.com> --- changelog/12667.bugfix.rst | 1 + src/_pytest/_code/code.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/12667.bugfix.rst diff --git a/changelog/12667.bugfix.rst b/changelog/12667.bugfix.rst new file mode 100644 index 00000000000..eceee86cb53 --- /dev/null +++ b/changelog/12667.bugfix.rst @@ -0,0 +1 @@ +Fixed a regression where type change in `ExceptionInfo.errisinstance` caused `mypy` to fail. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index e7452825756..8fac39ea298 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -56,7 +56,7 @@ TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] -EXCEPTION_OR_MORE = Union[Type[Exception], Tuple[Type[Exception], ...]] +EXCEPTION_OR_MORE = Union[Type[BaseException], Tuple[Type[BaseException], ...]] class Code: From 6d59143aa4170a9174d253adc826043eb7545788 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:13:28 +0000 Subject: [PATCH 14/78] docs: clarify `pytest_deselected`/`pytest_collection_modifyitems` usage (#12729) (#12733) Closes #12663 (cherry picked from commit 51845fc70dba0fba27387e21e2db39d583892dec) Co-authored-by: Stefaan Lippens --- changelog/12663.doc.rst | 1 + src/_pytest/hookspec.py | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 changelog/12663.doc.rst diff --git a/changelog/12663.doc.rst b/changelog/12663.doc.rst new file mode 100644 index 00000000000..e665bc67a10 --- /dev/null +++ b/changelog/12663.doc.rst @@ -0,0 +1 @@ +Clarify that the `pytest_deselected` hook should be called from `pytest_collection_modifyitems` hook implementations when items are deselected. diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 99614899994..0a41b0aca47 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -274,6 +274,11 @@ def pytest_collection_modifyitems( """Called after collection has been performed. May filter or re-order the items in-place. + When items are deselected (filtered out from ``items``), + the hook :hook:`pytest_deselected` must be called explicitly + with the deselected items to properly notify other plugins, + e.g. with ``config.hook.pytest_deselected(deselected_items)``. + :param session: The pytest session object. :param config: The pytest config object. :param items: List of item objects. @@ -454,6 +459,12 @@ def pytest_collectreport(report: CollectReport) -> None: def pytest_deselected(items: Sequence[Item]) -> None: """Called for deselected test items, e.g. by keyword. + Note that this hook has two integration aspects for plugins: + + - it can be *implemented* to be notified of deselected items + - it must be *called* from :hook:`pytest_collection_modifyitems` + implementations when items are deselected (to properly notify other plugins). + May be called multiple times. :param items: From 2d5618ec4e9499ba4f39131f1fb8423aa89d1d4b Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 29 Aug 2024 07:54:47 -0400 Subject: [PATCH 15/78] Replaced `typing.Self` with `typing_extensions.Self` (#12744) Fix incorrect Self import from typing instead of typing_extensions. --------- Co-authored-by: Bruno Oliveira (cherry picked from commit c947145fbb4aeec810a259b19f70fcb52fd53ad4) --- changelog/12744.bugfix.rst | 1 + src/_pytest/doctest.py | 3 ++- src/_pytest/nodes.py | 2 +- src/_pytest/python.py | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelog/12744.bugfix.rst diff --git a/changelog/12744.bugfix.rst b/changelog/12744.bugfix.rst new file mode 100644 index 00000000000..7814b59c180 --- /dev/null +++ b/changelog/12744.bugfix.rst @@ -0,0 +1 @@ +Fixed typing compatibility with Python 3.9 or less -- replaced `typing.Self` with `typing_extensions.Self` -- by :user:`Avasam` diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index cb46d9a3bb5..7f09e1794ac 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -44,7 +44,8 @@ if TYPE_CHECKING: import doctest - from typing import Self + + from typing_extensions import Self DOCTEST_REPORT_CHOICE_NONE = "none" DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index bbde2664b90..cc678695848 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -43,7 +43,7 @@ if TYPE_CHECKING: - from typing import Self + from typing_extensions import Self # Imported here due to circular import. from _pytest.main import Session diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 9182ce7dfe9..3a2c4d0d1d3 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -78,7 +78,7 @@ if TYPE_CHECKING: - from typing import Self + from typing_extensions import Self def pytest_addoption(parser: Parser) -> None: From cafc729ae1835f68c600d25f9709787600f78398 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 22:33:00 +0000 Subject: [PATCH 16/78] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.2 → v0.5.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.2...v0.5.4) - [github.com/pre-commit/mirrors-mypy: v1.10.1 → v1.11.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.10.1...v1.11.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 419addd95be..79697cf1caa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.5.2" + rev: "v0.5.4" hooks: - id: ruff args: ["--fix"] @@ -21,7 +21,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.1 + rev: v1.11.0 hooks: - id: mypy files: ^(src/|testing/|scripts/) From c185df620b3f26379116713c3d5bd998fa79591f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 07:25:50 +0200 Subject: [PATCH 17/78] [pre-commit.ci] pre-commit autoupdate (#12669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.4 → v0.5.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.4...v0.5.5) - [github.com/asottile/pyupgrade: v3.16.0 → v3.17.0](https://github.com/asottile/pyupgrade/compare/v3.16.0...v3.17.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 79697cf1caa..aaa28d6b7c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.5.4" + rev: "v0.5.5" hooks: - id: ruff args: ["--fix"] @@ -44,7 +44,7 @@ repos: # https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version additional_dependencies: ["tox>=4.9"] - repo: https://github.com/asottile/pyupgrade - rev: v3.16.0 + rev: v3.17.0 hooks: - id: pyupgrade stages: [manual] From 3b829563ab76455bb01c874dca64840a8ff20dc3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 23:10:25 +0000 Subject: [PATCH 18/78] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.5 → v0.5.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.5...v0.5.6) - [github.com/pre-commit/mirrors-mypy: v1.11.0 → v1.11.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.0...v1.11.1) - [github.com/tox-dev/pyproject-fmt: 2.1.4 → 2.2.1](https://github.com/tox-dev/pyproject-fmt/compare/2.1.4...2.2.1) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aaa28d6b7c4..5348fdde93d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.5.5" + rev: "v0.5.6" hooks: - id: ruff args: ["--fix"] @@ -21,7 +21,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.0 + rev: v1.11.1 hooks: - id: mypy files: ^(src/|testing/|scripts/) @@ -38,7 +38,7 @@ repos: # on <3.11 - exceptiongroup>=1.0.0rc8 - repo: https://github.com/tox-dev/pyproject-fmt - rev: "2.1.4" + rev: "2.2.1" hooks: - id: pyproject-fmt # https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version From 0765c55658e2d18dce3c86511273e55bd8698652 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 7 Aug 2024 08:42:58 -0300 Subject: [PATCH 19/78] Replace types-pkg-resources by types-setuptools According to https://pypi.org/project/types-pkg-resources/#history, that package has been supplanted by `types-setuptools`. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5348fdde93d..f27c46f3a54 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: - pluggy>=1.5.0 - packaging - tomli - - types-pkg_resources + - types-setuptools - types-tabulate # for mypy running on python>=3.11 since exceptiongroup is only a dependency # on <3.11 From d2bb828eef219cd7c7581493d0ad4f81eb18799e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 21:53:50 +0200 Subject: [PATCH 20/78] [pre-commit.ci] pre-commit autoupdate (#12712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.6 → v0.5.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.6...v0.5.7) * Apply pyupgrade latest changes Co-authored-by: Pierre Sassoulas --- .pre-commit-config.yaml | 2 +- src/_pytest/cacheprovider.py | 6 ++---- src/_pytest/capture.py | 22 +++++++++++----------- src/_pytest/doctest.py | 2 +- src/_pytest/logging.py | 22 ++++++++++------------ src/_pytest/monkeypatch.py | 4 ++-- src/_pytest/pytester.py | 4 ++-- src/_pytest/python.py | 8 ++++---- src/_pytest/recwarn.py | 2 +- src/_pytest/skipping.py | 2 +- src/_pytest/terminal.py | 4 ++-- src/_pytest/threadexception.py | 8 ++++---- src/_pytest/tmpdir.py | 2 +- src/_pytest/unittest.py | 4 ++-- src/_pytest/unraisableexception.py | 8 ++++---- src/_pytest/warnings.py | 8 ++++---- testing/conftest.py | 2 +- testing/io/test_terminalwriter.py | 4 +--- testing/test_assertrewrite.py | 2 +- testing/test_cacheprovider.py | 2 +- testing/test_capture.py | 2 +- testing/test_conftest.py | 4 +--- testing/test_monkeypatch.py | 2 +- testing/test_pathlib.py | 2 +- 24 files changed, 60 insertions(+), 68 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f27c46f3a54..0e36d1e6137 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.5.6" + rev: "v0.5.7" hooks: - id: ruff args: ["--fix"] diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 20bb262e05d..7190810570e 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -369,7 +369,7 @@ def pytest_collectreport(self, report: CollectReport) -> None: @hookimpl(wrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, config: Config, items: list[nodes.Item] - ) -> Generator[None, None, None]: + ) -> Generator[None]: res = yield if not self.active: @@ -439,9 +439,7 @@ def __init__(self, config: Config) -> None: self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) @hookimpl(wrapper=True, tryfirst=True) - def pytest_collection_modifyitems( - self, items: list[nodes.Item] - ) -> Generator[None, None, None]: + def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> Generator[None]: res = yield if self.active: diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index c4dfcc27552..f4586a42a69 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -135,7 +135,7 @@ def _reopen_stdio(f, mode): @hookimpl(wrapper=True) -def pytest_load_initial_conftests(early_config: Config) -> Generator[None, None, None]: +def pytest_load_initial_conftests(early_config: Config) -> Generator[None]: ns = early_config.known_args_namespace if ns.capture == "fd": _windowsconsoleio_workaround(sys.stdout) @@ -817,7 +817,7 @@ def resume_fixture(self) -> None: # Helper context managers @contextlib.contextmanager - def global_and_fixture_disabled(self) -> Generator[None, None, None]: + def global_and_fixture_disabled(self) -> Generator[None]: """Context manager to temporarily disable global and current fixture capturing.""" do_fixture = self._capture_fixture and self._capture_fixture._is_started() if do_fixture: @@ -834,7 +834,7 @@ def global_and_fixture_disabled(self) -> Generator[None, None, None]: self.resume_fixture() @contextlib.contextmanager - def item_capture(self, when: str, item: Item) -> Generator[None, None, None]: + def item_capture(self, when: str, item: Item) -> Generator[None]: self.resume_global_capture() self.activate_fixture() try: @@ -869,17 +869,17 @@ def pytest_make_collect_report( return rep @hookimpl(wrapper=True) - def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: + def pytest_runtest_setup(self, item: Item) -> Generator[None]: with self.item_capture("setup", item): return (yield) @hookimpl(wrapper=True) - def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: + def pytest_runtest_call(self, item: Item) -> Generator[None]: with self.item_capture("call", item): return (yield) @hookimpl(wrapper=True) - def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]: + def pytest_runtest_teardown(self, item: Item) -> Generator[None]: with self.item_capture("teardown", item): return (yield) @@ -961,7 +961,7 @@ def _is_started(self) -> bool: return False @contextlib.contextmanager - def disabled(self) -> Generator[None, None, None]: + def disabled(self) -> Generator[None]: """Temporarily disable capturing while inside the ``with`` block.""" capmanager: CaptureManager = self.request.config.pluginmanager.getplugin( "capturemanager" @@ -974,7 +974,7 @@ def disabled(self) -> Generator[None, None, None]: @fixture -def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: +def capsys(request: SubRequest) -> Generator[CaptureFixture[str]]: r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method @@ -1002,7 +1002,7 @@ def test_output(capsys): @fixture -def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: +def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]: r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` @@ -1030,7 +1030,7 @@ def test_output(capsysbinary): @fixture -def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: +def capfd(request: SubRequest) -> Generator[CaptureFixture[str]]: r"""Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -1058,7 +1058,7 @@ def test_system_echo(capfd): @fixture -def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: +def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes]]: r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 7f09e1794ac..384dea976ad 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -468,7 +468,7 @@ def _is_mocked(obj: object) -> bool: @contextmanager -def _patch_unwrap_mock_aware() -> Generator[None, None, None]: +def _patch_unwrap_mock_aware() -> Generator[None]: """Context manager which replaces ``inspect.unwrap`` with a version that's aware of mock objects and doesn't recurse into them.""" real_unwrap = inspect.unwrap diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 44af8ff2041..9f3417e9af2 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -554,9 +554,7 @@ def set_level(self, level: int | str, logger: str | None = None) -> None: self._initial_disabled_logging_level = initial_disabled_logging_level @contextmanager - def at_level( - self, level: int | str, logger: str | None = None - ) -> Generator[None, None, None]: + def at_level(self, level: int | str, logger: str | None = None) -> Generator[None]: """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the level is restored to its original value. @@ -580,7 +578,7 @@ def at_level( logging.disable(original_disable_level) @contextmanager - def filtering(self, filter_: logging.Filter) -> Generator[None, None, None]: + def filtering(self, filter_: logging.Filter) -> Generator[None]: """Context manager that temporarily adds the given filter to the caplog's :meth:`handler` for the 'with' statement block, and removes that filter at the end of the block. @@ -597,7 +595,7 @@ def filtering(self, filter_: logging.Filter) -> Generator[None, None, None]: @fixture -def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]: +def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture]: """Access and control log capturing. Captured logs are available through the following properties/methods:: @@ -776,7 +774,7 @@ def _log_cli_enabled(self) -> bool: return True @hookimpl(wrapper=True, tryfirst=True) - def pytest_sessionstart(self) -> Generator[None, None, None]: + def pytest_sessionstart(self) -> Generator[None]: self.log_cli_handler.set_when("sessionstart") with catching_logs(self.log_cli_handler, level=self.log_cli_level): @@ -784,7 +782,7 @@ def pytest_sessionstart(self) -> Generator[None, None, None]: return (yield) @hookimpl(wrapper=True, tryfirst=True) - def pytest_collection(self) -> Generator[None, None, None]: + def pytest_collection(self) -> Generator[None]: self.log_cli_handler.set_when("collection") with catching_logs(self.log_cli_handler, level=self.log_cli_level): @@ -813,7 +811,7 @@ def pytest_runtest_logstart(self) -> None: def pytest_runtest_logreport(self) -> None: self.log_cli_handler.set_when("logreport") - def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]: + def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None]: """Implement the internals of the pytest_runtest_xxx() hooks.""" with catching_logs( self.caplog_handler, @@ -834,7 +832,7 @@ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, Non item.add_report_section(when, "log", log) @hookimpl(wrapper=True) - def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: + def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None]: self.log_cli_handler.set_when("setup") empty: dict[str, list[logging.LogRecord]] = {} @@ -842,13 +840,13 @@ def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: yield from self._runtest_for(item, "setup") @hookimpl(wrapper=True) - def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]: + def pytest_runtest_call(self, item: nodes.Item) -> Generator[None]: self.log_cli_handler.set_when("call") yield from self._runtest_for(item, "call") @hookimpl(wrapper=True) - def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]: + def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None]: self.log_cli_handler.set_when("teardown") try: @@ -862,7 +860,7 @@ def pytest_runtest_logfinish(self) -> None: self.log_cli_handler.set_when("finish") @hookimpl(wrapper=True, tryfirst=True) - def pytest_sessionfinish(self) -> Generator[None, None, None]: + def pytest_sessionfinish(self) -> Generator[None]: self.log_cli_handler.set_when("sessionfinish") with catching_logs(self.log_cli_handler, level=self.log_cli_level): diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 75b019a3be6..46eb1724e35 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -28,7 +28,7 @@ @fixture -def monkeypatch() -> Generator[MonkeyPatch, None, None]: +def monkeypatch() -> Generator[MonkeyPatch]: """A convenient fixture for monkey-patching. The fixture provides these methods to modify objects, dictionaries, or @@ -135,7 +135,7 @@ def __init__(self) -> None: @classmethod @contextmanager - def context(cls) -> Generator[MonkeyPatch, None, None]: + def context(cls) -> Generator[MonkeyPatch]: """Context manager that returns a new :class:`MonkeyPatch` object which undoes any patching done inside the ``with`` block upon exit. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 5c6ce5e889f..3f7520ee4ad 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -491,7 +491,7 @@ def pytester( @fixture -def _sys_snapshot() -> Generator[None, None, None]: +def _sys_snapshot() -> Generator[None]: snappaths = SysPathsSnapshot() snapmods = SysModulesSnapshot() yield @@ -500,7 +500,7 @@ def _sys_snapshot() -> Generator[None, None, None]: @fixture -def _config_for_test() -> Generator[Config, None, None]: +def _config_for_test() -> Generator[Config]: from _pytest.config import get_config config = get_config() diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 3a2c4d0d1d3..b2199c76fc6 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -568,7 +568,7 @@ def _register_setup_module_fixture(self) -> None: if setup_module is None and teardown_module is None: return - def xunit_setup_module_fixture(request) -> Generator[None, None, None]: + def xunit_setup_module_fixture(request) -> Generator[None]: module = request.module if setup_module is not None: _call_with_optional_argument(setup_module, module) @@ -599,7 +599,7 @@ def _register_setup_function_fixture(self) -> None: if setup_function is None and teardown_function is None: return - def xunit_setup_function_fixture(request) -> Generator[None, None, None]: + def xunit_setup_function_fixture(request) -> Generator[None]: if request.instance is not None: # in this case we are bound to an instance, so we need to let # setup_method handle this @@ -780,7 +780,7 @@ def _register_setup_class_fixture(self) -> None: if setup_class is None and teardown_class is None: return - def xunit_setup_class_fixture(request) -> Generator[None, None, None]: + def xunit_setup_class_fixture(request) -> Generator[None]: cls = request.cls if setup_class is not None: func = getimfunc(setup_class) @@ -813,7 +813,7 @@ def _register_setup_method_fixture(self) -> None: if setup_method is None and teardown_method is None: return - def xunit_setup_method_fixture(request) -> Generator[None, None, None]: + def xunit_setup_method_fixture(request) -> Generator[None]: instance = request.instance method = request.function if setup_method is not None: diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 3fc00d94736..85d8de84abb 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -32,7 +32,7 @@ @fixture -def recwarn() -> Generator[WarningsRecorder, None, None]: +def recwarn() -> Generator[WarningsRecorder]: """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 08fcb283eb2..9818be2ab03 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -245,7 +245,7 @@ def pytest_runtest_setup(item: Item) -> None: @hookimpl(wrapper=True) -def pytest_runtest_call(item: Item) -> Generator[None, None, None]: +def pytest_runtest_call(item: Item) -> Generator[None]: xfailed = item.stash.get(xfailed_key, None) if xfailed is None: item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 8c722124d04..ed267bf5bfd 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -889,7 +889,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: @hookimpl(wrapper=True) def pytest_sessionfinish( self, session: Session, exitstatus: int | ExitCode - ) -> Generator[None, None, None]: + ) -> Generator[None]: result = yield self._tw.line("") summary_exit_codes = ( @@ -914,7 +914,7 @@ def pytest_sessionfinish( return result @hookimpl(wrapper=True) - def pytest_terminal_summary(self) -> Generator[None, None, None]: + def pytest_terminal_summary(self) -> Generator[None]: self.summary_errors() self.summary_failures() self.summary_xfailures() diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py index d78c32c852f..c1ed80387aa 100644 --- a/src/_pytest/threadexception.py +++ b/src/_pytest/threadexception.py @@ -62,7 +62,7 @@ def __exit__( del self.args -def thread_exception_runtest_hook() -> Generator[None, None, None]: +def thread_exception_runtest_hook() -> Generator[None]: with catch_threading_exception() as cm: try: yield @@ -83,15 +83,15 @@ def thread_exception_runtest_hook() -> Generator[None, None, None]: @pytest.hookimpl(wrapper=True, trylast=True) -def pytest_runtest_setup() -> Generator[None, None, None]: +def pytest_runtest_setup() -> Generator[None]: yield from thread_exception_runtest_hook() @pytest.hookimpl(wrapper=True, tryfirst=True) -def pytest_runtest_call() -> Generator[None, None, None]: +def pytest_runtest_call() -> Generator[None]: yield from thread_exception_runtest_hook() @pytest.hookimpl(wrapper=True, tryfirst=True) -def pytest_runtest_teardown() -> Generator[None, None, None]: +def pytest_runtest_teardown() -> Generator[None]: yield from thread_exception_runtest_hook() diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 91109ea69ef..de0cbcfeb1c 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -256,7 +256,7 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: @fixture def tmp_path( request: FixtureRequest, tmp_path_factory: TempPathFactory -) -> Generator[Path, None, None]: +) -> Generator[Path]: """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index aefea1333d9..8cecd4f9339 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -137,7 +137,7 @@ def process_teardown_exceptions() -> None: def unittest_setup_class_fixture( request: FixtureRequest, - ) -> Generator[None, None, None]: + ) -> Generator[None]: cls = request.cls if _is_skipped(cls): reason = cls.__unittest_skip_why__ @@ -178,7 +178,7 @@ def _register_unittest_setup_method_fixture(self, cls: type) -> None: def unittest_setup_method_fixture( request: FixtureRequest, - ) -> Generator[None, None, None]: + ) -> Generator[None]: self = request.instance if _is_skipped(self): reason = self.__unittest_skip_why__ diff --git a/src/_pytest/unraisableexception.py b/src/_pytest/unraisableexception.py index c191703a3de..77a2de20041 100644 --- a/src/_pytest/unraisableexception.py +++ b/src/_pytest/unraisableexception.py @@ -64,7 +64,7 @@ def __exit__( del self.unraisable -def unraisable_exception_runtest_hook() -> Generator[None, None, None]: +def unraisable_exception_runtest_hook() -> Generator[None]: with catch_unraisable_exception() as cm: try: yield @@ -86,15 +86,15 @@ def unraisable_exception_runtest_hook() -> Generator[None, None, None]: @pytest.hookimpl(wrapper=True, tryfirst=True) -def pytest_runtest_setup() -> Generator[None, None, None]: +def pytest_runtest_setup() -> Generator[None]: yield from unraisable_exception_runtest_hook() @pytest.hookimpl(wrapper=True, tryfirst=True) -def pytest_runtest_call() -> Generator[None, None, None]: +def pytest_runtest_call() -> Generator[None]: yield from unraisable_exception_runtest_hook() @pytest.hookimpl(wrapper=True, tryfirst=True) -def pytest_runtest_teardown() -> Generator[None, None, None]: +def pytest_runtest_teardown() -> Generator[None]: yield from unraisable_exception_runtest_hook() diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 5c59e55c5db..eeb4772649d 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -30,7 +30,7 @@ def catch_warnings_for_item( ihook, when: Literal["config", "collect", "runtest"], item: Item | None, -) -> Generator[None, None, None]: +) -> Generator[None]: """Context manager that catches warnings generated in the contained execution block. ``item`` can be None if we are not in the context of an item execution. @@ -124,7 +124,7 @@ def pytest_collection(session: Session) -> Generator[None, object, object]: @pytest.hookimpl(wrapper=True) def pytest_terminal_summary( terminalreporter: TerminalReporter, -) -> Generator[None, None, None]: +) -> Generator[None]: config = terminalreporter.config with catch_warnings_for_item( config=config, ihook=config.hook, when="config", item=None @@ -133,7 +133,7 @@ def pytest_terminal_summary( @pytest.hookimpl(wrapper=True) -def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: +def pytest_sessionfinish(session: Session) -> Generator[None]: config = session.config with catch_warnings_for_item( config=config, ihook=config.hook, when="config", item=None @@ -144,7 +144,7 @@ def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: @pytest.hookimpl(wrapper=True) def pytest_load_initial_conftests( early_config: Config, -) -> Generator[None, None, None]: +) -> Generator[None]: with catch_warnings_for_item( config=early_config, ihook=early_config.hook, when="config", item=None ): diff --git a/testing/conftest.py b/testing/conftest.py index 24e5d183094..046bb77a109 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -46,7 +46,7 @@ def reset_colors(monkeypatch: pytest.MonkeyPatch) -> None: @pytest.hookimpl(wrapper=True, tryfirst=True) -def pytest_collection_modifyitems(items) -> Generator[None, None, None]: +def pytest_collection_modifyitems(items) -> Generator[None]: """Prefer faster tests. Use a hook wrapper to do this in the beginning, so e.g. --ff still works diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index 043c2d1d904..92cde240a11 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -67,9 +67,7 @@ def test_terminalwriter_not_unicode() -> None: class TestTerminalWriter: @pytest.fixture(params=["path", "stringio"]) - def tw( - self, request, tmp_path: Path - ) -> Generator[terminalwriter.TerminalWriter, None, None]: + def tw(self, request, tmp_path: Path) -> Generator[terminalwriter.TerminalWriter]: if request.param == "path": p = tmp_path.joinpath("tmpfile") f = open(str(p), "w+", encoding="utf8") diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 11688acb6f5..73c11a1a9d8 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1660,7 +1660,7 @@ class TestEarlyRewriteBailout: @pytest.fixture def hook( self, pytestconfig, monkeypatch, pytester: Pytester - ) -> Generator[AssertionRewritingHook, None, None]: + ) -> Generator[AssertionRewritingHook]: """Returns a patched AssertionRewritingHook instance so we can configure its initial paths and track if PathFinder.find_spec has been called. """ diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 72b4265cf75..94bc55d3047 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -69,7 +69,7 @@ def test_cache_writefail_cachefile_silent(self, pytester: Pytester) -> None: cache.set("test/broken", []) @pytest.fixture - def unwritable_cache_dir(self, pytester: Pytester) -> Generator[Path, None, None]: + def unwritable_cache_dir(self, pytester: Pytester) -> Generator[Path]: cache_dir = pytester.path.joinpath(".pytest_cache") cache_dir.mkdir() mode = cache_dir.stat().st_mode diff --git a/testing/test_capture.py b/testing/test_capture.py index fe6bd7d14fa..328de740e8a 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -939,7 +939,7 @@ def test_captureresult() -> None: @pytest.fixture -def tmpfile(pytester: Pytester) -> Generator[BinaryIO, None, None]: +def tmpfile(pytester: Pytester) -> Generator[BinaryIO]: f = pytester.makepyfile("").open("wb+") yield f if not f.closed: diff --git a/testing/test_conftest.py b/testing/test_conftest.py index d51846f2f5f..ea60c1909c2 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -44,9 +44,7 @@ def conftest_setinitial( @pytest.mark.usefixtures("_sys_snapshot") class TestConftestValueAccessGlobal: @pytest.fixture(scope="module", params=["global", "inpackage"]) - def basedir( - self, request, tmp_path_factory: TempPathFactory - ) -> Generator[Path, None, None]: + def basedir(self, request, tmp_path_factory: TempPathFactory) -> Generator[Path]: tmp_path = tmp_path_factory.mktemp("basedir", numbered=True) tmp_path.joinpath("adir/b").mkdir(parents=True) tmp_path.joinpath("adir/conftest.py").write_text( diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 079d8ff60ad..1a429e3e681 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -14,7 +14,7 @@ @pytest.fixture -def mp() -> Generator[MonkeyPatch, None, None]: +def mp() -> Generator[MonkeyPatch]: cwd = os.getcwd() sys_path = list(sys.path) yield MonkeyPatch() diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 81aba25f78f..377c30ca900 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -125,7 +125,7 @@ class TestImportPath: """ @pytest.fixture(scope="session") - def path1(self, tmp_path_factory: TempPathFactory) -> Generator[Path, None, None]: + def path1(self, tmp_path_factory: TempPathFactory) -> Generator[Path]: path = tmp_path_factory.mktemp("path") self.setuptestfs(path) yield path From 87bc32f6e28a7e60bd387a2f2df4aebebf98f867 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 21:06:03 -0300 Subject: [PATCH 21/78] [pre-commit.ci] pre-commit autoupdate (#12725) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.5.7 → v0.6.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.5.7...v0.6.1) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e36d1e6137..a841a4a9a0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.5.7" + rev: "v0.6.1" hooks: - id: ruff args: ["--fix"] From eaf553d9a7fa3fb3119e8033edf623e8b5187f70 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 23:32:54 +0200 Subject: [PATCH 22/78] [pre-commit.ci] pre-commit autoupdate (#12741) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.6.1 → v0.6.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.6.1...v0.6.2) - [github.com/pre-commit/mirrors-mypy: v1.11.1 → v1.11.2](https://github.com/pre-commit/mirrors-mypy/compare/v1.11.1...v1.11.2) * Fix sphinx with a more robust fix Until https://github.com/sphinx-contrib/sphinxcontrib-towncrier/issues/92 is resolved. Co-authored-by: Pierre Sassoulas --- .pre-commit-config.yaml | 4 ++-- doc/en/broken-dep-constraints.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a841a4a9a0f..938b0bf407b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.6.1" + rev: "v0.6.2" hooks: - id: ruff args: ["--fix"] @@ -21,7 +21,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.11.1 + rev: v1.11.2 hooks: - id: mypy files: ^(src/|testing/|scripts/) diff --git a/doc/en/broken-dep-constraints.txt b/doc/en/broken-dep-constraints.txt index f7c3211a9a0..3a8d2c34b5a 100644 --- a/doc/en/broken-dep-constraints.txt +++ b/doc/en/broken-dep-constraints.txt @@ -3,4 +3,4 @@ # Pin towncrier temporarily due to incompatibility with sphinxcontrib-towncrier: # https://github.com/sphinx-contrib/sphinxcontrib-towncrier/issues/92 -towncrier!=24.7.0,!=24.7.1 +towncrier<24.7 From 6af50c05bd88b2fa2816e91161bd318bb527b18d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 23 Jul 2024 09:57:35 -0300 Subject: [PATCH 23/78] Adapt code after updating to mypy 1.11 --- src/_pytest/_io/pprint.py | 4 ++-- src/_pytest/capture.py | 1 + src/_pytest/faulthandler.py | 1 + testing/test_assertion.py | 2 +- testing/test_monkeypatch.py | 2 +- 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index 7213be7ba9b..fc29989be0b 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -111,7 +111,7 @@ def _format( p(self, object, stream, indent, allowance, context, level + 1) context.remove(objid) elif ( - _dataclasses.is_dataclass(object) + _dataclasses.is_dataclass(object) # type:ignore[unreachable] and not isinstance(object, type) and object.__dataclass_params__.repr and @@ -119,7 +119,7 @@ def _format( hasattr(object.__repr__, "__wrapped__") and "__create_fn__" in object.__repr__.__wrapped__.__qualname__ ): - context.add(objid) + context.add(objid) # type:ignore[unreachable] self._pprint_dataclass( object, stream, indent, allowance, context, level + 1 ) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index f4586a42a69..506c0b3d287 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -202,6 +202,7 @@ def write(self, s: str) -> int: class DontReadFromInput(TextIO): @property def encoding(self) -> str: + assert sys.__stdin__ is not None return sys.__stdin__.encoding def read(self, size: int = -1) -> str: diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 07e60f03fc9..d16aea1eb88 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -64,6 +64,7 @@ def get_stderr_fileno() -> int: # pytest-xdist monkeypatches sys.stderr with an object that is not an actual file. # https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors # This is potentially dangerous, but the best we can do. + assert sys.__stderr__ is not None return sys.__stderr__.fileno() diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 69ca0f73ff2..31192df0f6f 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -823,7 +823,7 @@ def __setitem__(self, item, value): def __delitem__(self, item): pass - def insert(self, item, index): + def insert(self, index, value): pass expl = callequal(TestSequence([0, 1]), list([0, 2])) diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 1a429e3e681..7c62d90f2b9 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -415,7 +415,7 @@ def test_context() -> None: with monkeypatch.context() as m: m.setattr(functools, "partial", 3) assert not inspect.isclass(functools.partial) - assert inspect.isclass(functools.partial) + assert inspect.isclass(functools.partial) # type:ignore[unreachable] def test_context_classmethod() -> None: From be9e36edb938af2a819aa72c4f9f61d55b1ffc9f Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 21:15:38 +0000 Subject: [PATCH 24/78] Use oldest supported Python in mypy pre-commit checks (#12747) (#12750) Follow up to #12744, this ensures type checking works at the oldest Python version supported by pytest. (cherry picked from commit 419bc7a7c39d21af8a2b7cf29928ee93b7e02b19) Co-authored-by: Bruno Oliveira --- .pre-commit-config.yaml | 1 + src/_pytest/main.py | 2 +- src/_pytest/runner.py | 4 ++-- testing/test_runner.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 938b0bf407b..8c7fde3f2c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,6 +26,7 @@ repos: - id: mypy files: ^(src/|testing/|scripts/) args: [] + language_version: "3.8" additional_dependencies: - iniconfig>=1.1.0 - attrs>=19.2.0 diff --git a/src/_pytest/main.py b/src/_pytest/main.py index befc7ccce6e..e5534e98d69 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -49,7 +49,7 @@ if TYPE_CHECKING: - from typing import Self + from typing_extensions import Self from _pytest.fixtures import FixtureManager diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 716c4948f4a..d0e1cdc7faa 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -167,7 +167,7 @@ def pytest_runtest_call(item: Item) -> None: del sys.last_value del sys.last_traceback if sys.version_info >= (3, 12, 0): - del sys.last_exc + del sys.last_exc # type:ignore[attr-defined] except AttributeError: pass try: @@ -177,7 +177,7 @@ def pytest_runtest_call(item: Item) -> None: sys.last_type = type(e) sys.last_value = e if sys.version_info >= (3, 12, 0): - sys.last_exc = e + sys.last_exc = e # type:ignore[attr-defined] assert e.__traceback__ is not None # Skip *this* frame sys.last_traceback = e.__traceback__.tb_next diff --git a/testing/test_runner.py b/testing/test_runner.py index 1b59ff78633..0d9facdcd71 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -1030,7 +1030,7 @@ def runtest(self): assert sys.last_type is IndexError assert isinstance(sys.last_value, IndexError) if sys.version_info >= (3, 12, 0): - assert isinstance(sys.last_exc, IndexError) + assert isinstance(sys.last_exc, IndexError) # type:ignore[attr-defined] assert sys.last_value.args[0] == "TEST" assert sys.last_traceback From bf379f44b6e4f19c6242edd65344f961243a29f4 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:55:28 +0000 Subject: [PATCH 25/78] Fix assertion rewriting with importlib mode (#12716) (#12755) Fixes #12659 (cherry picked from commit 9a444d113658be6ccb8dc9f57ed118a1ef17c94c) Co-authored-by: dongfangtianyu <7629022+dongfangtianyu@users.noreply.github.com> --- AUTHORS | 1 + changelog/12659.bugfix.rst | 1 + src/_pytest/assertion/rewrite.py | 10 +++++++++ src/_pytest/pathlib.py | 4 +++- testing/test_pathlib.py | 36 ++++++++++++++++++++++++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 changelog/12659.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 8103a1d52a5..db993353689 100644 --- a/AUTHORS +++ b/AUTHORS @@ -412,6 +412,7 @@ Ted Xiao Terje Runde Thomas Grainger Thomas Hisch +Tianyu Dongfang Tim Hoffmann Tim Strazny TJ Bruno diff --git a/changelog/12659.bugfix.rst b/changelog/12659.bugfix.rst new file mode 100644 index 00000000000..297381b9602 --- /dev/null +++ b/changelog/12659.bugfix.rst @@ -0,0 +1 @@ +Fixed the issue of not displaying assertion failure differences when using the parameter ``--import-mode=importlib`` in pytest>=8.1. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index f7ff4f6f7a2..a7a92c0f1fe 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -101,6 +101,16 @@ def find_spec( # Type ignored because mypy is confused about the `self` binding here. spec = self._find_spec(name, path) # type: ignore + + if spec is None and path is not None: + # With --import-mode=importlib, PathFinder cannot find spec without modifying `sys.path`, + # causing inability to assert rewriting (#12659). + # At this point, try using the file path to find the module spec. + for _path_str in path: + spec = importlib.util.spec_from_file_location(name, _path_str) + if spec is not None: + break + if ( # the import machinery could not find a file to import spec is None diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index e4dc4eddc9c..81e52ea729d 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -621,7 +621,9 @@ def _import_module_using_spec( # Checking with sys.meta_path first in case one of its hooks can import this module, # such as our own assertion-rewrite hook. for meta_importer in sys.meta_path: - spec = meta_importer.find_spec(module_name, [str(module_location)]) + spec = meta_importer.find_spec( + module_name, [str(module_location), str(module_path)] + ) if spec_matches_module_path(spec, module_path): break else: diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 377c30ca900..7c0a0e03d76 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -1372,6 +1372,42 @@ def test_resolve_pkg_root_and_module_name_ns_multiple_levels( ) assert mod is mod2 + def test_ns_multiple_levels_import_rewrite_assertions( + self, + tmp_path: Path, + monkeypatch: MonkeyPatch, + pytester: Pytester, + ) -> None: + """Check assert rewriting with `--import-mode=importlib` (#12659).""" + self.setup_directories(tmp_path, monkeypatch, pytester) + code = dedent(""" + def test(): + assert "four lights" == "five lights" + """) + + # A case is in a subdirectory with an `__init__.py` file. + test_py = tmp_path / "src/dist2/com/company/calc/algo/test_demo.py" + test_py.write_text(code, encoding="UTF-8") + + pkg_root, module_name = resolve_pkg_root_and_module_name( + test_py, consider_namespace_packages=True + ) + assert (pkg_root, module_name) == ( + tmp_path / "src/dist2", + "com.company.calc.algo.test_demo", + ) + + result = pytester.runpytest("--import-mode=importlib", test_py) + + result.stdout.fnmatch_lines( + [ + "E AssertionError: assert 'four lights' == 'five lights'", + "E *", + "E - five lights*", + "E + four lights", + ] + ) + @pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"]) def test_incorrect_namespace_package( self, From 2dfea2742e7c4dad56c97fe634646456df7a0460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= Date: Tue, 3 Sep 2024 00:28:23 +0200 Subject: [PATCH 26/78] Merge pull request #12761 from pytest-dev/dependabot/github_actions/pypa/gh-action-pypi-publish-1.10.0 (cherry picked from commit c335a376b0f4f0f48b70b73910599049f6d84e09) --- .github/workflows/deploy.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index f5ea4d39764..3b1a16837b9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,7 +54,9 @@ jobs: path: dist - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.9.0 + uses: pypa/gh-action-pypi-publish@v1.10.0 + with: + attestations: true - name: Push tag run: | From 4873394d53635ef62d1915d23972ed4281a784eb Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 20:21:35 +0000 Subject: [PATCH 27/78] doc: Remove past training (#12772) (#12773) (cherry picked from commit c6a0ef175a802efd01115454e8d18d0711ca350d) Co-authored-by: Florian Bruhin --- doc/en/index.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index 95044e8a544..8b34d589643 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -2,7 +2,6 @@ .. sidebar:: **Next Open Trainings and Events** - - `pytest: Professionelles Testen (nicht nur) für Python `_, at `CH Open Workshoptage `_, **September 2nd 2024**, HSLU Rotkreuz (CH) - `Professional Testing with Python `_, via `Python Academy `_ (3 day in-depth training), **March 4th -- 6th 2025**, Leipzig (DE) / Remote Also see :doc:`previous talks and blogposts ` From 01cfcc9f2dda817b25511772593012fd93e092d0 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 20:46:47 +0000 Subject: [PATCH 28/78] Fix typos and introduce codespell pre-commit hook (#12769) (#12774) (cherry picked from commit 09e386e3e0df5a58ef9ab4f8e43f55c650633be7) Co-authored-by: Christian Clauss --- .pre-commit-config.yaml | 7 +++++++ AUTHORS | 1 + CONTRIBUTING.rst | 2 +- changelog/12769.misc.rst | 1 + doc/en/announce/release-2.3.0.rst | 4 ++-- doc/en/changelog.rst | 8 ++++---- doc/en/deprecations.rst | 2 +- doc/en/explanation/fixtures.rst | 2 +- doc/en/funcarg_compare.rst | 6 +++--- doc/en/how-to/fixtures.rst | 2 +- doc/en/how-to/unittest.rst | 2 +- pyproject.toml | 5 +++++ src/_pytest/config/__init__.py | 2 +- src/_pytest/debugging.py | 2 +- testing/python/fixtures.py | 2 +- testing/test_config.py | 2 +- 16 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 changelog/12769.misc.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8c7fde3f2c4..4cb466cbb2c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,13 @@ repos: hooks: - id: blacken-docs additional_dependencies: [black==24.1.1] +- repo: https://github.com/codespell-project/codespell + rev: v2.3.0 + hooks: + - id: codespell + args: ["--toml=pyproject.toml"] + additional_dependencies: + - tomli - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: diff --git a/AUTHORS b/AUTHORS index db993353689..8bc8ad5cbde 100644 --- a/AUTHORS +++ b/AUTHORS @@ -85,6 +85,7 @@ Chris NeJame Chris Rose Chris Wheeler Christian Boelsen +Christian Clauss Christian Fetzer Christian Neumüller Christian Theunert diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 6e96fd24c40..d615e5fb113 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -543,4 +543,4 @@ Closing issues When a pull request is submitted to fix an issue, add text like ``closes #XYZW`` to the PR description and/or commits (where ``XYZW`` is the issue number). See the `GitHub docs `_ for more information. -When an issue is due to user error (e.g. misunderstanding of a functionality), please politely explain to the user why the issue raised is really a non-issue and ask them to close the issue if they have no further questions. If the original requestor is unresponsive, the issue will be handled as described in the section `Handling stale issues/PRs`_ above. +When an issue is due to user error (e.g. misunderstanding of a functionality), please politely explain to the user why the issue raised is really a non-issue and ask them to close the issue if they have no further questions. If the original requester is unresponsive, the issue will be handled as described in the section `Handling stale issues/PRs`_ above. diff --git a/changelog/12769.misc.rst b/changelog/12769.misc.rst new file mode 100644 index 00000000000..aae4d137a0d --- /dev/null +++ b/changelog/12769.misc.rst @@ -0,0 +1 @@ +Fix typos discovered by codespell and add codespell to pre-commit hooks. diff --git a/doc/en/announce/release-2.3.0.rst b/doc/en/announce/release-2.3.0.rst index 6905b77b923..c405073ef40 100644 --- a/doc/en/announce/release-2.3.0.rst +++ b/doc/en/announce/release-2.3.0.rst @@ -6,10 +6,10 @@ and parametrized testing in Python. It is now easier, more efficient and more predictable to re-run the same tests with different fixture instances. Also, you can directly declare the caching "scope" of fixtures so that dependent tests throughout your whole test suite can -re-use database or other expensive fixture objects with ease. Lastly, +reuse database or other expensive fixture objects with ease. Lastly, it's possible for fixture functions (formerly known as funcarg factories) to use other fixtures, allowing for a completely modular and -re-usable fixture design. +reusable fixture design. For detailed info and tutorial-style examples, see: diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 3178f82044d..9da6f487f8d 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -215,7 +215,7 @@ Improved documentation -- by :user:`webknjaz` -- `#12577 `_: `CI` and `BUILD_NUMBER` environment variables role is discribed in +- `#12577 `_: `CI` and `BUILD_NUMBER` environment variables role is described in the reference doc. They now also appear when doing `pytest -h` -- by :user:`MarcBresson`. @@ -319,7 +319,7 @@ Bug Fixes - `#12367 `_: Fix a regression in pytest 8.2.0 where unittest class instances (a fresh one is created for each test) were not released promptly on test teardown but only on session teardown. -- `#12381 `_: Fix possible "Directory not empty" crashes arising from concurent cache dir (``.pytest_cache``) creation. Regressed in pytest 8.2.0. +- `#12381 `_: Fix possible "Directory not empty" crashes arising from concurrent cache dir (``.pytest_cache``) creation. Regressed in pytest 8.2.0. @@ -330,7 +330,7 @@ Improved Documentation - `#12356 `_: Added a subsection to the documentation for debugging flaky tests to mention - lack of thread safety in pytest as a possible source of flakyness. + lack of thread safety in pytest as a possible source of flakiness. - `#12363 `_: The documentation webpages now links to a canonical version to reduce outdated documentation in search engine results. @@ -676,7 +676,7 @@ Bug Fixes This bug was introduced in pytest 8.0.0rc1. -- `#9765 `_, `#11816 `_: Fixed a frustrating bug that afflicted some users with the only error being ``assert mod not in mods``. The issue was caused by the fact that ``str(Path(mod))`` and ``mod.__file__`` don't necessarily produce the same string, and was being erroneously used interchangably in some places in the code. +- `#9765 `_, `#11816 `_: Fixed a frustrating bug that afflicted some users with the only error being ``assert mod not in mods``. The issue was caused by the fact that ``str(Path(mod))`` and ``mod.__file__`` don't necessarily produce the same string, and was being erroneously used interchangeably in some places in the code. This fix also broke the internal API of ``PytestPluginManager.consider_conftest`` by introducing a new parameter -- we mention this in case it is being used by external code, even if marked as *private*. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index bf6268a4980..153d5195476 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -43,7 +43,7 @@ If the import attempt raises :class:`ModuleNotFoundError` (the usual case), then warning is emitted. This way, the usual cases will keep working the same way, while unexpected errors will now issue a warning, with -users being able to supress the warning by passing ``exc_type=ImportError`` explicitly. +users being able to suppress the warning by passing ``exc_type=ImportError`` explicitly. In ``9.0``, the warning will turn into an error, and in ``9.1`` :func:`pytest.importorskip` will only capture :class:`ModuleNotFoundError` by default and no warnings will be issued anymore -- but users can still capture diff --git a/doc/en/explanation/fixtures.rst b/doc/en/explanation/fixtures.rst index 0bb3bf49fb0..53d4796c825 100644 --- a/doc/en/explanation/fixtures.rst +++ b/doc/en/explanation/fixtures.rst @@ -75,7 +75,7 @@ style of setup/teardown functions: * fixture management scales from simple unit to complex functional testing, allowing to parametrize fixtures and tests according - to configuration and component options, or to re-use fixtures + to configuration and component options, or to reuse fixtures across function, class, module or whole test session scopes. * teardown logic can be easily, and safely managed, no matter how many fixtures diff --git a/doc/en/funcarg_compare.rst b/doc/en/funcarg_compare.rst index 8b900d30f20..bc5e7d3c515 100644 --- a/doc/en/funcarg_compare.rst +++ b/doc/en/funcarg_compare.rst @@ -16,7 +16,7 @@ Shortcomings of the previous ``pytest_funcarg__`` mechanism The pre pytest-2.3 funcarg mechanism calls a factory each time a funcarg for a test function is required. If a factory wants to -re-use a resource across different scopes, it often used +reuse a resource across different scopes, it often used the ``request.cached_setup()`` helper to manage caching of resources. Here is a basic example how we could implement a per-session Database object: @@ -107,7 +107,7 @@ the tests requiring "db" will run twice as well. The "mysql" and "pg" values will also be used for reporting the test-invocation variants. This new way of parametrizing funcarg factories should in many cases -allow to re-use already written factories because effectively +allow to reuse already written factories because effectively ``request.param`` was already used when test functions/classes were parametrized via :py:func:`metafunc.parametrize(indirect=True) ` calls. @@ -164,7 +164,7 @@ hook which are often used to setup global resources. This suffers from several problems: 1. in distributed testing the managing process would setup test resources - that are never needed because it only co-ordinates the test run + that are never needed because it only coordinates the test run activities of the worker processes. 2. if you only perform a collection (with "--collect-only") diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index f4d59ff93c0..d60635c4fea 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1487,7 +1487,7 @@ Modularity: using fixtures from a fixture function In addition to using fixtures in test functions, fixture functions can use other fixtures themselves. This contributes to a modular design -of your fixtures and allows re-use of framework-specific fixtures across +of your fixtures and allows reuse of framework-specific fixtures across many projects. As a simple example, we can extend the previous example and instantiate an object ``app`` where we stick the already defined ``smtp_connection`` resource into it: diff --git a/doc/en/how-to/unittest.rst b/doc/en/how-to/unittest.rst index 508aebde016..62e32b6d28f 100644 --- a/doc/en/how-to/unittest.rst +++ b/doc/en/how-to/unittest.rst @@ -109,7 +109,7 @@ achieves this by receiving a special ``request`` object which gives access to :ref:`the requesting test context ` such as the ``cls`` attribute, denoting the class from which the fixture is used. This architecture de-couples fixture writing from actual test -code and allows re-use of the fixture by a minimal reference, the fixture +code and allows reuse of the fixture by a minimal reference, the fixture name. So let's write an actual ``unittest.TestCase`` class using our fixture definition: diff --git a/pyproject.toml b/pyproject.toml index f3eba4a08a8..12764f3d127 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -306,6 +306,11 @@ disable = [ "wrong-import-position", # handled by isort / ruff ] +[tool.codespell] +ignore-words-list = "afile,asser,assertio,feld,hove,ned,noes,notin,paramete,parth,socio-economic,tesults,varius,wil" +skip = "*/plugin_list.rst" +write-changes = true + [tool.check-wheel-contents] # check-wheel-contents is executed by the build-and-inspect-python-package action. # W009: Wheel contains multiple toplevel library entries diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 0c1850df503..5c1becbf40c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -361,7 +361,7 @@ def _get_legacy_hook_marks( opt_names: tuple[str, ...], ) -> dict[str, bool]: if TYPE_CHECKING: - # abuse typeguard from importlib to avoid massive method type union thats lacking a alias + # abuse typeguard from importlib to avoid massive method type union that's lacking an alias assert inspect.isroutine(method) known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])} must_warn: list[str] = [] diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 3e1463fff26..2dfe321e968 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -332,7 +332,7 @@ def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None: def _enter_pdb( node: Node, excinfo: ExceptionInfo[BaseException], rep: BaseReport ) -> BaseReport: - # XXX we re-use the TerminalReporter's terminalwriter + # XXX we reuse the TerminalReporter's terminalwriter # because this seems to avoid some encoding related troubles # for not completely clear reasons. tw = node.config.pluginmanager.getplugin("terminalreporter")._tw diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8d2646309a8..46f0a762cbb 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -4338,7 +4338,7 @@ def test_func(self, f2, f1, m2): assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split() def test_parametrized_package_scope_reordering(self, pytester: Pytester) -> None: - """A paramaterized package-scoped fixture correctly reorders items to + """A parameterized package-scoped fixture correctly reorders items to minimize setups & teardowns. Regression test for #12328. diff --git a/testing/test_config.py b/testing/test_config.py index 232839399e2..daf69845bb1 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -917,7 +917,7 @@ def pytest_addoption(parser): # default for string is "" value = config.getini("string1") assert value == "" - # should return None if None is explicity set as default value + # should return None if None is explicitly set as default value # irrespective of the type argument value = config.getini("none_1") assert value is None From bc913d194ec009699194b016ca619d5ae7f22c91 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 5 Sep 2024 11:22:37 +0000 Subject: [PATCH 29/78] Streamline checks for verbose option (#12706) (#12778) Instead of calling `Config.option.verbose`, call the new `Config.get_verbosity` function to determine the verbosity level. This enables pytest to run correctly with the terminal plugin disabled. Fix #9422 (cherry picked from commit 72c682ff9773ad2690711105a100423ebf7c7c15) Co-authored-by: GTowers1 <130098608+GTowers1@users.noreply.github.com> --- changelog/9422.bugfix.rst | 3 +++ doc/en/example/simple.rst | 2 +- src/_pytest/cacheprovider.py | 2 +- src/_pytest/config/__init__.py | 2 +- src/_pytest/fixtures.py | 6 +++--- src/_pytest/logging.py | 2 +- src/_pytest/nodes.py | 4 ++-- src/_pytest/python.py | 2 +- src/_pytest/runner.py | 2 +- src/_pytest/stepwise.py | 2 +- testing/acceptance_test.py | 7 +++++++ 11 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 changelog/9422.bugfix.rst diff --git a/changelog/9422.bugfix.rst b/changelog/9422.bugfix.rst new file mode 100644 index 00000000000..38196340fd5 --- /dev/null +++ b/changelog/9422.bugfix.rst @@ -0,0 +1,3 @@ +Fix bug where disabling the terminal plugin via ``-p no:terminal`` would cause crashes related to missing the ``verbose`` option. + +-- by :user:`GTowers1` diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index a5e2e78c397..a14c34c19c3 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -460,7 +460,7 @@ display more information if applicable: def pytest_report_header(config): - if config.getoption("verbose") > 0: + if config.get_verbosity() > 0: return ["info1: did you know that ...", "did you?"] which will add info only when run with "--v": diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 7190810570e..1b236efdc9b 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -347,7 +347,7 @@ def get_last_failed_paths(self) -> set[Path]: return {x for x in result if x.exists()} def pytest_report_collectionfinish(self) -> str | None: - if self.active and self.config.getoption("verbose") >= 0: + if self.active and self.config.get_verbosity() >= 0: return f"run-last-failure: {self._report_status}" return None diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 5c1becbf40c..cc0a1449970 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1744,7 +1744,7 @@ def get_verbosity(self, verbosity_type: str | None = None) -> int: print(config.get_verbosity()) # 1 print(config.get_verbosity(Config.VERBOSITY_ASSERTIONS)) # 2 """ - global_level = self.option.verbose + global_level = self.getoption("verbose", default=0) assert isinstance(global_level, int) if verbosity_type is None: return global_level diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 7d0b40b150a..aaa92c63725 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1348,7 +1348,7 @@ def pytestconfig(request: FixtureRequest) -> Config: Example:: def test_foo(pytestconfig): - if pytestconfig.getoption("verbose") > 0: + if pytestconfig.get_verbosity() > 0: ... """ @@ -1807,7 +1807,7 @@ def _show_fixtures_per_test(config: Config, session: Session) -> None: session.perform_collect() invocation_dir = config.invocation_params.dir tw = _pytest.config.create_terminal_writer(config) - verbose = config.getvalue("verbose") + verbose = config.get_verbosity() def get_best_relpath(func) -> str: loc = getlocation(func, invocation_dir) @@ -1866,7 +1866,7 @@ def _showfixtures_main(config: Config, session: Session) -> None: session.perform_collect() invocation_dir = config.invocation_params.dir tw = _pytest.config.create_terminal_writer(config) - verbose = config.getvalue("verbose") + verbose = config.get_verbosity() fm = session._fixturemanager diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 9f3417e9af2..08c826ff6d4 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -794,7 +794,7 @@ def pytest_runtestloop(self, session: Session) -> Generator[None, object, object if session.config.option.collectonly: return (yield) - if self._log_cli_enabled() and self._config.getoption("verbose") < 1: + if self._log_cli_enabled() and self._config.get_verbosity() < 1: # The verbose flag is needed to avoid messy test progress output. self._config.option.verbose = 1 diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index cc678695848..51bc5174628 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -435,12 +435,12 @@ def _repr_failure_py( else: style = "long" - if self.config.getoption("verbose", 0) > 1: + if self.config.get_verbosity() > 1: truncate_locals = False else: truncate_locals = True - truncate_args = False if self.config.getoption("verbose", 0) > 2 else True + truncate_args = False if self.config.get_verbosity() > 2 else True # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False. # It is possible for a fixture/test to change the CWD while this code runs, which diff --git a/src/_pytest/python.py b/src/_pytest/python.py index b2199c76fc6..3478c34c47d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -512,7 +512,7 @@ def importtestmodule( ) from e except ImportError as e: exc_info = ExceptionInfo.from_current() - if config.getoption("verbose") < 2: + if config.get_verbosity() < 2: exc_info.traceback = exc_info.traceback.filter(filter_traceback) exc_repr = ( exc_info.getrepr(style="short") diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index d0e1cdc7faa..0b60301bf5f 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -71,7 +71,7 @@ def pytest_addoption(parser: Parser) -> None: def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None: durations = terminalreporter.config.option.durations durations_min = terminalreporter.config.option.durations_min - verbose = terminalreporter.config.getvalue("verbose") + verbose = terminalreporter.config.get_verbosity() if durations is None: return tr = terminalreporter diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index bd906ce63c1..c7860808c35 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -113,7 +113,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self.lastfailed = None def pytest_report_collectionfinish(self) -> str | None: - if self.config.getoption("verbose") >= 0 and self.report_status: + if self.config.get_verbosity() >= 0 and self.report_status: return f"stepwise: {self.report_status}" return None diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 01d911e8ca4..64a07ba8554 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1486,3 +1486,10 @@ def my_fixture(self, request): raise AssertionError( f"pytest command failed:\n{exc.stdout=!s}\n{exc.stderr=!s}" ) from exc + + +def test_no_terminal_plugin(pytester: Pytester) -> None: + """Smoke test to ensure pytest can execute without the terminal plugin (#9422).""" + pytester.makepyfile("def test(): assert 1 == 2") + result = pytester.runpytest("-pno:terminal", "-s") + assert result.ret == ExitCode.TESTS_FAILED From e5d32c73abcf4fa1362b15aaf660074de8f710d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= Date: Sat, 7 Sep 2024 02:13:45 +0200 Subject: [PATCH 30/78] Merge pull request #12784 from svenevs/fix/docs-example-parametrize-minor-typo [doc] Fix minor typo in example/parametrize.rst (cherry picked from commit 57cccf7f44b73e95a4cd2b09376a00ab35df5ec2) --- doc/en/example/parametrize.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 3e449b2eaa2..b175510da6a 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -686,5 +686,5 @@ For example: assert (6 / example_input) == e In the example above, the first three test cases should run without any -exceptions, while the fourth should raise a``ZeroDivisionError`` exception, +exceptions, while the fourth should raise a ``ZeroDivisionError`` exception, which is expected by pytest. From 0f10b6b0d8138d3539de75cb7b2e33167b6fc882 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 8 Sep 2024 14:53:19 +0000 Subject: [PATCH 31/78] Fix issue with slashes being turned into backslashes on Windows (#12760) (#12787) Fix #12745 (cherry picked from commit d35b802805b210c6d5281864d663f53c6a72f153) Co-authored-by: Nauman Ahmed <90570675+nauman897@users.noreply.github.com> --- AUTHORS | 1 + changelog/12745.bugfix.rst | 1 + src/_pytest/config/__init__.py | 8 ++++++-- testing/test_terminal.py | 35 ++++++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 changelog/12745.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 8bc8ad5cbde..4f1b172ff33 100644 --- a/AUTHORS +++ b/AUTHORS @@ -300,6 +300,7 @@ mrbean-bremen Nathan Goldbaum Nathaniel Compton Nathaniel Waisbrot +Nauman Ahmed Ned Batchelder Neil Martin Neven Mundar diff --git a/changelog/12745.bugfix.rst b/changelog/12745.bugfix.rst new file mode 100644 index 00000000000..420be931ce9 --- /dev/null +++ b/changelog/12745.bugfix.rst @@ -0,0 +1 @@ +Fixed an issue with backslashes being incorrectly converted in nodeid paths on Windows, ensuring consistent path handling across environments. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index cc0a1449970..3cca1479381 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1179,8 +1179,12 @@ def notify_exception( def cwd_relative_nodeid(self, nodeid: str) -> str: # nodeid's are relative to the rootpath, compute relative to cwd. if self.invocation_params.dir != self.rootpath: - fullpath = self.rootpath / nodeid - nodeid = bestrelpath(self.invocation_params.dir, fullpath) + base_path_part, *nodeid_part = nodeid.split("::") + # Only process path part + fullpath = self.rootpath / base_path_part + relative_path = bestrelpath(self.invocation_params.dir, fullpath) + + nodeid = "::".join([relative_path, *nodeid_part]) return nodeid @classmethod diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 11ad623fb6b..14c152d6123 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -3067,3 +3067,38 @@ def test_pass(): "*= 1 xpassed in * =*", ] ) + + +class TestNodeIDHandling: + def test_nodeid_handling_windows_paths(self, pytester: Pytester, tmp_path) -> None: + """Test the correct handling of Windows-style paths with backslashes.""" + pytester.makeini("[pytest]") # Change `config.rootpath` + + test_path = pytester.path / "tests" / "test_foo.py" + test_path.parent.mkdir() + os.chdir(test_path.parent) # Change `config.invocation_params.dir` + + test_path.write_text( + textwrap.dedent( + """ + import pytest + + @pytest.mark.parametrize("a", ["x/y", "C:/path", "\\\\", "C:\\\\path", "a::b/"]) + def test_x(a): + assert False + """ + ), + encoding="utf-8", + ) + + result = pytester.runpytest("-v") + + result.stdout.re_match_lines( + [ + r".*test_foo.py::test_x\[x/y\] .*FAILED.*", + r".*test_foo.py::test_x\[C:/path\] .*FAILED.*", + r".*test_foo.py::test_x\[\\\\\] .*FAILED.*", + r".*test_foo.py::test_x\[C:\\\\path\] .*FAILED.*", + r".*test_foo.py::test_x\[a::b/\] .*FAILED.*", + ] + ) From a9910a413a691e1b216e2235a9cbec0921117702 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 8 Sep 2024 14:59:54 +0000 Subject: [PATCH 32/78] Do not discover properties when iterating fixtures (#12781) (#12788) Resolves #12446 (cherry picked from commit c6a529032231ccddd3040e8ab1a5a756eb9ea4a0) Co-authored-by: Anthony Sottile --- changelog/12446.bugfix.rst | 1 + src/_pytest/fixtures.py | 16 +++++++++++++--- testing/python/collect.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 changelog/12446.bugfix.rst diff --git a/changelog/12446.bugfix.rst b/changelog/12446.bugfix.rst new file mode 100644 index 00000000000..2f591c48eed --- /dev/null +++ b/changelog/12446.bugfix.rst @@ -0,0 +1 @@ +Avoid calling ``@property`` (and other instance descriptors) during fixture discovery -- by :user:`asottile` diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index aaa92c63725..6b882fa3515 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -53,6 +53,7 @@ from _pytest.compat import NOTSET from _pytest.compat import NotSetType from _pytest.compat import safe_getattr +from _pytest.compat import safe_isclass from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode @@ -1724,17 +1725,26 @@ def parsefactories( if holderobj in self._holderobjseen: return + # Avoid accessing `@property` (and other descriptors) when iterating fixtures. + if not safe_isclass(holderobj) and not isinstance(holderobj, types.ModuleType): + holderobj_tp: object = type(holderobj) + else: + holderobj_tp = holderobj + self._holderobjseen.add(holderobj) for name in dir(holderobj): # The attribute can be an arbitrary descriptor, so the attribute - # access below can raise. safe_getatt() ignores such exceptions. - obj = safe_getattr(holderobj, name, None) - marker = getfixturemarker(obj) + # access below can raise. safe_getattr() ignores such exceptions. + obj_ub = safe_getattr(holderobj_tp, name, None) + marker = getfixturemarker(obj_ub) if not isinstance(marker, FixtureFunctionMarker): # Magic globals with __getattr__ might have got us a wrong # fixture attribute. continue + # OK we know it is a fixture -- now safe to look up on the _instance_. + obj = getattr(holderobj, name) + if marker.name: name = marker.name diff --git a/testing/python/collect.py b/testing/python/collect.py index 06386611279..530f1c340ff 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -263,6 +263,43 @@ def prop(self): result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED + def test_does_not_discover_properties(self, pytester: Pytester) -> None: + """Regression test for #12446.""" + pytester.makepyfile( + """\ + class TestCase: + @property + def oops(self): + raise SystemExit('do not call me!') + """ + ) + result = pytester.runpytest() + assert result.ret == ExitCode.NO_TESTS_COLLECTED + + def test_does_not_discover_instance_descriptors(self, pytester: Pytester) -> None: + """Regression test for #12446.""" + pytester.makepyfile( + """\ + # not `@property`, but it acts like one + # this should cover the case of things like `@cached_property` / etc. + class MyProperty: + def __init__(self, func): + self._func = func + def __get__(self, inst, owner): + if inst is None: + return self + else: + return self._func.__get__(inst, owner)() + + class TestCase: + @MyProperty + def oops(self): + raise SystemExit('do not call me!') + """ + ) + result = pytester.runpytest() + assert result.ret == ExitCode.NO_TESTS_COLLECTED + def test_abstract_class_is_not_collected(self, pytester: Pytester) -> None: """Regression test for #12275 (non-unittest version).""" pytester.makepyfile( From 0dabdcfe4de99147a07bd577804b60818ea25bc4 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 18:40:49 -0300 Subject: [PATCH 33/78] Include co-authors in release announcement (#12795) (#12797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As noted in #12789, the `release.py` script did not consider `Co-authored-by` fields, and since we introduced the new backport bot, this problem became more apparent due to how the backport commit and PR are generated. Previously, the list of authors produced by the script in the #12789 branch was: ``` * Avasam * Bruno Oliveira * Ronny Pfannschmidt * Sviatoslav Sydorenko (Святослав Сидоренко) ``` With this script: ``` * Anthony Sottile * Avasam * Bruno Oliveira * Christian Clauss * Eugene Mwangi * Florian Bruhin * GTowers1 * Nauman Ahmed * Pierre Sassoulas * Reagan Lee * Ronny Pfannschmidt * Stefaan Lippens * Sviatoslav Sydorenko (Святослав Сидоренко) * dongfangtianyu ``` (cherry picked from commit e8504ed49be73e516ee1559a250daec659e96b9d) Co-authored-by: Bruno Oliveira --- scripts/release.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/scripts/release.py b/scripts/release.py index 545919cd60b..aef5d6d5f73 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -6,6 +6,7 @@ import argparse import os from pathlib import Path +import re from subprocess import call from subprocess import check_call from subprocess import check_output @@ -16,17 +17,27 @@ def announce(version: str, template_name: str, doc_version: str) -> None: """Generates a new release announcement entry in the docs.""" - # Get our list of authors + # Get our list of authors and co-authors. stdout = check_output(["git", "describe", "--abbrev=0", "--tags"], encoding="UTF-8") last_version = stdout.strip() + rev_range = f"{last_version}..HEAD" - stdout = check_output( - ["git", "log", f"{last_version}..HEAD", "--format=%aN"], encoding="UTF-8" + authors = check_output( + ["git", "log", rev_range, "--format=%aN"], encoding="UTF-8" + ).splitlines() + + co_authors_output = check_output( + ["git", "log", rev_range, "--format=%(trailers:key=Co-authored-by) "], + encoding="UTF-8", ) + co_authors: list[str] = [] + for co_author_line in co_authors_output.splitlines(): + if m := re.search(r"Co-authored-by: (.+?)<", co_author_line): + co_authors.append(m.group(1).strip()) contributors = { name - for name in stdout.splitlines() + for name in authors + co_authors if not name.endswith("[bot]") and name != "pytest bot" } From 972f307c7861ae498e705d3d12e003fa4b035ac0 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Mon, 9 Sep 2024 21:44:43 +0000 Subject: [PATCH 34/78] Prepare release version 8.3.3 --- changelog/12446.bugfix.rst | 1 - changelog/12659.bugfix.rst | 1 - changelog/12663.doc.rst | 1 - changelog/12667.bugfix.rst | 1 - changelog/12678.doc.rst | 1 - changelog/12744.bugfix.rst | 1 - changelog/12745.bugfix.rst | 1 - changelog/12769.misc.rst | 1 - changelog/6682.bugfix.rst | 1 - changelog/9422.bugfix.rst | 3 -- doc/en/announce/index.rst | 1 + doc/en/announce/release-8.3.3.rst | 31 +++++++++++++++++++ doc/en/builtin.rst | 18 +++++------ doc/en/changelog.rst | 46 +++++++++++++++++++++++++++++ doc/en/example/parametrize.rst | 6 ++-- doc/en/example/pythoncollection.rst | 4 +-- doc/en/getting-started.rst | 2 +- doc/en/how-to/fixtures.rst | 2 +- 18 files changed, 94 insertions(+), 28 deletions(-) delete mode 100644 changelog/12446.bugfix.rst delete mode 100644 changelog/12659.bugfix.rst delete mode 100644 changelog/12663.doc.rst delete mode 100644 changelog/12667.bugfix.rst delete mode 100644 changelog/12678.doc.rst delete mode 100644 changelog/12744.bugfix.rst delete mode 100644 changelog/12745.bugfix.rst delete mode 100644 changelog/12769.misc.rst delete mode 100644 changelog/6682.bugfix.rst delete mode 100644 changelog/9422.bugfix.rst create mode 100644 doc/en/announce/release-8.3.3.rst diff --git a/changelog/12446.bugfix.rst b/changelog/12446.bugfix.rst deleted file mode 100644 index 2f591c48eed..00000000000 --- a/changelog/12446.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Avoid calling ``@property`` (and other instance descriptors) during fixture discovery -- by :user:`asottile` diff --git a/changelog/12659.bugfix.rst b/changelog/12659.bugfix.rst deleted file mode 100644 index 297381b9602..00000000000 --- a/changelog/12659.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed the issue of not displaying assertion failure differences when using the parameter ``--import-mode=importlib`` in pytest>=8.1. diff --git a/changelog/12663.doc.rst b/changelog/12663.doc.rst deleted file mode 100644 index e665bc67a10..00000000000 --- a/changelog/12663.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Clarify that the `pytest_deselected` hook should be called from `pytest_collection_modifyitems` hook implementations when items are deselected. diff --git a/changelog/12667.bugfix.rst b/changelog/12667.bugfix.rst deleted file mode 100644 index eceee86cb53..00000000000 --- a/changelog/12667.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a regression where type change in `ExceptionInfo.errisinstance` caused `mypy` to fail. diff --git a/changelog/12678.doc.rst b/changelog/12678.doc.rst deleted file mode 100644 index 9d79ec41007..00000000000 --- a/changelog/12678.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Remove erroneous quotes from `tmp_path_retention_policy` example in docs. diff --git a/changelog/12744.bugfix.rst b/changelog/12744.bugfix.rst deleted file mode 100644 index 7814b59c180..00000000000 --- a/changelog/12744.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed typing compatibility with Python 3.9 or less -- replaced `typing.Self` with `typing_extensions.Self` -- by :user:`Avasam` diff --git a/changelog/12745.bugfix.rst b/changelog/12745.bugfix.rst deleted file mode 100644 index 420be931ce9..00000000000 --- a/changelog/12745.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed an issue with backslashes being incorrectly converted in nodeid paths on Windows, ensuring consistent path handling across environments. diff --git a/changelog/12769.misc.rst b/changelog/12769.misc.rst deleted file mode 100644 index aae4d137a0d..00000000000 --- a/changelog/12769.misc.rst +++ /dev/null @@ -1 +0,0 @@ -Fix typos discovered by codespell and add codespell to pre-commit hooks. diff --git a/changelog/6682.bugfix.rst b/changelog/6682.bugfix.rst deleted file mode 100644 index 7f756cbd9c2..00000000000 --- a/changelog/6682.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed bug where the verbosity levels where not being respected when printing the "msg" part of failed assertion (as in ``assert condition, msg``). diff --git a/changelog/9422.bugfix.rst b/changelog/9422.bugfix.rst deleted file mode 100644 index 38196340fd5..00000000000 --- a/changelog/9422.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix bug where disabling the terminal plugin via ``-p no:terminal`` would cause crashes related to missing the ``verbose`` option. - --- by :user:`GTowers1` diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 09311a1a1ab..61e4a772beb 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-8.3.3 release-8.3.2 release-8.3.1 release-8.3.0 diff --git a/doc/en/announce/release-8.3.3.rst b/doc/en/announce/release-8.3.3.rst new file mode 100644 index 00000000000..5e3eb36b921 --- /dev/null +++ b/doc/en/announce/release-8.3.3.rst @@ -0,0 +1,31 @@ +pytest-8.3.3 +======================================= + +pytest 8.3.3 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Anthony Sottile +* Avasam +* Bruno Oliveira +* Christian Clauss +* Eugene Mwangi +* Florian Bruhin +* GTowers1 +* Nauman Ahmed +* Pierre Sassoulas +* Reagan Lee +* Ronny Pfannschmidt +* Stefaan Lippens +* Sviatoslav Sydorenko (Святослав Сидоренко) +* dongfangtianyu + + +Happy testing, +The pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index b4c0f91f8e5..9b406a6a512 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a cachedir: .pytest_cache rootdir: /home/sweet/project collected 0 items - cache -- .../_pytest/cacheprovider.py:558 + cache -- .../_pytest/cacheprovider.py:556 Return a cache object that can persist state between testing sessions. cache.get(key, default) @@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Values can be any object handled by the json stdlib module. - capsysbinary -- .../_pytest/capture.py:1005 + capsysbinary -- .../_pytest/capture.py:1006 Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` @@ -51,7 +51,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capsysbinary.readouterr() assert captured.out == b"hello\n" - capfd -- .../_pytest/capture.py:1033 + capfd -- .../_pytest/capture.py:1034 Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -69,7 +69,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capfd.readouterr() assert captured.out == "hello\n" - capfdbinary -- .../_pytest/capture.py:1061 + capfdbinary -- .../_pytest/capture.py:1062 Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -87,7 +87,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capfdbinary.readouterr() assert captured.out == b"hello\n" - capsys -- .../_pytest/capture.py:977 + capsys -- .../_pytest/capture.py:978 Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method @@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capsys.readouterr() assert captured.out == "hello\n" - doctest_namespace [session scope] -- .../_pytest/doctest.py:740 + doctest_namespace [session scope] -- .../_pytest/doctest.py:741 Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. @@ -119,14 +119,14 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a For more details: :ref:`doctest_namespace`. - pytestconfig [session scope] -- .../_pytest/fixtures.py:1344 + pytestconfig [session scope] -- .../_pytest/fixtures.py:1345 Session-scoped fixture that returns the session's :class:`pytest.Config` object. Example:: def test_foo(pytestconfig): - if pytestconfig.getoption("verbose") > 0: + if pytestconfig.get_verbosity() > 0: ... record_property -- .../_pytest/junitxml.py:280 @@ -196,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a .. _legacy_path: https://py.readthedocs.io/en/latest/path.html - caplog -- .../_pytest/logging.py:600 + caplog -- .../_pytest/logging.py:598 Access and control log capturing. Captured logs are available through the following properties/methods:: diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 9da6f487f8d..9f30c86be3a 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -31,6 +31,52 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 8.3.3 (2024-09-09) +========================= + +Bug fixes +--------- + +- `#12446 `_: Avoid calling ``@property`` (and other instance descriptors) during fixture discovery -- by :user:`asottile` + + +- `#12659 `_: Fixed the issue of not displaying assertion failure differences when using the parameter ``--import-mode=importlib`` in pytest>=8.1. + + +- `#12667 `_: Fixed a regression where type change in `ExceptionInfo.errisinstance` caused `mypy` to fail. + + +- `#12744 `_: Fixed typing compatibility with Python 3.9 or less -- replaced `typing.Self` with `typing_extensions.Self` -- by :user:`Avasam` + + +- `#12745 `_: Fixed an issue with backslashes being incorrectly converted in nodeid paths on Windows, ensuring consistent path handling across environments. + + +- `#6682 `_: Fixed bug where the verbosity levels where not being respected when printing the "msg" part of failed assertion (as in ``assert condition, msg``). + + +- `#9422 `_: Fix bug where disabling the terminal plugin via ``-p no:terminal`` would cause crashes related to missing the ``verbose`` option. + + -- by :user:`GTowers1` + + + +Improved documentation +---------------------- + +- `#12663 `_: Clarify that the `pytest_deselected` hook should be called from `pytest_collection_modifyitems` hook implementations when items are deselected. + + +- `#12678 `_: Remove erroneous quotes from `tmp_path_retention_policy` example in docs. + + + +Miscellaneous internal changes +------------------------------ + +- `#12769 `_: Fix typos discovered by codespell and add codespell to pre-commit hooks. + + pytest 8.3.2 (2024-07-24) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index b175510da6a..fa43308d045 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -162,7 +162,7 @@ objects, they are still using the default pytest representation: rootdir: /home/sweet/project collected 8 items - + @@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia rootdir: /home/sweet/project collected 4 items - + @@ -318,7 +318,7 @@ Let's first see how it looks like at collection time: rootdir: /home/sweet/project collected 2 items - + diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 5bd03035c14..42a603f114c 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -152,7 +152,7 @@ The test collection would look like this: configfile: pytest.ini collected 2 items - + @@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this: configfile: pytest.ini collected 3 items - + diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index 050fd2d80ec..faf81154c48 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 8.3.2 + pytest 8.3.3 .. _`simpletest`: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index d60635c4fea..4f6e8cbee06 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used: rootdir: /home/sweet/project collected 12 items - + From d0f136fe64f9374f18a04562305b178fb380d1ec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 09:04:10 -0300 Subject: [PATCH 35/78] build(deps): Bump pypa/gh-action-pypi-publish from 1.10.0 to 1.10.1 (#12790) Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.10.0 to 1.10.1. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.10.0...v1.10.1) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 3b1a16837b9..a58dc83639a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,7 +54,7 @@ jobs: path: dist - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.10.0 + uses: pypa/gh-action-pypi-publish@v1.10.1 with: attestations: true From 0cfca0801b9eb6e0aa811e6c18fe03f06fc1c8c8 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 19 Sep 2024 12:25:06 -0300 Subject: [PATCH 36/78] Added Databricks fixture plugin for PyTest (#12826) (#12827) Adding it as a `ADDITIONAL_PROJECTS`, because our naming conventions can't fit `pytest-` as a prefix. https://github.com/databrickslabs/pytester https://pypi.org/project/databricks-labs-pytester/ (cherry picked from commit 4508d0b2d465eb02c97dc526c0bc8119e2b162f5) Co-authored-by: Serge Smertin <259697+nfx@users.noreply.github.com> --- scripts/update-plugin-list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/update-plugin-list.py b/scripts/update-plugin-list.py index 75df0ddba40..556004d9e98 100644 --- a/scripts/update-plugin-list.py +++ b/scripts/update-plugin-list.py @@ -66,6 +66,7 @@ "logot", "nuts", "flask_fixture", + "databricks-labs-pytester", } From dd259f6a967288f7cbc3d80d31b3e75dd169a751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= Date: Sun, 22 Sep 2024 01:24:15 +0200 Subject: [PATCH 37/78] Merge pull request #12829 from Dreamsorcerer/patch-1 Remove missing msg parameter from docs (cherry picked from commit df5203e0b5ba16909532003b95a90216de6b1c9a) --- doc/en/reference/reference.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index f1110097ecc..a3c81cf7748 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -54,7 +54,7 @@ pytest.fail **Tutorial**: :ref:`skipping` -.. autofunction:: pytest.fail(reason, [pytrace=True, msg=None]) +.. autofunction:: pytest.fail(reason, [pytrace=True]) .. class:: pytest.fail.Exception @@ -63,7 +63,7 @@ pytest.fail pytest.skip ~~~~~~~~~~~ -.. autofunction:: pytest.skip(reason, [allow_module_level=False, msg=None]) +.. autofunction:: pytest.skip(reason, [allow_module_level=False]) .. class:: pytest.skip.Exception @@ -88,7 +88,7 @@ pytest.xfail pytest.exit ~~~~~~~~~~~ -.. autofunction:: pytest.exit(reason, [returncode=None, msg=None]) +.. autofunction:: pytest.exit(reason, [returncode=None]) .. class:: pytest.exit.Exception From 72c9c350cb1ea4449122c52030880a62628967d6 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 25 Sep 2024 13:16:15 +0000 Subject: [PATCH 38/78] Docs: improve "How to capture stdout/stderr output" (#12840) (#12841) Improve the section by reordering how the topics about text/binary capture are presented, and add proper links to the fixtures. Co-authored-by: Bruno Oliveira (cherry picked from commit 326faa25f4e776f082eea5603d84b0812b57773c) Co-authored-by: Pradeep Kumar --- doc/en/how-to/capture-stdout-stderr.rst | 42 +++++++++---------------- 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/doc/en/how-to/capture-stdout-stderr.rst b/doc/en/how-to/capture-stdout-stderr.rst index 5e23f0c024e..9f7ddce3499 100644 --- a/doc/en/how-to/capture-stdout-stderr.rst +++ b/doc/en/how-to/capture-stdout-stderr.rst @@ -106,9 +106,10 @@ of the failing function and hide the other one: Accessing captured output from a test function --------------------------------------------------- -The ``capsys``, ``capsysbinary``, ``capfd``, and ``capfdbinary`` fixtures -allow access to stdout/stderr output created during test execution. Here is -an example test function that performs some output related checks: +The :fixture:`capsys`, :fixture:`capsysbinary`, :fixture:`capfd`, and :fixture:`capfdbinary` fixtures +allow access to ``stdout``/``stderr`` output created during test execution. + +Here is an example test function that performs some output related checks: .. code-block:: python @@ -125,40 +126,27 @@ an example test function that performs some output related checks: The ``readouterr()`` call snapshots the output so far - and capturing will be continued. After the test function finishes the original streams will -be restored. Using ``capsys`` this way frees your +be restored. Using :fixture:`capsys` this way frees your test from having to care about setting/resetting output streams and also interacts well with pytest's own per-test capturing. -If you want to capture on filedescriptor level you can use -the ``capfd`` fixture which offers the exact -same interface but allows to also capture output from -libraries or subprocesses that directly write to operating -system level output streams (FD1 and FD2). - - - The return value from ``readouterr`` changed to a ``namedtuple`` with two attributes, ``out`` and ``err``. - - -If the code under test writes non-textual data, you can capture this using -the ``capsysbinary`` fixture which instead returns ``bytes`` from +If the code under test writes non-textual data (``bytes``), you can capture this using +the :fixture:`capsysbinary` fixture which instead returns ``bytes`` from the ``readouterr`` method. +If you want to capture at the file descriptor level you can use +the :fixture:`capfd` fixture which offers the exact +same interface but allows to also capture output from +libraries or subprocesses that directly write to operating +system level output streams (FD1 and FD2). Similarly to :fixture:`capsysbinary`, :fixture:`capfdbinary` can be +used to capture ``bytes`` at the file descriptor level. - -If the code under test writes non-textual data, you can capture this using -the ``capfdbinary`` fixture which instead returns ``bytes`` from -the ``readouterr`` method. The ``capfdbinary`` fixture operates on the -filedescriptor level. - - - - -To temporarily disable capture within a test, both ``capsys`` -and ``capfd`` have a ``disabled()`` method that can be used +To temporarily disable capture within a test, the capture fixtures +have a ``disabled()`` method that can be used as a context manager, disabling capture inside the ``with`` block: .. code-block:: python From 3d3ec5724c6f76bc07d0631ec8061f26f9ecac4c Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 26 Sep 2024 01:26:01 +0000 Subject: [PATCH 39/78] Fix KeyError with importlib mode (directories with same name) (#12752) (#12843) Directories inside a namespace package with the same name as the namespace package would cause a `KeyError` with `--import-mode=importlib`. Fixes #12592 Co-authored-by: Bruno Oliveira (cherry picked from commit 6486c3f3a858a0c8043f5c3f7c24297b82a0abe4) Co-authored-by: dongfangtianyu <7629022+dongfangtianyu@users.noreply.github.com> --- changelog/12592.bugfix.rst | 1 + src/_pytest/pathlib.py | 131 +++++++++++++++++++++++++++---------- testing/test_pathlib.py | 74 ++++++++++++++++++++- 3 files changed, 171 insertions(+), 35 deletions(-) create mode 100644 changelog/12592.bugfix.rst diff --git a/changelog/12592.bugfix.rst b/changelog/12592.bugfix.rst new file mode 100644 index 00000000000..605783bcab4 --- /dev/null +++ b/changelog/12592.bugfix.rst @@ -0,0 +1 @@ +Fixed :class:`KeyError` crash when using ``--import-mode=importlib`` in a directory layout where a directory contains a child directory with the same name. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 81e52ea729d..dd36559ce1b 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -10,6 +10,7 @@ import fnmatch from functools import partial from importlib.machinery import ModuleSpec +from importlib.machinery import PathFinder import importlib.util import itertools import os @@ -37,8 +38,12 @@ from _pytest.warning_types import PytestWarning -LOCK_TIMEOUT = 60 * 60 * 24 * 3 +if sys.version_info < (3, 11): + from importlib._bootstrap_external import _NamespaceLoader as NamespaceLoader +else: + from importlib.machinery import NamespaceLoader +LOCK_TIMEOUT = 60 * 60 * 24 * 3 _AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath) @@ -611,13 +616,78 @@ def _import_module_using_spec( module_name: str, module_path: Path, module_location: Path, *, insert_modules: bool ) -> ModuleType | None: """ - Tries to import a module by its canonical name, path to the .py file, and its - parent location. + Tries to import a module by its canonical name, path, and its parent location. + + :param module_name: + The expected module name, will become the key of `sys.modules`. + + :param module_path: + The file path of the module, for example `/foo/bar/test_demo.py`. + If module is a package, pass the path to the `__init__.py` of the package. + If module is a namespace package, pass directory path. + + :param module_location: + The parent location of the module. + If module is a package, pass the directory containing the `__init__.py` file. :param insert_modules: - If True, will call insert_missing_modules to create empty intermediate modules - for made-up module names (when importing test files not reachable from sys.path). + If True, will call `insert_missing_modules` to create empty intermediate modules + with made-up module names (when importing test files not reachable from `sys.path`). + + Example 1 of parent_module_*: + + module_name: "a.b.c.demo" + module_path: Path("a/b/c/demo.py") + module_location: Path("a/b/c/") + if "a.b.c" is package ("a/b/c/__init__.py" exists), then + parent_module_name: "a.b.c" + parent_module_path: Path("a/b/c/__init__.py") + parent_module_location: Path("a/b/c/") + else: + parent_module_name: "a.b.c" + parent_module_path: Path("a/b/c") + parent_module_location: Path("a/b/") + + Example 2 of parent_module_*: + + module_name: "a.b.c" + module_path: Path("a/b/c/__init__.py") + module_location: Path("a/b/c/") + if "a.b" is package ("a/b/__init__.py" exists), then + parent_module_name: "a.b" + parent_module_path: Path("a/b/__init__.py") + parent_module_location: Path("a/b/") + else: + parent_module_name: "a.b" + parent_module_path: Path("a/b/") + parent_module_location: Path("a/") """ + # Attempt to import the parent module, seems is our responsibility: + # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311 + parent_module_name, _, name = module_name.rpartition(".") + parent_module: ModuleType | None = None + if parent_module_name: + parent_module = sys.modules.get(parent_module_name) + if parent_module is None: + # Get parent_location based on location, get parent_path based on path. + if module_path.name == "__init__.py": + # If the current module is in a package, + # need to leave the package first and then enter the parent module. + parent_module_path = module_path.parent.parent + else: + parent_module_path = module_path.parent + + if (parent_module_path / "__init__.py").is_file(): + # If the parent module is a package, loading by __init__.py file. + parent_module_path = parent_module_path / "__init__.py" + + parent_module = _import_module_using_spec( + parent_module_name, + parent_module_path, + parent_module_path.parent, + insert_modules=insert_modules, + ) + # Checking with sys.meta_path first in case one of its hooks can import this module, # such as our own assertion-rewrite hook. for meta_importer in sys.meta_path: @@ -627,36 +697,18 @@ def _import_module_using_spec( if spec_matches_module_path(spec, module_path): break else: - spec = importlib.util.spec_from_file_location(module_name, str(module_path)) + loader = None + if module_path.is_dir(): + # The `spec_from_file_location` matches a loader based on the file extension by default. + # For a namespace package, need to manually specify a loader. + loader = NamespaceLoader(name, module_path, PathFinder()) + + spec = importlib.util.spec_from_file_location( + module_name, str(module_path), loader=loader + ) if spec_matches_module_path(spec, module_path): assert spec is not None - # Attempt to import the parent module, seems is our responsibility: - # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311 - parent_module_name, _, name = module_name.rpartition(".") - parent_module: ModuleType | None = None - if parent_module_name: - parent_module = sys.modules.get(parent_module_name) - if parent_module is None: - # Find the directory of this module's parent. - parent_dir = ( - module_path.parent.parent - if module_path.name == "__init__.py" - else module_path.parent - ) - # Consider the parent module path as its __init__.py file, if it has one. - parent_module_path = ( - parent_dir / "__init__.py" - if (parent_dir / "__init__.py").is_file() - else parent_dir - ) - parent_module = _import_module_using_spec( - parent_module_name, - parent_module_path, - parent_dir, - insert_modules=insert_modules, - ) - # Find spec and import this module. mod = importlib.util.module_from_spec(spec) sys.modules[module_name] = mod @@ -675,10 +727,21 @@ def _import_module_using_spec( def spec_matches_module_path(module_spec: ModuleSpec | None, module_path: Path) -> bool: """Return true if the given ModuleSpec can be used to import the given module path.""" - if module_spec is None or module_spec.origin is None: + if module_spec is None: return False - return Path(module_spec.origin) == module_path + if module_spec.origin: + return Path(module_spec.origin) == module_path + + # Compare the path with the `module_spec.submodule_Search_Locations` in case + # the module is part of a namespace package. + # https://docs.python.org/3/library/importlib.html#importlib.machinery.ModuleSpec.submodule_search_locations + if module_spec.submodule_search_locations: # can be None. + for path in module_spec.submodule_search_locations: + if Path(path) == module_path: + return True + + return False # Implement a special _is_same function on Windows which returns True if the two filenames diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 7c0a0e03d76..62359303f3b 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -17,7 +17,9 @@ from typing import Sequence import unittest.mock +from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch +from _pytest.pathlib import _import_module_using_spec from _pytest.pathlib import bestrelpath from _pytest.pathlib import commonpath from _pytest.pathlib import compute_module_name @@ -36,6 +38,7 @@ from _pytest.pathlib import resolve_package_path from _pytest.pathlib import resolve_pkg_root_and_module_name from _pytest.pathlib import safe_exists +from _pytest.pathlib import spec_matches_module_path from _pytest.pathlib import symlink_or_skip from _pytest.pathlib import visit from _pytest.pytester import Pytester @@ -416,7 +419,7 @@ def test_no_meta_path_found( del sys.modules[module.__name__] monkeypatch.setattr( - importlib.util, "spec_from_file_location", lambda *args: None + importlib.util, "spec_from_file_location", lambda *args, **kwargs: None ) with pytest.raises(ImportError): import_path( @@ -780,6 +783,62 @@ def test_insert_missing_modules( insert_missing_modules(modules, "") assert modules == {} + @pytest.mark.parametrize("b_is_package", [True, False]) + @pytest.mark.parametrize("insert_modules", [True, False]) + def test_import_module_using_spec( + self, b_is_package, insert_modules, tmp_path: Path + ): + """ + Verify that `_import_module_using_spec` can obtain a spec based on the path, thereby enabling the import. + When importing, not only the target module is imported, but also the parent modules are recursively imported. + """ + file_path = tmp_path / "a/b/c/demo.py" + file_path.parent.mkdir(parents=True) + file_path.write_text("my_name='demo'", encoding="utf-8") + + if b_is_package: + (tmp_path / "a/b/__init__.py").write_text( + "my_name='b.__init__'", encoding="utf-8" + ) + + mod = _import_module_using_spec( + "a.b.c.demo", + file_path, + file_path.parent, + insert_modules=insert_modules, + ) + + # target module is imported + assert mod is not None + assert spec_matches_module_path(mod.__spec__, file_path) is True + + mod_demo = sys.modules["a.b.c.demo"] + assert "demo.py" in str(mod_demo) + assert mod_demo.my_name == "demo" # Imported and available for use + + # parent modules are recursively imported. + mod_a = sys.modules["a"] + mod_b = sys.modules["a.b"] + mod_c = sys.modules["a.b.c"] + + assert mod_a.b is mod_b + assert mod_a.b.c is mod_c + assert mod_a.b.c.demo is mod_demo + + assert "namespace" in str(mod_a).lower() + assert "namespace" in str(mod_c).lower() + + # Compatibility package and namespace package. + if b_is_package: + assert "namespace" not in str(mod_b).lower() + assert "__init__.py" in str(mod_b).lower() # Imported __init__.py + assert mod_b.my_name == "b.__init__" # Imported and available for use + + else: + assert "namespace" in str(mod_b).lower() + with pytest.raises(AttributeError): # Not imported __init__.py + assert mod_b.my_name + def test_parent_contains_child_module_attribute( self, monkeypatch: MonkeyPatch, tmp_path: Path ): @@ -1542,6 +1601,19 @@ def test_full_ns_packages_without_init_files( ) == (tmp_path / "src/dist2", "ns.a.core.foo.m") +def test_ns_import_same_name_directory_12592( + tmp_path: Path, pytester: Pytester +) -> None: + """Regression for `--import-mode=importlib` with directory parent and child with same name (#12592).""" + y_dir = tmp_path / "x/y/y" + y_dir.mkdir(parents=True) + test_y = tmp_path / "x/y/test_y.py" + test_y.write_text("def test(): pass", encoding="UTF-8") + + result = pytester.runpytest("--import-mode=importlib", test_y) + assert result.ret == ExitCode.OK + + def test_is_importable(pytester: Pytester) -> None: pytester.syspathinsert() From 40741c4aca50582cc9701ff01504b9e6dcd3396f Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 9 Oct 2024 19:28:08 +0000 Subject: [PATCH 40/78] Preserve source positions for assertion rewriting (#12867) Closes #12818 (cherry picked from commit fb740251fe1d7facd1f8a341e22d4aa07ba7833c) Co-authored-by: Frank Hoffmann <44680962+15r10nk@users.noreply.github.com> --- AUTHORS | 1 + changelog/12818.bugfix.rst | 1 + src/_pytest/assertion/rewrite.py | 21 ++- testing/test_assertrewrite.py | 211 ++++++++++++++++++++++++++++++- 4 files changed, 223 insertions(+), 11 deletions(-) create mode 100644 changelog/12818.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 4f1b172ff33..d0efcdfe122 100644 --- a/AUTHORS +++ b/AUTHORS @@ -159,6 +159,7 @@ Feng Ma Florian Bruhin Florian Dahlitz Floris Bruynooghe +Frank Hoffmann Fraser Stark Gabriel Landau Gabriel Reis diff --git a/changelog/12818.bugfix.rst b/changelog/12818.bugfix.rst new file mode 100644 index 00000000000..9d74f2fda2a --- /dev/null +++ b/changelog/12818.bugfix.rst @@ -0,0 +1 @@ +Assertion rewriting now preserves the source ranges of the original instructions, making it play well with tools that deal with the ``AST``, like `executing `__. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index a7a92c0f1fe..37c09b03467 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -792,7 +792,7 @@ def assign(self, expr: ast.expr) -> ast.Name: """Give *expr* a name.""" name = self.variable() self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr)) - return ast.Name(name, ast.Load()) + return ast.copy_location(ast.Name(name, ast.Load()), expr) def display(self, expr: ast.expr) -> ast.expr: """Call saferepr on the expression.""" @@ -975,7 +975,10 @@ def visit_Assert(self, assert_: ast.Assert) -> list[ast.stmt]: # Fix locations (line numbers/column offsets). for stmt in self.statements: for node in traverse_node(stmt): - ast.copy_location(node, assert_) + if getattr(node, "lineno", None) is None: + # apply the assertion location to all generated ast nodes without source location + # and preserve the location of existing nodes or generated nodes with an correct location. + ast.copy_location(node, assert_) return self.statements def visit_NamedExpr(self, name: ast.NamedExpr) -> tuple[ast.NamedExpr, str]: @@ -1052,7 +1055,7 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> tuple[ast.Name, str]: def visit_UnaryOp(self, unary: ast.UnaryOp) -> tuple[ast.Name, str]: pattern = UNARY_MAP[unary.op.__class__] operand_res, operand_expl = self.visit(unary.operand) - res = self.assign(ast.UnaryOp(unary.op, operand_res)) + res = self.assign(ast.copy_location(ast.UnaryOp(unary.op, operand_res), unary)) return res, pattern % (operand_expl,) def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]: @@ -1060,7 +1063,9 @@ def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]: left_expr, left_expl = self.visit(binop.left) right_expr, right_expl = self.visit(binop.right) explanation = f"({left_expl} {symbol} {right_expl})" - res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) + res = self.assign( + ast.copy_location(ast.BinOp(left_expr, binop.op, right_expr), binop) + ) return res, explanation def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]: @@ -1089,7 +1094,7 @@ def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]: arg_expls.append("**" + expl) expl = "{}({})".format(func_expl, ", ".join(arg_expls)) - new_call = ast.Call(new_func, new_args, new_kwargs) + new_call = ast.copy_location(ast.Call(new_func, new_args, new_kwargs), call) res = self.assign(new_call) res_expl = self.explanation_param(self.display(res)) outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}" @@ -1105,7 +1110,9 @@ def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]: if not isinstance(attr.ctx, ast.Load): return self.generic_visit(attr) value, value_expl = self.visit(attr.value) - res = self.assign(ast.Attribute(value, attr.attr, ast.Load())) + res = self.assign( + ast.copy_location(ast.Attribute(value, attr.attr, ast.Load()), attr) + ) res_expl = self.explanation_param(self.display(res)) pat = "%s\n{%s = %s.%s\n}" expl = pat % (res_expl, res_expl, value_expl, attr.attr) @@ -1146,7 +1153,7 @@ def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]: syms.append(ast.Constant(sym)) expl = f"{left_expl} {sym} {next_expl}" expls.append(ast.Constant(expl)) - res_expr = ast.Compare(left_res, [op], [next_res]) + res_expr = ast.copy_location(ast.Compare(left_res, [op], [next_res]), comp) self.statements.append(ast.Assign([store_names[i]], res_expr)) left_res, left_expl = next_res, next_expl # Use pytest.assertion.util._reprcompare if that's available. diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 73c11a1a9d8..7be473d897a 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -2,10 +2,12 @@ from __future__ import annotations import ast +import dis import errno from functools import partial import glob import importlib +import inspect import marshal import os from pathlib import Path @@ -131,10 +133,211 @@ def test_location_is_set(self) -> None: continue for n in [node, *ast.iter_child_nodes(node)]: assert isinstance(n, (ast.stmt, ast.expr)) - assert n.lineno == 3 - assert n.col_offset == 0 - assert n.end_lineno == 6 - assert n.end_col_offset == 3 + for location in [ + (n.lineno, n.col_offset), + (n.end_lineno, n.end_col_offset), + ]: + assert (3, 0) <= location <= (6, 3) + + def test_positions_are_preserved(self) -> None: + """Ensure AST positions are preserved during rewriting (#12818).""" + + def preserved(code: str) -> None: + s = textwrap.dedent(code) + locations = [] + + def loc(msg: str | None = None) -> None: + frame = inspect.currentframe() + assert frame + frame = frame.f_back + assert frame + frame = frame.f_back + assert frame + + offset = frame.f_lasti + + instructions = {i.offset: i for i in dis.get_instructions(frame.f_code)} + + # skip CACHE instructions + while offset not in instructions and offset >= 0: + offset -= 1 + + instruction = instructions[offset] + if sys.version_info >= (3, 11): + position = instruction.positions + else: + position = instruction.starts_line + + locations.append((msg, instruction.opname, position)) + + globals = {"loc": loc} + + m = rewrite(s) + mod = compile(m, "", "exec") + exec(mod, globals, globals) + transformed_locations = locations + locations = [] + + mod = compile(s, "", "exec") + exec(mod, globals, globals) + original_locations = locations + + assert len(original_locations) > 0 + assert original_locations == transformed_locations + + preserved(""" + def f(): + loc() + return 8 + + assert f() in [8] + assert (f() + in + [8]) + """) + + preserved(""" + class T: + def __init__(self): + loc("init") + def __getitem__(self,index): + loc("getitem") + return index + + assert T()[5] == 5 + assert (T + () + [5] + == + 5) + """) + + for name, op in [ + ("pos", "+"), + ("neg", "-"), + ("invert", "~"), + ]: + preserved(f""" + class T: + def __{name}__(self): + loc("{name}") + return "{name}" + + assert {op}T() == "{name}" + assert ({op} + T + () + == + "{name}") + """) + + for name, op in [ + ("add", "+"), + ("sub", "-"), + ("mul", "*"), + ("truediv", "/"), + ("floordiv", "//"), + ("mod", "%"), + ("pow", "**"), + ("lshift", "<<"), + ("rshift", ">>"), + ("or", "|"), + ("xor", "^"), + ("and", "&"), + ("matmul", "@"), + ]: + preserved(f""" + class T: + def __{name}__(self,other): + loc("{name}") + return other + + def __r{name}__(self,other): + loc("r{name}") + return other + + assert T() {op} 2 == 2 + assert 2 {op} T() == 2 + + assert (T + () + {op} + 2 + == + 2) + + assert (2 + {op} + T + () + == + 2) + """) + + for name, op in [ + ("eq", "=="), + ("ne", "!="), + ("lt", "<"), + ("le", "<="), + ("gt", ">"), + ("ge", ">="), + ]: + preserved(f""" + class T: + def __{name}__(self,other): + loc() + return True + + assert T() {op} 5 + assert (T + () + {op} + 5) + """) + + for name, op in [ + ("eq", "=="), + ("ne", "!="), + ("lt", ">"), + ("le", ">="), + ("gt", "<"), + ("ge", "<="), + ("contains", "in"), + ]: + preserved(f""" + class T: + def __{name}__(self,other): + loc() + return True + + assert 5 {op} T() + assert (5 + {op} + T + ()) + """) + + preserved(""" + def func(value): + loc("func") + return value + + class T: + def __iter__(self): + loc("iter") + return iter([5]) + + assert func(*T()) == 5 + """) + + preserved(""" + class T: + def __getattr__(self,name): + loc() + return name + + assert T().attr == "attr" + """) def test_dont_rewrite(self) -> None: s = """'PYTEST_DONT_REWRITE'\nassert 14""" From 09da0de673dad13220e7a1373adf07221e6b6bb0 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:01:13 +0000 Subject: [PATCH 41/78] Docs: improve/cleanup reference from/to recwarn (#12866) (#12870) Co-authored-by: Bruno Oliveira (cherry picked from commit 26a29bdade7efccdf6233942749f02cef57ce694) Co-authored-by: Stefaan Lippens --- changelog/12866.doc.rst | 1 + doc/en/builtin.rst | 3 +-- doc/en/how-to/capture-warnings.rst | 17 ++++++++--------- doc/en/reference/reference.rst | 3 ++- src/_pytest/recwarn.py | 3 +-- 5 files changed, 13 insertions(+), 14 deletions(-) create mode 100644 changelog/12866.doc.rst diff --git a/changelog/12866.doc.rst b/changelog/12866.doc.rst new file mode 100644 index 00000000000..865b2bbc600 --- /dev/null +++ b/changelog/12866.doc.rst @@ -0,0 +1 @@ +Improved cross-references concerning the :fixture:`recwarn` fixture. diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 9b406a6a512..822ee48b3fb 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -234,8 +234,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a recwarn -- .../_pytest/recwarn.py:35 Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. - See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information - on warning categories. + See :ref:`warnings` for information on warning categories. tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:242 Return a :class:`pytest.TempPathFactory` instance for the test session. diff --git a/doc/en/how-to/capture-warnings.rst b/doc/en/how-to/capture-warnings.rst index afabad5da14..44ed87508a3 100644 --- a/doc/en/how-to/capture-warnings.rst +++ b/doc/en/how-to/capture-warnings.rst @@ -195,7 +195,7 @@ user code and third-party libraries, as recommended by :pep:`565`. This helps users keep their code modern and avoid breakages when deprecated warnings are effectively removed. However, in the specific case where users capture any type of warnings in their test, either with -:func:`pytest.warns`, :func:`pytest.deprecated_call` or using the :ref:`recwarn ` fixture, +:func:`pytest.warns`, :func:`pytest.deprecated_call` or using the :fixture:`recwarn` fixture, no warning will be displayed at all. Sometimes it is useful to hide some specific deprecation warnings that happen in code that you have no control over @@ -332,10 +332,10 @@ additional information: assert record[0].message.args[0] == "another warning" Alternatively, you can examine raised warnings in detail using the -:ref:`recwarn ` fixture (see below). +:fixture:`recwarn` fixture (see :ref:`below `). -The :ref:`recwarn ` fixture automatically ensures to reset the warnings +The :fixture:`recwarn` fixture automatically ensures to reset the warnings filter at the end of the test, so no global state is leaked. .. _`recording warnings`: @@ -345,8 +345,8 @@ filter at the end of the test, so no global state is leaked. Recording warnings ------------------ -You can record raised warnings either using :func:`pytest.warns` or with -the ``recwarn`` fixture. +You can record raised warnings either using the :func:`pytest.warns` context manager or with +the :fixture:`recwarn` fixture. To record with :func:`pytest.warns` without asserting anything about the warnings, pass no arguments as the expected warning type and it will default to a generic Warning: @@ -361,7 +361,7 @@ pass no arguments as the expected warning type and it will default to a generic assert str(record[0].message) == "user" assert str(record[1].message) == "runtime" -The ``recwarn`` fixture will record warnings for the whole function: +The :fixture:`recwarn` fixture will record warnings for the whole function: .. code-block:: python @@ -377,12 +377,11 @@ The ``recwarn`` fixture will record warnings for the whole function: assert w.filename assert w.lineno -Both ``recwarn`` and :func:`pytest.warns` return the same interface for recorded -warnings: a WarningsRecorder instance. To view the recorded warnings, you can +Both the :fixture:`recwarn` fixture and the :func:`pytest.warns` context manager return the same interface for recorded +warnings: a :class:`~_pytest.recwarn.WarningsRecorder` instance. To view the recorded warnings, you can iterate over this instance, call ``len`` on it to get the number of recorded warnings, or index into it to get a particular recorded warning. -Full API: :class:`~_pytest.recwarn.WarningsRecorder`. .. _`warns use cases`: diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index a3c81cf7748..3c0f9ae6cec 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -529,13 +529,14 @@ record_testsuite_property recwarn ~~~~~~~ -**Tutorial**: :ref:`assertwarnings` +**Tutorial**: :ref:`recwarn` .. autofunction:: _pytest.recwarn.recwarn() :no-auto-options: .. autoclass:: pytest.WarningsRecorder() :members: + :special-members: __getitem__, __iter__, __len__ .. fixture:: request diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 85d8de84abb..0dc002edd94 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -35,8 +35,7 @@ def recwarn() -> Generator[WarningsRecorder]: """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. - See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information - on warning categories. + See :ref:`warnings` for information on warning categories. """ wrec = WarningsRecorder(_ispytest=True) with wrec: From d450afef18a0ed7c7b09368358f70242b4ea17b5 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 10 Oct 2024 11:07:25 +0000 Subject: [PATCH 42/78] [pre-commit] Use --python-version for mypy instead of forcing a python interpreter (#12869) (#12871) (cherry picked from commit 2242cd43eb5601e3f282003c049e37e56ece932a) Co-authored-by: Pierre Sassoulas --- .pre-commit-config.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4cb466cbb2c..2d6018d8f6c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,8 +32,7 @@ repos: hooks: - id: mypy files: ^(src/|testing/|scripts/) - args: [] - language_version: "3.8" + args: ["--python-version=3.8"] additional_dependencies: - iniconfig>=1.1.0 - attrs>=19.2.0 From 5b6a8d6505772332b4a88a3364089916b76fc73a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sviatoslav=20Sydorenko=20=28=D0=A1=D0=B2=D1=8F=D1=82=D0=BE?= =?UTF-8?q?=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1=D0=B8=D0=B4=D0=BE=D1=80=D0=B5?= =?UTF-8?q?=D0=BD=D0=BA=D0=BE=29?= Date: Thu, 10 Oct 2024 14:50:57 +0200 Subject: [PATCH 43/78] Merge pull request #12872 from Pierre-Sassoulas/fix-pre-commit-python-3.8 (cherry picked from commit f373974707f57a0b28d12563e4d03c7cd54c70d9) --- .pre-commit-config.yaml | 1 - pyproject.toml | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2d6018d8f6c..7806f68e2d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,6 @@ repos: hooks: - id: mypy files: ^(src/|testing/|scripts/) - args: ["--python-version=3.8"] additional_dependencies: - iniconfig>=1.1.0 - attrs>=19.2.0 diff --git a/pyproject.toml b/pyproject.toml index 12764f3d127..c7941f9dafa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -479,6 +479,7 @@ files = [ mypy_path = [ "src", ] +python_version = "3.8" check_untyped_defs = true disallow_any_generics = true disallow_untyped_defs = true From 6fe63829a9bd112e0f1108a39366d49d57825362 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 13 Oct 2024 15:33:56 -0300 Subject: [PATCH 44/78] Improve pytest.Config.getoption docstring (#12886) Closes #10558. --------- Co-authored-by: suspe (cherry picked from commit d8d607e937bf5a36815007322bf10239f3330475) --- AUTHORS | 1 + changelog/10558.doc.rst | 1 + src/_pytest/config/__init__.py | 9 +++++---- testing/test_config.py | 20 +++++++++++++------- 4 files changed, 20 insertions(+), 11 deletions(-) create mode 100644 changelog/10558.doc.rst diff --git a/AUTHORS b/AUTHORS index d0efcdfe122..7823904baf6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -116,6 +116,7 @@ Dave Hunt David Díaz-Barquero David Mohr David Paul Röthlisberger +David Peled David Szotten David Vierra Daw-Ran Liou diff --git a/changelog/10558.doc.rst b/changelog/10558.doc.rst new file mode 100644 index 00000000000..1c242b9cf71 --- /dev/null +++ b/changelog/10558.doc.rst @@ -0,0 +1 @@ +Fix ambiguous docstring of :func:`pytest.Config.getoption`. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 3cca1479381..710e03e4fe0 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1682,11 +1682,12 @@ def _get_override_ini_value(self, name: str) -> str | None: def getoption(self, name: str, default=notset, skip: bool = False): """Return command line option value. - :param name: Name of the option. You may also specify + :param name: Name of the option. You may also specify the literal ``--OPT`` option instead of the "dest" option name. - :param default: Default value if no option of that name exists. - :param skip: If True, raise pytest.skip if option does not exists - or has a None value. + :param default: Fallback value if no option of that name is **declared** via :hook:`pytest_addoption`. + Note this parameter will be ignored when the option is **declared** even if the option's value is ``None``. + :param skip: If ``True``, raise :func:`pytest.skip` if option is undeclared or has a ``None`` value. + Note that even if ``True``, if a default was specified it will be returned instead of a skip. """ name = self._opt2dest.get(name, name) try: diff --git a/testing/test_config.py b/testing/test_config.py index daf69845bb1..bdeaed14721 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -636,7 +636,7 @@ def test_config_trace(self, pytester: Pytester) -> None: assert len(values) == 1 assert values[0] == "hello [config]\n" - def test_config_getoption(self, pytester: Pytester) -> None: + def test_config_getoption_declared_option_name(self, pytester: Pytester) -> None: pytester.makeconftest( """ def pytest_addoption(parser): @@ -648,6 +648,18 @@ def pytest_addoption(parser): assert config.getoption(x) == "this" pytest.raises(ValueError, config.getoption, "qweqwe") + config_novalue = pytester.parseconfig() + assert config_novalue.getoption("hello") is None + assert config_novalue.getoption("hello", default=1) is None + assert config_novalue.getoption("hello", default=1, skip=True) == 1 + + def test_config_getoption_undeclared_option_name(self, pytester: Pytester) -> None: + config = pytester.parseconfig() + with pytest.raises(ValueError): + config.getoption("x") + assert config.getoption("x", default=1) == 1 + assert config.getoption("x", default=1, skip=True) == 1 + def test_config_getoption_unicode(self, pytester: Pytester) -> None: pytester.makeconftest( """ @@ -675,12 +687,6 @@ def pytest_addoption(parser): with pytest.raises(pytest.skip.Exception): config.getvalueorskip("hello") - def test_getoption(self, pytester: Pytester) -> None: - config = pytester.parseconfig() - with pytest.raises(ValueError): - config.getvalue("x") - assert config.getoption("x", 1) == 1 - def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None: somepath = tmp_path.joinpath("x", "y", "z") p = tmp_path.joinpath("conftest.py") From 52135b033fb949efbec6aed9dd9000275bb199fd Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 13 Oct 2024 17:27:25 -0300 Subject: [PATCH 45/78] Merge pull request #12885 from The-Compiler/pdb-py311 (#12887) Fix pdb selftests on Python 3.13 (cherry picked from commit a4e40bcf77f29615a12a12758e5307d3f4ee0663) Co-authored-by: Florian Bruhin --- .github/workflows/test.yml | 2 +- changelog/12497.contrib.rst | 1 + testing/test_debugging.py | 16 ++++++++++++---- 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 changelog/12497.contrib.rst diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9158d6bcc72..3131f487ab9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -147,7 +147,7 @@ jobs: - name: "ubuntu-py313" python: "3.13-dev" os: ubuntu-latest - tox_env: "py313" + tox_env: "py313-pexpect" use_coverage: true - name: "ubuntu-pypy3" python: "pypy-3.9" diff --git a/changelog/12497.contrib.rst b/changelog/12497.contrib.rst new file mode 100644 index 00000000000..ccf89731053 --- /dev/null +++ b/changelog/12497.contrib.rst @@ -0,0 +1 @@ +Fixed two failing pdb-related tests on Python 3.13. diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 37032f92354..73a4b769ff7 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -768,9 +768,13 @@ def test_pdb_used_outside_test(self, pytester: Pytester) -> None: x = 5 """ ) + if sys.version_info[:2] >= (3, 13): + break_line = "pytest.set_trace()" + else: + break_line = "x = 5" child = pytester.spawn(f"{sys.executable} {p1}") - child.expect("x = 5") - child.expect("Pdb") + child.expect_exact(break_line) + child.expect_exact("Pdb") child.sendeof() self.flush(child) @@ -785,9 +789,13 @@ def test_foo(a): pass """ ) + if sys.version_info[:2] >= (3, 13): + break_line = "pytest.set_trace()" + else: + break_line = "x = 5" child = pytester.spawn_pytest(str(p1)) - child.expect("x = 5") - child.expect("Pdb") + child.expect_exact(break_line) + child.expect_exact("Pdb") child.sendeof() self.flush(child) From 6502816d977fcdbd65a3f4d8a63c0ce7c1f25649 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 25 Oct 2024 13:17:27 +0300 Subject: [PATCH 46/78] Merge pull request #12913 from jakkdl/dontfailonbadpath [minor] avoid test failing if the working path contains the string 'error' (cherry picked from commit ded1f44e5cdf02e5823f52e07414236982200894) --- testing/python/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 46f0a762cbb..c939b221f22 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -2996,7 +2996,7 @@ def test_finish(): *3 passed* """ ) - result.stdout.no_fnmatch_line("*error*") + assert result.ret == 0 def test_fixture_finalizer(self, pytester: Pytester) -> None: pytester.makeconftest( From 7aeb72bbc67bd1b8271eee57caa0a4e9b07038fc Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 10:46:29 +0000 Subject: [PATCH 47/78] Improve docs on basetemp and retention (#12912) (#12928) Improve coverage of current handling of `--basetemp` option and its lack of retention functionality. Also document `PYTEST_DEBUG_TEMPROOT`. Related to #10829 --------- Co-authored-by: Bruno Oliveira (cherry picked from commit a1a491837b35b719d77b4f03be318b505d495386) Co-authored-by: Stefaan Lippens --- changelog/10829.doc.rst | 1 + doc/en/how-to/tmp_path.rst | 50 ++++++++++++++++++++++++---------- doc/en/reference/reference.rst | 5 ++++ src/_pytest/legacypath.py | 15 ++++------ src/_pytest/tmpdir.py | 22 +++++---------- 5 files changed, 53 insertions(+), 40 deletions(-) create mode 100644 changelog/10829.doc.rst diff --git a/changelog/10829.doc.rst b/changelog/10829.doc.rst new file mode 100644 index 00000000000..1be45c7049d --- /dev/null +++ b/changelog/10829.doc.rst @@ -0,0 +1 @@ +Improve documentation on the current handling of the ``--basetemp`` option and its lack of retention functionality (:ref:`temporary directory location and retention`). diff --git a/doc/en/how-to/tmp_path.rst b/doc/en/how-to/tmp_path.rst index 3cc5152e992..d19950431e5 100644 --- a/doc/en/how-to/tmp_path.rst +++ b/doc/en/how-to/tmp_path.rst @@ -133,27 +133,47 @@ API for details. Temporary directory location and retention ------------------------------------------ -Temporary directories are by default created as sub-directories of -the system temporary directory. The base name will be ``pytest-NUM`` where -``NUM`` will be incremented with each test run. -By default, entries older than 3 temporary directories will be removed. -This behavior can be configured with :confval:`tmp_path_retention_count` and -:confval:`tmp_path_retention_policy`. +The temporary directories, +as returned by the :fixture:`tmp_path` and (now deprecated) :fixture:`tmpdir` fixtures, +are automatically created under a base temporary directory, +in a structure that depends on the ``--basetemp`` option: -Using the ``--basetemp`` -option will remove the directory before every run, effectively meaning the temporary directories -of only the most recent run will be kept. +- By default (when the ``--basetemp`` option is not set), + the temporary directories will follow this template: -You can override the default temporary directory setting like this: + .. code-block:: text -.. code-block:: bash + {temproot}/pytest-of-{user}/pytest-{num}/{testname}/ - pytest --basetemp=mydir + where: -.. warning:: + - ``{temproot}`` is the system temporary directory + as determined by :py:func:`tempfile.gettempdir`. + It can be overridden by the :envvar:`PYTEST_DEBUG_TEMPROOT` environment variable. + - ``{user}`` is the user name running the tests, + - ``{num}`` is a number that is incremented with each test suite run + - ``{testname}`` is a sanitized version of :py:attr:`the name of the current test <_pytest.nodes.Node.name>`. - The contents of ``mydir`` will be completely removed, so make sure to use a directory - for that purpose only. + The auto-incrementing ``{num}`` placeholder provides a basic retention feature + and avoids that existing results of previous test runs are blindly removed. + By default, the last 3 temporary directories are kept, + but this behavior can be configured with + :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy`. + +- When the ``--basetemp`` option is used (e.g. ``pytest --basetemp=mydir``), + it will be used directly as base temporary directory: + + .. code-block:: text + + {basetemp}/{testname}/ + + Note that there is no retention feature in this case: + only the results of the most recent run will be kept. + + .. warning:: + + The directory given to ``--basetemp`` will be cleared blindly before each test run, + so make sure to use a directory for that purpose only. When distributing tests on the local machine using ``pytest-xdist``, care is taken to automatically configure a `basetemp` directory for the sub processes such that all temporary diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 3c0f9ae6cec..31cf363100a 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1146,6 +1146,11 @@ processes can inspect it, see :ref:`pytest current test env` for more informatio When set, pytest will print tracing and debug information. +.. envvar:: PYTEST_DEBUG_TEMPROOT + +Root for temporary directories produced by fixtures like :fixture:`tmp_path` +as discussed in :ref:`temporary directory location and retention`. + .. envvar:: PYTEST_DISABLE_PLUGIN_AUTOLOAD When set, disables plugin auto-loading through :std:doc:`entry point packaging diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 61476d68932..59e8ef6e742 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -304,16 +304,11 @@ def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: @staticmethod @fixture def tmpdir(tmp_path: Path) -> LEGACY_PATH: - """Return a temporary directory path object which is unique to each test - function invocation, created as a sub directory of the base temporary - directory. - - By default, a new base temporary directory is created each test session, - and old bases are removed after 3 sessions, to aid in debugging. If - ``--basetemp`` is used then it is cleared each session. See - :ref:`temporary directory location and retention`. - - The returned object is a `legacy_path`_ object. + """Return a temporary directory (as `legacy_path`_ object) + which is unique to each test function invocation. + The temporary directory is created as a subdirectory + of the base temporary directory, with configurable retention, + as discussed in :ref:`temporary directory location and retention`. .. note:: These days, it is preferred to use ``tmp_path``. diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index de0cbcfeb1c..1731a4b8d0d 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -41,9 +41,8 @@ @final @dataclasses.dataclass class TempPathFactory: - """Factory for temporary directories under the common base temp directory. - - The base directory can be configured using the ``--basetemp`` option. + """Factory for temporary directories under the common base temp directory, + as discussed at :ref:`temporary directory location and retention`. """ _given_basetemp: Path | None @@ -257,18 +256,11 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: def tmp_path( request: FixtureRequest, tmp_path_factory: TempPathFactory ) -> Generator[Path]: - """Return a temporary directory path object which is unique to each test - function invocation, created as a sub directory of the base temporary - directory. - - By default, a new base temporary directory is created each test session, - and old bases are removed after 3 sessions, to aid in debugging. - This behavior can be configured with :confval:`tmp_path_retention_count` and - :confval:`tmp_path_retention_policy`. - If ``--basetemp`` is used then it is cleared each session. See - :ref:`temporary directory location and retention`. - - The returned object is a :class:`pathlib.Path` object. + """Return a temporary directory (as :class:`pathlib.Path` object) + which is unique to each test function invocation. + The temporary directory is created as a subdirectory + of the base temporary directory, with configurable retention, + as discussed in :ref:`temporary directory location and retention`. """ path = _mk_tmp(request, tmp_path_factory) yield path From be6bc812b02454b2915755dd76ce74b877aeafad Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 16 Nov 2024 19:15:09 +0000 Subject: [PATCH 48/78] Issue #12966 Clarify filterwarnings docs on precedence when using multiple marks (#12967) (#12969) (cherry picked from commit 71a35d4a3c8bf626cb73be0cb900ede18b1b123d) Co-authored-by: Stefaan Lippens --- changelog/12966.doc.rst | 1 + doc/en/how-to/capture-warnings.rst | 33 +++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 changelog/12966.doc.rst diff --git a/changelog/12966.doc.rst b/changelog/12966.doc.rst new file mode 100644 index 00000000000..8a440c2ec0f --- /dev/null +++ b/changelog/12966.doc.rst @@ -0,0 +1 @@ +Clarify :ref:`filterwarnings` docs on filter precedence/order when using multiple :ref:`@pytest.mark.filterwarnings ` marks. diff --git a/doc/en/how-to/capture-warnings.rst b/doc/en/how-to/capture-warnings.rst index 44ed87508a3..65a43cec6e8 100644 --- a/doc/en/how-to/capture-warnings.rst +++ b/doc/en/how-to/capture-warnings.rst @@ -128,7 +128,7 @@ is performed. -You can use the ``@pytest.mark.filterwarnings`` to add warning filters to specific test items, +You can use the :ref:`@pytest.mark.filterwarnings ` mark to add warning filters to specific test items, allowing you to have finer control of which warnings should be captured at test, class or even module level: @@ -147,10 +147,30 @@ even module level: assert api_v1() == 1 +You can specify multiple filters with separate decorators: + +.. code-block:: python + + # Ignore "api v1" warnings, but fail on all other warnings + @pytest.mark.filterwarnings("ignore:api v1") + @pytest.mark.filterwarnings("error") + def test_one(): + assert api_v1() == 1 + +.. important:: + + Regarding decorator order and filter precedence: + it's important to remember that decorators are evaluated in reverse order, + so you have to list the warning filters in the reverse order + compared to traditional :py:func:`warnings.filterwarnings` and :option:`-W option ` usage. + This means in practice that filters from earlier :ref:`@pytest.mark.filterwarnings ` decorators + take precedence over filters from later decorators, as illustrated in the example above. + + Filters applied using a mark take precedence over filters passed on the command line or configured -by the ``filterwarnings`` ini option. +by the :confval:`filterwarnings` ini option. -You may apply a filter to all tests of a class by using the ``filterwarnings`` mark as a class +You may apply a filter to all tests of a class by using the :ref:`filterwarnings ` mark as a class decorator or to all tests in a module by setting the :globalvar:`pytestmark` variable: .. code-block:: python @@ -159,6 +179,13 @@ decorator or to all tests in a module by setting the :globalvar:`pytestmark` var pytestmark = pytest.mark.filterwarnings("error") +.. note:: + + If you want to apply multiple filters + (by assigning a list of :ref:`filterwarnings ` mark to :globalvar:`pytestmark`), + you must use the traditional :py:func:`warnings.filterwarnings` ordering approach (later filters take precedence), + which is the reverse of the decorator approach mentioned above. + *Credits go to Florian Schulze for the reference implementation in the* `pytest-warnings`_ *plugin.* From 16cb87b65036300d74472cd55eebca8fc3f8e703 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:44:22 +0000 Subject: [PATCH 49/78] pytest.fail: fix ANSI escape codes for colored output (#12959) (#12990) - When `ReprEntry.style == "value"` (happens when calling `pytest.fail(..., pytrace=False)`, the message should not be printed to terminal using `TerminalWriter._write_source` because then it'll try to highlight the message as source code - The message should be printed to terminal directly using `TerminalWriter.line` or `TerminalWriter.write`, I went with the later for testing purposes https://github.com/pytest-dev/pytest/pull/12959#discussion_r1842574618 Closes #12849 (cherry picked from commit 76e044477010dcc0e31d4a736b7130e044a01a7e) Co-authored-by: Leonardus Chen --- AUTHORS | 1 + changelog/12849.bugfix.rst | 1 + src/_pytest/_code/code.py | 16 +++++++++++----- testing/code/test_excinfo.py | 17 +++++++++++++++++ testing/conftest.py | 4 ++-- 5 files changed, 32 insertions(+), 7 deletions(-) create mode 100644 changelog/12849.bugfix.rst diff --git a/AUTHORS b/AUTHORS index 7823904baf6..e25156cde19 100644 --- a/AUTHORS +++ b/AUTHORS @@ -245,6 +245,7 @@ Kristoffer Nordström Kyle Altendorf Lawrence Mitchell Lee Kamentsky +Leonardus Chen Lev Maximov Levon Saldamli Lewis Cowles diff --git a/changelog/12849.bugfix.rst b/changelog/12849.bugfix.rst new file mode 100644 index 00000000000..fb72263aadd --- /dev/null +++ b/changelog/12849.bugfix.rst @@ -0,0 +1 @@ +ANSI escape codes for colored output now handled correctly in :func:`pytest.fail` with `pytrace=False`. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 8fac39ea298..fec627b3a36 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1221,6 +1221,15 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None: if not self.lines: return + if self.style == "value": + # Using tw.write instead of tw.line for testing purposes due to TWMock implementation; + # lines written with TWMock.line and TWMock._write_source cannot be distinguished + # from each other, whereas lines written with TWMock.write are marked with TWMock.WRITE + for line in self.lines: + tw.write(line) + tw.write("\n") + return + # separate indents and source lines that are not failures: we want to # highlight the code but not the indentation, which may contain markers # such as "> assert 0" @@ -1236,11 +1245,8 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None: failure_lines.extend(self.lines[index:]) break else: - if self.style == "value": - source_lines.append(line) - else: - indents.append(line[:indent_size]) - source_lines.append(line[indent_size:]) + indents.append(line[:indent_size]) + source_lines.append(line[indent_size:]) tw._write_source(source_lines, indents) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index fc60ae9ac99..97c207e9795 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1194,6 +1194,23 @@ def f(): line = tw_mock.lines[-1] assert line == ":3: ValueError" + def test_toterminal_value(self, importasmod, tw_mock): + mod = importasmod( + """ + def g(x): + raise ValueError(x) + def f(): + g('some_value') + """ + ) + excinfo = pytest.raises(ValueError, mod.f) + excinfo.traceback = excinfo.traceback.filter(excinfo) + repr = excinfo.getrepr(style="value") + repr.toterminal(tw_mock) + + assert tw_mock.get_write_msg(0) == "some_value" + assert tw_mock.get_write_msg(1) == "\n" + @pytest.mark.parametrize( "reproptions", [ diff --git a/testing/conftest.py b/testing/conftest.py index 046bb77a109..69af03324d6 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -119,8 +119,8 @@ def markup(self, text, **kw): return text def get_write_msg(self, idx): - flag, msg = self.lines[idx] - assert flag == TWMock.WRITE + assert self.lines[idx][0] == TWMock.WRITE + msg = self.lines[idx][1] return msg fullwidth = 80 From b541721529feba7fcd0d069fa2437a817f340eba Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 12:49:00 +0000 Subject: [PATCH 50/78] docs: Fix wrong statement about sys.modules with importlib import mode (#12985) (#12991) Follow-up to #7870, see #12983. (cherry picked from commit 2157caf87960d904c8547c9168c94a7d535f21e0) Co-authored-by: Florian Bruhin --- doc/en/explanation/goodpractices.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/explanation/goodpractices.rst b/doc/en/explanation/goodpractices.rst index 1390ba4e8fe..51c0b960aed 100644 --- a/doc/en/explanation/goodpractices.rst +++ b/doc/en/explanation/goodpractices.rst @@ -210,8 +210,8 @@ Note that this layout also works in conjunction with the ``src`` layout mentione to avoid surprises such as a test module getting imported twice. With ``--import-mode=importlib`` things are less convoluted because - pytest doesn't need to change ``sys.path`` or ``sys.modules``, making things - much less surprising. + pytest doesn't need to change ``sys.path``, making things much less + surprising. .. _which-import-mode: From 1b474e221d5ced2c8c73924a0087e6e24ab6cd61 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 07:38:14 -0300 Subject: [PATCH 51/78] approx: use exact comparison for bool (#13013) Fixes #9353 (cherry picked from commit a16e8eac8c91b8d0f91c461a4de39adbf8a75b0f) Co-authored-by: Jakob van Santen --- changelog/9353.bugfix.rst | 1 + src/_pytest/python_api.py | 44 +++++++++++++++++++++++---------------- testing/python/approx.py | 23 +++++++++++++++++++- 3 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 changelog/9353.bugfix.rst diff --git a/changelog/9353.bugfix.rst b/changelog/9353.bugfix.rst new file mode 100644 index 00000000000..414f5a007b6 --- /dev/null +++ b/changelog/9353.bugfix.rst @@ -0,0 +1 @@ +:func:`pytest.approx` now uses strict equality when given booleans. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 4174a55b589..f0035f0c393 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -262,19 +262,22 @@ def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]: ): if approx_value != other_value: if approx_value.expected is not None and other_value is not None: - max_abs_diff = max( - max_abs_diff, abs(approx_value.expected - other_value) - ) - if approx_value.expected == 0.0: - max_rel_diff = math.inf - else: - max_rel_diff = max( - max_rel_diff, - abs( - (approx_value.expected - other_value) - / approx_value.expected - ), + try: + max_abs_diff = max( + max_abs_diff, abs(approx_value.expected - other_value) ) + if approx_value.expected == 0.0: + max_rel_diff = math.inf + else: + max_rel_diff = max( + max_rel_diff, + abs( + (approx_value.expected - other_value) + / approx_value.expected + ), + ) + except ZeroDivisionError: + pass different_ids.append(approx_key) message_data = [ @@ -398,8 +401,10 @@ def __repr__(self) -> str: # Don't show a tolerance for values that aren't compared using # tolerances, i.e. non-numerics and infinities. Need to call abs to # handle complex numbers, e.g. (inf + 1j). - if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf( - abs(self.expected) + if ( + isinstance(self.expected, bool) + or (not isinstance(self.expected, (Complex, Decimal))) + or math.isinf(abs(self.expected) or isinstance(self.expected, bool)) ): return str(self.expected) @@ -427,14 +432,17 @@ def __eq__(self, actual) -> bool: # numpy<1.13. See #3748. return all(self.__eq__(a) for a in asarray.flat) - # Short-circuit exact equality. - if actual == self.expected: + # Short-circuit exact equality, except for bool + if isinstance(self.expected, bool) and not isinstance(actual, bool): + return False + elif actual == self.expected: return True # If either type is non-numeric, fall back to strict equality. # NB: we need Complex, rather than just Number, to ensure that __abs__, - # __sub__, and __float__ are defined. - if not ( + # __sub__, and __float__ are defined. Also, consider bool to be + # nonnumeric, even though it has the required arithmetic. + if isinstance(self.expected, bool) or not ( isinstance(self.expected, (Complex, Decimal)) and isinstance(actual, (Complex, Decimal)) ): diff --git a/testing/python/approx.py b/testing/python/approx.py index 69743cdbe17..cb1704f6ec4 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -90,12 +90,25 @@ def do_assert(lhs, rhs, expected_message, verbosity_level=0): return do_assert -SOME_FLOAT = r"[+-]?([0-9]*[.])?[0-9]+\s*" +SOME_FLOAT = r"[+-]?((?:([0-9]*[.])?[0-9]+(e-?[0-9]+)?)|inf|nan)\s*" SOME_INT = r"[0-9]+\s*" class TestApprox: def test_error_messages_native_dtypes(self, assert_approx_raises_regex): + # Treat bool exactly. + assert_approx_raises_regex( + {"a": 1.0, "b": True}, + {"a": 1.0, "b": False}, + [ + "", + " comparison failed. Mismatched elements: 1 / 2:", + f" Max absolute difference: {SOME_FLOAT}", + f" Max relative difference: {SOME_FLOAT}", + r" Index\s+\| Obtained\s+\| Expected", + r".*(True|False)\s+", + ], + ) assert_approx_raises_regex( 2.0, 1.0, @@ -590,6 +603,13 @@ def test_complex(self): assert approx(x, rel=5e-6, abs=0) == a assert approx(x, rel=5e-7, abs=0) != a + def test_expecting_bool(self) -> None: + assert True == approx(True) # noqa: E712 + assert False == approx(False) # noqa: E712 + assert True != approx(False) # noqa: E712 + assert True != approx(False, abs=2) # noqa: E712 + assert 1 != approx(True) + def test_list(self): actual = [1 + 1e-7, 2 + 1e-8] expected = [1, 2] @@ -655,6 +675,7 @@ def test_dict_wrong_len(self): def test_dict_nonnumeric(self): assert {"a": 1.0, "b": None} == pytest.approx({"a": 1.0, "b": None}) assert {"a": 1.0, "b": 1} != pytest.approx({"a": 1.0, "b": None}) + assert {"a": 1.0, "b": True} != pytest.approx({"a": 1.0, "b": False}, abs=2) def test_dict_vs_other(self): assert 1 != approx({"a": 0}) From 98dff36c9dc0a44881e9e90daf381f9079adf4cc Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sun, 1 Dec 2024 10:40:53 +0000 Subject: [PATCH 52/78] Prepare release version 8.3.4 --- changelog/10558.doc.rst | 1 - changelog/10829.doc.rst | 1 - changelog/12497.contrib.rst | 1 - changelog/12592.bugfix.rst | 1 - changelog/12818.bugfix.rst | 1 - changelog/12849.bugfix.rst | 1 - changelog/12866.doc.rst | 1 - changelog/12966.doc.rst | 1 - changelog/9353.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-8.3.4.rst | 30 +++++++++++++++++++++ doc/en/builtin.rst | 36 +++++++++---------------- doc/en/changelog.rst | 41 +++++++++++++++++++++++++++++ doc/en/example/parametrize.rst | 6 ++--- doc/en/example/pythoncollection.rst | 4 +-- doc/en/getting-started.rst | 2 +- doc/en/how-to/fixtures.rst | 2 +- 17 files changed, 91 insertions(+), 40 deletions(-) delete mode 100644 changelog/10558.doc.rst delete mode 100644 changelog/10829.doc.rst delete mode 100644 changelog/12497.contrib.rst delete mode 100644 changelog/12592.bugfix.rst delete mode 100644 changelog/12818.bugfix.rst delete mode 100644 changelog/12849.bugfix.rst delete mode 100644 changelog/12866.doc.rst delete mode 100644 changelog/12966.doc.rst delete mode 100644 changelog/9353.bugfix.rst create mode 100644 doc/en/announce/release-8.3.4.rst diff --git a/changelog/10558.doc.rst b/changelog/10558.doc.rst deleted file mode 100644 index 1c242b9cf71..00000000000 --- a/changelog/10558.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ambiguous docstring of :func:`pytest.Config.getoption`. diff --git a/changelog/10829.doc.rst b/changelog/10829.doc.rst deleted file mode 100644 index 1be45c7049d..00000000000 --- a/changelog/10829.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Improve documentation on the current handling of the ``--basetemp`` option and its lack of retention functionality (:ref:`temporary directory location and retention`). diff --git a/changelog/12497.contrib.rst b/changelog/12497.contrib.rst deleted file mode 100644 index ccf89731053..00000000000 --- a/changelog/12497.contrib.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed two failing pdb-related tests on Python 3.13. diff --git a/changelog/12592.bugfix.rst b/changelog/12592.bugfix.rst deleted file mode 100644 index 605783bcab4..00000000000 --- a/changelog/12592.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed :class:`KeyError` crash when using ``--import-mode=importlib`` in a directory layout where a directory contains a child directory with the same name. diff --git a/changelog/12818.bugfix.rst b/changelog/12818.bugfix.rst deleted file mode 100644 index 9d74f2fda2a..00000000000 --- a/changelog/12818.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Assertion rewriting now preserves the source ranges of the original instructions, making it play well with tools that deal with the ``AST``, like `executing `__. diff --git a/changelog/12849.bugfix.rst b/changelog/12849.bugfix.rst deleted file mode 100644 index fb72263aadd..00000000000 --- a/changelog/12849.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -ANSI escape codes for colored output now handled correctly in :func:`pytest.fail` with `pytrace=False`. diff --git a/changelog/12866.doc.rst b/changelog/12866.doc.rst deleted file mode 100644 index 865b2bbc600..00000000000 --- a/changelog/12866.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Improved cross-references concerning the :fixture:`recwarn` fixture. diff --git a/changelog/12966.doc.rst b/changelog/12966.doc.rst deleted file mode 100644 index 8a440c2ec0f..00000000000 --- a/changelog/12966.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Clarify :ref:`filterwarnings` docs on filter precedence/order when using multiple :ref:`@pytest.mark.filterwarnings ` marks. diff --git a/changelog/9353.bugfix.rst b/changelog/9353.bugfix.rst deleted file mode 100644 index 414f5a007b6..00000000000 --- a/changelog/9353.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -:func:`pytest.approx` now uses strict equality when given booleans. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 61e4a772beb..8a38df7a2e1 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-8.3.4 release-8.3.3 release-8.3.2 release-8.3.1 diff --git a/doc/en/announce/release-8.3.4.rst b/doc/en/announce/release-8.3.4.rst new file mode 100644 index 00000000000..f76d60396dc --- /dev/null +++ b/doc/en/announce/release-8.3.4.rst @@ -0,0 +1,30 @@ +pytest-8.3.4 +======================================= + +pytest 8.3.4 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Florian Bruhin +* Frank Hoffmann +* Jakob van Santen +* Leonardus Chen +* Pierre Sassoulas +* Pradeep Kumar +* Ran Benita +* Serge Smertin +* Stefaan Lippens +* Sviatoslav Sydorenko (Святослав Сидоренко) +* dongfangtianyu +* suspe + + +Happy testing, +The pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 822ee48b3fb..85d280da50f 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -178,16 +178,11 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Return a :class:`pytest.TempdirFactory` instance for the test session. tmpdir -- .../_pytest/legacypath.py:305 - Return a temporary directory path object which is unique to each test - function invocation, created as a sub directory of the base temporary - directory. - - By default, a new base temporary directory is created each test session, - and old bases are removed after 3 sessions, to aid in debugging. If - ``--basetemp`` is used then it is cleared each session. See - :ref:`temporary directory location and retention`. - - The returned object is a `legacy_path`_ object. + Return a temporary directory (as `legacy_path`_ object) + which is unique to each test function invocation. + The temporary directory is created as a subdirectory + of the base temporary directory, with configurable retention, + as discussed in :ref:`temporary directory location and retention`. .. note:: These days, it is preferred to use ``tmp_path``. @@ -236,22 +231,15 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a See :ref:`warnings` for information on warning categories. - tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:242 + tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:241 Return a :class:`pytest.TempPathFactory` instance for the test session. - tmp_path -- .../_pytest/tmpdir.py:257 - Return a temporary directory path object which is unique to each test - function invocation, created as a sub directory of the base temporary - directory. - - By default, a new base temporary directory is created each test session, - and old bases are removed after 3 sessions, to aid in debugging. - This behavior can be configured with :confval:`tmp_path_retention_count` and - :confval:`tmp_path_retention_policy`. - If ``--basetemp`` is used then it is cleared each session. See - :ref:`temporary directory location and retention`. - - The returned object is a :class:`pathlib.Path` object. + tmp_path -- .../_pytest/tmpdir.py:256 + Return a temporary directory (as :class:`pathlib.Path` object) + which is unique to each test function invocation. + The temporary directory is created as a subdirectory + of the base temporary directory, with configurable retention, + as discussed in :ref:`temporary directory location and retention`. ========================== no tests ran in 0.12s =========================== diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 9f30c86be3a..a01c2589f1f 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -31,6 +31,47 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 8.3.4 (2024-12-01) +========================= + +Bug fixes +--------- + +- `#12592 `_: Fixed :class:`KeyError` crash when using ``--import-mode=importlib`` in a directory layout where a directory contains a child directory with the same name. + + +- `#12818 `_: Assertion rewriting now preserves the source ranges of the original instructions, making it play well with tools that deal with the ``AST``, like `executing `__. + + +- `#12849 `_: ANSI escape codes for colored output now handled correctly in :func:`pytest.fail` with `pytrace=False`. + + +- `#9353 `_: :func:`pytest.approx` now uses strict equality when given booleans. + + + +Improved documentation +---------------------- + +- `#10558 `_: Fix ambiguous docstring of :func:`pytest.Config.getoption`. + + +- `#10829 `_: Improve documentation on the current handling of the ``--basetemp`` option and its lack of retention functionality (:ref:`temporary directory location and retention`). + + +- `#12866 `_: Improved cross-references concerning the :fixture:`recwarn` fixture. + + +- `#12966 `_: Clarify :ref:`filterwarnings` docs on filter precedence/order when using multiple :ref:`@pytest.mark.filterwarnings ` marks. + + + +Contributor-facing changes +-------------------------- + +- `#12497 `_: Fixed two failing pdb-related tests on Python 3.13. + + pytest 8.3.3 (2024-09-09) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index fa43308d045..f1f7deb909c 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -162,7 +162,7 @@ objects, they are still using the default pytest representation: rootdir: /home/sweet/project collected 8 items - + @@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia rootdir: /home/sweet/project collected 4 items - + @@ -318,7 +318,7 @@ Let's first see how it looks like at collection time: rootdir: /home/sweet/project collected 2 items - + diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 42a603f114c..1d1ce07c10a 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -152,7 +152,7 @@ The test collection would look like this: configfile: pytest.ini collected 2 items - + @@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this: configfile: pytest.ini collected 3 items - + diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index faf81154c48..d4308e5aab3 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 8.3.3 + pytest 8.3.4 .. _`simpletest`: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index 4f6e8cbee06..9232c9e8449 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used: rootdir: /home/sweet/project collected 12 items - + From 53f8b4e634c5066c4f797a87b20060edbb086240 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 1 Dec 2024 09:43:00 -0300 Subject: [PATCH 53/78] Update pypa/gh-action-pypi-publish to v1.12.2 The previous deploy failed with `HTTPError: 502 Bad Gateway`. --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index a58dc83639a..6238a6fedcc 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,7 +54,7 @@ jobs: path: dist - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.10.1 + uses: pypa/gh-action-pypi-publish@v1.12.2 with: attestations: true From ade10c1d65723e149e41f27da7b6d5e8edfd7493 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 1 Dec 2024 13:51:36 +0000 Subject: [PATCH 54/78] Merge pull request #13019 from nicoddemus/release-template (#13020) Remove 'pip' command hint from release announcement (cherry picked from commit e135d760281f343cfe01c39ffa74ba920456f96d) Co-authored-by: Bruno Oliveira --- RELEASING.rst | 2 +- scripts/release.patch.rst | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/RELEASING.rst b/RELEASING.rst index 08004a84c00..033c27ac2b1 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -170,4 +170,4 @@ Both automatic and manual processes described above follow the same steps from t * python-announce-list@python.org (all releases) * testing-in-python@lists.idyll.org (only major/minor releases) - And announce it on `Twitter `_ with the ``#pytest`` hashtag. + And announce it on `Twitter `_ and `BlueSky `_ with the ``#pytest`` hashtag. diff --git a/scripts/release.patch.rst b/scripts/release.patch.rst index 59fbe50ce0e..120cae51702 100644 --- a/scripts/release.patch.rst +++ b/scripts/release.patch.rst @@ -3,9 +3,7 @@ pytest-{version} pytest {version} has just been released to PyPI. -This is a bug-fix release, being a drop-in replacement. To upgrade:: - - pip install --upgrade pytest +This is a bug-fix release, being a drop-in replacement. The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. From 179b690a724819d62f1cad6549671a753ed831b6 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 7 Dec 2024 14:02:52 +0000 Subject: [PATCH 55/78] fix `AttributeError` crash when using `--import-mode=importlib` (#13029) (#13037) Only parent modules with the `__path__` attribute can be used by the `find_spec` function, and most of the standard library does not meet this condition. Fixes #13026 . (cherry picked from commit 8cff12865691b14b4bce70e0e304524619be385d) Co-authored-by: dongfangtianyu <7629022+dongfangtianyu@users.noreply.github.com> --- changelog/13026.bugfix.rst | 1 + src/_pytest/pathlib.py | 5 ++++- testing/test_pathlib.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 changelog/13026.bugfix.rst diff --git a/changelog/13026.bugfix.rst b/changelog/13026.bugfix.rst new file mode 100644 index 00000000000..d10edbd111a --- /dev/null +++ b/changelog/13026.bugfix.rst @@ -0,0 +1 @@ +Fixed :class:`AttributeError` crash when using ``--import-mode=importlib`` when top-level directory same name as another module of the standard library. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index dd36559ce1b..a2318e79280 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -668,7 +668,10 @@ def _import_module_using_spec( parent_module: ModuleType | None = None if parent_module_name: parent_module = sys.modules.get(parent_module_name) - if parent_module is None: + # If the parent_module lacks the `__path__` attribute, AttributeError when finding a submodule's spec, + # requiring re-import according to the path. + need_reimport = not hasattr(parent_module, "__path__") + if parent_module is None or need_reimport: # Get parent_location based on location, get parent_path based on path. if module_path.name == "__init__.py": # If the current module is in a package, diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 62359303f3b..e57e031c445 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -922,6 +922,37 @@ def test_my_test(): result = pytester.runpytest("--import-mode=importlib") result.stdout.fnmatch_lines("* 1 passed *") + @pytest.mark.parametrize("name", ["code", "time", "math"]) + def test_importlib_same_name_as_stl( + self, pytester, ns_param: bool, tmp_path: Path, name: str + ): + """Import a namespace package with the same name as the standard library (#13026).""" + file_path = pytester.path / f"{name}/foo/test_demo.py" + file_path.parent.mkdir(parents=True) + file_path.write_text( + dedent( + """ + def test_demo(): + pass + """ + ), + encoding="utf-8", + ) + + # unit test + __import__(name) # import standard library + + import_path( # import user files + file_path, + mode=ImportMode.importlib, + root=pytester.path, + consider_namespace_packages=ns_param, + ) + + # E2E test + result = pytester.runpytest("--import-mode=importlib") + result.stdout.fnmatch_lines("* 1 passed *") + def create_installed_doctests_and_tests_dir( self, path: Path, monkeypatch: MonkeyPatch ) -> tuple[Path, Path, Path]: From 4e7af3572a1783f988bfa6758f6af3084c7ca54f Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 13:08:36 -0300 Subject: [PATCH 56/78] Fix `ImportError` crash when using `--import-mode=importlib` (#13053) (#13054) Regression in #12716 In short: `PathFinder.find_spec` received the argument `/cow/moo` but loaded `/cow/moo/moo.py` instead. **Trigger conditions:** 1. `/cow/moo/moo.py` exists (a file and directory with the same name). 2. `/cow/moo/test_moo.py` exists (test case resides in the directory). When pytest loads test files in `importlib` mode, it continues recursive loading upward: - When loading `cow.moo`, it should return a namespace but unexpectedly returns a module. - When loading `cow.moo.moo`, it should return a module but unexpectedly returns a namespace. **Complete example:** [[GitHub repository](https://github.com/dongfangtianyu/pytest_importlib_issue)](https://github.com/dongfangtianyu/pytest_importlib_issue) - `main.py`: Reproduces the error. - `debug.py`: Demonstrates the behavior of `PathFinder.find_spec`. **Context:** https://github.com/pytest-dev/pytest/issues/12592#issuecomment-2512039696 https://github.com/pytest-dev/pytest/issues/12592#issuecomment-2515100428 --------- Co-authored-by: Bruno Oliveira (cherry picked from commit 28e1e25a6782513db8a2963bd5ed5a9d66682f86) Co-authored-by: dongfangtianyu <7629022+dongfangtianyu@users.noreply.github.com> --- changelog/13053.bugfix.rst | 1 + src/_pytest/pathlib.py | 13 ++++++++++--- testing/test_pathlib.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 changelog/13053.bugfix.rst diff --git a/changelog/13053.bugfix.rst b/changelog/13053.bugfix.rst new file mode 100644 index 00000000000..b6744331394 --- /dev/null +++ b/changelog/13053.bugfix.rst @@ -0,0 +1 @@ +Fixed a regression in pytest 8.3.4 where, when using ``--import-mode=importlib``, a directory containing py file with the same name would cause an ``ImportError`` diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index a2318e79280..decf8af0b4f 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -694,9 +694,16 @@ def _import_module_using_spec( # Checking with sys.meta_path first in case one of its hooks can import this module, # such as our own assertion-rewrite hook. for meta_importer in sys.meta_path: - spec = meta_importer.find_spec( - module_name, [str(module_location), str(module_path)] - ) + module_name_of_meta = getattr(meta_importer.__class__, "__module__", "") + if module_name_of_meta == "_pytest.assertion.rewrite" and module_path.is_file(): + # Import modules in subdirectories by module_path + # to ensure assertion rewrites are not missed (#12659). + find_spec_path = [str(module_location), str(module_path)] + else: + find_spec_path = [str(module_location)] + + spec = meta_importer.find_spec(module_name, find_spec_path) + if spec_matches_module_path(spec, module_path): break else: diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index e57e031c445..1acc96915d4 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -1498,6 +1498,34 @@ def test(): ] ) + def test_ns_multiple_levels_import_error( + self, + tmp_path: Path, + pytester: Pytester, + ) -> None: + # Trigger condition 1: ns and file with the same name + file = pytester.path / "cow/moo/moo.py" + file.parent.mkdir(parents=True) + file.write_text("data=123", encoding="utf-8") + + # Trigger condition 2: tests are located in ns + tests = pytester.path / "cow/moo/test_moo.py" + + tests.write_text( + dedent( + """ + from cow.moo.moo import data + + def test_moo(): + print(data) + """ + ), + encoding="utf-8", + ) + + result = pytester.runpytest("--import-mode=importlib") + assert result.ret == ExitCode.OK + @pytest.mark.parametrize("import_mode", ["prepend", "append", "importlib"]) def test_incorrect_namespace_package( self, From 544bf20d4394901ba80d4fd790a3003977626b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=87=BA=F0=9F=87=A6=20Sviatoslav=20Sydorenko=20=28?= =?UTF-8?q?=D0=A1=D0=B2=D1=8F=D1=82=D0=BE=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1?= =?UTF-8?q?=D0=B8=D0=B4=D0=BE=D1=80=D0=B5=D0=BD=D0=BA=D0=BE=29?= Date: Mon, 16 Dec 2024 06:02:44 +0100 Subject: [PATCH 57/78] Merge pull request #13064 from pytest-dev/dependabot/github_actions/pypa/gh-action-pypi-publish-1.12.3 build(deps): Bump pypa/gh-action-pypi-publish from 1.12.2 to 1.12.3 (cherry picked from commit d5254b50f8c1302e86a4fc81780ca513cdf2fb24) --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 6238a6fedcc..7f4381e7f61 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,7 +54,7 @@ jobs: path: dist - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.12.2 + uses: pypa/gh-action-pypi-publish@v1.12.3 with: attestations: true From e4d842ac5deae61773961f94d8b2b401b34802ad Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 16 Dec 2024 12:18:31 +0200 Subject: [PATCH 58/78] [8.3.x] ci: fix failing jobs due to pluggy requiring Python>=3.9 Some of the pluggy jobs (I guess the ones fetching from git) started to fail since pluggy dropped support for 3.8 and the jobs are running with 3.8. So bump them to 3.9. --- .github/workflows/test.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3131f487ab9..687807faf9a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: matrix: name: [ "windows-py38", - "windows-py38-pluggy", + "windows-py39-pluggy", "windows-py39", "windows-py310", "windows-py311", @@ -63,7 +63,7 @@ jobs: "windows-py313", "ubuntu-py38", - "ubuntu-py38-pluggy", + "ubuntu-py39-pluggy", "ubuntu-py38-freeze", "ubuntu-py39", "ubuntu-py310", @@ -88,10 +88,10 @@ jobs: os: windows-latest tox_env: "py38-unittestextras" use_coverage: true - - name: "windows-py38-pluggy" - python: "3.8" + - name: "windows-py39-pluggy" + python: "3.9" os: windows-latest - tox_env: "py38-pluggymain-pylib-xdist" + tox_env: "py39-pluggymain-pylib-xdist" - name: "windows-py39" python: "3.9" os: windows-latest @@ -118,10 +118,10 @@ jobs: os: ubuntu-latest tox_env: "py38-lsof-numpy-pexpect" use_coverage: true - - name: "ubuntu-py38-pluggy" - python: "3.8" + - name: "ubuntu-py39-pluggy" + python: "3.9" os: ubuntu-latest - tox_env: "py38-pluggymain-pylib-xdist" + tox_env: "py39-pluggymain-pylib-xdist" - name: "ubuntu-py38-freeze" python: "3.8" os: ubuntu-latest @@ -192,9 +192,9 @@ jobs: contains( fromJSON( '[ - "windows-py38-pluggy", + "windows-py39-pluggy", "windows-py313", - "ubuntu-py38-pluggy", + "ubuntu-py39-pluggy", "ubuntu-py38-freeze", "ubuntu-py313", "macos-py38", From c2abd05a8801c617c6d3204bdc9c447e6ed32bbd Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:15:40 +0000 Subject: [PATCH 59/78] ci: harden github actions according to "zizmor" recommendations (#13062) (#13067) Fix all issues reported by zizmor 0.9.2 running locally. See: https://woodruffw.github.io/zizmor/ (cherry picked from commit ee8f98d2f976a1df17093eab12e00f0f3c4bee29) Co-authored-by: Ran Benita --- .github/workflows/deploy.yml | 12 +++++++++--- .github/workflows/prepare-release-pr.yml | 11 +++++++++-- .github/workflows/update-plugin-list.yml | 6 ++++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7f4381e7f61..7613f41fbb5 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -46,6 +46,8 @@ jobs: contents: write steps: - uses: actions/checkout@v4 + with: + persist-credentials: true - name: Download Package uses: actions/download-artifact@v4 @@ -59,11 +61,13 @@ jobs: attestations: true - name: Push tag + env: + VERSION: ${{ github.event.inputs.version }} run: | git config user.name "pytest bot" git config user.email "pytestbot@gmail.com" - git tag --annotate --message=v${{ github.event.inputs.version }} ${{ github.event.inputs.version }} ${{ github.sha }} - git push origin ${{ github.event.inputs.version }} + git tag --annotate --message=v"$VERSION" "$VERSION" ${{ github.sha }} + git push origin "$VERSION" release-notes: @@ -98,9 +102,11 @@ jobs: pip install --upgrade tox - name: Generate release notes + env: + VERSION: ${{ github.event.inputs.version }} run: | sudo apt-get install pandoc - tox -e generate-gh-release-notes -- ${{ github.event.inputs.version }} scripts/latest-release-notes.md + tox -e generate-gh-release-notes -- "$VERSION" scripts/latest-release-notes.md - name: Publish GitHub Release uses: softprops/action-gh-release@v2 diff --git a/.github/workflows/prepare-release-pr.yml b/.github/workflows/prepare-release-pr.yml index 1bb23fab844..fdb770317df 100644 --- a/.github/workflows/prepare-release-pr.yml +++ b/.github/workflows/prepare-release-pr.yml @@ -30,6 +30,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false - name: Set up Python uses: actions/setup-python@v5 @@ -43,10 +44,16 @@ jobs: - name: Prepare release PR (minor/patch release) if: github.event.inputs.major == 'no' + env: + BRANCH: ${{ github.event.inputs.branch }} + PRERELEASE: ${{ github.event.inputs.prerelease }} run: | - tox -e prepare-release-pr -- ${{ github.event.inputs.branch }} ${{ github.token }} --prerelease='${{ github.event.inputs.prerelease }}' + tox -e prepare-release-pr -- "$BRANCH" ${{ github.token }} --prerelease="$PRERELEASE" - name: Prepare release PR (major release) if: github.event.inputs.major == 'yes' + env: + BRANCH: ${{ github.event.inputs.branch }} + PRERELEASE: ${{ github.event.inputs.prerelease }} run: | - tox -e prepare-release-pr -- ${{ github.event.inputs.branch }} ${{ github.token }} --major --prerelease='${{ github.event.inputs.prerelease }}' + tox -e prepare-release-pr -- "$BRANCH" ${{ github.token }} --major --prerelease="$PRERELEASE" diff --git a/.github/workflows/update-plugin-list.yml b/.github/workflows/update-plugin-list.yml index ade8452afd5..267ed69396e 100644 --- a/.github/workflows/update-plugin-list.yml +++ b/.github/workflows/update-plugin-list.yml @@ -23,12 +23,14 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + persist-credentials: false - name: Setup Python uses: actions/setup-python@v5 with: python-version: "3.11" cache: pip + - name: requests-cache uses: actions/cache@v4 with: @@ -41,7 +43,6 @@ jobs: python -m pip install --upgrade pip pip install packaging requests tabulate[widechars] tqdm requests-cache platformdirs - - name: Update Plugin List run: python scripts/update-plugin-list.py @@ -61,8 +62,9 @@ jobs: - name: Instruct the maintainers to trigger CI by undrafting the PR env: GITHUB_TOKEN: ${{ github.token }} + PULL_REQUEST_NUMBER: ${{ steps.pr.outputs.pull-request-number }} run: >- gh pr comment --body 'Please mark the PR as ready for review to trigger PR checks.' --repo '${{ github.repository }}' - '${{ steps.pr.outputs.pull-request-number }}' + "$PULL_REQUEST_NUMBER" From 763a1aab9fbf6e35554b8d18de5678499dbc680a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=F0=9F=87=BA=F0=9F=87=A6=20Sviatoslav=20Sydorenko=20=28?= =?UTF-8?q?=D0=A1=D0=B2=D1=8F=D1=82=D0=BE=D1=81=D0=BB=D0=B0=D0=B2=20=D0=A1?= =?UTF-8?q?=D0=B8=D0=B4=D0=BE=D1=80=D0=B5=D0=BD=D0=BA=D0=BE=29?= Date: Mon, 16 Dec 2024 22:20:06 +0100 Subject: [PATCH 60/78] Merge pull request #13069 from bluetech/zizmor-pre-commit pre-commit: add zizmor hook (cherry picked from commit 76a7f59a90e078a498253f68cb6d26d39cc17d5b) --- .pre-commit-config.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7806f68e2d8..1a8469a2f04 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,10 @@ repos: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml +- repo: https://github.com/woodruffw/zizmor-pre-commit + rev: v0.9.2 + hooks: + - id: zizmor - repo: https://github.com/adamchainz/blacken-docs rev: 1.18.0 hooks: From ad4081f63985ff42f54fc52baa7ce0e7b5cffba4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 8 Jan 2025 18:04:15 +0100 Subject: [PATCH 61/78] Merge pull request #13113 from The-Compiler/fix-pygments-whitespace Fix selftests with Pygments >= 2.19.0 (cherry picked from commit bdfc3a99bd733f385f150446caef6d5843bb6418) --- changelog/13112.contrib.rst | 1 + testing/conftest.py | 7 +++++++ testing/test_terminal.py | 10 +++++----- 3 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 changelog/13112.contrib.rst diff --git a/changelog/13112.contrib.rst b/changelog/13112.contrib.rst new file mode 100644 index 00000000000..5e59a736edb --- /dev/null +++ b/changelog/13112.contrib.rst @@ -0,0 +1 @@ +Fixed selftest failures in ``test_terminal.py`` with Pygments >= 2.19.0 diff --git a/testing/conftest.py b/testing/conftest.py index 69af03324d6..194da0f8309 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -2,10 +2,13 @@ from __future__ import annotations import dataclasses +import importlib.metadata import re import sys from typing import Generator +from packaging.version import Version + from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester import pytest @@ -168,6 +171,9 @@ def color_mapping(): Used by tests which check the actual colors output by pytest. """ + # https://github.com/pygments/pygments/commit/d24e272894a56a98b1b718d9ac5fabc20124882a + pygments_version = Version(importlib.metadata.version("pygments")) + pygments_has_kwspace_hl = pygments_version >= Version("2.19") class ColorMapping: COLORS = { @@ -180,6 +186,7 @@ class ColorMapping: "bold": "\x1b[1m", "reset": "\x1b[0m", "kw": "\x1b[94m", + "kwspace": "\x1b[90m \x1b[39;49;00m" if pygments_has_kwspace_hl else " ", "hl-reset": "\x1b[39;49;00m", "function": "\x1b[92m", "number": "\x1b[94m", diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 14c152d6123..cbbe9cbd7a9 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1299,13 +1299,13 @@ def test_this(): "=*= FAILURES =*=", "{red}{bold}_*_ test_this _*_{reset}", "", - " {reset}{kw}def{hl-reset} {function}test_this{hl-reset}():{endline}", + " {reset}{kw}def{hl-reset}{kwspace}{function}test_this{hl-reset}():{endline}", "> fail(){endline}", "", "{bold}{red}test_color_yes.py{reset}:5: ", "_ _ * _ _*", "", - " {reset}{kw}def{hl-reset} {function}fail{hl-reset}():{endline}", + " {reset}{kw}def{hl-reset}{kwspace}{function}fail{hl-reset}():{endline}", "> {kw}assert{hl-reset} {number}0{hl-reset}{endline}", "{bold}{red}E assert 0{reset}", "", @@ -2585,7 +2585,7 @@ def test_foo(): result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ - " {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}", + " {reset}{kw}def{hl-reset}{kwspace}{function}test_foo{hl-reset}():{endline}", "> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}", "{bold}{red}E assert 1 == 10{reset}", ] @@ -2607,7 +2607,7 @@ def test_foo(): result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ - " {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}", + " {reset}{kw}def{hl-reset}{kwspace}{function}test_foo{hl-reset}():{endline}", " {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}", "> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}{endline}", "{bold}{red}E assert 0{reset}", @@ -2630,7 +2630,7 @@ def test_foo(): result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch( [ - " {reset}{kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}", + " {reset}{kw}def{hl-reset}{kwspace}{function}test_foo{hl-reset}():{endline}", "> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}", "{bold}{red}E assert 1 == 10{reset}", ] From f1c6bab42d696b0d2c5158038c5c891b6c7f8e69 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 18:29:39 -0300 Subject: [PATCH 62/78] Fix crash when directory is removed during collection (#13086) (#13117) Fixes #13083 --------- Co-authored-by: Bruno Oliveira (cherry picked from commit 3214263b2f0d17b265c40cf5f87d70357b9a219d) Co-authored-by: delta87 <124760624+delta87@users.noreply.github.com> --- AUTHORS | 1 + changelog/13083.bugfix.rst | 1 + src/_pytest/pathlib.py | 13 ++++++++++--- testing/test_pathlib.py | 24 ++++++++++++++++++++++++ 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 changelog/13083.bugfix.rst diff --git a/AUTHORS b/AUTHORS index e25156cde19..deb42adbe30 100644 --- a/AUTHORS +++ b/AUTHORS @@ -353,6 +353,7 @@ Ran Benita Raphael Castaneda Raphael Pierzina Rafal Semik +Reza Mousavi Raquel Alegre Ravi Chandra Reagan Lee diff --git a/changelog/13083.bugfix.rst b/changelog/13083.bugfix.rst new file mode 100644 index 00000000000..fc4564755ba --- /dev/null +++ b/changelog/13083.bugfix.rst @@ -0,0 +1 @@ +Fixed issue where pytest could crash if one of the collected directories got removed during collection. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index decf8af0b4f..ca8a1d2d4f9 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -955,17 +955,24 @@ def scandir( The returned entries are sorted according to the given key. The default is to sort by name. + If the directory does not exist, return an empty list. """ entries = [] - with os.scandir(path) as s: - # Skip entries with symlink loops and other brokenness, so the caller - # doesn't have to deal with it. + # Attempt to create a scandir iterator for the given path. + try: + scandir_iter = os.scandir(path) + except FileNotFoundError: + # If the directory does not exist, return an empty list. + return [] + # Use the scandir iterator in a context manager to ensure it is properly closed. + with scandir_iter as s: for entry in s: try: entry.is_file() except OSError as err: if _ignore_error(err): continue + # Reraise non-ignorable errors to avoid hiding issues. raise entries.append(entry) entries.sort(key=sort_key) # type: ignore[arg-type] diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 1acc96915d4..dbf76887859 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -38,6 +38,7 @@ from _pytest.pathlib import resolve_package_path from _pytest.pathlib import resolve_pkg_root_and_module_name from _pytest.pathlib import safe_exists +from _pytest.pathlib import scandir from _pytest.pathlib import spec_matches_module_path from _pytest.pathlib import symlink_or_skip from _pytest.pathlib import visit @@ -569,6 +570,29 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N assert getattr(module, "foo")() == 42 +def test_scandir_with_non_existent_directory() -> None: + # Test with a directory that does not exist + non_existent_dir = "path_to_non_existent_dir" + result = scandir(non_existent_dir) + # Assert that the result is an empty list + assert result == [] + + +def test_scandir_handles_os_error() -> None: + # Create a mock entry that will raise an OSError when is_file is called + mock_entry = unittest.mock.MagicMock() + mock_entry.is_file.side_effect = OSError("some permission error") + # Mock os.scandir to return an iterator with our mock entry + with unittest.mock.patch("os.scandir") as mock_scandir: + mock_scandir.return_value.__enter__.return_value = [mock_entry] + # Call the scandir function with a path + # We expect an OSError to be raised here + with pytest.raises(OSError, match="some permission error"): + scandir("/fake/path") + # Verify that the is_file method was called on the mock entry + mock_entry.is_file.assert_called_once() + + class TestImportLibMode: def test_importmode_importlib_with_dataclass( self, tmp_path: Path, ns_param: bool From 02519fdf2386db733adb238c17aa8459f2e7a5df Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 12 Jan 2025 10:48:20 -0300 Subject: [PATCH 63/78] Update links to microblogging pages (#13123) (#13127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update links to microblogging pages * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Apply suggestions from code review Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) (cherry picked from commit 05538acf906d6f6e0f75d1d0e2e2052e933761de) Co-authored-by: Florian Bruhin --- RELEASING.rst | 6 +++++- doc/en/contact.rst | 7 +++++++ pyproject.toml | 3 ++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/RELEASING.rst b/RELEASING.rst index 033c27ac2b1..000eae814d3 100644 --- a/RELEASING.rst +++ b/RELEASING.rst @@ -170,4 +170,8 @@ Both automatic and manual processes described above follow the same steps from t * python-announce-list@python.org (all releases) * testing-in-python@lists.idyll.org (only major/minor releases) - And announce it on `Twitter `_ and `BlueSky `_ with the ``#pytest`` hashtag. + And announce it with the ``#pytest`` hashtag on: + + * `Bluesky `_ + * `Fosstodon `_ + * `Twitter/X `_ diff --git a/doc/en/contact.rst b/doc/en/contact.rst index d650a7efbab..cd34f548e99 100644 --- a/doc/en/contact.rst +++ b/doc/en/contact.rst @@ -24,6 +24,13 @@ Chat `_) - ``#pytest`` `on Matrix `_. +Microblogging +------------- + +- Bluesky: `@pytest.org `_ +- Mastodon: `@pytest@fosstodon.org `_ +- Twitter/X: `@pytestdotorg `_ + Mail ---- diff --git a/pyproject.toml b/pyproject.toml index c7941f9dafa..ff27906537a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,10 +64,11 @@ optional-dependencies.dev = [ "xmlschema", ] urls.Changelog = "https://docs.pytest.org/en/stable/changelog.html" +urls.Contact = "https://docs.pytest.org/en/stable/contact.html" +urls.Funding = "https://docs.pytest.org/en/stable/sponsor.html" urls.Homepage = "https://docs.pytest.org/en/latest/" urls.Source = "https://github.com/pytest-dev/pytest" urls.Tracker = "https://github.com/pytest-dev/pytest/issues" -urls.Twitter = "https://twitter.com/pytestdotorg" scripts."py.test" = "pytest:console_main" scripts.pytest = "pytest:console_main" From ef06e438e50994381bc235a60d59b946732619d5 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:27:12 +0000 Subject: [PATCH 64/78] Fix example in pytest_collection_modifyitems (#13152) (#13154) Ref #13149 (cherry picked from commit 4a6a512443e5e1aaed0b5bb0cc82adb2c09d4b66) Co-authored-by: Bruno Oliveira --- src/_pytest/hookspec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 0a41b0aca47..ed843606fea 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -277,7 +277,7 @@ def pytest_collection_modifyitems( When items are deselected (filtered out from ``items``), the hook :hook:`pytest_deselected` must be called explicitly with the deselected items to properly notify other plugins, - e.g. with ``config.hook.pytest_deselected(deselected_items)``. + e.g. with ``config.hook.pytest_deselected(items=deselected_items)``. :param session: The pytest session object. :param config: The pytest config object. From 4dbec99454cf9fc1282e77fa6ca607939d710abe Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 26 Jan 2025 10:15:35 -0300 Subject: [PATCH 65/78] Fix sequences being shortened even with `-vv` verbosity. (#13163) Fix #11777 Co-authored-by: Kenny Y <24802984+kenny-y-dev@users.noreply.github.com> --- changelog/11777.bugfix.rst | 1 + src/_pytest/assertion/rewrite.py | 3 +++ testing/test_terminal.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 changelog/11777.bugfix.rst diff --git a/changelog/11777.bugfix.rst b/changelog/11777.bugfix.rst new file mode 100644 index 00000000000..3f1ffc27a68 --- /dev/null +++ b/changelog/11777.bugfix.rst @@ -0,0 +1 @@ +Fixed issue where sequences were still being shortened even with ``-vv`` verbosity. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 37c09b03467..1f11935c70e 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -28,6 +28,7 @@ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest._io.saferepr import saferepr +from _pytest._io.saferepr import saferepr_unlimited from _pytest._version import version from _pytest.assertion import util from _pytest.config import Config @@ -432,6 +433,8 @@ def _saferepr(obj: object) -> str: return obj.__name__ maxsize = _get_maxsize_for_saferepr(util._config) + if not maxsize: + return saferepr_unlimited(obj).replace("\n", "\\n") return saferepr(obj, maxsize=maxsize).replace("\n", "\\n") diff --git a/testing/test_terminal.py b/testing/test_terminal.py index cbbe9cbd7a9..14125560d5a 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -2525,6 +2525,35 @@ def test(): ) +def test_full_sequence_print_with_vv( + monkeypatch: MonkeyPatch, pytester: Pytester +) -> None: + """Do not truncate sequences in summaries with -vv (#11777).""" + monkeypatch.setattr(_pytest.terminal, "running_on_ci", lambda: False) + + pytester.makepyfile( + """ + def test_len_list(): + l = list(range(10)) + assert len(l) == 9 + + def test_len_dict(): + d = dict(zip(range(10), range(10))) + assert len(d) == 9 + """ + ) + + result = pytester.runpytest("-vv") + assert result.ret == 1 + result.stdout.fnmatch_lines( + [ + "*short test summary info*", + f"*{list(range(10))}*", + f"*{dict(zip(range(10), range(10)))}*", + ] + ) + + @pytest.mark.parametrize( "seconds, expected", [ From 8728b1afebd02ba0aa0aadeb38815f18adac1d28 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:48:21 +0100 Subject: [PATCH 66/78] Update pythonpath.rst (#13181) (#13182) Fix two tiny typos in `pythonpath.rst` (cherry picked from commit c962c86845aea805f47c33f1c67582640a7d3e6f) Co-authored-by: Sadra Barikbin --- doc/en/explanation/pythonpath.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/explanation/pythonpath.rst b/doc/en/explanation/pythonpath.rst index d0314a6dbcd..e68f455cedf 100644 --- a/doc/en/explanation/pythonpath.rst +++ b/doc/en/explanation/pythonpath.rst @@ -8,7 +8,7 @@ pytest import mechanisms and ``sys.path``/``PYTHONPATH`` Import modes ------------ -pytest as a testing framework that needs to import test modules and ``conftest.py`` files for execution. +pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution. Importing files in Python is a non-trivial process, so aspects of the import process can be controlled through the ``--import-mode`` command-line flag, which can assume @@ -78,7 +78,7 @@ these values: For non-test modules, this will work if they are accessible via :py:data:`sys.path`. So for example, ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``. - This is happens when plugins import non-test modules (for example doctesting). + This happens when plugins import non-test modules (for example doctesting). If this step succeeds, the module is returned. From 78cf1f67f707fc07372a89775fd10d2065b5f17a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Feb 2025 23:20:39 +0100 Subject: [PATCH 67/78] ci: Bump build-and-inspect-python-package (#13188) Otherwise the build is broken as Python 3.13 is used, but the pinned pydantic-core version doesn't have support. --- .github/workflows/deploy.yml | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7613f41fbb5..9dc69c3cca3 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,7 +31,7 @@ jobs: persist-credentials: false - name: Build and Check Package - uses: hynek/build-and-inspect-python-package@v2.6.0 + uses: hynek/build-and-inspect-python-package@v2.12.0 with: attest-build-provenance-github: 'true' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 687807faf9a..33dede68906 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,7 +40,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Build and Check Package - uses: hynek/build-and-inspect-python-package@v2.6.0 + uses: hynek/build-and-inspect-python-package@v2.12.0 build: needs: [package] From eb6496b79759f9acde581ed9d7a0777a49b5f820 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:09:37 +0000 Subject: [PATCH 68/78] doc: Change training to remote only (#13196) (#13197) (cherry picked from commit 1f1a10c0da051ada2cbb04d1592d0e14f6de1ac6) Co-authored-by: Florian Bruhin --- doc/en/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/en/index.rst b/doc/en/index.rst index 8b34d589643..7f3dbdd800d 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -2,7 +2,7 @@ .. sidebar:: **Next Open Trainings and Events** - - `Professional Testing with Python `_, via `Python Academy `_ (3 day in-depth training), **March 4th -- 6th 2025**, Leipzig (DE) / Remote + - `Professional Testing with Python `_, via `Python Academy `_ (3 day in-depth training), **March 4th -- 6th 2025**, Remote Also see :doc:`previous talks and blogposts ` From 2ebba0063c66b77a7bd171221de059f3b3e47b86 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 08:03:12 -0300 Subject: [PATCH 69/78] Merge pull request #13199 from jakkdl/tox_docs_no_fetch (#13200) remove `git fetch` from tox `docs` environment (cherry picked from commit de1a4885d6fd27e4189aba94b8922acc58364142) Co-authored-by: John Litborn <11260241+jakkdl@users.noreply.github.com> --- tox.ini | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tox.ini b/tox.ini index 61563ca2c5f..1a6ab344de8 100644 --- a/tox.ini +++ b/tox.ini @@ -104,13 +104,7 @@ basepython = python3.12 # sync with rtd to get errors usedevelop = True deps = -r{toxinidir}/doc/en/requirements.txt -allowlist_externals = - git commands = - # Retrieve possibly missing commits: - -git fetch --unshallow - -git fetch --tags - sphinx-build \ -j auto \ -W --keep-going \ From edbfff72a4051ed9c5f3d9b5d6f316b407cb6961 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 15 Feb 2025 15:09:53 +0000 Subject: [PATCH 70/78] doc: Clarify capturing .readouterr() return value (#13222) (#13225) This got added in 38fb6aae7830837209c40ec1a4ccb68950bc107c, where the "The return value ... changed" made a lot of sense. However, 9c5da9c0d15ccf7ab9f3a8fbd6540e4a56ea789f removed the "versionadded" without adjusting the wording. Also see 3a4435fb59604d40c5d2e2f65e9acba99dd9cff0. (cherry picked from commit 62aa4272229a4e604582db534796df1a729b4752) Co-authored-by: Florian Bruhin --- doc/en/how-to/capture-stdout-stderr.rst | 2 +- doc/en/how-to/parametrize.rst | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/en/how-to/capture-stdout-stderr.rst b/doc/en/how-to/capture-stdout-stderr.rst index 9f7ddce3499..d91bef736e7 100644 --- a/doc/en/how-to/capture-stdout-stderr.rst +++ b/doc/en/how-to/capture-stdout-stderr.rst @@ -131,7 +131,7 @@ test from having to care about setting/resetting output streams and also interacts well with pytest's own per-test capturing. -The return value from ``readouterr`` changed to a ``namedtuple`` with two attributes, ``out`` and ``err``. +The return value of ``readouterr()`` is a ``namedtuple`` with two attributes, ``out`` and ``err``. If the code under test writes non-textual data (``bytes``), you can capture this using the :fixture:`capsysbinary` fixture which instead returns ``bytes`` from diff --git a/doc/en/how-to/parametrize.rst b/doc/en/how-to/parametrize.rst index b6466c491b4..5a16684eb96 100644 --- a/doc/en/how-to/parametrize.rst +++ b/doc/en/how-to/parametrize.rst @@ -29,10 +29,6 @@ pytest enables test parametrization at several levels: .. regendoc: wipe - - - Several improvements. - The builtin :ref:`pytest.mark.parametrize ref` decorator enables parametrization of arguments for a test function. Here is a typical example of a test function that implements checking that a certain input leads From b75cfb162dbb927739698effa3fbcf279655da49 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Mon, 3 Feb 2025 14:46:28 +0100 Subject: [PATCH 71/78] Add readline workaround for libedit (#13176) * Add readline workaround for libedit We had a very similar workaround before for pyreadline, which had a similar issue: - Introduced in #1281 - Removed in #8848 for #8733 and #8847 This technically will regress the issues above, but those issues just mean that `import readline` is broken in general, so the user should fix it instead (by e.g. uninstalling pyreadline). Fixes #12888 Fixes #13170 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Handle no readline on Windows --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> (cherry picked from commit b4009b31978053fe077a32d219ad947a5ba7243c) --- changelog/12888.bugfix.rst | 1 + src/_pytest/capture.py | 18 ++++++++++++++++++ testing/test_capture.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 changelog/12888.bugfix.rst diff --git a/changelog/12888.bugfix.rst b/changelog/12888.bugfix.rst new file mode 100644 index 00000000000..635e35a11ea --- /dev/null +++ b/changelog/12888.bugfix.rst @@ -0,0 +1 @@ +Fixed broken input when using Python 3.13+ and a ``libedit`` build of Python, such as on macOS or with uv-managed Python binaries from the ``python-build-standalone`` project. This could manifest e.g. by a broken prompt when using ``Pdb``, or seeing empty inputs with manual usage of ``input()`` and suspended capturing. diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 506c0b3d287..ded8af87fd9 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -79,6 +79,23 @@ def _colorama_workaround() -> None: pass +def _readline_workaround() -> None: + """Ensure readline is imported early so it attaches to the correct stdio handles. + + This isn't a problem with the default GNU readline implementation, but in + some configurations, Python uses libedit instead (on macOS, and for prebuilt + binaries such as used by uv). + + In theory this is only needed if readline.backend == "libedit", but the + workaround consists of importing readline here, so we already worked around + the issue by the time we could check if we need to. + """ + try: + import readline # noqa: F401 + except ImportError: + pass + + def _windowsconsoleio_workaround(stream: TextIO) -> None: """Workaround for Windows Unicode console handling. @@ -140,6 +157,7 @@ def pytest_load_initial_conftests(early_config: Config) -> Generator[None]: if ns.capture == "fd": _windowsconsoleio_workaround(sys.stdout) _colorama_workaround() + _readline_workaround() pluginmanager = early_config.pluginmanager capman = CaptureManager(ns.capture) pluginmanager.register(capman, "capturemanager") diff --git a/testing/test_capture.py b/testing/test_capture.py index 328de740e8a..adf1c3b2657 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -5,6 +5,7 @@ import io from io import UnsupportedOperation import os +import re import subprocess import sys import textwrap @@ -1666,3 +1667,32 @@ def test_logging(): ) result.stdout.no_fnmatch_line("*Captured stderr call*") result.stdout.no_fnmatch_line("*during collection*") + + +def test_libedit_workaround(pytester: Pytester) -> None: + pytester.makeconftest(""" + import pytest + + + def pytest_terminal_summary(config): + capture = config.pluginmanager.getplugin("capturemanager") + capture.suspend_global_capture(in_=True) + + print("Enter 'hi'") + value = input() + print(f"value: {value!r}") + + capture.resume_global_capture() + """) + readline = pytest.importorskip("readline") + backend = getattr(readline, "backend", readline.__doc__) # added in Python 3.13 + print(f"Readline backend: {backend}") + + child = pytester.spawn_pytest("") + child.expect(r"Enter 'hi'") + child.sendline("hi") + rest = child.read().decode("utf8") + print(rest) + match = re.search(r"^value: '(.*)'\r?$", rest, re.MULTILINE) + assert match is not None + assert match.group(1) == "hi" From e5c2efe3c36199731b41fd68bbf4df5e21404a8b Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 1 Mar 2025 13:59:48 +0000 Subject: [PATCH 72/78] Merge pull request #13256 from webknjaz/maintenance/towncrier-bump (#13258) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit c4028e90bbd2007a9054de66dedda61f358b568a) Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) --- changelog/13256.contrib.rst | 2 ++ doc/en/broken-dep-constraints.txt | 4 ---- 2 files changed, 2 insertions(+), 4 deletions(-) create mode 100644 changelog/13256.contrib.rst diff --git a/changelog/13256.contrib.rst b/changelog/13256.contrib.rst new file mode 100644 index 00000000000..fe3bac87ca1 --- /dev/null +++ b/changelog/13256.contrib.rst @@ -0,0 +1,2 @@ +Support for Towncier versions released in 2024 has been re-enabled +when building Sphinx docs -- by :user:`webknjaz`. diff --git a/doc/en/broken-dep-constraints.txt b/doc/en/broken-dep-constraints.txt index 3a8d2c34b5a..1488e06fa23 100644 --- a/doc/en/broken-dep-constraints.txt +++ b/doc/en/broken-dep-constraints.txt @@ -1,6 +1,2 @@ # This file contains transitive dependencies that need to be pinned for some reason. # Eventually this file will be empty, but in this case keep it around for future use. - -# Pin towncrier temporarily due to incompatibility with sphinxcontrib-towncrier: -# https://github.com/sphinx-contrib/sphinxcontrib-towncrier/issues/92 -towncrier<24.7 From 2fa3f8306c3da4aad7f7349a4947ac37ba6c652f Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 1 Mar 2025 14:45:35 +0000 Subject: [PATCH 73/78] Add more resources and studies to flaky tests page in docs (#13250) (#13259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) Co-authored-by: Bruno Oliveira (cherry picked from commit 9f3c4d11d9b162d0926e9a3ae57ec6f262362d73) Co-authored-by: Vincent (Wen Yu) Ge --- doc/en/explanation/flaky.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/en/explanation/flaky.rst b/doc/en/explanation/flaky.rst index cb6c3983424..8369e1d9311 100644 --- a/doc/en/explanation/flaky.rst +++ b/doc/en/explanation/flaky.rst @@ -117,8 +117,11 @@ This is a limited list, please submit an issue or pull request to expand it! * Gao, Zebao, Yalan Liang, Myra B. Cohen, Atif M. Memon, and Zhen Wang. "Making system user interactive tests repeatable: When and what should we control?." In *Software Engineering (ICSE), 2015 IEEE/ACM 37th IEEE International Conference on*, vol. 1, pp. 55-65. IEEE, 2015. `PDF `__ * Palomba, Fabio, and Andy Zaidman. "Does refactoring of test smells induce fixing flaky tests?." In *Software Maintenance and Evolution (ICSME), 2017 IEEE International Conference on*, pp. 1-12. IEEE, 2017. `PDF in Google Drive `__ -* Bell, Jonathan, Owolabi Legunsen, Michael Hilton, Lamyaa Eloussi, Tifany Yung, and Darko Marinov. "DeFlaker: Automatically detecting flaky tests." In *Proceedings of the 2018 International Conference on Software Engineering*. 2018. `PDF `__ -* Dutta, Saikat and Shi, August and Choudhary, Rutvik and Zhang, Zhekun and Jain, Aryaman and Misailovic, Sasa. "Detecting flaky tests in probabilistic and machine learning applications." In *Proceedings of the 29th ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA)*, pp. 211-224. ACM, 2020. `PDF `__ +* Bell, Jonathan, Owolabi Legunsen, Michael Hilton, Lamyaa Eloussi, Tifany Yung, and Darko Marinov. "DeFlaker: Automatically detecting flaky tests." In *Proceedings of the 2018 International Conference on Software Engineering*. 2018. `PDF `__ +* Dutta, Saikat and Shi, August and Choudhary, Rutvik and Zhang, Zhekun and Jain, Aryaman and Misailovic, Sasa. "Detecting flaky tests in probabilistic and machine learning applications." In *Proceedings of the 29th ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA)*, pp. 211-224. ACM, 2020. `PDF `__ +* Habchi, Sarra and Haben, Guillaume and Sohn, Jeongju and Franci, Adriano and Papadakis, Mike and Cordy, Maxime and Le Traon, Yves. "What Made This Test Flake? Pinpointing Classes Responsible for Test Flakiness." In Proceedings of the 38th IEEE International Conference on Software Maintenance and Evolution (ICSME), IEEE, 2022. `PDF `__ +* Lamprou, Sokrates. "Non-deterministic tests and where to find them: Empirically investigating the relationship between flaky tests and test smells by examining test order dependency." Bachelor thesis, Department of Computer and Information Science, Linköping University, 2022. LIU-IDA/LITH-EX-G–19/056–SE. `PDF `__ +* Leinen, Fabian and Elsner, Daniel and Pretschner, Alexander and Stahlbauer, Andreas and Sailer, Michael and Jürgens, Elmar. "Cost of Flaky Tests in Continuous Integration: An Industrial Case Study." Technical University of Munich and CQSE GmbH, Munich, Germany, 2023. `PDF `__ Resources ^^^^^^^^^ @@ -137,5 +140,12 @@ Resources * `Flaky Tests at Google and How We Mitigate Them `_ by John Micco, 2016 * `Where do Google's flaky tests come from? `_ by Jeff Listfield, 2017 +* Dropbox: + * `Athena: Our automated build health management system `_ by Utsav Shah, 2019 + * `How To Manage Flaky Tests in your CI Workflows `_ by Li Haoyi, 2025 + +* Uber: + * `Handling Flaky Unit Tests in Java `_ by Uber Engineering, 2021 + * `Flaky Tests Overhaul at Uber `_ by Uber Engineering, 2024 .. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist From e217726d2a0edfaf58eae95bf835b85834b96da3 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 1 Mar 2025 16:17:22 +0000 Subject: [PATCH 74/78] Added dedicated page about using types with pytest #12842 (#12963) (#13260) Fixes #12842 --------- Co-authored-by: Bruno Oliveira (cherry picked from commit ca351458d85d0276ee4aa3079c8305feafe42b76) Co-authored-by: mwychung <145583690+mwychung@users.noreply.github.com> --- AUTHORS | 1 + changelog/12842.doc.rst | 3 ++ doc/en/explanation/index.rst | 1 + doc/en/explanation/types.rst | 89 ++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+) create mode 100644 changelog/12842.doc.rst create mode 100644 doc/en/explanation/types.rst diff --git a/AUTHORS b/AUTHORS index deb42adbe30..e8ae5e4beaa 100644 --- a/AUTHORS +++ b/AUTHORS @@ -255,6 +255,7 @@ lovetheguitar Lukas Bednar Luke Murphy Maciek Fijalkowski +Maggie Chung Maho Maik Figura Mandeep Bhutani diff --git a/changelog/12842.doc.rst b/changelog/12842.doc.rst new file mode 100644 index 00000000000..0a0f5c5bc23 --- /dev/null +++ b/changelog/12842.doc.rst @@ -0,0 +1,3 @@ +Added dedicated page about using types with pytest. + +See :ref:`types` for detailed usage. diff --git a/doc/en/explanation/index.rst b/doc/en/explanation/index.rst index 2edf60a5d8b..2606d7d4b34 100644 --- a/doc/en/explanation/index.rst +++ b/doc/en/explanation/index.rst @@ -12,5 +12,6 @@ Explanation fixtures goodpractices pythonpath + types ci flaky diff --git a/doc/en/explanation/types.rst b/doc/en/explanation/types.rst new file mode 100644 index 00000000000..827a2bf02b6 --- /dev/null +++ b/doc/en/explanation/types.rst @@ -0,0 +1,89 @@ +.. _types: + +Typing in pytest +================ + +.. note:: + This page assumes the reader is familiar with Python's typing system and its advantages. + + For more information, refer to `Python's Typing Documentation `_. + +Why type tests? +--------------- + +Typing tests provides significant advantages: + +- **Readability:** Clearly defines expected inputs and outputs, improving readability, especially in complex or parameterized tests. + +- **Refactoring:** This is the main benefit in typing tests, as it will greatly help with refactoring, letting the type checker point out the necessary changes in both production and tests, without needing to run the full test suite. + +For production code, typing also helps catching some bugs that might not be caught by tests at all (regardless of coverage), for example: + +.. code-block:: python + + def get_caption(target: int, items: list[tuple[int, str]]) -> str: + for value, caption in items: + if value == target: + return caption + + +The type checker will correctly error out that the function might return `None`, however even a full coverage test suite might miss that case: + +.. code-block:: python + + def test_get_caption() -> None: + assert get_caption(10, [(1, "foo"), (10, "bar")]) == "bar" + + +Note the code above has 100% coverage, but the bug is not caught (of course the example is "obvious", but serves to illustrate the point). + + + +Using typing in test suites +--------------------------- + +To type fixtures in pytest, just add normal types to the fixture functions -- there is nothing special that needs to be done just because of the `fixture` decorator. + +.. code-block:: python + + import pytest + + + @pytest.fixture + def sample_fixture() -> int: + return 38 + +In the same manner, the fixtures passed to test functions need be annotated with the fixture's return type: + +.. code-block:: python + + def test_sample_fixture(sample_fixture: int) -> None: + assert sample_fixture == 38 + +From the POV of the type checker, it does not matter that `sample_fixture` is actually a fixture managed by pytest, all it matters to it is that `sample_fixture` is a parameter of type `int`. + + +The same logic applies to :ref:`@pytest.mark.parametrize <@pytest.mark.parametrize>`: + +.. code-block:: python + + + @pytest.mark.parametrize("input_value, expected_output", [(1, 2), (5, 6), (10, 11)]) + def test_increment(input_value: int, expected_output: int) -> None: + assert input_value + 1 == expected_output + + +The same logic applies when typing fixture functions which receive other fixtures: + +.. code-block:: python + + @pytest.fixture + def mock_env_user(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("USER", "TestingUser") + + +Conclusion +---------- + +Incorporating typing into pytest tests enhances **clarity**, improves **debugging** and **maintenance**, and ensures **type safety**. +These practices lead to a **robust**, **readable**, and **easily maintainable** test suite that is better equipped to handle future changes with minimal risk of errors. From b55ab2aabb68c0ce94c3903139b062d0c2790152 Mon Sep 17 00:00:00 2001 From: pytest bot Date: Sun, 2 Mar 2025 12:47:09 +0000 Subject: [PATCH 75/78] Prepare release version 8.3.5 --- changelog/11777.bugfix.rst | 1 - changelog/12842.doc.rst | 3 --- changelog/12888.bugfix.rst | 1 - changelog/13026.bugfix.rst | 1 - changelog/13053.bugfix.rst | 1 - changelog/13083.bugfix.rst | 1 - changelog/13112.contrib.rst | 1 - changelog/13256.contrib.rst | 2 -- doc/en/announce/index.rst | 1 + doc/en/announce/release-8.3.5.rst | 26 ++++++++++++++++++ doc/en/builtin.rst | 8 +++--- doc/en/changelog.rst | 41 +++++++++++++++++++++++++++++ doc/en/example/parametrize.rst | 15 ++++++----- doc/en/example/pythoncollection.rst | 4 +-- doc/en/example/reportingdemo.rst | 18 ++++++------- doc/en/example/simple.rst | 2 +- doc/en/getting-started.rst | 2 +- doc/en/how-to/fixtures.rst | 2 +- doc/en/reference/reference.rst | 14 +++++----- 19 files changed, 102 insertions(+), 42 deletions(-) delete mode 100644 changelog/11777.bugfix.rst delete mode 100644 changelog/12842.doc.rst delete mode 100644 changelog/12888.bugfix.rst delete mode 100644 changelog/13026.bugfix.rst delete mode 100644 changelog/13053.bugfix.rst delete mode 100644 changelog/13083.bugfix.rst delete mode 100644 changelog/13112.contrib.rst delete mode 100644 changelog/13256.contrib.rst create mode 100644 doc/en/announce/release-8.3.5.rst diff --git a/changelog/11777.bugfix.rst b/changelog/11777.bugfix.rst deleted file mode 100644 index 3f1ffc27a68..00000000000 --- a/changelog/11777.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed issue where sequences were still being shortened even with ``-vv`` verbosity. diff --git a/changelog/12842.doc.rst b/changelog/12842.doc.rst deleted file mode 100644 index 0a0f5c5bc23..00000000000 --- a/changelog/12842.doc.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added dedicated page about using types with pytest. - -See :ref:`types` for detailed usage. diff --git a/changelog/12888.bugfix.rst b/changelog/12888.bugfix.rst deleted file mode 100644 index 635e35a11ea..00000000000 --- a/changelog/12888.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed broken input when using Python 3.13+ and a ``libedit`` build of Python, such as on macOS or with uv-managed Python binaries from the ``python-build-standalone`` project. This could manifest e.g. by a broken prompt when using ``Pdb``, or seeing empty inputs with manual usage of ``input()`` and suspended capturing. diff --git a/changelog/13026.bugfix.rst b/changelog/13026.bugfix.rst deleted file mode 100644 index d10edbd111a..00000000000 --- a/changelog/13026.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed :class:`AttributeError` crash when using ``--import-mode=importlib`` when top-level directory same name as another module of the standard library. diff --git a/changelog/13053.bugfix.rst b/changelog/13053.bugfix.rst deleted file mode 100644 index b6744331394..00000000000 --- a/changelog/13053.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a regression in pytest 8.3.4 where, when using ``--import-mode=importlib``, a directory containing py file with the same name would cause an ``ImportError`` diff --git a/changelog/13083.bugfix.rst b/changelog/13083.bugfix.rst deleted file mode 100644 index fc4564755ba..00000000000 --- a/changelog/13083.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed issue where pytest could crash if one of the collected directories got removed during collection. diff --git a/changelog/13112.contrib.rst b/changelog/13112.contrib.rst deleted file mode 100644 index 5e59a736edb..00000000000 --- a/changelog/13112.contrib.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed selftest failures in ``test_terminal.py`` with Pygments >= 2.19.0 diff --git a/changelog/13256.contrib.rst b/changelog/13256.contrib.rst deleted file mode 100644 index fe3bac87ca1..00000000000 --- a/changelog/13256.contrib.rst +++ /dev/null @@ -1,2 +0,0 @@ -Support for Towncier versions released in 2024 has been re-enabled -when building Sphinx docs -- by :user:`webknjaz`. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 8a38df7a2e1..51edc964a0c 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-8.3.5 release-8.3.4 release-8.3.3 release-8.3.2 diff --git a/doc/en/announce/release-8.3.5.rst b/doc/en/announce/release-8.3.5.rst new file mode 100644 index 00000000000..3de02c1d7a4 --- /dev/null +++ b/doc/en/announce/release-8.3.5.rst @@ -0,0 +1,26 @@ +pytest-8.3.5 +======================================= + +pytest 8.3.5 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Florian Bruhin +* John Litborn +* Kenny Y +* Ran Benita +* Sadra Barikbin +* Vincent (Wen Yu) Ge +* delta87 +* dongfangtianyu +* mwychung +* 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) + + +Happy testing, +The pytest Development Team diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 85d280da50f..8aa6fef681c 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Values can be any object handled by the json stdlib module. - capsysbinary -- .../_pytest/capture.py:1006 + capsysbinary -- .../_pytest/capture.py:1024 Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` @@ -51,7 +51,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capsysbinary.readouterr() assert captured.out == b"hello\n" - capfd -- .../_pytest/capture.py:1034 + capfd -- .../_pytest/capture.py:1052 Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -69,7 +69,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capfd.readouterr() assert captured.out == "hello\n" - capfdbinary -- .../_pytest/capture.py:1062 + capfdbinary -- .../_pytest/capture.py:1080 Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -87,7 +87,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capfdbinary.readouterr() assert captured.out == b"hello\n" - capsys -- .../_pytest/capture.py:978 + capsys -- .../_pytest/capture.py:996 Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index a01c2589f1f..c95ba561969 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -31,6 +31,47 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 8.3.5 (2025-03-02) +========================= + +Bug fixes +--------- + +- `#11777 `_: Fixed issue where sequences were still being shortened even with ``-vv`` verbosity. + + +- `#12888 `_: Fixed broken input when using Python 3.13+ and a ``libedit`` build of Python, such as on macOS or with uv-managed Python binaries from the ``python-build-standalone`` project. This could manifest e.g. by a broken prompt when using ``Pdb``, or seeing empty inputs with manual usage of ``input()`` and suspended capturing. + + +- `#13026 `_: Fixed :class:`AttributeError` crash when using ``--import-mode=importlib`` when top-level directory same name as another module of the standard library. + + +- `#13053 `_: Fixed a regression in pytest 8.3.4 where, when using ``--import-mode=importlib``, a directory containing py file with the same name would cause an ``ImportError`` + + +- `#13083 `_: Fixed issue where pytest could crash if one of the collected directories got removed during collection. + + + +Improved documentation +---------------------- + +- `#12842 `_: Added dedicated page about using types with pytest. + + See :ref:`types` for detailed usage. + + + +Contributor-facing changes +-------------------------- + +- `#13112 `_: Fixed selftest failures in ``test_terminal.py`` with Pygments >= 2.19.0 + + +- `#13256 `_: Support for Towncier versions released in 2024 has been re-enabled + when building Sphinx docs -- by :user:`webknjaz`. + + pytest 8.3.4 (2024-12-01) ========================= diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index f1f7deb909c..69e715c9db1 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -162,7 +162,7 @@ objects, they are still using the default pytest representation: rootdir: /home/sweet/project collected 8 items - + @@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia rootdir: /home/sweet/project collected 4 items - + @@ -318,7 +318,7 @@ Let's first see how it looks like at collection time: rootdir: /home/sweet/project collected 2 items - + @@ -503,11 +503,12 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssss...ssssssssssss [100%] + sssssssssssssssssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [12] multipython.py:67: 'python3.9' not found - SKIPPED [12] multipython.py:67: 'python3.11' not found - 3 passed, 24 skipped in 0.12s + SKIPPED [9] multipython.py:67: 'python3.9' not found + SKIPPED [9] multipython.py:67: 'python3.10' not found + SKIPPED [9] multipython.py:67: 'python3.11' not found + 27 skipped in 0.12s Parametrization of optional implementations/imports --------------------------------------------------- diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 1d1ce07c10a..89d7ee42614 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -152,7 +152,7 @@ The test collection would look like this: configfile: pytest.ini collected 2 items - + @@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this: configfile: pytest.ini collected 3 items - + diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 0da58d0490e..5e48815bbc9 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -568,12 +568,12 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = ('456') E + where = '123'.startswith E + where '123' = .f at 0xdeadbeef0029>() - E + and '456' = .g at 0xdeadbeef002a>() + E + and '456' = .g at 0xdeadbeef0003>() failure_demo.py:237: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ - self = + self = def test_global_func(self): > assert isinstance(globf(42), float) @@ -584,18 +584,18 @@ Here is a nice run of several failures and how ``pytest`` presents things: failure_demo.py:240: AssertionError _______________________ TestMoreErrors.test_instance _______________________ - self = + self = def test_instance(self): self.x = 6 * 7 > assert self.x != 42 E assert 42 != 42 - E + where 42 = .x + E + where 42 = .x failure_demo.py:244: AssertionError _______________________ TestMoreErrors.test_compare ________________________ - self = + self = def test_compare(self): > assert globf(10) < 5 @@ -605,7 +605,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: failure_demo.py:247: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ - self = + self = def test_try_finally(self): x = 1 @@ -616,7 +616,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: failure_demo.py:252: AssertionError ___________________ TestCustomAssertMsg.test_single_line ___________________ - self = + self = def test_single_line(self): class A: @@ -631,7 +631,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: failure_demo.py:263: AssertionError ____________________ TestCustomAssertMsg.test_multiline ____________________ - self = + self = def test_multiline(self): class A: @@ -650,7 +650,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: failure_demo.py:270: AssertionError ___________________ TestCustomAssertMsg.test_custom_repr ___________________ - self = + self = def test_custom_repr(self): class JSON: diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index a14c34c19c3..bc6c2784849 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -164,7 +164,7 @@ Now we'll get feedback on a bad argument: $ pytest -q --cmdopt=type3 ERROR: usage: pytest [options] [file_or_dir] [file_or_dir] [...] - pytest: error: argument --cmdopt: invalid choice: 'type3' (choose from 'type1', 'type2') + pytest: error: argument --cmdopt: invalid choice: 'type3' (choose from type1, type2) If you need to provide more detailed error messages, you can use the diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index d4308e5aab3..5b9f38d7bf7 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 8.3.4 + pytest 8.3.5 .. _`simpletest`: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index 9232c9e8449..8f84e4867a6 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used: rootdir: /home/sweet/project collected 12 items - + diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 31cf363100a..5035353fb49 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -1988,7 +1988,7 @@ All the command-line flags can be obtained by running ``pytest --help``:: Show cache contents, don't perform collection or tests. Optional argument: glob (default: '*'). --cache-clear Remove all cache contents at start of test run - --lfnf={all,none}, --last-failed-no-failures={all,none} + --lfnf, --last-failed-no-failures={all,none} With ``--lf``, determines whether to execute tests when there are no previously (known) failures or when no cached ``lastfailed`` data was found. @@ -2034,11 +2034,13 @@ All the command-line flags can be obtained by running ``pytest --help``:: Whether code should be highlighted (only if --color is also enabled). Default: yes. --pastebin=mode Send failed|all info to bpaste.net pastebin service - --junit-xml=path Create junit-xml style report file at given path - --junit-prefix=str Prepend prefix to classnames in junit-xml output + --junitxml, --junit-xml=path + Create junit-xml style report file at given path + --junitprefix, --junit-prefix=str + Prepend prefix to classnames in junit-xml output pytest-warnings: - -W PYTHONWARNINGS, --pythonwarnings=PYTHONWARNINGS + -W, --pythonwarnings PYTHONWARNINGS Set which warnings to report, see -W option of Python itself --maxfail=num Exit after first num failures or errors @@ -2047,7 +2049,7 @@ All the command-line flags can be obtained by running ``pytest --help``:: --strict-markers Markers not registered in the `markers` section of the configuration file raise errors --strict (Deprecated) alias to --strict-markers - -c FILE, --config-file=FILE + -c, --config-file FILE Load configuration from `FILE` instead of trying to locate one of the implicit configuration files. --continue-on-collection-errors @@ -2101,7 +2103,7 @@ All the command-line flags can be obtained by running ``pytest --help``:: Store internal tracing debug information in this log file. This file is opened with 'w' and truncated as a result, care advised. Default: pytestdebug.log. - -o OVERRIDE_INI, --override-ini=OVERRIDE_INI + -o, --override-ini OVERRIDE_INI Override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`. --assert=MODE Control assertion debugging tools. From ef890f4e853d9cff4b354fa556e026bb2ae10f42 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 1 Nov 2025 11:43:53 -0300 Subject: [PATCH 76/78] Integrate pytest-subtests (#13738) Fixes #1367 Fixes pytest-dev/pytest-subtests#71 --------- Co-authored-by: Ran Benita --- .coveragerc | 1 + changelog/1367.feature.rst | 28 + doc/en/how-to/index.rst | 1 + doc/en/how-to/parametrize.rst | 6 + doc/en/how-to/subtests.rst | 109 ++++ doc/en/how-to/unittest.rst | 13 +- doc/en/reference/fixtures.rst | 3 + doc/en/reference/reference.rst | 48 +- src/_pytest/config/__init__.py | 4 + src/_pytest/deprecated.py | 1 + src/_pytest/reports.py | 53 +- src/_pytest/runner.py | 17 +- src/_pytest/subtests.py | 411 ++++++++++++++ src/_pytest/terminal.py | 13 +- src/_pytest/unittest.py | 120 +++- src/pytest/__init__.py | 7 +- testing/test_subtests.py | 977 +++++++++++++++++++++++++++++++++ 17 files changed, 1776 insertions(+), 36 deletions(-) create mode 100644 changelog/1367.feature.rst create mode 100644 doc/en/how-to/subtests.rst create mode 100644 src/_pytest/subtests.py create mode 100644 testing/test_subtests.py diff --git a/.coveragerc b/.coveragerc index b810471417f..27fc47686bd 100644 --- a/.coveragerc +++ b/.coveragerc @@ -25,6 +25,7 @@ exclude_lines = ^\s*raise NotImplementedError\b ^\s*return NotImplemented\b ^\s*assert False(,|$) + ^\s*case unreachable: ^\s*assert_never\( ^\s*if TYPE_CHECKING: diff --git a/changelog/1367.feature.rst b/changelog/1367.feature.rst new file mode 100644 index 00000000000..b88480338b5 --- /dev/null +++ b/changelog/1367.feature.rst @@ -0,0 +1,28 @@ +**Support for subtests** has been added. + +:ref:`subtests ` are an alternative to parametrization, useful in situations where the parametrization values are not all known at collection time. + +**Example** + +.. code-block:: python + + def contains_docstring(p: Path) -> bool: + """Return True if the given Python file contains a top-level docstring.""" + ... + + + def test_py_files_contain_docstring(subtests: pytest.Subtests) -> None: + for path in Path.cwd().glob("*.py"): + with subtests.test(path=str(path)): + assert contains_docstring(path) + + +Each assert failure or error is caught by the context manager and reported individually, giving a clear picture of all files that are missing a docstring. + +In addition, :meth:`unittest.TestCase.subTest` is now also supported. + +This feature was originally implemented as a separate plugin in `pytest-subtests `__, but since then has been merged into the core. + +.. note:: + + This feature is experimental and will likely evolve in future releases. By that we mean that we might change how subtests are reported on failure, but the functionality and how to use it are stable. diff --git a/doc/en/how-to/index.rst b/doc/en/how-to/index.rst index 225f289651e..9796f1f8090 100644 --- a/doc/en/how-to/index.rst +++ b/doc/en/how-to/index.rst @@ -16,6 +16,7 @@ Core pytest functionality fixtures mark parametrize + subtests tmp_path monkeypatch doctest diff --git a/doc/en/how-to/parametrize.rst b/doc/en/how-to/parametrize.rst index 5a16684eb96..3773d362bce 100644 --- a/doc/en/how-to/parametrize.rst +++ b/doc/en/how-to/parametrize.rst @@ -20,6 +20,11 @@ pytest enables test parametrization at several levels: * `pytest_generate_tests`_ allows one to define custom parametrization schemes or extensions. + +.. note:: + + See :ref:`subtests` for an alternative to parametrization. + .. _parametrizemark: .. _`@pytest.mark.parametrize`: @@ -194,6 +199,7 @@ To get all combinations of multiple parametrized arguments you can stack This will run the test with the arguments set to ``x=0/y=2``, ``x=1/y=2``, ``x=0/y=3``, and ``x=1/y=3`` exhausting parameters in the order of the decorators. + .. _`pytest_generate_tests`: Basic ``pytest_generate_tests`` example diff --git a/doc/en/how-to/subtests.rst b/doc/en/how-to/subtests.rst new file mode 100644 index 00000000000..956405bc7cf --- /dev/null +++ b/doc/en/how-to/subtests.rst @@ -0,0 +1,109 @@ +.. _subtests: + +How to use subtests +=================== + +.. versionadded:: 9.0 + +.. note:: + + This feature is experimental. Its behavior, particularly how failures are reported, may evolve in future releases. However, the core functionality and usage are considered stable. + +pytest allows for grouping assertions within a normal test, known as *subtests*. + +Subtests are an alternative to parametrization, particularly useful when the exact parametrization values are not known at collection time. + + +.. code-block:: python + + # content of test_subtest.py + + + def test(subtests): + for i in range(5): + with subtests.test(msg="custom message", i=i): + assert i % 2 == 0 + +Each assertion failure or error is caught by the context manager and reported individually: + +.. code-block:: pytest + + $ pytest -q test_subtest.py + + +In the output above: + +* Subtest failures are reported as ``SUBFAILED``. +* Subtests are reported first and the "top-level" test is reported at the end on its own. + +Note that it is possible to use ``subtests`` multiple times in the same test, or even mix and match with normal assertions +outside the ``subtests.test`` block: + +.. code-block:: python + + def test(subtests): + for i in range(5): + with subtests.test("stage 1", i=i): + assert i % 2 == 0 + + assert func() == 10 + + for i in range(10, 20): + with subtests.test("stage 2", i=i): + assert i % 2 == 0 + +.. note:: + + See :ref:`parametrize` for an alternative to subtests. + + +Verbosity +--------- + +By default, only **subtest failures** are shown. Higher verbosity levels (``-v``) will also show progress output for **passed** subtests. + +It is possible to control the verbosity of subtests by setting :confval:`verbosity_subtests`. + + +Typing +------ + +:class:`pytest.Subtests` is exported so it can be used in type annotations: + +.. code-block:: python + + def test(subtests: pytest.Subtests) -> None: ... + +.. _parametrize_vs_subtests: + +Parametrization vs Subtests +--------------------------- + +While :ref:`traditional pytest parametrization ` and ``subtests`` are similar, they have important differences and use cases. + + +Parametrization +~~~~~~~~~~~~~~~ + +* Happens at collection time. +* Generates individual tests. +* Parametrized tests can be referenced from the command line. +* Plays well with plugins that handle test execution, such as ``--last-failed``. +* Ideal for decision table testing. + +Subtests +~~~~~~~~ + +* Happen during test execution. +* Are not known at collection time. +* Can be generated dynamically. +* Cannot be referenced individually from the command line. +* Plugins that handle test execution cannot target individual subtests. +* An assertion failure inside a subtest does not interrupt the test, letting users see all failures in the same report. + + +.. note:: + + This feature was originally implemented as a separate plugin in `pytest-subtests `__, but since ``9.0`` has been merged into the core. + + The core implementation should be compatible to the plugin implementation, except it does not contain custom command-line options to control subtest output. diff --git a/doc/en/how-to/unittest.rst b/doc/en/how-to/unittest.rst index 62e32b6d28f..0c4d9d2949d 100644 --- a/doc/en/how-to/unittest.rst +++ b/doc/en/how-to/unittest.rst @@ -22,17 +22,14 @@ their ``test`` methods in ``test_*.py`` or ``*_test.py`` files. Almost all ``unittest`` features are supported: -* ``@unittest.skip`` style decorators; -* ``setUp/tearDown``; -* ``setUpClass/tearDownClass``; -* ``setUpModule/tearDownModule``; +* :func:`unittest.skip`/:func:`unittest.skipIf` style decorators +* :meth:`unittest.TestCase.setUp`/:meth:`unittest.TestCase.tearDown` +* :meth:`unittest.TestCase.setUpClass`/:meth:`unittest.TestCase.tearDownClass` +* :func:`unittest.setUpModule`/:func:`unittest.tearDownModule` +* :meth:`unittest.TestCase.subTest` (since version ``9.0``) -.. _`pytest-subtests`: https://github.com/pytest-dev/pytest-subtests .. _`load_tests protocol`: https://docs.python.org/3/library/unittest.html#load-tests-protocol -Additionally, :ref:`subtests ` are supported by the -`pytest-subtests`_ plugin. - Up to this point pytest does not have support for the following features: * `load_tests protocol`_; diff --git a/doc/en/reference/fixtures.rst b/doc/en/reference/fixtures.rst index dff93a035ef..41294eba1f8 100644 --- a/doc/en/reference/fixtures.rst +++ b/doc/en/reference/fixtures.rst @@ -48,6 +48,9 @@ Built-in fixtures :fixture:`pytestconfig` Access to configuration values, pluginmanager and plugin hooks. + :fixture:`subtests` + Enable declaring subtests inside test functions. + :fixture:`record_property` Add extra properties to the test. diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 5035353fb49..745d439895b 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -552,6 +552,19 @@ The ``request`` fixture is a special fixture providing information of the reques :members: +.. fixture:: subtests + +subtests +~~~~~~~~ + +The ``subtests`` fixture enables declaring subtests inside test functions. + +**Tutorial**: :ref:`subtests` + +.. autoclass:: pytest.Subtests() + :members: + + .. fixture:: testdir testdir @@ -1899,8 +1912,35 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] verbosity_assertions = 2 - Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of - "auto" can be used to explicitly use the global verbosity level. + If not set, defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of + ``"auto"`` can be used to explicitly use the global verbosity level. + + +.. confval:: verbosity_subtests + + Set the verbosity level specifically for **passed** subtests. + + .. tab:: toml + + .. code-block:: toml + + [pytest] + verbosity_subtests = 1 + + .. tab:: ini + + .. code-block:: ini + + [pytest] + verbosity_subtests = 1 + + A value of ``1`` or higher will show output for **passed** subtests (**failed** subtests are always reported). + Passed subtests output can be suppressed with the value ``0``, which overwrites the ``-v`` command-line option. + + If not set, defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of + ``"auto"`` can be used to explicitly use the global verbosity level. + + See also: :ref:`subtests`. .. confval:: verbosity_test_cases @@ -1912,8 +1952,8 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] verbosity_test_cases = 2 - Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of - "auto" can be used to explicitly use the global verbosity level. + If not set, defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of + ``"auto"`` can be used to explicitly use the global verbosity level. .. confval:: xfail_strict diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 710e03e4fe0..5c7ba554f2f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -272,6 +272,7 @@ def directory_arg(path: str, optname: str) -> str: "unraisableexception", "threadexception", "faulthandler", + "subtests", ) builtin_plugins = set(default_plugins) @@ -1716,6 +1717,9 @@ def getvalueorskip(self, name: str, path=None): VERBOSITY_ASSERTIONS: Final = "assertions" #: Verbosity type for test case execution (see :confval:`verbosity_test_cases`). VERBOSITY_TEST_CASES: Final = "test_cases" + #: Verbosity type for failed subtests (see :confval:`verbosity_subtests`). + VERBOSITY_SUBTESTS: Final = "subtests" + _VERBOSITY_INI_DEFAULT: Final = "auto" def get_verbosity(self, verbosity_type: str | None = None) -> int: diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index a605c24e58f..778d3614715 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -24,6 +24,7 @@ "pytest_catchlog", "pytest_capturelog", "pytest_faulthandler", + "pytest_subtests", } diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 77cbf773e23..9f47471359e 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -251,7 +251,52 @@ def _report_unserialization_failure( raise RuntimeError(stream.getvalue()) -@final +def _format_failed_longrepr( + item: Item, call: CallInfo[None], excinfo: ExceptionInfo[BaseException] +): + if call.when == "call": + longrepr = item.repr_failure(excinfo) + else: + # Exception in setup or teardown. + longrepr = item._repr_failure_py( + excinfo, style=item.config.getoption("tbstyle", "auto") + ) + return longrepr + + +def _format_exception_group_all_skipped_longrepr( + item: Item, + excinfo: ExceptionInfo[BaseExceptionGroup[BaseException | BaseExceptionGroup]], +) -> tuple[str, int, str]: + r = excinfo._getreprcrash() + assert r is not None, ( + "There should always be a traceback entry for skipping a test." + ) + if all( + getattr(skip, "_use_item_location", False) for skip in excinfo.value.exceptions + ): + path, line = item.reportinfo()[:2] + assert line is not None + loc = (os.fspath(path), line + 1) + default_msg = "skipped" + else: + loc = (str(r.path), r.lineno) + default_msg = r.message + + # Get all unique skip messages. + msgs: list[str] = [] + for exception in excinfo.value.exceptions: + m = getattr(exception, "msg", None) or ( + exception.args[0] if exception.args else None + ) + if m and m not in msgs: + msgs.append(m) + + reason = "; ".join(msgs) if msgs else default_msg + longrepr = (*loc, reason) + return longrepr + + class TestReport(BaseReport): """Basic test report object (also used for setup and teardown calls if they fail). @@ -361,9 +406,9 @@ def from_item_and_call(cls, item: Item, call: CallInfo[None]) -> TestReport: elif isinstance(excinfo.value, skip.Exception): outcome = "skipped" r = excinfo._getreprcrash() - assert ( - r is not None - ), "There should always be a traceback entry for skipping a test." + assert r is not None, ( + "There should always be a traceback entry for skipping a test." + ) if excinfo.value._use_item_location: path, line = item.reportinfo()[:2] assert line is not None diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 0b60301bf5f..7d63095a86a 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING from typing import TypeVar +from .config import Config from .reports import BaseReport from .reports import CollectErrorRepr from .reports import CollectReport @@ -235,11 +236,11 @@ def call_and_report( runtest_hook = ihook.pytest_runtest_teardown else: assert False, f"Unhandled runtest hook case: {when}" - reraise: tuple[type[BaseException], ...] = (Exit,) - if not item.config.getoption("usepdb", False): - reraise += (KeyboardInterrupt,) + call = CallInfo.from_call( - lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise + lambda: runtest_hook(item=item, **kwds), + when=when, + reraise=get_reraise_exceptions(item.config), ) report: TestReport = ihook.pytest_runtest_makereport(item=item, call=call) if log: @@ -249,6 +250,14 @@ def call_and_report( return report +def get_reraise_exceptions(config: Config) -> tuple[type[BaseException], ...]: + """Return exception types that should not be suppressed in general.""" + reraise: tuple[type[BaseException], ...] = (Exit,) + if not config.getoption("usepdb", False): + reraise += (KeyboardInterrupt,) + return reraise + + def check_interactive_exception(call: CallInfo[object], report: BaseReport) -> bool: """Check whether the call raised an exception that should be reported as interactive.""" diff --git a/src/_pytest/subtests.py b/src/_pytest/subtests.py new file mode 100644 index 00000000000..e0ceb27f4b1 --- /dev/null +++ b/src/_pytest/subtests.py @@ -0,0 +1,411 @@ +"""Builtin plugin that adds subtests support.""" + +from __future__ import annotations + +from collections import defaultdict +from collections.abc import Callable +from collections.abc import Iterator +from collections.abc import Mapping +from contextlib import AbstractContextManager +from contextlib import contextmanager +from contextlib import ExitStack +from contextlib import nullcontext +import dataclasses +import time +from types import TracebackType +from typing import Any +from typing import TYPE_CHECKING + +import pluggy + +from _pytest._code import ExceptionInfo +from _pytest._io.saferepr import saferepr +from _pytest.capture import CaptureFixture +from _pytest.capture import FDCapture +from _pytest.capture import SysCapture +from _pytest.config import Config +from _pytest.config import hookimpl +from _pytest.config.argparsing import Parser +from _pytest.deprecated import check_ispytest +from _pytest.fixtures import fixture +from _pytest.fixtures import SubRequest +from _pytest.logging import catching_logs +from _pytest.logging import LogCaptureHandler +from _pytest.logging import LoggingPlugin +from _pytest.reports import TestReport +from _pytest.runner import CallInfo +from _pytest.runner import check_interactive_exception +from _pytest.runner import get_reraise_exceptions +from _pytest.stash import StashKey + + +if TYPE_CHECKING: + from typing_extensions import Self + + +def pytest_addoption(parser: Parser) -> None: + Config._add_verbosity_ini( + parser, + Config.VERBOSITY_SUBTESTS, + help=( + "Specify verbosity level for subtests. " + "Higher levels will generate output for passed subtests. Failed subtests are always reported." + ), + ) + + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class SubtestContext: + """The values passed to Subtests.test() that are included in the test report.""" + + msg: str | None + kwargs: Mapping[str, Any] + + def _to_json(self) -> dict[str, Any]: + return dataclasses.asdict(self) + + @classmethod + def _from_json(cls, d: dict[str, Any]) -> Self: + return cls(msg=d["msg"], kwargs=d["kwargs"]) + + +@dataclasses.dataclass(init=False) +class SubtestReport(TestReport): + context: SubtestContext + + @property + def head_line(self) -> str: + _, _, domain = self.location + return f"{domain} {self._sub_test_description()}" + + def _sub_test_description(self) -> str: + parts = [] + if self.context.msg is not None: + parts.append(f"[{self.context.msg}]") + if self.context.kwargs: + params_desc = ", ".join( + f"{k}={saferepr(v)}" for (k, v) in self.context.kwargs.items() + ) + parts.append(f"({params_desc})") + return " ".join(parts) or "()" + + def _to_json(self) -> dict[str, Any]: + data = super()._to_json() + del data["context"] + data["_report_type"] = "SubTestReport" + data["_subtest.context"] = self.context._to_json() + return data + + @classmethod + def _from_json(cls, reportdict: dict[str, Any]) -> SubtestReport: + report = super()._from_json(reportdict) + report.context = SubtestContext._from_json(reportdict["_subtest.context"]) + return report + + @classmethod + def _new( + cls, + test_report: TestReport, + context: SubtestContext, + captured_output: Captured | None, + captured_logs: CapturedLogs | None, + ) -> Self: + result = super()._from_json(test_report._to_json()) + result.context = context + + if captured_output: + if captured_output.out: + result.sections.append(("Captured stdout call", captured_output.out)) + if captured_output.err: + result.sections.append(("Captured stderr call", captured_output.err)) + + if captured_logs and (log := captured_logs.handler.stream.getvalue()): + result.sections.append(("Captured log call", log)) + + return result + + +@fixture +def subtests(request: SubRequest) -> Subtests: + """Provides subtests functionality.""" + capmam = request.node.config.pluginmanager.get_plugin("capturemanager") + suspend_capture_ctx = ( + capmam.global_and_fixture_disabled if capmam is not None else nullcontext + ) + return Subtests(request.node.ihook, suspend_capture_ctx, request, _ispytest=True) + + +class Subtests: + """Subtests fixture, enables declaring subtests inside test functions via the :meth:`test` method.""" + + def __init__( + self, + ihook: pluggy.HookRelay, + suspend_capture_ctx: Callable[[], AbstractContextManager[None]], + request: SubRequest, + *, + _ispytest: bool = False, + ) -> None: + check_ispytest(_ispytest) + self._ihook = ihook + self._suspend_capture_ctx = suspend_capture_ctx + self._request = request + + def test( + self, + msg: str | None = None, + **kwargs: Any, + ) -> _SubTestContextManager: + """ + Context manager for subtests, capturing exceptions raised inside the subtest scope and + reporting assertion failures and errors individually. + + Usage + ----- + + .. code-block:: python + + def test(subtests): + for i in range(5): + with subtests.test("custom message", i=i): + assert i % 2 == 0 + + :param msg: + If given, the message will be shown in the test report in case of subtest failure. + + :param kwargs: + Arbitrary values that are also added to the subtest report. + """ + return _SubTestContextManager( + self._ihook, + msg, + kwargs, + request=self._request, + suspend_capture_ctx=self._suspend_capture_ctx, + config=self._request.config, + ) + + +@dataclasses.dataclass +class _SubTestContextManager: + """ + Context manager for subtests, capturing exceptions raised inside the subtest scope and handling + them through the pytest machinery. + """ + + # Note: initially the logic for this context manager was implemented directly + # in Subtests.test() as a @contextmanager, however, it is not possible to control the output fully when + # exiting from it due to an exception when in `--exitfirst` mode, so this was refactored into an + # explicit context manager class (pytest-dev/pytest-subtests#134). + + ihook: pluggy.HookRelay + msg: str | None + kwargs: dict[str, Any] + suspend_capture_ctx: Callable[[], AbstractContextManager[None]] + request: SubRequest + config: Config + + def __enter__(self) -> None: + __tracebackhide__ = True + + self._start = time.time() + self._precise_start = time.perf_counter() + self._exc_info = None + + self._exit_stack = ExitStack() + self._captured_output = self._exit_stack.enter_context( + capturing_output(self.request) + ) + self._captured_logs = self._exit_stack.enter_context( + capturing_logs(self.request) + ) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + __tracebackhide__ = True + if exc_val is not None: + exc_info = ExceptionInfo.from_exception(exc_val) + else: + exc_info = None + + self._exit_stack.close() + + precise_stop = time.perf_counter() + duration = precise_stop - self._precise_start + stop = time.time() + + call_info = CallInfo[None]( + None, + exc_info, + start=self._start, + stop=stop, + duration=duration, + when="call", + _ispytest=True, + ) + report = self.ihook.pytest_runtest_makereport( + item=self.request.node, call=call_info + ) + sub_report = SubtestReport._new( + report, + SubtestContext(msg=self.msg, kwargs=self.kwargs), + captured_output=self._captured_output, + captured_logs=self._captured_logs, + ) + + if sub_report.failed: + failed_subtests = self.config.stash[failed_subtests_key] + failed_subtests[self.request.node.nodeid] += 1 + + with self.suspend_capture_ctx(): + self.ihook.pytest_runtest_logreport(report=sub_report) + + if check_interactive_exception(call_info, sub_report): + self.ihook.pytest_exception_interact( + node=self.request.node, call=call_info, report=sub_report + ) + + if exc_val is not None: + if isinstance(exc_val, get_reraise_exceptions(self.config)): + return False + if self.request.session.shouldfail: + return False + return True + + +@contextmanager +def capturing_output(request: SubRequest) -> Iterator[Captured]: + option = request.config.getoption("capture", None) + + capman = request.config.pluginmanager.getplugin("capturemanager") + if getattr(capman, "_capture_fixture", None): + # capsys or capfd are active, subtest should not capture. + fixture = None + elif option == "sys": + fixture = CaptureFixture(SysCapture, request, _ispytest=True) + elif option == "fd": + fixture = CaptureFixture(FDCapture, request, _ispytest=True) + else: + fixture = None + + if fixture is not None: + fixture._start() + + captured = Captured() + try: + yield captured + finally: + if fixture is not None: + out, err = fixture.readouterr() + fixture.close() + captured.out = out + captured.err = err + + +@contextmanager +def capturing_logs( + request: SubRequest, +) -> Iterator[CapturedLogs | None]: + logging_plugin: LoggingPlugin | None = request.config.pluginmanager.getplugin( + "logging-plugin" + ) + if logging_plugin is None: + yield None + else: + handler = LogCaptureHandler() + handler.setFormatter(logging_plugin.formatter) + + captured_logs = CapturedLogs(handler) + with catching_logs(handler, level=logging_plugin.log_level): + yield captured_logs + + +@dataclasses.dataclass +class Captured: + out: str = "" + err: str = "" + + +@dataclasses.dataclass +class CapturedLogs: + handler: LogCaptureHandler + + +def pytest_report_to_serializable(report: TestReport) -> dict[str, Any] | None: + if isinstance(report, SubtestReport): + return report._to_json() + return None + + +def pytest_report_from_serializable(data: dict[str, Any]) -> SubtestReport | None: + if data.get("_report_type") == "SubTestReport": + return SubtestReport._from_json(data) + return None + + +# Dict of nodeid -> number of failed subtests. +# Used to fail top-level tests that passed but contain failed subtests. +failed_subtests_key = StashKey[defaultdict[str, int]]() + + +def pytest_configure(config: Config) -> None: + config.stash[failed_subtests_key] = defaultdict(lambda: 0) + + +@hookimpl(tryfirst=True) +def pytest_report_teststatus( + report: TestReport, + config: Config, +) -> tuple[str, str, str | Mapping[str, bool]] | None: + if report.when != "call": + return None + + quiet = config.get_verbosity(Config.VERBOSITY_SUBTESTS) == 0 + if isinstance(report, SubtestReport): + outcome = report.outcome + description = report._sub_test_description() + + if hasattr(report, "wasxfail"): + if quiet: + return "", "", "" + elif outcome == "skipped": + category = "xfailed" + short = "y" # x letter is used for regular xfail, y for subtest xfail + status = "SUBXFAIL" + # outcome == "passed" in an xfail is only possible via a @pytest.mark.xfail mark, which + # is not applicable to a subtest, which only handles pytest.xfail(). + else: # pragma: no cover + # This should not normally happen, unless some plugin is setting wasxfail without + # the correct outcome. Pytest expects the call outcome to be either skipped or + # passed in case of xfail. + # Let's pass this report to the next hook. + return None + return category, short, f"{status}{description}" + + if report.failed: + return outcome, "u", f"SUBFAILED{description}" + else: + if report.passed: + if quiet: + return "", "", "" + else: + return f"subtests {outcome}", "u", f"SUBPASSED{description}" + elif report.skipped: + if quiet: + return "", "", "" + else: + return outcome, "-", f"SUBSKIPPED{description}" + + else: + failed_subtests_count = config.stash[failed_subtests_key][report.nodeid] + # Top-level test, fail if it contains failed subtests and it has passed. + if report.passed and failed_subtests_count > 0: + report.outcome = "failed" + suffix = "s" if failed_subtests_count > 1 else "" + report.longrepr = f"contains {failed_subtests_count} failed subtest{suffix}" + + return None diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index ed267bf5bfd..cd5b279462e 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -68,6 +68,9 @@ "xpassed", "warnings", "error", + "subtests passed", + "subtests failed", + "subtests skipped", ) _REPORTCHARS_DEFAULT = "fE" @@ -1463,9 +1466,13 @@ def _get_line_with_reprcrash_message( line = f"{word} {node}" line_width = wcswidth(line) + msg: str | None try: - # Type ignored intentionally -- possible AttributeError expected. - msg = rep.longrepr.reprcrash.message # type: ignore[union-attr] + if isinstance(rep.longrepr, str): + msg = rep.longrepr + else: + # Type ignored intentionally -- possible AttributeError expected. + msg = rep.longrepr.reprcrash.message # type: ignore[union-attr] except AttributeError: pass else: @@ -1516,6 +1523,8 @@ def _folded_skips( "error": "red", "warnings": "yellow", "passed": "green", + "subtests passed": "green", + "subtests failed": "red", } _color_for_type_default = "yellow" diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 8cecd4f9339..f26d461f7a7 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -8,15 +8,12 @@ import traceback import types from typing import Any -from typing import Callable -from typing import Generator -from typing import Iterable -from typing import Tuple -from typing import Type from typing import TYPE_CHECKING -from typing import Union +from unittest import TestCase import _pytest._code +from _pytest._code import ExceptionInfo +from _pytest.compat import assert_never from _pytest.compat import is_async_function from _pytest.config import hookimpl from _pytest.fixtures import FixtureRequest @@ -30,13 +27,16 @@ from _pytest.python import Function from _pytest.python import Module from _pytest.runner import CallInfo -import pytest +from _pytest.runner import check_interactive_exception +from _pytest.subtests import SubtestContext +from _pytest.subtests import SubtestReport if sys.version_info[:2] < (3, 11): from exceptiongroup import ExceptionGroup if TYPE_CHECKING: + from types import TracebackType import unittest import twisted.trial.unittest @@ -201,6 +201,7 @@ def unittest_setup_method_fixture( class TestCaseFunction(Function): nofuncargs = True + failfast = False _excinfo: list[_pytest._code.ExceptionInfo[BaseException]] | None = None def _getinstance(self): @@ -279,11 +280,42 @@ def addFailure( ) -> None: self._addexcinfo(rawexcinfo) - def addSkip(self, testcase: unittest.TestCase, reason: str) -> None: - try: - raise pytest.skip.Exception(reason, _use_item_location=True) - except skip.Exception: - self._addexcinfo(sys.exc_info()) + def addSkip( + self, testcase: unittest.TestCase, reason: str, *, handle_subtests: bool = True + ) -> None: + from unittest.case import _SubTest # type: ignore[attr-defined] + + def add_skip() -> None: + try: + raise skip.Exception(reason, _use_item_location=True) + except skip.Exception: + self._addexcinfo(sys.exc_info()) + + if not handle_subtests: + add_skip() + return + + if isinstance(testcase, _SubTest): + add_skip() + if self._excinfo is not None: + exc_info = self._excinfo[-1] + self.addSubTest(testcase.test_case, testcase, exc_info) + else: + # For python < 3.11: the non-subtest skips have to be added by `add_skip` only after all subtest + # failures are processed by `_addSubTest`: `self.instance._outcome` has no attribute + # `skipped/errors` anymore. + # We also need to check if `self.instance._outcome` is `None` (this happens if the test + # class/method is decorated with `unittest.skip`, see pytest-dev/pytest-subtests#173). + if sys.version_info < (3, 11) and self.instance._outcome is not None: + subtest_errors = [ + x + for x, y in self.instance._outcome.errors + if isinstance(x, _SubTest) and y is not None + ] + if len(subtest_errors) == 0: + add_skip() + else: + add_skip() def addExpectedFailure( self, @@ -363,6 +395,70 @@ def _traceback_filter( ntraceback = traceback return ntraceback + def addSubTest( + self, + test_case: Any, + test: TestCase, + exc_info: ExceptionInfo[BaseException] + | tuple[type[BaseException], BaseException, TracebackType] + | None, + ) -> None: + exception_info: ExceptionInfo[BaseException] | None + match exc_info: + case tuple(): + exception_info = ExceptionInfo(exc_info, _ispytest=True) + case ExceptionInfo() | None: + exception_info = exc_info + case unreachable: + assert_never(unreachable) + + call_info = CallInfo[None]( + None, + exception_info, + start=0, + stop=0, + duration=0, + when="call", + _ispytest=True, + ) + msg = test._message if isinstance(test._message, str) else None # type: ignore[attr-defined] + report = self.ihook.pytest_runtest_makereport(item=self, call=call_info) + sub_report = SubtestReport._new( + report, + SubtestContext(msg=msg, kwargs=dict(test.params)), # type: ignore[attr-defined] + captured_output=None, + captured_logs=None, + ) + self.ihook.pytest_runtest_logreport(report=sub_report) + if check_interactive_exception(call_info, sub_report): + self.ihook.pytest_exception_interact( + node=self, call=call_info, report=sub_report + ) + + # For python < 3.11: add non-subtest skips once all subtest failures are processed by # `_addSubTest`. + if sys.version_info < (3, 11): + from unittest.case import _SubTest # type: ignore[attr-defined] + + non_subtest_skip = [ + (x, y) + for x, y in self.instance._outcome.skipped + if not isinstance(x, _SubTest) + ] + subtest_errors = [ + (x, y) + for x, y in self.instance._outcome.errors + if isinstance(x, _SubTest) and y is not None + ] + # Check if we have non-subtest skips: if there are also sub failures, non-subtest skips are not treated in + # `_addSubTest` and have to be added using `add_skip` after all subtest failures are processed. + if len(non_subtest_skip) > 0 and len(subtest_errors) > 0: + # Make sure we have processed the last subtest failure + last_subset_error = subtest_errors[-1] + if exc_info is last_subset_error[-1]: + # Add non-subtest skips (as they could not be treated in `_addSkip`) + for testcase, reason in non_subtest_skip: + self.addSkip(testcase, reason, handle_subtests=False) + @hookimpl(tryfirst=True) def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 90abcdab036..a98517577d8 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -68,6 +68,9 @@ from _pytest.runner import CallInfo from _pytest.stash import Stash from _pytest.stash import StashKey +from _pytest.subtests import SubtestReport +from _pytest.subtests import Subtests +from _pytest.terminal import TerminalReporter from _pytest.terminal import TestShortLogReport from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestAssertRewriteWarning @@ -158,8 +161,8 @@ "skip", "Stash", "StashKey", - "version_tuple", - "TempdirFactory", + "SubtestReport", + "Subtests", "TempPathFactory", "Testdir", "TestReport", diff --git a/testing/test_subtests.py b/testing/test_subtests.py new file mode 100644 index 00000000000..6849df53622 --- /dev/null +++ b/testing/test_subtests.py @@ -0,0 +1,977 @@ +from __future__ import annotations + +import sys +from typing import Literal + +from _pytest.subtests import SubtestContext +from _pytest.subtests import SubtestReport +import pytest + + +IS_PY311 = sys.version_info[:2] >= (3, 11) + + +def test_failures(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("COLUMNS", "120") + pytester.makepyfile( + """ + def test_foo(subtests): + with subtests.test("foo subtest"): + assert False, "foo subtest failure" + + def test_bar(subtests): + with subtests.test("bar subtest"): + assert False, "bar subtest failure" + assert False, "test_bar also failed" + + def test_zaz(subtests): + with subtests.test("zaz subtest"): + pass + """ + ) + summary_lines = [ + "*=== FAILURES ===*", + # + "*___ test_foo [[]foo subtest[]] ___*", + "*AssertionError: foo subtest failure", + # + "*___ test_foo ___*", + "contains 1 failed subtest", + # + "*___ test_bar [[]bar subtest[]] ___*", + "*AssertionError: bar subtest failure", + # + "*___ test_bar ___*", + "*AssertionError: test_bar also failed", + # + "*=== short test summary info ===*", + "SUBFAILED[[]foo subtest[]] test_*.py::test_foo - AssertionError*", + "FAILED test_*.py::test_foo - contains 1 failed subtest", + "SUBFAILED[[]bar subtest[]] test_*.py::test_bar - AssertionError*", + "FAILED test_*.py::test_bar - AssertionError*", + ] + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "test_*.py uFuF. * [[]100%[]]", + *summary_lines, + "* 4 failed, 1 passed in *", + ] + ) + + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "test_*.py::test_foo SUBFAILED[[]foo subtest[]] * [[] 33%[]]", + "test_*.py::test_foo FAILED * [[] 33%[]]", + "test_*.py::test_bar SUBFAILED[[]bar subtest[]] * [[] 66%[]]", + "test_*.py::test_bar FAILED * [[] 66%[]]", + "test_*.py::test_zaz SUBPASSED[[]zaz subtest[]] * [[]100%[]]", + "test_*.py::test_zaz PASSED * [[]100%[]]", + *summary_lines, + "* 4 failed, 1 passed, 1 subtests passed in *", + ] + ) + pytester.makeini( + """ + [pytest] + verbosity_subtests = 0 + """ + ) + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "test_*.py::test_foo SUBFAILED[[]foo subtest[]] * [[] 33%[]]", + "test_*.py::test_foo FAILED * [[] 33%[]]", + "test_*.py::test_bar SUBFAILED[[]bar subtest[]] * [[] 66%[]]", + "test_*.py::test_bar FAILED * [[] 66%[]]", + "test_*.py::test_zaz PASSED * [[]100%[]]", + *summary_lines, + "* 4 failed, 1 passed in *", + ] + ) + result.stdout.no_fnmatch_line("test_*.py::test_zaz SUBPASSED[[]zaz subtest[]]*") + + +def test_passes(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("COLUMNS", "120") + pytester.makepyfile( + """ + def test_foo(subtests): + with subtests.test("foo subtest"): + pass + + def test_bar(subtests): + with subtests.test("bar subtest"): + pass + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "test_*.py .. * [[]100%[]]", + "* 2 passed in *", + ] + ) + + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "*.py::test_foo SUBPASSED[[]foo subtest[]] * [[] 50%[]]", + "*.py::test_foo PASSED * [[] 50%[]]", + "*.py::test_bar SUBPASSED[[]bar subtest[]] * [[]100%[]]", + "*.py::test_bar PASSED * [[]100%[]]", + "* 2 passed, 2 subtests passed in *", + ] + ) + + pytester.makeini( + """ + [pytest] + verbosity_subtests = 0 + """ + ) + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "*.py::test_foo PASSED * [[] 50%[]]", + "*.py::test_bar PASSED * [[]100%[]]", + "* 2 passed in *", + ] + ) + result.stdout.no_fnmatch_line("*.py::test_foo SUBPASSED[[]foo subtest[]]*") + result.stdout.no_fnmatch_line("*.py::test_bar SUBPASSED[[]bar subtest[]]*") + + +def test_skip(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("COLUMNS", "120") + pytester.makepyfile( + """ + import pytest + def test_foo(subtests): + with subtests.test("foo subtest"): + pytest.skip("skip foo subtest") + + def test_bar(subtests): + with subtests.test("bar subtest"): + pytest.skip("skip bar subtest") + pytest.skip("skip test_bar") + """ + ) + result = pytester.runpytest("-ra") + result.stdout.fnmatch_lines( + [ + "test_*.py .s * [[]100%[]]", + "*=== short test summary info ===*", + "SKIPPED [[]1[]] test_skip.py:9: skip test_bar", + "* 1 passed, 1 skipped in *", + ] + ) + + result = pytester.runpytest("-v", "-ra") + result.stdout.fnmatch_lines( + [ + "*.py::test_foo SUBSKIPPED[[]foo subtest[]] (skip foo subtest) * [[] 50%[]]", + "*.py::test_foo PASSED * [[] 50%[]]", + "*.py::test_bar SUBSKIPPED[[]bar subtest[]] (skip bar subtest) * [[]100%[]]", + "*.py::test_bar SKIPPED (skip test_bar) * [[]100%[]]", + "*=== short test summary info ===*", + "SUBSKIPPED[[]foo subtest[]] [[]1[]] *.py:*: skip foo subtest", + "SUBSKIPPED[[]foo subtest[]] [[]1[]] *.py:*: skip bar subtest", + "SUBSKIPPED[[]foo subtest[]] [[]1[]] *.py:*: skip test_bar", + "* 1 passed, 3 skipped in *", + ] + ) + + pytester.makeini( + """ + [pytest] + verbosity_subtests = 0 + """ + ) + result = pytester.runpytest("-v", "-ra") + result.stdout.fnmatch_lines( + [ + "*.py::test_foo PASSED * [[] 50%[]]", + "*.py::test_bar SKIPPED (skip test_bar) * [[]100%[]]", + "*=== short test summary info ===*", + "* 1 passed, 1 skipped in *", + ] + ) + result.stdout.no_fnmatch_line("*.py::test_foo SUBPASSED[[]foo subtest[]]*") + result.stdout.no_fnmatch_line("*.py::test_bar SUBPASSED[[]bar subtest[]]*") + result.stdout.no_fnmatch_line( + "SUBSKIPPED[[]foo subtest[]] [[]1[]] *.py:*: skip foo subtest" + ) + result.stdout.no_fnmatch_line( + "SUBSKIPPED[[]foo subtest[]] [[]1[]] *.py:*: skip test_bar" + ) + + +def test_xfail(pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("COLUMNS", "120") + pytester.makepyfile( + """ + import pytest + def test_foo(subtests): + with subtests.test("foo subtest"): + pytest.xfail("xfail foo subtest") + + def test_bar(subtests): + with subtests.test("bar subtest"): + pytest.xfail("xfail bar subtest") + pytest.xfail("xfail test_bar") + """ + ) + result = pytester.runpytest("-ra") + result.stdout.fnmatch_lines( + [ + "test_*.py .x * [[]100%[]]", + "*=== short test summary info ===*", + "* 1 passed, 1 xfailed in *", + ] + ) + + result = pytester.runpytest("-v", "-ra") + result.stdout.fnmatch_lines( + [ + "*.py::test_foo SUBXFAIL[[]foo subtest[]] (xfail foo subtest) * [[] 50%[]]", + "*.py::test_foo PASSED * [[] 50%[]]", + "*.py::test_bar SUBXFAIL[[]bar subtest[]] (xfail bar subtest) * [[]100%[]]", + "*.py::test_bar XFAIL (xfail test_bar) * [[]100%[]]", + "*=== short test summary info ===*", + "SUBXFAIL[[]foo subtest[]] *.py::test_foo - xfail foo subtest", + "SUBXFAIL[[]bar subtest[]] *.py::test_bar - xfail bar subtest", + "XFAIL *.py::test_bar - xfail test_bar", + "* 1 passed, 3 xfailed in *", + ] + ) + + pytester.makeini( + """ + [pytest] + verbosity_subtests = 0 + """ + ) + result = pytester.runpytest("-v", "-ra") + result.stdout.fnmatch_lines( + [ + "*.py::test_foo PASSED * [[] 50%[]]", + "*.py::test_bar XFAIL (xfail test_bar) * [[]100%[]]", + "*=== short test summary info ===*", + "* 1 passed, 1 xfailed in *", + ] + ) + result.stdout.no_fnmatch_line( + "SUBXFAIL[[]foo subtest[]] *.py::test_foo - xfail foo subtest" + ) + result.stdout.no_fnmatch_line( + "SUBXFAIL[[]bar subtest[]] *.py::test_bar - xfail bar subtest" + ) + + +def test_typing_exported(pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + from pytest import Subtests + + def test_typing_exported(subtests: Subtests) -> None: + assert isinstance(subtests, Subtests) + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines(["*1 passed*"]) + + +def test_subtests_and_parametrization( + pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setenv("COLUMNS", "120") + pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("x", [0, 1]) + def test_foo(subtests, x): + for i in range(3): + with subtests.test("custom", i=i): + assert i % 2 == 0 + assert x == 0 + """ + ) + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "*.py::test_foo[[]0[]] SUBFAILED[[]custom[]] (i=1) *[[] 50%[]]", + "*.py::test_foo[[]0[]] FAILED *[[] 50%[]]", + "*.py::test_foo[[]1[]] SUBFAILED[[]custom[]] (i=1) *[[]100%[]]", + "*.py::test_foo[[]1[]] FAILED *[[]100%[]]", + "contains 1 failed subtest", + "* 4 failed, 4 subtests passed in *", + ] + ) + + pytester.makeini( + """ + [pytest] + verbosity_subtests = 0 + """ + ) + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "*.py::test_foo[[]0[]] SUBFAILED[[]custom[]] (i=1) *[[] 50%[]]", + "*.py::test_foo[[]0[]] FAILED *[[] 50%[]]", + "*.py::test_foo[[]1[]] SUBFAILED[[]custom[]] (i=1) *[[]100%[]]", + "*.py::test_foo[[]1[]] FAILED *[[]100%[]]", + "contains 1 failed subtest", + "* 4 failed in *", + ] + ) + + +def test_subtests_fail_top_level_test(pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + def test_foo(subtests): + for i in range(3): + with subtests.test("custom", i=i): + assert i % 2 == 0 + """ + ) + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "* 2 failed, 2 subtests passed in *", + ] + ) + + +def test_subtests_do_not_overwrite_top_level_failure(pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + def test_foo(subtests): + for i in range(3): + with subtests.test("custom", i=i): + assert i % 2 == 0 + assert False, "top-level failure" + """ + ) + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "*AssertionError: top-level failure", + "* 2 failed, 2 subtests passed in *", + ] + ) + + +@pytest.mark.parametrize("flag", ["--last-failed", "--stepwise"]) +def test_subtests_last_failed_step_wise(pytester: pytest.Pytester, flag: str) -> None: + """Check that --last-failed and --step-wise correctly rerun tests with failed subtests.""" + pytester.makepyfile( + """ + import pytest + + def test_foo(subtests): + for i in range(3): + with subtests.test("custom", i=i): + assert i % 2 == 0 + """ + ) + result = pytester.runpytest("-v") + result.stdout.fnmatch_lines( + [ + "* 2 failed, 2 subtests passed in *", + ] + ) + + result = pytester.runpytest("-v", flag) + result.stdout.fnmatch_lines( + [ + "* 2 failed, 2 subtests passed in *", + ] + ) + + +class TestUnittestSubTest: + """Test unittest.TestCase.subTest functionality.""" + + def test_failures( + self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("COLUMNS", "120") + pytester.makepyfile( + """ + from unittest import TestCase + + class T(TestCase): + def test_foo(self): + with self.subTest("foo subtest"): + assert False, "foo subtest failure" + + def test_bar(self): + with self.subTest("bar subtest"): + assert False, "bar subtest failure" + assert False, "test_bar also failed" + + def test_zaz(self): + with self.subTest("zaz subtest"): + pass + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "* 3 failed, 2 passed in *", + ] + ) + + def test_passes( + self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("COLUMNS", "120") + pytester.makepyfile( + """ + from unittest import TestCase + + class T(TestCase): + def test_foo(self): + with self.subTest("foo subtest"): + pass + + def test_bar(self): + with self.subTest("bar subtest"): + pass + + def test_zaz(self): + with self.subTest("zaz subtest"): + pass + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "* 3 passed in *", + ] + ) + + def test_skip( + self, + pytester: pytest.Pytester, + ) -> None: + pytester.makepyfile( + """ + from unittest import TestCase, main + + class T(TestCase): + + def test_foo(self): + for i in range(5): + with self.subTest(msg="custom", i=i): + if i % 2 == 0: + self.skipTest('even number') + """ + ) + # This output might change #13756. + result = pytester.runpytest() + result.stdout.fnmatch_lines(["* 1 passed in *"]) + + def test_non_subtest_skip( + self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setenv("COLUMNS", "120") + pytester.makepyfile( + """ + from unittest import TestCase, main + + class T(TestCase): + + def test_foo(self): + with self.subTest(msg="subtest"): + assert False, "failed subtest" + self.skipTest('non-subtest skip') + """ + ) + # This output might change #13756. + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "SUBFAILED[[]subtest[]] test_non_subtest_skip.py::T::test_foo*", + "* 1 failed, 1 skipped in *", + ] + ) + + def test_xfail( + self, + pytester: pytest.Pytester, + ) -> None: + pytester.makepyfile( + """ + import pytest + from unittest import expectedFailure, TestCase + + class T(TestCase): + @expectedFailure + def test_foo(self): + for i in range(5): + with self.subTest(msg="custom", i=i): + if i % 2 == 0: + raise pytest.xfail('even number') + + if __name__ == '__main__': + main() + """ + ) + # This output might change #13756. + result = pytester.runpytest() + result.stdout.fnmatch_lines(["* 1 xfailed in *"]) + + def test_only_original_skip_is_called( + self, + pytester: pytest.Pytester, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + """Regression test for pytest-dev/pytest-subtests#173.""" + monkeypatch.setenv("COLUMNS", "120") + pytester.makepyfile( + """ + import unittest + from unittest import TestCase + + @unittest.skip("skip this test") + class T(unittest.TestCase): + def test_foo(self): + assert 1 == 2 + """ + ) + result = pytester.runpytest("-v", "-rsf") + result.stdout.fnmatch_lines( + ["SKIPPED [1] test_only_original_skip_is_called.py:6: skip this test"] + ) + + def test_skip_with_failure( + self, + pytester: pytest.Pytester, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + monkeypatch.setenv("COLUMNS", "120") + pytester.makepyfile( + """ + import pytest + from unittest import TestCase + + class T(TestCase): + def test_foo(self): + with self.subTest("subtest 1"): + self.skipTest(f"skip subtest 1") + with self.subTest("subtest 2"): + assert False, "fail subtest 2" + """ + ) + + result = pytester.runpytest("-ra") + result.stdout.fnmatch_lines( + [ + "*.py u. * [[]100%[]]", + "*=== short test summary info ===*", + "SUBFAILED[[]subtest 2[]] *.py::T::test_foo - AssertionError: fail subtest 2", + "* 1 failed, 1 passed in *", + ] + ) + + result = pytester.runpytest("-v", "-ra") + result.stdout.fnmatch_lines( + [ + "*.py::T::test_foo SUBSKIPPED[[]subtest 1[]] (skip subtest 1) * [[]100%[]]", + "*.py::T::test_foo SUBFAILED[[]subtest 2[]] * [[]100%[]]", + "*.py::T::test_foo PASSED * [[]100%[]]", + "SUBSKIPPED[[]subtest 1[]] [[]1[]] *.py:*: skip subtest 1", + "SUBFAILED[[]subtest 2[]] *.py::T::test_foo - AssertionError: fail subtest 2", + "* 1 failed, 1 passed, 1 skipped in *", + ] + ) + + pytester.makeini( + """ + [pytest] + verbosity_subtests = 0 + """ + ) + result = pytester.runpytest("-v", "-ra") + result.stdout.fnmatch_lines( + [ + "*.py::T::test_foo SUBFAILED[[]subtest 2[]] * [[]100%[]]", + "*.py::T::test_foo PASSED * [[]100%[]]", + "*=== short test summary info ===*", + r"SUBFAILED[[]subtest 2[]] *.py::T::test_foo - AssertionError: fail subtest 2", + r"* 1 failed, 1 passed in *", + ] + ) + result.stdout.no_fnmatch_line( + "*.py::T::test_foo SUBSKIPPED[[]subtest 1[]] (skip subtest 1) * [[]100%[]]" + ) + result.stdout.no_fnmatch_line( + "SUBSKIPPED[[]subtest 1[]] [[]1[]] *.py:*: skip subtest 1" + ) + + +class TestCapture: + def create_file(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + import sys + def test(subtests): + print() + print('start test') + + with subtests.test(i='A'): + print("hello stdout A") + print("hello stderr A", file=sys.stderr) + assert 0 + + with subtests.test(i='B'): + print("hello stdout B") + print("hello stderr B", file=sys.stderr) + assert 0 + + print('end test') + assert 0 + """ + ) + + @pytest.mark.parametrize("mode", ["fd", "sys"]) + def test_capturing(self, pytester: pytest.Pytester, mode: str) -> None: + self.create_file(pytester) + result = pytester.runpytest(f"--capture={mode}") + result.stdout.fnmatch_lines( + [ + "*__ test (i='A') __*", + "*Captured stdout call*", + "hello stdout A", + "*Captured stderr call*", + "hello stderr A", + "*__ test (i='B') __*", + "*Captured stdout call*", + "hello stdout B", + "*Captured stderr call*", + "hello stderr B", + "*__ test __*", + "*Captured stdout call*", + "start test", + "end test", + ] + ) + + def test_no_capture(self, pytester: pytest.Pytester) -> None: + self.create_file(pytester) + result = pytester.runpytest("-s") + result.stdout.fnmatch_lines( + [ + "start test", + "hello stdout A", + "uhello stdout B", + "uend test", + "*__ test (i='A') __*", + "*__ test (i='B') __*", + "*__ test __*", + ] + ) + result.stderr.fnmatch_lines(["hello stderr A", "hello stderr B"]) + + @pytest.mark.parametrize("fixture", ["capsys", "capfd"]) + def test_capture_with_fixture( + self, pytester: pytest.Pytester, fixture: Literal["capsys", "capfd"] + ) -> None: + pytester.makepyfile( + rf""" + import sys + + def test(subtests, {fixture}): + print('start test') + + with subtests.test(i='A'): + print("hello stdout A") + print("hello stderr A", file=sys.stderr) + + out, err = {fixture}.readouterr() + assert out == 'start test\nhello stdout A\n' + assert err == 'hello stderr A\n' + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*1 passed*", + ] + ) + + +class TestLogging: + def create_file(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + import logging + + def test_foo(subtests): + logging.info("before") + + with subtests.test("sub1"): + print("sub1 stdout") + logging.info("sub1 logging") + logging.debug("sub1 logging debug") + + with subtests.test("sub2"): + print("sub2 stdout") + logging.info("sub2 logging") + logging.debug("sub2 logging debug") + assert False + """ + ) + + def test_capturing_info(self, pytester: pytest.Pytester) -> None: + self.create_file(pytester) + result = pytester.runpytest("--log-level=INFO") + result.stdout.fnmatch_lines( + [ + "*___ test_foo [[]sub2[]] __*", + "*-- Captured stdout call --*", + "sub2 stdout", + "*-- Captured log call ---*", + "INFO * before", + "INFO * sub1 logging", + "INFO * sub2 logging", + "*== short test summary info ==*", + ] + ) + result.stdout.no_fnmatch_line("sub1 logging debug") + result.stdout.no_fnmatch_line("sub2 logging debug") + + def test_capturing_debug(self, pytester: pytest.Pytester) -> None: + self.create_file(pytester) + result = pytester.runpytest("--log-level=DEBUG") + result.stdout.fnmatch_lines( + [ + "*___ test_foo [[]sub2[]] __*", + "*-- Captured stdout call --*", + "sub2 stdout", + "*-- Captured log call ---*", + "INFO * before", + "INFO * sub1 logging", + "DEBUG * sub1 logging debug", + "INFO * sub2 logging", + "DEBUG * sub2 logging debug", + "*== short test summary info ==*", + ] + ) + + def test_caplog(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + import logging + + def test(subtests, caplog): + caplog.set_level(logging.INFO) + logging.info("start test") + + with subtests.test("sub1"): + logging.info("inside %s", "subtest1") + + assert len(caplog.records) == 2 + assert caplog.records[0].getMessage() == "start test" + assert caplog.records[1].getMessage() == "inside subtest1" + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*1 passed*", + ] + ) + + def test_no_logging(self, pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + import logging + + def test(subtests): + logging.info("start log line") + + with subtests.test("sub passing"): + logging.info("inside %s", "passing log line") + + with subtests.test("sub failing"): + logging.info("inside %s", "failing log line") + assert False + + logging.info("end log line") + """ + ) + result = pytester.runpytest("-p no:logging") + result.stdout.fnmatch_lines( + [ + "*2 failed in*", + ] + ) + result.stdout.no_fnmatch_line("*root:test_no_logging.py*log line*") + + +class TestDebugging: + """Check --pdb support for subtests fixture and TestCase.subTest.""" + + class _FakePdb: + """Fake debugger class implementation that tracks which methods were called on it.""" + + quitting: bool = False + calls: list[str] = [] + + def __init__(self, *_: object, **__: object) -> None: + self.calls.append("init") + + def reset(self) -> None: + self.calls.append("reset") + + def interaction(self, *_: object) -> None: + self.calls.append("interaction") + + @pytest.fixture(autouse=True) + def cleanup_calls(self) -> None: + self._FakePdb.calls.clear() + + def test_pdb_fixture( + self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + pytester.makepyfile( + """ + def test(subtests): + with subtests.test(): + assert 0 + """ + ) + self.runpytest_and_check_pdb(pytester, monkeypatch) + + def test_pdb_unittest( + self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + pytester.makepyfile( + """ + from unittest import TestCase + class Test(TestCase): + def test(self): + with self.subTest(): + assert 0 + """ + ) + self.runpytest_and_check_pdb(pytester, monkeypatch) + + def runpytest_and_check_pdb( + self, pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + # Install the fake pdb implementation in _pytest.subtests so we can reference + # it in the command line (any module would do). + import _pytest.subtests + + monkeypatch.setattr( + _pytest.subtests, "_CustomPdb", self._FakePdb, raising=False + ) + result = pytester.runpytest("--pdb", "--pdbcls=_pytest.subtests:_CustomPdb") + + # Ensure pytest entered in debugging mode when encountering the failing + # assert. + result.stdout.fnmatch_lines("*entering PDB*") + assert self._FakePdb.calls == ["init", "reset", "interaction"] + + +def test_exitfirst(pytester: pytest.Pytester) -> None: + """Validate that when passing --exitfirst the test exits after the first failed subtest.""" + pytester.makepyfile( + """ + def test_foo(subtests): + with subtests.test("sub1"): + assert False + + with subtests.test("sub2"): + assert False + """ + ) + result = pytester.runpytest("--exitfirst") + assert result.parseoutcomes()["failed"] == 2 + result.stdout.fnmatch_lines( + [ + "SUBFAILED*[[]sub1[]] *.py::test_foo - assert False*", + "FAILED *.py::test_foo - assert False", + "* stopping after 2 failures*", + ], + consecutive=True, + ) + result.stdout.no_fnmatch_line("*sub2*") # sub2 not executed. + + +def test_do_not_swallow_pytest_exit(pytester: pytest.Pytester) -> None: + pytester.makepyfile( + """ + import pytest + def test(subtests): + with subtests.test(): + pytest.exit() + + def test2(): pass + """ + ) + result = pytester.runpytest_subprocess() + result.stdout.fnmatch_lines( + [ + "* _pytest.outcomes.Exit *", + "* 1 failed in *", + ] + ) + + +def test_nested(pytester: pytest.Pytester) -> None: + """ + Currently we do nothing special with nested subtests. + + This test only sediments how they work now, we might reconsider adding some kind of nesting support in the future. + """ + pytester.makepyfile( + """ + import pytest + def test(subtests): + with subtests.test("a"): + with subtests.test("b"): + assert False, "b failed" + assert False, "a failed" + """ + ) + result = pytester.runpytest_subprocess() + result.stdout.fnmatch_lines( + [ + "SUBFAILED[b] test_nested.py::test - AssertionError: b failed", + "SUBFAILED[a] test_nested.py::test - AssertionError: a failed", + "* 3 failed in *", + ] + ) + + +def test_serialization() -> None: + from _pytest.subtests import pytest_report_from_serializable + from _pytest.subtests import pytest_report_to_serializable + + report = SubtestReport( + "test_foo::test_foo", + ("test_foo.py", 12, ""), + keywords={}, + outcome="passed", + when="call", + longrepr=None, + context=SubtestContext(msg="custom message", kwargs=dict(i=10)), + ) + data = pytest_report_to_serializable(report) + assert data is not None + new_report = pytest_report_from_serializable(data) + assert new_report is not None + assert new_report.context == SubtestContext(msg="custom message", kwargs=dict(i=10)) From 010f62f30dfb449137e3b02618a38cd61786c0a7 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 20 Dec 2025 07:37:02 -0300 Subject: [PATCH 77/78] Ensure subtest's context kwargs are JSON serializable (#13963) Convert all the values of `SubtestContext.kwargs` to strings using `saferepr`. This complies with the requirement that the returned dict from `pytest_report_to_serializable` is serializable to JSON, at the cost of losing type information for objects that are natively supported by JSON. Fixes pytest-dev/pytest-xdist#1273 --- changelog/13963.bugfix.rst | 3 ++ src/_pytest/subtests.py | 9 ++++- testing/test_subtests.py | 69 ++++++++++++++++++++++++++++++-------- 3 files changed, 66 insertions(+), 15 deletions(-) create mode 100644 changelog/13963.bugfix.rst diff --git a/changelog/13963.bugfix.rst b/changelog/13963.bugfix.rst new file mode 100644 index 00000000000..a5f7ebe5c03 --- /dev/null +++ b/changelog/13963.bugfix.rst @@ -0,0 +1,3 @@ +Fixed subtests running with `pytest-xdist `__ when their contexts contain objects that are not JSON-serializable. + +Fixes `pytest-dev/pytest-xdist#1273 `__. diff --git a/src/_pytest/subtests.py b/src/_pytest/subtests.py index e0ceb27f4b1..d9455c80544 100644 --- a/src/_pytest/subtests.py +++ b/src/_pytest/subtests.py @@ -61,8 +61,15 @@ class SubtestContext: msg: str | None kwargs: Mapping[str, Any] + def __post_init__(self) -> None: + # Brute-force the returned kwargs dict to be JSON serializable (pytest-dev/pytest-xdist#1273). + object.__setattr__( + self, "kwargs", {k: saferepr(v) for (k, v) in self.kwargs.items()} + ) + def _to_json(self) -> dict[str, Any]: - return dataclasses.asdict(self) + result = dataclasses.asdict(self) + return result @classmethod def _from_json(cls, d: dict[str, Any]) -> Self: diff --git a/testing/test_subtests.py b/testing/test_subtests.py index 6849df53622..c480bb01658 100644 --- a/testing/test_subtests.py +++ b/testing/test_subtests.py @@ -1,8 +1,11 @@ from __future__ import annotations +from enum import Enum +import json import sys from typing import Literal +from _pytest._io.saferepr import saferepr from _pytest.subtests import SubtestContext from _pytest.subtests import SubtestReport import pytest @@ -302,10 +305,10 @@ def test_foo(subtests, x): result = pytester.runpytest("-v") result.stdout.fnmatch_lines( [ - "*.py::test_foo[[]0[]] SUBFAILED[[]custom[]] (i=1) *[[] 50%[]]", - "*.py::test_foo[[]0[]] FAILED *[[] 50%[]]", - "*.py::test_foo[[]1[]] SUBFAILED[[]custom[]] (i=1) *[[]100%[]]", - "*.py::test_foo[[]1[]] FAILED *[[]100%[]]", + "*.py::test_foo[[]0[]] SUBFAILED[[]custom[]] (i='1') *[[] 50%[]]", + "*.py::test_foo[[]0[]] FAILED *[[] 50%[]]", + "*.py::test_foo[[]1[]] SUBFAILED[[]custom[]] (i='1') *[[]100%[]]", + "*.py::test_foo[[]1[]] FAILED *[[]100%[]]", "contains 1 failed subtest", "* 4 failed, 4 subtests passed in *", ] @@ -320,10 +323,10 @@ def test_foo(subtests, x): result = pytester.runpytest("-v") result.stdout.fnmatch_lines( [ - "*.py::test_foo[[]0[]] SUBFAILED[[]custom[]] (i=1) *[[] 50%[]]", - "*.py::test_foo[[]0[]] FAILED *[[] 50%[]]", - "*.py::test_foo[[]1[]] SUBFAILED[[]custom[]] (i=1) *[[]100%[]]", - "*.py::test_foo[[]1[]] FAILED *[[]100%[]]", + "*.py::test_foo[[]0[]] SUBFAILED[[]custom[]] (i='1') *[[] 50%[]]", + "*.py::test_foo[[]0[]] FAILED *[[] 50%[]]", + "*.py::test_foo[[]1[]] SUBFAILED[[]custom[]] (i='1') *[[]100%[]]", + "*.py::test_foo[[]1[]] FAILED *[[]100%[]]", "contains 1 failed subtest", "* 4 failed in *", ] @@ -650,12 +653,12 @@ def test_capturing(self, pytester: pytest.Pytester, mode: str) -> None: result = pytester.runpytest(f"--capture={mode}") result.stdout.fnmatch_lines( [ - "*__ test (i='A') __*", + "*__ test (i=\"'A'\") __*", "*Captured stdout call*", "hello stdout A", "*Captured stderr call*", "hello stderr A", - "*__ test (i='B') __*", + "*__ test (i=\"'B'\") __*", "*Captured stdout call*", "hello stdout B", "*Captured stderr call*", @@ -676,8 +679,8 @@ def test_no_capture(self, pytester: pytest.Pytester) -> None: "hello stdout A", "uhello stdout B", "uend test", - "*__ test (i='A') __*", - "*__ test (i='B') __*", + "*__ test (i=\"'A'\") __*", + "*__ test (i=\"'B'\") __*", "*__ test __*", ] ) @@ -957,7 +960,14 @@ def test(subtests): ) +class MyEnum(Enum): + """Used in test_serialization, needs to be declared at the module level to be pickled.""" + + A = "A" + + def test_serialization() -> None: + """Ensure subtest's kwargs are serialized using `saferepr` (pytest-dev/pytest-xdist#1273).""" from _pytest.subtests import pytest_report_from_serializable from _pytest.subtests import pytest_report_to_serializable @@ -968,10 +978,41 @@ def test_serialization() -> None: outcome="passed", when="call", longrepr=None, - context=SubtestContext(msg="custom message", kwargs=dict(i=10)), + context=SubtestContext(msg="custom message", kwargs=dict(i=10, a=MyEnum.A)), ) data = pytest_report_to_serializable(report) assert data is not None + # Ensure the report is actually serializable to JSON. + _ = json.dumps(data) new_report = pytest_report_from_serializable(data) assert new_report is not None - assert new_report.context == SubtestContext(msg="custom message", kwargs=dict(i=10)) + assert new_report.context == SubtestContext( + msg="custom message", kwargs=dict(i=saferepr(10), a=saferepr(MyEnum.A)) + ) + + +def test_serialization_xdist(pytester: pytest.Pytester) -> None: # pragma: no cover + """Regression test for pytest-dev/pytest-xdist#1273.""" + pytest.importorskip("xdist") + pytester.makepyfile( + """ + from enum import Enum + import unittest + + class MyEnum(Enum): + A = "A" + + def test(subtests): + with subtests.test(a=MyEnum.A): + pass + + class T(unittest.TestCase): + + def test(self): + with self.subTest(a=MyEnum.A): + pass + """ + ) + pytester.syspathinsert() + result = pytester.runpytest("-n1", "-pxdist.plugin") + result.assert_outcomes(passed=2) From dbeb846e66a24795a64ae603bd9bf7ec7ed9b9fb Mon Sep 17 00:00:00 2001 From: Jayshil Date: Fri, 16 Jan 2026 20:37:34 +0000 Subject: [PATCH 78/78] upgrade version --- .github/workflows/test.yml | 177 ++++++++++++++++++------------------- 1 file changed, 88 insertions(+), 89 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 468e6a59cb8..78d31b52712 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,12 +15,12 @@ on: - main - "[0-9]+.[0-9]+.x" types: - - opened # default - - synchronize # default - - reopened # default - - ready_for_review # used in PRs created from the release workflow + - opened # default + - synchronize # default + - reopened # default + - ready_for_review # used in PRs created from the release workflow - workflow_dispatch: # allows manual triggering of the workflow + workflow_dispatch: # allows manual triggering of the workflow env: PYTEST_ADDOPTS: "--color=yes" @@ -37,12 +37,12 @@ jobs: package: runs-on: ubuntu-latest steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - name: Build and Check Package - uses: hynek/build-and-inspect-python-package@v2.12.0 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - name: Build and Check Package + uses: hynek/build-and-inspect-python-package@v2.14.0 build: needs: [package] @@ -55,35 +55,36 @@ jobs: strategy: fail-fast: false matrix: - name: [ - "windows-py38", - "windows-py39-pluggy", - "windows-py39", - "windows-py310", - "windows-py311", - "windows-py312", - "windows-py313", - "windows-py314", - - "ubuntu-py38", - "ubuntu-py39-pluggy", - "ubuntu-py38-freeze", - "ubuntu-py39", - "ubuntu-py310", - "ubuntu-py311", - "ubuntu-py312", - "ubuntu-py313-pexpect", - "ubuntu-py314", - "ubuntu-pypy3-xdist", - - "macos-py310", - "macos-py312", - "macos-py313", - "macos-py314", - - "doctesting", - "plugins", - ] + name: + [ + "windows-py38", + "windows-py39-pluggy", + "windows-py39", + "windows-py310", + "windows-py311", + "windows-py312", + "windows-py313", + "windows-py314", + + "ubuntu-py38", + "ubuntu-py39-pluggy", + "ubuntu-py38-freeze", + "ubuntu-py39", + "ubuntu-py310", + "ubuntu-py311", + "ubuntu-py312", + "ubuntu-py313-pexpect", + "ubuntu-py314", + "ubuntu-pypy3-xdist", + + "macos-py310", + "macos-py312", + "macos-py313", + "macos-py314", + + "doctesting", + "plugins", + ] include: # Use separate jobs for different unittest flavors (twisted, asynctest) to ensure proper coverage. @@ -203,7 +204,6 @@ jobs: os: ubuntu-latest tox_env: "pypy3-xdist" - - name: "macos-py310" python: "3.10" os: macos-latest @@ -231,7 +231,6 @@ jobs: os: ubuntu-latest tox_env: "plugins" - - name: "doctesting" python: "3.10" os: ubuntu-latest @@ -259,57 +258,57 @@ jobs: }} steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - - name: Download Package - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - with: - name: Packages - path: dist - - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 - with: - python-version: ${{ matrix.python }} - check-latest: true - allow-prereleases: true - - - name: Install tox - run: | - python -m pip install --upgrade pip - pip install tox - - - name: Test without coverage - if: "! matrix.use_coverage" - shell: bash - run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz` - - - name: Test with coverage - if: "matrix.use_coverage" - shell: bash - run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz` - - - name: Upload coverage to Codecov - if: "matrix.use_coverage" - uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de - with: - fail_ci_if_error: false - files: ./coverage.xml - verbose: true - - check: # This job does nothing and is only used for the branch protection + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Download Package + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: Packages + path: dist + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: ${{ matrix.python }} + check-latest: true + allow-prereleases: true + + - name: Install tox + run: | + python -m pip install --upgrade pip + pip install tox + + - name: Test without coverage + if: "! matrix.use_coverage" + shell: bash + run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz` + + - name: Test with coverage + if: "matrix.use_coverage" + shell: bash + run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz` + + - name: Upload coverage to Codecov + if: "matrix.use_coverage" + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de + with: + fail_ci_if_error: false + files: ./coverage.xml + verbose: true + + check: # This job does nothing and is only used for the branch protection if: always() needs: - - build + - build runs-on: ubuntu-latest steps: - - name: Decide whether the needed jobs succeeded or failed - uses: re-actors/alls-green@2765efec08f0fd63e83ad900f5fd75646be69ff6 - with: - jobs: ${{ toJSON(needs) }} + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@2765efec08f0fd63e83ad900f5fd75646be69ff6 + with: + jobs: ${{ toJSON(needs) }}