diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33b292c2b2b..0eeafd468cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -67,7 +67,6 @@ repos: - attrs>=19.2.0 - packaging - tomli - - types-atomicwrites - types-pkg_resources - repo: local hooks: @@ -101,7 +100,7 @@ repos: types: [python] - id: py-path-deprecated name: py.path usage is deprecated - exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py + exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py|src/_pytest/legacypath.py language: pygrep entry: \bpy\.path\.local types: [python] diff --git a/AUTHORS b/AUTHORS index c0d1236b8b1..914e036a44a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,6 +15,7 @@ Alan Velasco Alexander Johnson Alexander King Alexei Kozlenok +Alice Purcell Allan Feldman Aly Sivji Amir Elkess @@ -83,6 +84,7 @@ Damian Skrzypczak Daniel Grana Daniel Hahler Daniel Nuri +Daniel Sánchez Castelló Daniel Wandschneider Daniele Procida Danielle Jenkins @@ -184,6 +186,7 @@ Katarzyna Król Katerina Koukiou Keri Volans Kevin Cox +Kevin Hierro Carrasco Kevin J. Foley Kian Eliasi Kian-Meng Ang @@ -246,6 +249,7 @@ Nicholas Murphy Niclas Olofsson Nicolas Delaby Nikolay Kondratyev +Nipunn Koorapati Olga Matoula Oleg Pidsadnyi Oleg Sushchenko @@ -257,6 +261,7 @@ Oscar Benjamin Parth Patel Patrick Hayes Paul Müller +Paul Reece Pauli Virtanen Pavel Karateev Paweł Adamczak @@ -302,6 +307,7 @@ Seth Junot Shantanu Jain Shubham Adep Simon Gomizelj +Simon Holesch Simon Kerr Skylar Downes Srinivas Reddy Thatiparthy @@ -319,6 +325,7 @@ Taneli Hukkinen Tanvi Mehta Tarcisio Fischer Tareq Alayan +Tatiana Ovary Ted Xiao Terje Runde Thomas Grainger @@ -329,12 +336,14 @@ Tom Dalton Tom Viner Tomáš Gavenčiak Tomer Keren +Tony Narlock Tor Colvin Trevor Bekolay Tyler Goodlet Tzu-ping Chung Vasily Kuznetsov Victor Maryama +Victor Rodriguez Victor Uriarte Vidar T. Fauske Virgil Dupras @@ -355,5 +364,6 @@ Yoav Caspi Yuval Shimon Zac Hatfield-Dodds Zachary Kneupper +Zachary OBrien Zoltán Máté Zsolt Cserna diff --git a/doc/en/_templates/globaltoc.html b/doc/en/_templates/globaltoc.html index 7c595e7ebf2..09d970b64ed 100644 --- a/doc/en/_templates/globaltoc.html +++ b/doc/en/_templates/globaltoc.html @@ -17,7 +17,6 @@

About the project

  • Changelog
  • Contributing
  • Backwards Compatibility
  • -
  • Python 2.7 and 3.4 Support
  • Sponsor
  • pytest for Enterprise
  • License
  • @@ -30,5 +29,3 @@

    About the project

    {%- endif %}
    -Index -
    diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index bcc0b45664b..142425cdee7 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-7.1.3 release-7.1.2 release-7.1.1 release-7.1.0 diff --git a/doc/en/announce/release-7.1.3.rst b/doc/en/announce/release-7.1.3.rst new file mode 100644 index 00000000000..4cb1b271704 --- /dev/null +++ b/doc/en/announce/release-7.1.3.rst @@ -0,0 +1,28 @@ +pytest-7.1.3 +======================================= + +pytest 7.1.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 +* Bruno Oliveira +* Gergely Kalmár +* Nipunn Koorapati +* Pax +* Sviatoslav Sydorenko +* Tim Hoffmann +* Tony Narlock +* Wolfremium +* Zach OBrien +* aizpurua23a + + +Happy testing, +The pytest Development Team diff --git a/doc/en/backwards-compatibility.rst b/doc/en/backwards-compatibility.rst index 3a0ff126164..64bcbf5bd49 100644 --- a/doc/en/backwards-compatibility.rst +++ b/doc/en/backwards-compatibility.rst @@ -77,3 +77,18 @@ Deprecation Roadmap Features currently deprecated and removed in previous releases can be found in :ref:`deprecations`. We track future deprecation and removal of features using milestones and the `deprecation `_ and `removal `_ labels on GitHub. + + +Python version support +====================== + +Released pytest versions support all Python versions that are actively maintained at the time of the release: + +============== =================== +pytest version min. Python version +============== =================== +7.1+ 3.7+ +6.2 - 7.0 3.6+ +5.0 - 6.1 3.5+ +3.3 - 4.6 2.7, 3.4+ +============== =================== diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 77ae5a13028..7e22002245d 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -40,32 +40,86 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. - capsysbinary -- .../_pytest/capture.py:895 + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_output(capsys): + print("hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" + + capsysbinary -- .../_pytest/capture.py:906 Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``bytes`` objects. - capfd -- .../_pytest/capture.py:912 + Returns an instance of :class:`CaptureFixture[bytes] `. + + Example: + + .. code-block:: python + + def test_output(capsysbinary): + print("hello") + captured = capsysbinary.readouterr() + assert captured.out == b"hello\n" + + capfd -- .../_pytest/capture.py:934 Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. - capfdbinary -- .../_pytest/capture.py:929 + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_system_echo(capfd): + os.system('echo "hello"') + captured = capfd.readouterr() + assert captured.out == "hello\n" + + capfdbinary -- .../_pytest/capture.py:962 Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``byte`` objects. - doctest_namespace [session scope] -- .../_pytest/doctest.py:731 + Returns an instance of :class:`CaptureFixture[bytes] `. + + Example: + + .. code-block:: python + + def test_system_echo(capfdbinary): + os.system('echo "hello"') + captured = capfdbinary.readouterr() + assert captured.out == b"hello\n" + + doctest_namespace [session scope] -- .../_pytest/doctest.py:735 Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. - pytestconfig [session scope] -- .../_pytest/fixtures.py:1334 + Usually this fixture is used in conjunction with another ``autouse`` fixture: + + .. code-block:: python + + @pytest.fixture(autouse=True) + def add_np(doctest_namespace): + doctest_namespace["np"] = numpy + + For more details: :ref:`doctest_namespace`. + + pytestconfig [session scope] -- .../_pytest/fixtures.py:1344 Session-scoped fixture that returns the session's :class:`pytest.Config` object. @@ -117,10 +171,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:295 + tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302 Return a :class:`pytest.TempdirFactory` instance for the test session. - tmpdir -- .../_pytest/legacypath.py:302 + tmpdir -- .../_pytest/legacypath.py:309 Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. @@ -132,6 +186,11 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a The returned object is a `legacy_path`_ object. + .. note:: + These days, it is preferred to use ``tmp_path``. + + :ref:`About the tmpdir and tmpdir_factory fixtures`. + .. _legacy_path: https://py.readthedocs.io/en/latest/path.html caplog -- .../_pytest/logging.py:487 @@ -148,21 +207,26 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a monkeypatch -- .../_pytest/monkeypatch.py:29 A convenient fixture for monkey-patching. - The fixture provides these methods to modify objects, dictionaries or - os.environ:: + The fixture provides these methods to modify objects, dictionaries, or + :data:`os.environ`: - monkeypatch.setattr(obj, name, value, raising=True) - monkeypatch.delattr(obj, name, raising=True) - monkeypatch.setitem(mapping, name, value) - monkeypatch.delitem(obj, name, raising=True) - monkeypatch.setenv(name, value, prepend=None) - monkeypatch.delenv(name, raising=True) - monkeypatch.syspath_prepend(path) - monkeypatch.chdir(path) + * :meth:`monkeypatch.setattr(obj, name, value, raising=True) ` + * :meth:`monkeypatch.delattr(obj, name, raising=True) ` + * :meth:`monkeypatch.setitem(mapping, name, value) ` + * :meth:`monkeypatch.delitem(obj, name, raising=True) ` + * :meth:`monkeypatch.setenv(name, value, prepend=None) ` + * :meth:`monkeypatch.delenv(name, raising=True) ` + * :meth:`monkeypatch.syspath_prepend(path) ` + * :meth:`monkeypatch.chdir(path) ` + * :meth:`monkeypatch.context() ` All modifications will be undone after the requesting test function or - fixture has finished. The ``raising`` parameter determines if a KeyError - or AttributeError will be raised if the set/deletion operation has no target. + fixture has finished. The ``raising`` parameter determines if a :class:`KeyError` + or :class:`AttributeError` will be raised if the set/deletion operation does not have the + specified target. + + To undo modifications done by the fixture in a contained scope, + use :meth:`context() `. recwarn -- .../_pytest/recwarn.py:29 Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 46e18d32255..e60b08c555e 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,47 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 7.1.3 (2022-08-31) +========================= + +Bug Fixes +--------- + +- `#10060 `_: When running with ``--pdb``, ``TestCase.tearDown`` is no longer called for tests when the *class* has been skipped via ``unittest.skip`` or ``pytest.mark.skip``. + + +- `#10190 `_: Invalid XML characters in setup or teardown error messages are now properly escaped for JUnit XML reports. + + +- `#10230 `_: Ignore ``.py`` files created by ``pyproject.toml``-based editable builds introduced in `pip 21.3 `__. + + +- `#3396 `_: Doctests now respect the ``--import-mode`` flag. + + +- `#9514 `_: Type-annotate ``FixtureRequest.param`` as ``Any`` as a stop gap measure until :issue:`8073` is fixed. + + +- `#9791 `_: Fixed a path handling code in ``rewrite.py`` that seems to work fine, but was incorrect and fails in some systems. + + +- `#9917 `_: Fixed string representation for :func:`pytest.approx` when used to compare tuples. + + + +Improved Documentation +---------------------- + +- `#9937 `_: Explicit note that :fixture:`tmpdir` fixture is discouraged in favour of :fixture:`tmp_path`. + + + +Trivial/Internal Changes +------------------------ + +- `#10114 `_: Replace `atomicwrites `__ dependency on windows with `os.replace`. + + pytest 7.1.2 (2022-04-23) ========================= @@ -2618,7 +2659,8 @@ Important This release is a Python3.5+ only release. -For more details, see our :std:doc:`Python 2.7 and 3.4 support plan `. +For more details, see our `Python 2.7 and 3.4 support plan +`_. Removals -------- @@ -2842,7 +2884,11 @@ Features - :issue:`6870`: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. - Remark: while this is technically a new feature and according to our :ref:`policy ` it should not have been backported, we have opened an exception in this particular case because it fixes a serious interaction with ``pytest-xdist``, so it can also be considered a bugfix. + Remark: while this is technically a new feature and according to our + `policy `_ + it should not have been backported, we have opened an exception in this + particular case because it fixes a serious interaction with ``pytest-xdist``, + so it can also be considered a bugfix. Trivial/Internal Changes ------------------------ @@ -3014,7 +3060,8 @@ Important The ``4.6.X`` series will be the last series to support **Python 2 and Python 3.4**. -For more details, see our :std:doc:`Python 2.7 and 3.4 support plan `. +For more details, see our `Python 2.7 and 3.4 support plan +`_. Features diff --git a/doc/en/conf.py b/doc/en/conf.py index 1adc3493ad5..3c26b8ce0b7 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -247,7 +247,7 @@ html_domain_indices = True # If false, no index is generated. -html_use_index = True +html_use_index = False # If true, the index is split into individual pages for each letter. # html_split_index = False @@ -320,7 +320,9 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [("usage", "pytest", "pytest usage", ["holger krekel at merlinux eu"], 1)] +man_pages = [ + ("how-to/usage", "pytest", "pytest usage", ["holger krekel at merlinux eu"], 1) +] # -- Options for Epub output --------------------------------------------------- diff --git a/doc/en/contents.rst b/doc/en/contents.rst index 049d44ba9d8..ae42884f658 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -85,7 +85,6 @@ Further topics backwards-compatibility deprecations - py27-py34-deprecation contributing development_guide diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 66d72f3cc0d..f8147683975 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -657,18 +657,15 @@ Use :func:`pytest.raises` with the :ref:`pytest.mark.parametrize ref` decorator to write parametrized tests in which some tests raise exceptions and others do not. -It is helpful to define a no-op context manager ``does_not_raise`` to serve -as a complement to ``raises``. For example: +It may be helpful to use ``nullcontext`` as a complement to ``raises``. -.. code-block:: python +For example: - from contextlib import contextmanager - import pytest +.. code-block:: python + from contextlib import nullcontext as does_not_raise - @contextmanager - def does_not_raise(): - yield + import pytest @pytest.mark.parametrize( @@ -687,22 +684,3 @@ as a complement to ``raises``. For example: In the example above, the first three test cases should run unexceptionally, while the fourth should raise ``ZeroDivisionError``. - -If you're only supporting Python 3.7+, you can simply use ``nullcontext`` -to define ``does_not_raise``: - -.. code-block:: python - - from contextlib import nullcontext as does_not_raise - -Or, if you're supporting Python 3.3+ you can use: - -.. code-block:: python - - from contextlib import ExitStack as does_not_raise - -Or, if desired, you can ``pip install contextlib2`` and use: - -.. code-block:: python - - from contextlib2 import nullcontext as does_not_raise diff --git a/doc/en/explanation/goodpractices.rst b/doc/en/explanation/goodpractices.rst index 32a14991ae2..9ea7d226f5c 100644 --- a/doc/en/explanation/goodpractices.rst +++ b/doc/en/explanation/goodpractices.rst @@ -173,10 +173,9 @@ This layout prevents a lot of common pitfalls and has many benefits, which are b `blog post by Ionel Cristian Mărieș `_. .. note:: - The new ``--import-mode=importlib`` (see :ref:`import-modes`) doesn't have + The ``--import-mode=importlib`` option (see :ref:`import-modes`) does not have any of the drawbacks above because ``sys.path`` is not changed when importing - test modules, so users that run - into this issue are strongly encouraged to try it and report if the new option works well for them. + test modules, so users that run into this issue are strongly encouraged to try it. The ``src`` directory layout is still strongly recommended however. diff --git a/doc/en/explanation/pythonpath.rst b/doc/en/explanation/pythonpath.rst index 2330356b863..e1d240b498a 100644 --- a/doc/en/explanation/pythonpath.rst +++ b/doc/en/explanation/pythonpath.rst @@ -45,10 +45,19 @@ these values: * ``importlib``: new in pytest-6.0, this mode uses :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`. - For this reason this doesn't require test module names to be unique, but also makes test - modules non-importable by each other. + For this reason this doesn't require test module names to be unique. + + One drawback however is that test modules are non-importable by each other. Also, utility + modules in the tests directories are not automatically importable because the tests directory is no longer + added to :py:data:`sys.path`. + + Initially we intended to make ``importlib`` the default in future releases, however it is clear now that + it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future. + +.. seealso:: + + The :confval:`pythonpath` configuration variable. - We intend to make ``importlib`` the default in future releases, depending on feedback. ``prepend`` and ``append`` import modes scenarios ------------------------------------------------- diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index f7e03cc8b67..ce8ce815260 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 7.1.2 + pytest 7.1.3 .. _`simpletest`: diff --git a/doc/en/how-to/doctest.rst b/doc/en/how-to/doctest.rst index ce0b5a5f649..09de800c703 100644 --- a/doc/en/how-to/doctest.rst +++ b/doc/en/how-to/doctest.rst @@ -126,14 +126,17 @@ pytest also introduces new options: in expected doctest output. * ``NUMBER``: when enabled, floating-point numbers only need to match as far as - the precision you have written in the expected doctest output. For example, - the following output would only need to match to 2 decimal places:: + the precision you have written in the expected doctest output. The numbers are + compared using :func:`pytest.approx` with relative tolerance equal to the + precision. For example, the following output would only need to match to 2 + decimal places when comparing ``3.14`` to + ``pytest.approx(math.pi, rel=10**-2)``:: >>> math.pi 3.14 - If you wrote ``3.1416`` then the actual output would need to match to 4 - decimal places; and so on. + If you wrote ``3.1416`` then the actual output would need to match to + approximately 4 decimal places; and so on. This avoids false positives caused by limited floating-point precision, like this:: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index 08013877455..368dc04469f 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -736,6 +736,87 @@ does offer some nuances for when you're in a pinch. . [100%] 1 passed in 0.12s +Note on finalizer order +"""""""""""""""""""""""" + +Finalizers are executed in a first-in-last-out order. +For yield fixtures, the first teardown code to run is from the right-most fixture, i.e. the last test parameter. + + +.. code-block:: python + + # content of test_finalizers.py + import pytest + + + def test_bar(fix_w_yield1, fix_w_yield2): + print("test_bar") + + + @pytest.fixture + def fix_w_yield1(): + yield + print("after_yield_1") + + + @pytest.fixture + def fix_w_yield2(): + yield + print("after_yield_2") + + +.. code-block:: pytest + + $ pytest -s test_finalizers.py + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + rootdir: /home/sweet/project + collected 1 item + + test_finalizers.py test_bar + .after_yield_2 + after_yield_1 + + + ============================ 1 passed in 0.12s ============================= + +For finalizers, the first fixture to run is last call to `request.addfinalizer`. + +.. code-block:: python + + # content of test_finalizers.py + from functools import partial + import pytest + + + @pytest.fixture + def fix_w_finalizers(request): + request.addfinalizer(partial(print, "finalizer_2")) + request.addfinalizer(partial(print, "finalizer_1")) + + + def test_bar(fix_w_finalizers): + print("test_bar") + + +.. code-block:: pytest + + $ pytest -s test_finalizers.py + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y + rootdir: /home/sweet/project + collected 1 item + + test_finalizers.py test_bar + .finalizer_1 + finalizer_2 + + + ============================ 1 passed in 0.12s ============================= + +This is so because yield fixtures use `addfinalizer` behind the scenes: when the fixture executes, `addfinalizer` registers a function that resumes the generator, which in turn calls the teardown code. + + .. _`safe teardowns`: Safe teardowns @@ -1332,13 +1413,15 @@ Running the above tests results in the following test IDs being used: =========================== test session starts ============================ platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y rootdir: /home/sweet/project - collected 11 items + collected 12 items + + @@ -1350,7 +1433,7 @@ Running the above tests results in the following test IDs being used: - ======================= 11 tests collected in 0.12s ======================== + ======================= 12 tests collected in 0.12s ======================== .. _`fixture-parametrize-marks`: diff --git a/doc/en/how-to/logging.rst b/doc/en/how-to/logging.rst index 5a089c2fba0..c9fe62b492c 100644 --- a/doc/en/how-to/logging.rst +++ b/doc/en/how-to/logging.rst @@ -180,8 +180,8 @@ logging records as they are emitted directly into the console. You can specify the logging level for which log records with equal or higher level are printed to the console by passing ``--log-cli-level``. This setting -accepts the logging level names as seen in python's documentation or an integer -as the logging level num. +accepts the logging level names or numeric values as seen in +:ref:`logging's documentation `. Additionally, you can also specify ``--log-cli-format`` and ``--log-cli-date-format`` which mirror and default to ``--log-format`` and @@ -202,9 +202,8 @@ Note that relative paths for the log-file location, whether passed on the CLI or config file, are always resolved relative to the current working directory. You can also specify the logging level for the log file by passing -``--log-file-level``. This setting accepts the logging level names as seen in -python's documentation(ie, uppercased level names) or an integer as the logging -level num. +``--log-file-level``. This setting accepts the logging level names or numeric +values as seen in :ref:`logging's documentation `. Additionally, you can also specify ``--log-file-format`` and ``--log-file-date-format`` which are equal to ``--log-format`` and diff --git a/doc/en/how-to/monkeypatch.rst b/doc/en/how-to/monkeypatch.rst index 9c61233f7e5..81edd00bda5 100644 --- a/doc/en/how-to/monkeypatch.rst +++ b/doc/en/how-to/monkeypatch.rst @@ -3,7 +3,7 @@ How to monkeypatch/mock modules and environments ================================================================ -.. currentmodule:: _pytest.monkeypatch +.. currentmodule:: pytest Sometimes tests need to invoke functionality which depends on global settings or which invokes code which cannot be easily @@ -14,17 +14,16 @@ environment variable, or to modify ``sys.path`` for importing. The ``monkeypatch`` fixture provides these helper methods for safely patching and mocking functionality in tests: -.. code-block:: python +* :meth:`monkeypatch.setattr(obj, name, value, raising=True) ` +* :meth:`monkeypatch.delattr(obj, name, raising=True) ` +* :meth:`monkeypatch.setitem(mapping, name, value) ` +* :meth:`monkeypatch.delitem(obj, name, raising=True) ` +* :meth:`monkeypatch.setenv(name, value, prepend=None) ` +* :meth:`monkeypatch.delenv(name, raising=True) ` +* :meth:`monkeypatch.syspath_prepend(path) ` +* :meth:`monkeypatch.chdir(path) ` +* :meth:`monkeypatch.context() ` - monkeypatch.setattr(obj, name, value, raising=True) - monkeypatch.setattr("somemodule.obj.name", value, raising=True) - monkeypatch.delattr(obj, name, raising=True) - monkeypatch.setitem(mapping, name, value) - monkeypatch.delitem(obj, name, raising=True) - monkeypatch.setenv(name, value, prepend=None) - monkeypatch.delenv(name, raising=True) - monkeypatch.syspath_prepend(path) - monkeypatch.chdir(path) All modifications will be undone after the requesting test function or fixture has finished. The ``raising`` @@ -55,13 +54,16 @@ during a test. 5. Use :py:meth:`monkeypatch.syspath_prepend ` to modify ``sys.path`` which will also call ``pkg_resources.fixup_namespace_packages`` and :py:func:`importlib.invalidate_caches`. +6. Use :py:meth:`monkeypatch.context ` to apply patches only in a specific scope, which can help +control teardown of complex fixtures or patches to the stdlib. + See the `monkeypatch blog post`_ for some introduction material and a discussion of its motivation. .. _`monkeypatch blog post`: https://tetamap.wordpress.com//2009/03/03/monkeypatching-in-unit-tests-done-right/ -Simple example: monkeypatching functions ----------------------------------------- +Monkeypatching functions +------------------------ Consider a scenario where you are working with user directories. In the context of testing, you do not want your test to depend on the running user. ``monkeypatch`` @@ -436,7 +438,7 @@ separate fixtures for each potential mock and reference them in the needed tests _ = app.create_connection_string() -.. currentmodule:: _pytest.monkeypatch +.. currentmodule:: pytest API Reference ------------- diff --git a/doc/en/how-to/tmp_path.rst b/doc/en/how-to/tmp_path.rst index b261a55637e..99e9e55367a 100644 --- a/doc/en/how-to/tmp_path.rst +++ b/doc/en/how-to/tmp_path.rst @@ -104,8 +104,10 @@ The ``tmpdir`` and ``tmpdir_factory`` fixtures The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path`` and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects -rather than standard :class:`pathlib.Path` objects. These days, prefer to -use ``tmp_path`` and ``tmp_path_factory``. +rather than standard :class:`pathlib.Path` objects. + +.. note:: + These days, it is preferred to use ``tmp_path`` and ``tmp_path_factory``. See :fixture:`tmpdir ` :fixture:`tmpdir_factory ` API for details. diff --git a/doc/en/how-to/unittest.rst b/doc/en/how-to/unittest.rst index bff75110778..0d9e9f90542 100644 --- a/doc/en/how-to/unittest.rst +++ b/doc/en/how-to/unittest.rst @@ -27,12 +27,15 @@ Almost all ``unittest`` features are supported: * ``setUpClass/tearDownClass``; * ``setUpModule/tearDownModule``; +.. _`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`_; -* :ref:`subtests `; Benefits out of the box ----------------------- diff --git a/doc/en/index.rst b/doc/en/index.rst index 03a39aaaae8..870fd5a2b15 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -2,16 +2,11 @@ .. sidebar:: Next Open Trainings - - `PyConDE `__, April 11th 2022 (3h), Berlin, Germany - - `PyConIT `__, June 3rd 2022 (4h), Florence, Italy + - `CH Open Workshop-Tage `__ (German), September 8th 2022, Bern, Switzerland - `Professional Testing with Python `_, via `Python Academy `_, March 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany Also see :doc:`previous talks and blogposts `. -.. - - `Europython `__, July 11th to 17th (3h), Dublin, Ireland - - `CH Open Workshoptage `__ (German), September 6th to 8th (1 day), Bern, Switzerland - .. _features: pytest: helps you write better programs @@ -27,8 +22,6 @@ scale to support complex functional testing for applications and libraries. **PyPI package name**: :pypi:`pytest` -**Documentation as PDF**: `download latest `_ - A quick example --------------- @@ -104,11 +97,6 @@ Bugs/Requests Please use the `GitHub issue tracker `_ to submit bugs or request features. -Changelog ---------- - -Consult the :ref:`Changelog ` page for fixes and enhancements of each version. - Support pytest -------------- @@ -141,13 +129,3 @@ Security pytest has never been associated with a security vulnerability, but in any case, to report a security vulnerability please use the `Tidelift security contact `_. Tidelift will coordinate the fix and disclosure. - - -License -------- - -Copyright Holger Krekel and others, 2004. - -Distributed under the terms of the `MIT`_ license, pytest is free and open source software. - -.. _`MIT`: https://github.com/pytest-dev/pytest/blob/main/LICENSE diff --git a/doc/en/py27-py34-deprecation.rst b/doc/en/py27-py34-deprecation.rst deleted file mode 100644 index 660b078e30e..00000000000 --- a/doc/en/py27-py34-deprecation.rst +++ /dev/null @@ -1,99 +0,0 @@ -Python 2.7 and 3.4 support -========================== - -It is demanding on the maintainers of an open source project to support many Python versions, as -there's extra cost of keeping code compatible between all versions, while holding back on -features only made possible on newer Python versions. - -In case of Python 2 and 3, the difference between the languages makes it even more prominent, -because many new Python 3 features cannot be used in a Python 2/3 compatible code base. - -Python 2.7 EOL has been reached :pep:`in 2020 <0373#maintenance-releases>`, with -the last release made in April, 2020. - -Python 3.4 EOL has been reached :pep:`in 2019 <0429#release-schedule>`, with the last release made in March, 2019. - -For those reasons, in Jun 2019 it was decided that **pytest 4.6** series will be the last to support Python 2.7 and 3.4. - -What this means for general users ---------------------------------- - -Thanks to the `python_requires`_ setuptools option, -Python 2.7 and Python 3.4 users using a modern pip version -will install the last pytest 4.6.X version automatically even if 5.0 or later versions -are available on PyPI. - -Users should ensure they are using the latest pip and setuptools versions for this to work. - -Maintenance of 4.6.X versions ------------------------------ - -Until January 2020, the pytest core team ported many bug-fixes from the main release into the -``4.6.x`` branch, with several 4.6.X releases being made along the year. - -From now on, the core team will **no longer actively backport patches**, but the ``4.6.x`` -branch will continue to exist so the community itself can contribute patches. - -The core team will be happy to accept those patches, and make new 4.6.X releases **until mid-2020** -(but consider that date as a ballpark, after that date the team might still decide to make new releases -for critical bugs). - -.. _`python_requires`: https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires - -Technical aspects -~~~~~~~~~~~~~~~~~ - -(This section is a transcript from :issue:`5275`). - -In this section we describe the technical aspects of the Python 2.7 and 3.4 support plan. - -.. _what goes into 4.6.x releases: - -What goes into 4.6.X releases -+++++++++++++++++++++++++++++ - -New 4.6.X releases will contain bug fixes only. - -When will 4.6.X releases happen -+++++++++++++++++++++++++++++++ - -New 4.6.X releases will happen after we have a few bugs in place to release, or if a few weeks have -passed (say a single bug has been fixed a month after the latest 4.6.X release). - -No hard rules here, just ballpark. - -Who will handle applying bug fixes -++++++++++++++++++++++++++++++++++ - -We core maintainers expect that people still using Python 2.7/3.4 and being affected by -bugs to step up and provide patches and/or port bug fixes from the active branches. - -We will be happy to guide users interested in doing so, so please don't hesitate to ask. - -**Backporting changes into 4.6** - -Please follow these instructions: - -#. ``git fetch --all --prune`` - -#. ``git checkout origin/4.6.x -b backport-XXXX`` # use the PR number here - -#. Locate the merge commit on the PR, in the *merged* message, for example: - - nicoddemus merged commit 0f8b462 into pytest-dev:features - -#. ``git cherry-pick -m1 REVISION`` # use the revision you found above (``0f8b462``). - -#. Open a PR targeting ``4.6.x``: - - * Prefix the message with ``[4.6]`` so it is an obvious backport - * Delete the PR body, it usually contains a duplicate commit message. - -**Providing new PRs to 4.6** - -Fresh pull requests to ``4.6.x`` will be accepted provided that -the equivalent code in the active branches does not contain that bug (for example, a bug is specific -to Python 2 only). - -Bug fixes that also happen in the mainstream version should be first fixed -there, and then backported as per instructions above. diff --git a/doc/en/reference/index.rst b/doc/en/reference/index.rst index d9648400317..ee1b2e6214d 100644 --- a/doc/en/reference/index.rst +++ b/doc/en/reference/index.rst @@ -8,8 +8,8 @@ Reference guides .. toctree:: :maxdepth: 1 + reference fixtures - plugin_list customize - reference exit-codes + plugin_list diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 167c8fed9a3..b113deb3215 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -92,7 +92,7 @@ pytest.param pytest.raises ~~~~~~~~~~~~~ -**Tutorial**: :ref:`assertraises`. +**Tutorial**: :ref:`assertraises` .. autofunction:: pytest.raises(expected_exception: Exception [, *, match]) :with: excinfo @@ -100,7 +100,7 @@ pytest.raises pytest.deprecated_call ~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`ensuring_function_triggers`. +**Tutorial**: :ref:`ensuring_function_triggers` .. autofunction:: pytest.deprecated_call() :with: @@ -108,7 +108,7 @@ pytest.deprecated_call pytest.register_assert_rewrite ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`assertion-rewriting`. +**Tutorial**: :ref:`assertion-rewriting` .. autofunction:: pytest.register_assert_rewrite @@ -123,7 +123,7 @@ pytest.warns pytest.freeze_includes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`freezing-pytest`. +**Tutorial**: :ref:`freezing-pytest` .. autofunction:: pytest.freeze_includes @@ -143,7 +143,7 @@ fixtures or plugins. pytest.mark.filterwarnings ~~~~~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`filterwarnings`. +**Tutorial**: :ref:`filterwarnings` Add warning filters to marked test items. @@ -169,7 +169,7 @@ Add warning filters to marked test items. pytest.mark.parametrize ~~~~~~~~~~~~~~~~~~~~~~~ -:ref:`parametrize`. +**Tutorial**: :ref:`parametrize` This mark has the same signature as :py:meth:`pytest.Metafunc.parametrize`; see there. @@ -179,7 +179,7 @@ This mark has the same signature as :py:meth:`pytest.Metafunc.parametrize`; see pytest.mark.skip ~~~~~~~~~~~~~~~~ -:ref:`skip`. +**Tutorial**: :ref:`skip` Unconditionally skip a test function. @@ -193,7 +193,7 @@ Unconditionally skip a test function. pytest.mark.skipif ~~~~~~~~~~~~~~~~~~ -:ref:`skipif`. +**Tutorial**: :ref:`skipif` Skip a test function if a condition is ``True``. @@ -209,7 +209,7 @@ Skip a test function if a condition is ``True``. pytest.mark.usefixtures ~~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`usefixtures`. +**Tutorial**: :ref:`usefixtures` Mark a test function as using the given fixture names. @@ -231,7 +231,7 @@ Mark a test function as using the given fixture names. pytest.mark.xfail ~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`xfail`. +**Tutorial**: :ref:`xfail` Marks a test function as *expected to fail*. @@ -245,7 +245,7 @@ Marks a test function as *expected to fail*. :keyword str reason: Reason why the test function is marked as xfail. :keyword Type[Exception] raises: - Exception subclass expected to be raised by the test function; other exceptions will fail the test. + Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test. :keyword bool run: If the test function should actually be executed. If ``False``, the function will always xfail and will not be executed (useful if a function is segfaulting). @@ -297,7 +297,7 @@ When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node. Fixtures -------- -**Tutorial**: :ref:`fixture`. +**Tutorial**: :ref:`fixture` Fixtures are requested by test functions or other fixtures by declaring them as argument names. @@ -338,7 +338,7 @@ For more details, consult the full :ref:`fixtures docs `. config.cache ~~~~~~~~~~~~ -**Tutorial**: :ref:`cache`. +**Tutorial**: :ref:`cache` The ``config.cache`` object allows other plugins and fixtures to store and retrieve values across test runs. To access it from fixtures @@ -358,22 +358,11 @@ Under the hood, the cache plugin uses the simple capsys ~~~~~~ -:ref:`captures`. +**Tutorial**: :ref:`captures` .. autofunction:: _pytest.capture.capsys() :no-auto-options: - Returns an instance of :class:`CaptureFixture[str] `. - - Example: - - .. code-block:: python - - def test_output(capsys): - print("hello") - captured = capsys.readouterr() - assert captured.out == "hello\n" - .. autoclass:: pytest.CaptureFixture() :members: @@ -383,93 +372,48 @@ capsys capsysbinary ~~~~~~~~~~~~ -:ref:`captures`. +**Tutorial**: :ref:`captures` .. autofunction:: _pytest.capture.capsysbinary() :no-auto-options: - Returns an instance of :class:`CaptureFixture[bytes] `. - - Example: - - .. code-block:: python - - def test_output(capsysbinary): - print("hello") - captured = capsysbinary.readouterr() - assert captured.out == b"hello\n" - - .. fixture:: capfd capfd ~~~~~~ -:ref:`captures`. +**Tutorial**: :ref:`captures` .. autofunction:: _pytest.capture.capfd() :no-auto-options: - Returns an instance of :class:`CaptureFixture[str] `. - - Example: - - .. code-block:: python - - def test_system_echo(capfd): - os.system('echo "hello"') - captured = capfd.readouterr() - assert captured.out == "hello\n" - - .. fixture:: capfdbinary capfdbinary ~~~~~~~~~~~~ -:ref:`captures`. +**Tutorial**: :ref:`captures` .. autofunction:: _pytest.capture.capfdbinary() :no-auto-options: - Returns an instance of :class:`CaptureFixture[bytes] `. - - Example: - - .. code-block:: python - - def test_system_echo(capfdbinary): - os.system('echo "hello"') - captured = capfdbinary.readouterr() - assert captured.out == b"hello\n" - .. fixture:: doctest_namespace doctest_namespace ~~~~~~~~~~~~~~~~~ -:ref:`doctest`. +**Tutorial**: :ref:`doctest` .. autofunction:: _pytest.doctest.doctest_namespace() - Usually this fixture is used in conjunction with another ``autouse`` fixture: - - .. code-block:: python - - @pytest.fixture(autouse=True) - def add_np(doctest_namespace): - doctest_namespace["np"] = numpy - - For more details: :ref:`doctest_namespace`. - .. fixture:: request request ~~~~~~~ -:ref:`request example`. +**Example**: :ref:`request example` The ``request`` fixture is a special fixture providing information of the requesting test function. @@ -490,7 +434,7 @@ pytestconfig record_property ~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`record_property example`. +**Tutorial**: :ref:`record_property example` .. autofunction:: _pytest.junitxml.record_property() @@ -500,7 +444,7 @@ record_property record_testsuite_property ~~~~~~~~~~~~~~~~~~~~~~~~~ -**Tutorial**: :ref:`record_testsuite_property example`. +**Tutorial**: :ref:`record_testsuite_property example` .. autofunction:: _pytest.junitxml.record_testsuite_property() @@ -510,7 +454,7 @@ record_testsuite_property caplog ~~~~~~ -:ref:`logging`. +**Tutorial**: :ref:`logging` .. autofunction:: _pytest.logging.caplog() :no-auto-options: @@ -526,7 +470,7 @@ caplog monkeypatch ~~~~~~~~~~~ -:ref:`monkeypatching`. +**Tutorial**: :ref:`monkeypatching` .. autofunction:: _pytest.monkeypatch.monkeypatch() :no-auto-options: @@ -600,19 +544,13 @@ recwarn .. autoclass:: pytest.WarningsRecorder() :members: -Each recorded warning is an instance of :class:`warnings.WarningMessage`. - -.. note:: - ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated - differently; see :ref:`ensuring_function_triggers`. - .. fixture:: tmp_path tmp_path ~~~~~~~~ -:ref:`tmp_path` +**Tutorial**: :ref:`tmp_path` .. autofunction:: _pytest.tmpdir.tmp_path() :no-auto-options: @@ -623,7 +561,7 @@ tmp_path tmp_path_factory ~~~~~~~~~~~~~~~~ -:ref:`tmp_path_factory example` +**Tutorial**: :ref:`tmp_path_factory example` .. _`tmp_path_factory factory api`: @@ -638,7 +576,7 @@ tmp_path_factory tmpdir ~~~~~~ -:ref:`tmpdir and tmpdir_factory` +**Tutorial**: :ref:`tmpdir and tmpdir_factory` .. autofunction:: _pytest.legacypath.LegacyTmpdirPlugin.tmpdir() :no-auto-options: @@ -649,7 +587,7 @@ tmpdir tmpdir_factory ~~~~~~~~~~~~~~ -:ref:`tmpdir and tmpdir_factory` +**Tutorial**: :ref:`tmpdir and tmpdir_factory` ``tmpdir_factory`` is an instance of :class:`~pytest.TempdirFactory`: @@ -662,7 +600,7 @@ tmpdir_factory Hooks ----- -:ref:`writing-plugins`. +**Tutorial**: :ref:`writing-plugins` .. currentmodule:: _pytest.hookspec diff --git a/doc/en/talks.rst b/doc/en/talks.rst index 160345614c8..b9b153a792e 100644 --- a/doc/en/talks.rst +++ b/doc/en/talks.rst @@ -17,6 +17,8 @@ Books Talks and blog postings --------------------------------------------- +- Training: `pytest - simple, rapid and fun testing with Python `_, Florian Bruhin, PyConDE 2022 + - `pytest: Simple, rapid and fun testing with Python, `_ (@ 4:22:32), Florian Bruhin, WeAreDevelopers World Congress 2021 - Webinar: `pytest: Test Driven Development für Python (German) `_, Florian Bruhin, via mylearning.ch, 2020 diff --git a/setup.cfg b/setup.cfg index fe6ea4095bc..c922ce434cd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -47,7 +47,6 @@ install_requires = pluggy>=0.12,<2.0 py>=1.8.2 tomli>=1.0.0 - atomicwrites>=1.0;sys_platform=="win32" colorama;sys_platform=="win32" importlib-metadata>=0.12;python_version<"3.8" python_requires = >=3.7 diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 81096764e04..63f9dd8f27b 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -190,7 +190,7 @@ def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool: return False # For matching the name it must be as if it was a filename. - path = PurePath(os.path.sep.join(parts) + ".py") + path = PurePath(*parts).with_suffix(".py") for pat in self.fnpats: # if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based @@ -281,7 +281,9 @@ def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: else: from importlib.resources.readers import FileReader - return FileReader(types.SimpleNamespace(path=self._rewritten_names[name])) + return FileReader( # type:ignore[no-any-return] + types.SimpleNamespace(path=self._rewritten_names[name]) + ) def _write_pyc_fp( @@ -302,53 +304,29 @@ def _write_pyc_fp( fp.write(marshal.dumps(co)) -if sys.platform == "win32": - from atomicwrites import atomic_write - - def _write_pyc( - state: "AssertionState", - co: types.CodeType, - source_stat: os.stat_result, - pyc: Path, - ) -> bool: - try: - with atomic_write(os.fspath(pyc), mode="wb", overwrite=True) as fp: - _write_pyc_fp(fp, source_stat, co) - except OSError as e: - state.trace(f"error writing pyc file at {pyc}: {e}") - # we ignore any failure to write the cache file - # there are many reasons, permission-denied, pycache dir being a - # file etc. - return False - return True - -else: - - def _write_pyc( - state: "AssertionState", - co: types.CodeType, - source_stat: os.stat_result, - pyc: Path, - ) -> bool: - proc_pyc = f"{pyc}.{os.getpid()}" - try: - fp = open(proc_pyc, "wb") - except OSError as e: - state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}") - return False - - try: +def _write_pyc( + state: "AssertionState", + co: types.CodeType, + source_stat: os.stat_result, + pyc: Path, +) -> bool: + proc_pyc = f"{pyc}.{os.getpid()}" + try: + with open(proc_pyc, "wb") as fp: _write_pyc_fp(fp, source_stat, co) - os.rename(proc_pyc, pyc) - except OSError as e: - state.trace(f"error writing pyc file at {pyc}: {e}") - # we ignore any failure to write the cache file - # there are many reasons, permission-denied, pycache dir being a - # file etc. - return False - finally: - fp.close() - return True + except OSError as e: + state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}") + return False + + try: + os.replace(proc_pyc, pyc) + except OSError as e: + state.trace(f"error writing pyc file at {pyc}: {e}") + # we ignore any failure to write the cache file + # there are many reasons, permission-denied, pycache dir being a + # file etc. + return False + return True def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]: diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index ee9de373325..2a3c4143be4 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -876,11 +876,22 @@ def disabled(self) -> Generator[None, None, None]: @fixture def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: - """Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. + r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. + + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_output(capsys): + print("hello") + captured = capsys.readouterr() + assert captured.out == "hello\n" """ capman = request.config.pluginmanager.getplugin("capturemanager") capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True) @@ -893,11 +904,22 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: @fixture def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: - """Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. + r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``bytes`` objects. + + Returns an instance of :class:`CaptureFixture[bytes] `. + + Example: + + .. code-block:: python + + def test_output(capsysbinary): + print("hello") + captured = capsysbinary.readouterr() + assert captured.out == b"hello\n" """ capman = request.config.pluginmanager.getplugin("capturemanager") capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True) @@ -910,11 +932,22 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, @fixture def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: - """Enable text capturing of writes to file descriptors ``1`` and ``2``. + r"""Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``text`` objects. + + Returns an instance of :class:`CaptureFixture[str] `. + + Example: + + .. code-block:: python + + def test_system_echo(capfd): + os.system('echo "hello"') + captured = capfd.readouterr() + assert captured.out == "hello\n" """ capman = request.config.pluginmanager.getplugin("capturemanager") capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True) @@ -927,11 +960,23 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: @fixture def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]: - """Enable bytes capturing of writes to file descriptors ``1`` and ``2``. + r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method calls, which return a ``(out, err)`` namedtuple. ``out`` and ``err`` will be ``byte`` objects. + + Returns an instance of :class:`CaptureFixture[bytes] `. + + Example: + + .. code-block:: python + + def test_system_echo(capfdbinary): + os.system('echo "hello"') + captured = capfdbinary.readouterr() + assert captured.out == b"hello\n" + """ capman = request.config.pluginmanager.getplugin("capturemanager") capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 91ad3f094ff..53f40c24571 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -833,7 +833,8 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]: if is_simple_module: module_name, _ = os.path.splitext(fn) # we ignore "setup.py" at the root of the distribution - if module_name != "setup": + # as well as editable installation finder modules made by setuptools + if module_name != "setup" and not module_name.startswith("__editable__"): seen_some = True yield module_name elif is_package: diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 7d37be2acc0..98796aaafea 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -542,7 +542,11 @@ def _find( ) else: try: - module = import_path(self.path, root=self.config.rootpath) + module = import_path( + self.path, + root=self.config.rootpath, + mode=self.config.getoption("importmode"), + ) except ImportError: if self.config.getvalue("doctest_ignore_import_errors"): pytest.skip("unable to import module %r" % self.path) @@ -730,5 +734,16 @@ def _get_report_choice(key: str) -> int: @pytest.fixture(scope="session") def doctest_namespace() -> Dict[str, Any]: """Fixture that returns a :py:class:`dict` that will be injected into the - namespace of doctests.""" + namespace of doctests. + + Usually this fixture is used in conjunction with another ``autouse`` fixture: + + .. code-block:: python + + @pytest.fixture(autouse=True) + def add_np(doctest_namespace): + doctest_namespace["np"] = numpy + + For more details: :ref:`doctest_namespace`. + """ return dict() diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index ee3e93f1908..7d4bd1433a0 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -350,7 +350,7 @@ def reorder_items_atscope( return items_done -def get_direct_param_fixture_func(request): +def get_direct_param_fixture_func(request: "FixtureRequest") -> Any: return request.param @@ -412,6 +412,15 @@ def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None: self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() self._arg2index: Dict[str, int] = {} self._fixturemanager: FixtureManager = pyfuncitem.session._fixturemanager + # Notes on the type of `param`: + # -`request.param` is only defined in parametrized fixtures, and will raise + # AttributeError otherwise. Python typing has no notion of "undefined", so + # this cannot be reflected in the type. + # - Technically `param` is only (possibly) defined on SubRequest, not + # FixtureRequest, but the typing of that is still in flux so this cheats. + # - In the future we might consider using a generic for the param type, but + # for now just using Any. + self.param: Any @property def scope(self) -> "_ScopeName": @@ -491,6 +500,7 @@ def module(self): @property def path(self) -> Path: + """Path where the test function was collected.""" if self.scope not in ("function", "class", "module", "package"): raise AttributeError(f"path not available in {self.scope}-scoped context") # TODO: Remove ignore once _pyfuncitem is properly typed. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 1b9e3bfecac..7c06a9a8a47 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -231,7 +231,7 @@ def append_error(self, report: TestReport) -> None: msg = f'failed on teardown with "{reason}"' else: msg = f'failed on setup with "{reason}"' - self._add_simple("error", msg, str(report.longrepr)) + self._add_simple("error", bin_xml_escape(msg), str(report.longrepr)) def append_skipped(self, report: TestReport) -> None: if hasattr(report, "wasxfail"): diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index 37e8c24220e..f71e7e96ead 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -270,8 +270,15 @@ def testdir(pytester: Pytester) -> Testdir: @final @attr.s(init=False, auto_attribs=True) class TempdirFactory: - """Backward compatibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH`` - for :class:``TempPathFactory``.""" + """Backward compatibility wrapper that implements :class:`py.path.local` + for :class:`TempPathFactory`. + + .. note:: + These days, it is preferred to use ``tmp_path_factory``. + + :ref:`About the tmpdir and tmpdir_factory fixtures`. + + """ _tmppath_factory: TempPathFactory @@ -282,11 +289,11 @@ def __init__( self._tmppath_factory = tmppath_factory def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH: - """Same as :meth:`TempPathFactory.mktemp`, but returns a ``_pytest.compat.LEGACY_PATH`` object.""" + """Same as :meth:`TempPathFactory.mktemp`, but returns a :class:`py.path.local` object.""" return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve()) def getbasetemp(self) -> LEGACY_PATH: - """Backward compat wrapper for ``_tmppath_factory.getbasetemp``.""" + """Same as :meth:`TempPathFactory.getbasetemp`, but returns a :class:`py.path.local` object.""" return legacy_path(self._tmppath_factory.getbasetemp().resolve()) @@ -312,6 +319,11 @@ def tmpdir(tmp_path: Path) -> LEGACY_PATH: The returned object is a `legacy_path`_ object. + .. note:: + These days, it is preferred to use ``tmp_path``. + + :ref:`About the tmpdir and tmpdir_factory fixtures`. + .. _legacy_path: https://py.readthedocs.io/en/latest/path.html """ return legacy_path(tmp_path) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 91d590fb3df..6c400ac9bdf 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -29,21 +29,26 @@ def monkeypatch() -> Generator["MonkeyPatch", None, None]: """A convenient fixture for monkey-patching. - The fixture provides these methods to modify objects, dictionaries or - os.environ:: - - monkeypatch.setattr(obj, name, value, raising=True) - monkeypatch.delattr(obj, name, raising=True) - monkeypatch.setitem(mapping, name, value) - monkeypatch.delitem(obj, name, raising=True) - monkeypatch.setenv(name, value, prepend=None) - monkeypatch.delenv(name, raising=True) - monkeypatch.syspath_prepend(path) - monkeypatch.chdir(path) + The fixture provides these methods to modify objects, dictionaries, or + :data:`os.environ`: + + * :meth:`monkeypatch.setattr(obj, name, value, raising=True) ` + * :meth:`monkeypatch.delattr(obj, name, raising=True) ` + * :meth:`monkeypatch.setitem(mapping, name, value) ` + * :meth:`monkeypatch.delitem(obj, name, raising=True) ` + * :meth:`monkeypatch.setenv(name, value, prepend=None) ` + * :meth:`monkeypatch.delenv(name, raising=True) ` + * :meth:`monkeypatch.syspath_prepend(path) ` + * :meth:`monkeypatch.chdir(path) ` + * :meth:`monkeypatch.context() ` All modifications will be undone after the requesting test function or - fixture has finished. The ``raising`` parameter determines if a KeyError - or AttributeError will be raised if the set/deletion operation has no target. + fixture has finished. The ``raising`` parameter determines if a :class:`KeyError` + or :class:`AttributeError` will be raised if the set/deletion operation does not have the + specified target. + + To undo modifications done by the fixture in a contained scope, + use :meth:`context() `. """ mpatch = MonkeyPatch() yield mpatch @@ -182,16 +187,40 @@ def setattr( value: object = notset, raising: bool = True, ) -> None: - """Set attribute value on target, memorizing the old value. + """ + Set attribute value on target, memorizing the old value. + + For example: + + .. code-block:: python + + import os + + monkeypatch.setattr(os, "getcwd", lambda: "/") + + The code above replaces the :func:`os.getcwd` function by a ``lambda`` which + always returns ``"/"``. - For convenience you can specify a string as ``target`` which + For convenience, you can specify a string as ``target`` which will be interpreted as a dotted import path, with the last part - being the attribute name. For example, - ``monkeypatch.setattr("os.getcwd", lambda: "/")`` - would set the ``getcwd`` function of the ``os`` module. + being the attribute name: - Raises AttributeError if the attribute does not exist, unless + .. code-block:: python + + monkeypatch.setattr("os.getcwd", lambda: "/") + + Raises :class:`AttributeError` if the attribute does not exist, unless ``raising`` is set to False. + + **Where to patch** + + ``monkeypatch.setattr`` works by (temporarily) changing the object that a name points to with another one. + There can be many names pointing to any individual object, so for patching to work you must ensure + that you patch the name used by the system under test. + + See the section :ref:`Where to patch ` in the :mod:`unittest.mock` + docs for a complete explanation, which is meant for :func:`unittest.mock.patch` but + applies to ``monkeypatch.setattr`` as well. """ __tracebackhide__ = True import inspect @@ -353,11 +382,14 @@ def undo(self) -> None: There is generally no need to call `undo()`, since it is called automatically during tear-down. - Note that the same `monkeypatch` fixture is used across a - single test function invocation. If `monkeypatch` is used both by - the test function itself and one of the test fixtures, - calling `undo()` will undo all of the changes made in - both functions. + .. note:: + The same `monkeypatch` fixture is used across a + single test function invocation. If `monkeypatch` is used both by + the test function itself and one of the test fixtures, + calling `undo()` will undo all of the changes made in + both functions. + + Prefer to use :meth:`context() ` instead. """ for obj, name, value in reversed(self._setattr): if value is not notset: diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 5fa21961924..724e71aa5c2 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -133,9 +133,11 @@ def _check_type(self) -> None: # raise if there are any non-numeric elements in the sequence. -def _recursive_list_map(f, x): - if isinstance(x, list): - return [_recursive_list_map(f, xi) for xi in x] +def _recursive_sequence_map(f, x): + """Recursively map a function over a sequence of arbitary depth""" + if isinstance(x, (list, tuple)): + seq_type = type(x) + return seq_type(_recursive_sequence_map(f, xi) for xi in x) else: return f(x) @@ -144,7 +146,9 @@ class ApproxNumpy(ApproxBase): """Perform approximate comparisons where the expected value is numpy array.""" def __repr__(self) -> str: - list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist()) + list_scalars = _recursive_sequence_map( + self._approx_scalar, self.expected.tolist() + ) return f"approx({list_scalars!r})" def _repr_compare(self, other_side: "ndarray") -> List[str]: @@ -164,7 +168,7 @@ def get_value_from_nested_list( return value np_array_shape = self.expected.shape - approx_side_as_list = _recursive_list_map( + approx_side_as_seq = _recursive_sequence_map( self._approx_scalar, self.expected.tolist() ) @@ -179,7 +183,7 @@ def get_value_from_nested_list( max_rel_diff = -math.inf different_ids = [] for index in itertools.product(*(range(i) for i in np_array_shape)): - approx_value = get_value_from_nested_list(approx_side_as_list, index) + approx_value = get_value_from_nested_list(approx_side_as_seq, index) other_value = get_value_from_nested_list(other_side, index) if approx_value != other_value: abs_diff = abs(approx_value.expected - other_value) @@ -194,7 +198,7 @@ def get_value_from_nested_list( ( str(index), str(get_value_from_nested_list(other_side, index)), - str(get_value_from_nested_list(approx_side_as_list, index)), + str(get_value_from_nested_list(approx_side_as_seq, index)), ) for index in different_ids ] @@ -326,7 +330,7 @@ def _repr_compare(self, other_side: Sequence[float]) -> List[str]: f"Lengths: {len(self.expected)} and {len(other_side)}", ] - approx_side_as_map = _recursive_list_map(self._approx_scalar, self.expected) + approx_side_as_map = _recursive_sequence_map(self._approx_scalar, self.expected) number_of_elements = len(approx_side_as_map) max_abs_diff = -math.inf diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 175b571a80c..410569c1693 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -158,7 +158,14 @@ def warns( class WarningsRecorder(warnings.catch_warnings): """A context manager to record raised warnings. + Each recorded warning is an instance of :class:`warnings.WarningMessage`. + Adapted from `warnings.catch_warnings`. + + .. note:: + ``DeprecationWarning`` and ``PendingDeprecationWarning`` are treated + differently; see :ref:`ensuring_function_triggers`. + """ def __init__(self, *, _ispytest: bool = False) -> None: diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 725fdf61739..3c58321e706 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -455,7 +455,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]: def serialize_repr_entry( entry: Union[ReprEntry, ReprEntryNative] ) -> Dict[str, Any]: - data = attr.asdict(entry) + data = attr.asdict(entry) # type:ignore[arg-type] for key, value in data.items(): if hasattr(value, "__dict__"): data[key] = attr.asdict(value) @@ -463,7 +463,7 @@ def serialize_repr_entry( return entry_data def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]: - result = attr.asdict(reprtraceback) + result = attr.asdict(reprtraceback) # type:ignore[arg-type] result["reprentries"] = [ serialize_repr_entry(x) for x in reprtraceback.reprentries ] @@ -473,7 +473,7 @@ def serialize_repr_crash( reprcrash: Optional[ReprFileLocation], ) -> Optional[Dict[str, Any]]: if reprcrash is not None: - return attr.asdict(reprcrash) + return attr.asdict(reprcrash) # type:ignore[arg-type] else: return None diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 851e4943b23..c2df986530c 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -316,7 +316,10 @@ def runtest(self) -> None: # Arguably we could always postpone tearDown(), but this changes the moment where the # TestCase instance interacts with the results object, so better to only do it # when absolutely needed. - if self.config.getoption("usepdb") and not _is_skipped(self.obj): + # We need to consider if the test itself is skipped, or the whole class. + assert isinstance(self.parent, UnitTestCase) + skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj) + if self.config.getoption("usepdb") and not skipped: self._explicit_tearDown = self._testcase.tearDown setattr(self._testcase, "tearDown", lambda *args: None) diff --git a/testing/python/approx.py b/testing/python/approx.py index 7b4fbad156e..6acb466ffb1 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -2,12 +2,14 @@ from contextlib import contextmanager from decimal import Decimal from fractions import Fraction +from math import sqrt from operator import eq from operator import ne from typing import Optional import pytest from _pytest.pytester import Pytester +from _pytest.python_api import _recursive_sequence_map from pytest import approx inf, nan = float("inf"), float("nan") @@ -133,6 +135,18 @@ def test_error_messages_native_dtypes(self, assert_approx_raises_regex): ], ) + assert_approx_raises_regex( + (1, 2.2, 4), + (1, 3.2, 4), + [ + r" comparison failed. Mismatched elements: 1 / 3:", + rf" Max absolute difference: {SOME_FLOAT}", + rf" Max relative difference: {SOME_FLOAT}", + r" Index \| Obtained\s+\| Expected ", + rf" 1 \| {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}", + ], + ) + # Specific test for comparison with 0.0 (relative diff will be 'inf') assert_approx_raises_regex( [0.0], @@ -878,3 +892,31 @@ def test_allow_ordered_sequences_only(self) -> None: """pytest.approx() should raise an error on unordered sequences (#9692).""" with pytest.raises(TypeError, match="only supports ordered sequences"): assert {1, 2, 3} == approx({1, 2, 3}) + + +class TestRecursiveSequenceMap: + def test_map_over_scalar(self): + assert _recursive_sequence_map(sqrt, 16) == 4 + + def test_map_over_empty_list(self): + assert _recursive_sequence_map(sqrt, []) == [] + + def test_map_over_list(self): + assert _recursive_sequence_map(sqrt, [4, 16, 25, 676]) == [2, 4, 5, 26] + + def test_map_over_tuple(self): + assert _recursive_sequence_map(sqrt, (4, 16, 25, 676)) == (2, 4, 5, 26) + + def test_map_over_nested_lists(self): + assert _recursive_sequence_map(sqrt, [4, [25, 64], [[49]]]) == [ + 2, + [5, 8], + [[7]], + ] + + def test_map_over_mixed_sequence(self): + assert _recursive_sequence_map(sqrt, [4, (25, 64), [(49)]]) == [ + 2, + (5, 8), + [(7)], + ] diff --git a/testing/python/raises.py b/testing/python/raises.py index 2d62e91091b..45e22870ebc 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -82,13 +82,9 @@ def test_raise_wrong_exception_passes_by(): def test_does_not_raise(self, pytester: Pytester) -> None: pytester.makepyfile( """ - from contextlib import contextmanager + from contextlib import nullcontext as does_not_raise import pytest - @contextmanager - def does_not_raise(): - yield - @pytest.mark.parametrize('example_input,expectation', [ (3, does_not_raise()), (2, does_not_raise()), @@ -107,13 +103,9 @@ def test_division(example_input, expectation): def test_does_not_raise_does_raise(self, pytester: Pytester) -> None: pytester.makepyfile( """ - from contextlib import contextmanager + from contextlib import nullcontext as does_not_raise import pytest - @contextmanager - def does_not_raise(): - yield - @pytest.mark.parametrize('example_input,expectation', [ (0, does_not_raise()), (1, pytest.raises(ZeroDivisionError)), diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index a515bbae96b..3c98392ed98 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1009,7 +1009,7 @@ def test_meta_path(): ) assert pytester.runpytest().ret == 0 - def test_write_pyc(self, pytester: Pytester, tmp_path, monkeypatch) -> None: + def test_write_pyc(self, pytester: Pytester, tmp_path) -> None: from _pytest.assertion.rewrite import _write_pyc from _pytest.assertion import AssertionState @@ -1021,27 +1021,8 @@ def test_write_pyc(self, pytester: Pytester, tmp_path, monkeypatch) -> None: co = compile("1", "f.py", "single") assert _write_pyc(state, co, os.stat(source_path), pycpath) - if sys.platform == "win32": - from contextlib import contextmanager - - @contextmanager - def atomic_write_failed(fn, mode="r", overwrite=False): - e = OSError() - e.errno = 10 - raise e - yield - - monkeypatch.setattr( - _pytest.assertion.rewrite, "atomic_write", atomic_write_failed - ) - else: - - def raise_oserror(*args): - raise OSError() - - monkeypatch.setattr("os.rename", raise_oserror) - - assert not _write_pyc(state, co, os.stat(source_path), pycpath) + with mock.patch.object(os, "replace", side_effect=OSError): + assert not _write_pyc(state, co, os.stat(source_path), pycpath) def test_resources_provider_for_loader(self, pytester: Pytester) -> None: """ diff --git a/testing/test_config.py b/testing/test_config.py index 6784809e097..3653ce47fa3 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -837,6 +837,9 @@ def test_confcutdir_check_isdir(self, pytester: Pytester) -> None: (["src/bar/__init__.py"], ["bar"]), (["src/bar/__init__.py", "setup.py"], ["bar"]), (["source/python/bar/__init__.py", "setup.py"], ["bar"]), + # editable installation finder modules + (["__editable___xyz_finder.py"], []), + (["bar/__init__.py", "__editable___xyz_finder.py"], ["bar"]), ], ) def test_iter_rewritable_modules(self, names, expected) -> None: diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 68048204581..d2bf860c6fe 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -553,7 +553,7 @@ def test_no_conftest(fxtr): ) ) print("created directory structure:") - for x in pytester.path.rglob(""): + for x in pytester.path.glob("**/"): print(" " + str(x.relative_to(pytester.path))) return {"runner": runner, "package": package, "swc": swc, "snc": snc} diff --git a/testing/test_debugging.py b/testing/test_debugging.py index a95b542adec..9298726a561 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -353,6 +353,7 @@ def test_pdb_prevent_ConftestImportFailure_hiding_exception( result = pytester.runpytest_subprocess("--pdb", ".") result.stdout.fnmatch_lines(["-> import unknown"]) + @pytest.mark.xfail(reason="#10042") def test_pdb_interaction_capturing_simple(self, pytester: Pytester) -> None: p1 = pytester.makepyfile( """ @@ -521,6 +522,7 @@ def function_1(): assert "BdbQuit" not in rest assert "UNEXPECTED EXCEPTION" not in rest + @pytest.mark.xfail(reason="#10042") def test_pdb_interaction_capturing_twice(self, pytester: Pytester) -> None: p1 = pytester.makepyfile( """ @@ -556,6 +558,7 @@ def test_1(): assert "1 failed" in rest self.flush(child) + @pytest.mark.xfail(reason="#10042") def test_pdb_with_injected_do_debug(self, pytester: Pytester) -> None: """Simulates pdbpp, which injects Pdb into do_debug, and uses self.__class__ in do_continue. @@ -1000,6 +1003,7 @@ def test_1(): assert "reading from stdin while output" not in rest TestPDB.flush(child) + @pytest.mark.xfail(reason="#10042") def test_pdb_not_altered(self, pytester: Pytester) -> None: p1 = pytester.makepyfile( """ @@ -1159,6 +1163,7 @@ def test_2(): @pytest.mark.parametrize("fixture", ("capfd", "capsys")) +@pytest.mark.xfail(reason="#10042") def test_pdb_suspends_fixture_capturing(pytester: Pytester, fixture: str) -> None: """Using "-s" with pytest should suspend/resume fixture capturing.""" p1 = pytester.makepyfile( diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 1b4809240c0..2f73feb8c4b 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -113,6 +113,28 @@ def test_simple_doctestfile(self, pytester: Pytester): reprec = pytester.inline_run(p) reprec.assertoutcome(failed=1) + def test_importmode(self, pytester: Pytester): + p = pytester.makepyfile( + **{ + "namespacepkg/innerpkg/__init__.py": "", + "namespacepkg/innerpkg/a.py": """ + def some_func(): + return 42 + """, + "namespacepkg/innerpkg/b.py": """ + from namespacepkg.innerpkg.a import some_func + def my_func(): + ''' + >>> my_func() + 42 + ''' + return some_func() + """, + } + ) + reprec = pytester.inline_run(p, "--doctest-modules", "--import-mode=importlib") + reprec.assertoutcome(passed=1) + def test_new_pattern(self, pytester: Pytester): p = pytester.maketxtfile( xdoc=""" @@ -201,7 +223,11 @@ def test_doctest_unexpected_exception(self, pytester: Pytester): "Traceback (most recent call last):", ' File "*/doctest.py", line *, in __run', " *", - *((" *^^^^*",) if sys.version_info >= (3, 11) else ()), + *( + (" *^^^^*",) + if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11) + else () + ), ' File "", line 1, in ', "ZeroDivisionError: division by zero", "*/test_doctest_unexpected_exception.txt:2: UnexpectedException", diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 02531e81435..b266c76d922 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1625,6 +1625,28 @@ def test_skip(): snode.assert_attr(message="1 <> 2") +def test_escaped_setup_teardown_error( + pytester: Pytester, run_and_parse: RunAndParse +) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.fixture() + def my_setup(): + raise Exception("error: \033[31mred\033[m") + + def test_esc(my_setup): + pass + """ + ) + _, dom = run_and_parse() + node = dom.find_first_by_tag("testcase") + snode = node.find_first_by_tag("error") + assert "#x1B[31mred#x1B[m" in snode["message"] + assert "#x1B[31mred#x1B[m" in snode.text + + @parametrize_families def test_logging_passing_tests_disabled_does_not_log_test_output( pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str diff --git a/testing/test_main.py b/testing/test_main.py index 2df51bb7bb9..71597626790 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -47,7 +47,7 @@ def pytest_internalerror(excrepr, excinfo): end_lines = ( result.stdout.lines[-4:] - if sys.version_info >= (3, 11) + if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11) else result.stdout.lines[-3:] ) @@ -57,7 +57,7 @@ def pytest_internalerror(excrepr, excinfo): 'INTERNALERROR> raise SystemExit("boom")', *( ("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",) - if sys.version_info >= (3, 11) + if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11) else () ), "INTERNALERROR> SystemExit: boom", @@ -68,7 +68,7 @@ def pytest_internalerror(excrepr, excinfo): 'INTERNALERROR> raise ValueError("boom")', *( ("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",) - if sys.version_info >= (3, 11) + if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11) else () ), "INTERNALERROR> ValueError: boom", diff --git a/testing/test_unittest.py b/testing/test_unittest.py index fb312814542..d917d331a03 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1241,12 +1241,15 @@ def test_2(self): @pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"]) -def test_pdb_teardown_skipped( +def test_pdb_teardown_skipped_for_functions( pytester: Pytester, monkeypatch: MonkeyPatch, mark: str ) -> None: - """With --pdb, setUp and tearDown should not be called for skipped tests.""" + """ + With --pdb, setUp and tearDown should not be called for tests skipped + via a decorator (#7215). + """ tracked: List[str] = [] - monkeypatch.setattr(pytest, "test_pdb_teardown_skipped", tracked, raising=False) + monkeypatch.setattr(pytest, "track_pdb_teardown_skipped", tracked, raising=False) pytester.makepyfile( """ @@ -1256,10 +1259,10 @@ def test_pdb_teardown_skipped( class MyTestCase(unittest.TestCase): def setUp(self): - pytest.test_pdb_teardown_skipped.append("setUp:" + self.id()) + pytest.track_pdb_teardown_skipped.append("setUp:" + self.id()) def tearDown(self): - pytest.test_pdb_teardown_skipped.append("tearDown:" + self.id()) + pytest.track_pdb_teardown_skipped.append("tearDown:" + self.id()) {mark}("skipped for reasons") def test_1(self): @@ -1274,6 +1277,43 @@ def test_1(self): assert tracked == [] +@pytest.mark.parametrize("mark", ["@unittest.skip", "@pytest.mark.skip"]) +def test_pdb_teardown_skipped_for_classes( + pytester: Pytester, monkeypatch: MonkeyPatch, mark: str +) -> None: + """ + With --pdb, setUp and tearDown should not be called for tests skipped + via a decorator on the class (#10060). + """ + tracked: List[str] = [] + monkeypatch.setattr(pytest, "track_pdb_teardown_skipped", tracked, raising=False) + + pytester.makepyfile( + """ + import unittest + import pytest + + {mark}("skipped for reasons") + class MyTestCase(unittest.TestCase): + + def setUp(self): + pytest.track_pdb_teardown_skipped.append("setUp:" + self.id()) + + def tearDown(self): + pytest.track_pdb_teardown_skipped.append("tearDown:" + self.id()) + + def test_1(self): + pass + + """.format( + mark=mark + ) + ) + result = pytester.runpytest_inprocess("--pdb") + result.stdout.fnmatch_lines("* 1 skipped in *") + assert tracked == [] + + def test_async_support(pytester: Pytester) -> None: pytest.importorskip("unittest.async_case")