diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 252809946..ca401f092 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -13,39 +13,53 @@ on: permissions: {} jobs: - - deploy: - if: github.repository == 'pytest-dev/pytest' - + build: runs-on: ubuntu-latest - timeout-minutes: 30 - permissions: - contents: write - + timeout-minutes: 10 steps: - uses: actions/checkout@v3 with: fetch-depth: 0 persist-credentials: false - - name: Build and Check Package uses: hynek/build-and-inspect-python-package@v1.5 + deploy: + if: github.repository == 'pytest-dev/pytest' + needs: [build] + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + id-token: write + steps: - name: Download Package uses: actions/download-artifact@v3 with: name: Packages path: dist - - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - with: - password: ${{ secrets.pypi_token }} + uses: pypa/gh-action-pypi-publish@v1.8.8 + release-notes: + + # todo: generate the content in the build job + # the goal being of using a github action script to push the release data + # after success instead of creating a complete python/tox env + needs: [deploy] + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + persist-credentials: false - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.11" + - name: Install tox run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cd1ffdbf9..b3f258f1c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,26 +37,26 @@ jobs: fail-fast: false matrix: name: [ - "windows-py37", - "windows-py37-pluggy", "windows-py38", + "windows-py38-pluggy", "windows-py39", "windows-py310", "windows-py311", + "windows-py312", - "ubuntu-py37", - "ubuntu-py37-pluggy", - "ubuntu-py37-freeze", "ubuntu-py38", + "ubuntu-py38-pluggy", + "ubuntu-py38-freeze", "ubuntu-py39", "ubuntu-py310", "ubuntu-py311", + "ubuntu-py312", "ubuntu-pypy3", - "macos-py37", "macos-py38", "macos-py39", "macos-py310", + "macos-py312", "docs", "doctesting", @@ -64,19 +64,15 @@ jobs: ] include: - - name: "windows-py37" - python: "3.7" - os: windows-latest - tox_env: "py37-numpy" - - name: "windows-py37-pluggy" - python: "3.7" - os: windows-latest - tox_env: "py37-pluggymain-pylib-xdist" - name: "windows-py38" python: "3.8" os: windows-latest tox_env: "py38-unittestextras" use_coverage: true + - name: "windows-py38-pluggy" + python: "3.8" + os: windows-latest + tox_env: "py38-pluggymain-pylib-xdist" - name: "windows-py39" python: "3.9" os: windows-latest @@ -86,27 +82,27 @@ jobs: os: windows-latest tox_env: "py310-xdist" - name: "windows-py311" - python: "3.11-dev" + python: "3.11" os: windows-latest tox_env: "py311" + - name: "windows-py312" + python: "3.12-dev" + os: windows-latest + tox_env: "py312" - - name: "ubuntu-py37" - python: "3.7" - os: ubuntu-latest - tox_env: "py37-lsof-numpy-pexpect" - use_coverage: true - - name: "ubuntu-py37-pluggy" - python: "3.7" - os: ubuntu-latest - tox_env: "py37-pluggymain-pylib-xdist" - - name: "ubuntu-py37-freeze" - python: "3.7" - os: ubuntu-latest - tox_env: "py37-freeze" - name: "ubuntu-py38" python: "3.8" os: ubuntu-latest - tox_env: "py38-xdist" + tox_env: "py38-lsof-numpy-pexpect" + use_coverage: true + - name: "ubuntu-py38-pluggy" + python: "3.8" + os: ubuntu-latest + tox_env: "py38-pluggymain-pylib-xdist" + - name: "ubuntu-py38-freeze" + python: "3.8" + os: ubuntu-latest + tox_env: "py38-freeze" - name: "ubuntu-py39" python: "3.9" os: ubuntu-latest @@ -116,32 +112,37 @@ jobs: os: ubuntu-latest tox_env: "py310-xdist" - name: "ubuntu-py311" - python: "3.11-dev" + python: "3.11" os: ubuntu-latest tox_env: "py311" use_coverage: true + - name: "ubuntu-py312" + python: "3.12-dev" + os: ubuntu-latest + tox_env: "py312" + use_coverage: true - name: "ubuntu-pypy3" - python: "pypy-3.7" + python: "pypy-3.8" os: ubuntu-latest tox_env: "pypy3-xdist" - - name: "macos-py37" - python: "3.7" - os: macos-latest - tox_env: "py37-xdist" - name: "macos-py38" python: "3.8" os: macos-latest tox_env: "py38-xdist" - use_coverage: true - name: "macos-py39" python: "3.9" os: macos-latest tox_env: "py39-xdist" + use_coverage: true - name: "macos-py310" python: "3.10" os: macos-latest tox_env: "py310-xdist" + - name: "macos-py312" + python: "3.12-dev" + os: macos-latest + tox_env: "py312-xdist" - name: "plugins" python: "3.9" @@ -149,11 +150,11 @@ jobs: tox_env: "plugins" - name: "docs" - python: "3.7" + python: "3.8" os: ubuntu-latest tox_env: "docs" - name: "doctesting" - python: "3.7" + python: "3.8" os: ubuntu-latest tox_env: "doctesting" use_coverage: true @@ -168,6 +169,7 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} + check-latest: ${{ endsWith(matrix.python, '-dev') }} - name: Install dependencies run: | diff --git a/.github/workflows/update-plugin-list.yml b/.github/workflows/update-plugin-list.yml index ef92fef88..e7cbc4e96 100644 --- a/.github/workflows/update-plugin-list.yml +++ b/.github/workflows/update-plugin-list.yml @@ -38,7 +38,7 @@ jobs: run: python scripts/update-plugin-list.py - name: Create Pull Request - uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 + uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38 with: commit-message: '[automated] Update plugin list' author: 'pytest bot ' diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af6cd2623..4bed87035 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,14 +1,14 @@ repos: - repo: https://github.com/psf/black - rev: 23.3.0 + rev: 23.7.0 hooks: - id: black args: [--safe, --quiet] - repo: https://github.com/asottile/blacken-docs - rev: 1.13.0 + rev: 1.15.0 hooks: - id: blacken-docs - additional_dependencies: [black==23.1.0] + additional_dependencies: [black==23.7.0] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.4.0 hooks: @@ -21,7 +21,7 @@ repos: exclude: _pytest/(debugging|hookspec).py language_version: python3 - repo: https://github.com/PyCQA/autoflake - rev: v2.1.1 + rev: v2.2.0 hooks: - id: autoflake name: autoflake @@ -37,26 +37,26 @@ repos: - flake8-typing-imports==1.12.0 - flake8-docstrings==1.5.0 - repo: https://github.com/asottile/reorder-python-imports - rev: v3.9.0 + rev: v3.10.0 hooks: - id: reorder-python-imports - args: ['--application-directories=.:src', --py37-plus] + args: ['--application-directories=.:src', --py38-plus] - repo: https://github.com/asottile/pyupgrade - rev: v3.4.0 + rev: v3.9.0 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/asottile/setup-cfg-fmt - rev: v2.2.0 + rev: v2.4.0 hooks: - id: setup-cfg-fmt - args: ["--max-py-version=3.11", "--include-version-classifiers"] + args: ["--max-py-version=3.12", "--include-version-classifiers"] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.10.0 hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.3.0 + rev: v1.4.1 hooks: - id: mypy files: ^(src/|testing/) diff --git a/AUTHORS b/AUTHORS index 090f709c0..4c42de21f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,6 +11,7 @@ Adam Johnson Adam Stewart Adam Uhlir Ahn Ki-Wook +Akhilesh Ramakrishnan Akiomi Kamakura Alan Velasco Alessio Izzo @@ -130,6 +131,7 @@ Eric Hunsberger Eric Liu Eric Siegerman Erik Aronesty +Erik Hasse Erik M. Bray Evan Kepner Evgeny Seliverstov @@ -167,6 +169,7 @@ Ian Bicking Ian Lesperance Ilya Konstantinov Ionuț Turturică +Isaac Virshup Itxaso Aizpurua Iwan Briquemont Jaap Broekhuizen @@ -262,6 +265,7 @@ Mickey Pashov Mihai Capotă Mike Hoyle (hoylemd) Mike Lundy +Milan Lesnek Miro Hrončok Nathaniel Compton Nathaniel Waisbrot @@ -311,6 +315,7 @@ Raphael Pierzina Rafal Semik Raquel Alegre Ravi Chandra +Reagan Lee Robert Holt Roberto Aldera Roberto Polli @@ -371,6 +376,7 @@ Tomer Keren Tony Narlock Tor Colvin Trevor Bekolay +Tushar Sadhwani Tyler Goodlet Tzu-ping Chung Vasily Kuznetsov @@ -378,6 +384,7 @@ Victor Maryama Victor Rodriguez Victor Uriarte Vidar T. Fauske +Vijay Arora Virgil Dupras Vitaly Lashmanov Vivaan Verma diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 791f98830..0f6d54351 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -201,7 +201,7 @@ Short version #. Follow **PEP-8** for naming and `black `_ for formatting. #. Tests are run using ``tox``:: - tox -e linting,py37 + tox -e linting,py39 The test environments above are usually enough to cover most cases locally. @@ -272,24 +272,24 @@ Here is a simple overview, with pytest-specific bits: #. Run all the tests - You need to have Python 3.7 available in your system. Now + You need to have Python 3.8 or later available in your system. Now running tests is as simple as issuing this command:: - $ tox -e linting,py37 + $ tox -e linting,py39 - This command will run tests via the "tox" tool against Python 3.7 + This command will run tests via the "tox" tool against Python 3.9 and also perform "lint" coding-style checks. #. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming. - You can pass different options to ``tox``. For example, to run tests on Python 3.7 and pass options to pytest + You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest (e.g. enter pdb on failure) to pytest you can do:: - $ tox -e py37 -- --pdb + $ tox -e py39 -- --pdb - Or to only run tests in a particular test module on Python 3.7:: + Or to only run tests in a particular test module on Python 3.9:: - $ tox -e py37 -- testing/test_config.py + $ tox -e py39 -- testing/test_config.py When committing, ``pre-commit`` will re-format the files if necessary. diff --git a/README.rst b/README.rst index 034034a40..e6bb6d4cf 100644 --- a/README.rst +++ b/README.rst @@ -100,7 +100,7 @@ Features - Can run `unittest `_ (or trial), `nose `_ test suites out of the box -- Python 3.7+ or PyPy3 +- Python 3.8+ or PyPy3 - Rich plugin architecture, with over 850+ `external plugins `_ and thriving community diff --git a/changelog/10169.bugfix.rst b/changelog/10169.bugfix.rst deleted file mode 100644 index cbf3516a9..000000000 --- a/changelog/10169.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems. diff --git a/changelog/10337.bugfix.rst b/changelog/10337.bugfix.rst new file mode 100644 index 000000000..629449e8d --- /dev/null +++ b/changelog/10337.bugfix.rst @@ -0,0 +1,2 @@ +Fixed that fake intermediate modules generated by ``--import-mode=importlib`` would not include the +child modules as attributes of the parent modules. diff --git a/changelog/10447.bugfix.rst b/changelog/10447.bugfix.rst new file mode 100644 index 000000000..fff94b28f --- /dev/null +++ b/changelog/10447.bugfix.rst @@ -0,0 +1,2 @@ +markers are now considered in the reverse mro order to ensure base class markers are considered first +this resolves a regression. diff --git a/changelog/10701.bugfix.rst b/changelog/10701.bugfix.rst new file mode 100644 index 000000000..f33fa7fb2 --- /dev/null +++ b/changelog/10701.bugfix.rst @@ -0,0 +1,2 @@ +:meth:`pytest.WarningsRecorder.pop` will return the most-closely-matched warning in the list, +rather than the first warning which is an instance of the requested type. diff --git a/changelog/10702.bugfix.rst b/changelog/10702.bugfix.rst new file mode 100644 index 000000000..4008cc882 --- /dev/null +++ b/changelog/10702.bugfix.rst @@ -0,0 +1 @@ +Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries. diff --git a/changelog/10811.bugfix.rst b/changelog/10811.bugfix.rst new file mode 100644 index 000000000..aa26414e4 --- /dev/null +++ b/changelog/10811.bugfix.rst @@ -0,0 +1,2 @@ +Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules +to be imported more than once, causing problems with modules that have import side effects. diff --git a/changelog/10831.bugfix.rst b/changelog/10831.bugfix.rst deleted file mode 100644 index ea641dee6..000000000 --- a/changelog/10831.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``. diff --git a/changelog/10872.improvement.rst b/changelog/10872.improvement.rst deleted file mode 100644 index fe0c01a02..000000000 --- a/changelog/10872.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook. diff --git a/changelog/10901.feature.rst b/changelog/10901.feature.rst deleted file mode 100644 index 0d99d66f6..000000000 --- a/changelog/10901.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Added :func:`ExceptionInfo.from_exception() `, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception. -This can replace :func:`ExceptionInfo.from_exc_info() ` for most uses. diff --git a/changelog/10907.improvement.rst b/changelog/10907.improvement.rst deleted file mode 100644 index 7f011a827..000000000 --- a/changelog/10907.improvement.rst +++ /dev/null @@ -1,5 +0,0 @@ -When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown: - -"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.". - -Previously, the last frame of the traceback was shown, even though it was hidden. diff --git a/changelog/10940.improvement.rst b/changelog/10940.improvement.rst deleted file mode 100644 index f3e33cbc6..000000000 --- a/changelog/10940.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output. - -Added :func:`TerminalReporter.wrap_write() ` as a helper for that. diff --git a/changelog/10987.bugfix.rst b/changelog/10987.bugfix.rst deleted file mode 100644 index 2aafff5f5..000000000 --- a/changelog/10987.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -:confval:`testpaths` is now honored to load root ``conftests``. diff --git a/changelog/10991.improvement.rst b/changelog/10991.improvement.rst deleted file mode 100644 index 768c08e55..000000000 --- a/changelog/10991.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``. diff --git a/changelog/10999.bugfix.rst b/changelog/10999.bugfix.rst deleted file mode 100644 index 08c68da01..000000000 --- a/changelog/10999.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments. diff --git a/changelog/11005.improvement.rst b/changelog/11005.improvement.rst deleted file mode 100644 index 295252514..000000000 --- a/changelog/11005.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Added underlying exception to cache provider path creation and write warning messages. diff --git a/changelog/11011.doc.rst b/changelog/11011.doc.rst new file mode 100644 index 000000000..5faabba9c --- /dev/null +++ b/changelog/11011.doc.rst @@ -0,0 +1 @@ +Added a warning about modifying the root logger during tests when using ``caplog``. diff --git a/changelog/11013.improvement.rst b/changelog/11013.improvement.rst deleted file mode 100644 index fe3ece93c..000000000 --- a/changelog/11013.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory. diff --git a/changelog/11028.bugfix.rst b/changelog/11028.bugfix.rst deleted file mode 100644 index 9efc04ba6..000000000 --- a/changelog/11028.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call. diff --git a/changelog/11031.trivial.rst b/changelog/11031.trivial.rst deleted file mode 100644 index fad2cf24d..000000000 --- a/changelog/11031.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file. diff --git a/changelog/11043.improvement.rst b/changelog/11043.improvement.rst deleted file mode 100644 index 1fe0361d7..000000000 --- a/changelog/11043.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir`. -Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem. -If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`. diff --git a/changelog/11054.bugfix.rst b/changelog/11054.bugfix.rst deleted file mode 100644 index a8ee04fe3..000000000 --- a/changelog/11054.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files). diff --git a/changelog/11122.improvement.rst b/changelog/11122.improvement.rst new file mode 100644 index 000000000..dedaa7d08 --- /dev/null +++ b/changelog/11122.improvement.rst @@ -0,0 +1,6 @@ +``pluggy>=1.2.0`` is now required. + +pytest now uses "new-style" hook wrappers internally, available since pluggy 1.2.0. +See `pluggy's 1.2.0 changelog `_ and the :ref:`updated docs ` for details. + +Plugins which want to use new-style wrappers can do so if they require this version of pytest or later. diff --git a/changelog/11146.bugfix.rst b/changelog/11146.bugfix.rst new file mode 100644 index 000000000..03b468f30 --- /dev/null +++ b/changelog/11146.bugfix.rst @@ -0,0 +1 @@ +- Prevent constants at the top of file from being detected as docstrings. diff --git a/changelog/11151.breaking.rst b/changelog/11151.breaking.rst new file mode 100644 index 000000000..2e86c5dfb --- /dev/null +++ b/changelog/11151.breaking.rst @@ -0,0 +1,2 @@ +Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27 +`__. diff --git a/changelog/11208.trivial.rst b/changelog/11208.trivial.rst new file mode 100644 index 000000000..fced57b20 --- /dev/null +++ b/changelog/11208.trivial.rst @@ -0,0 +1,2 @@ +The (internal) ``FixtureDef.cached_result`` type has changed. +Now the third item ``cached_result[2]``, when set, is an exception instance instead of an exception triplet. diff --git a/changelog/11216.improvement.rst b/changelog/11216.improvement.rst new file mode 100644 index 000000000..80761de5c --- /dev/null +++ b/changelog/11216.improvement.rst @@ -0,0 +1 @@ +If a test is skipped from inside an :ref:`xunit setup fixture `, the test summary now shows the test location instead of the fixture location. diff --git a/changelog/11227.improvement.rst b/changelog/11227.improvement.rst new file mode 100644 index 000000000..3c6748c3d --- /dev/null +++ b/changelog/11227.improvement.rst @@ -0,0 +1 @@ +Allow :func:`pytest.raises` ``match`` argument to match against `PEP-678 ` ``__notes__``. diff --git a/changelog/1904.bugfix.rst b/changelog/1904.bugfix.rst deleted file mode 100644 index 3e1a29215..000000000 --- a/changelog/1904.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message). diff --git a/changelog/3664.deprecation.rst b/changelog/3664.deprecation.rst new file mode 100644 index 000000000..0a00e26c1 --- /dev/null +++ b/changelog/3664.deprecation.rst @@ -0,0 +1,3 @@ +Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work. + +This will become an error in the future. diff --git a/changelog/7363.breaking.rst b/changelog/7363.breaking.rst new file mode 100644 index 000000000..93d87b1b1 --- /dev/null +++ b/changelog/7363.breaking.rst @@ -0,0 +1,22 @@ +**PytestRemovedIn8Warning deprecation warnings are now errors by default.** + +Following our plan to remove deprecated features with as little disruption as +possible, all warnings of type ``PytestRemovedIn8Warning`` now generate errors +instead of warning messages by default. + +**The affected features will be effectively removed in pytest 8.1**, so please consult the +:ref:`deprecations` section in the docs for directions on how to update existing code. + +In the pytest ``8.0.X`` series, it is possible to change the errors back into warnings as a +stopgap measure by adding this to your ``pytest.ini`` file: + +.. code-block:: ini + + [pytest] + filterwarnings = + ignore::pytest.PytestRemovedIn8Warning + +But this will stop working when pytest ``8.1`` is released. + +**If you have concerns** about the removal of a specific feature, please add a +comment to :issue:`7363`. diff --git a/changelog/7469.feature.rst b/changelog/7469.feature.rst new file mode 100644 index 000000000..8e9df7269 --- /dev/null +++ b/changelog/7469.feature.rst @@ -0,0 +1 @@ +:class:`~pytest.FixtureDef` is now exported as ``pytest.FixtureDef`` for typing purposes. diff --git a/changelog/8711.improvement.rst b/changelog/8711.improvement.rst deleted file mode 100644 index 20805e9f9..000000000 --- a/changelog/8711.improvement.rst +++ /dev/null @@ -1,3 +0,0 @@ -:func:`_pytest.logging.LogCaptureFixture.set_level` and :func:`_pytest.logging.LogCaptureFixture.at_level` -will temporarily enable the requested ``level`` if ``level`` was disabled globally via -``logging.disable(LEVEL)``. diff --git a/changelog/8976.breaking.rst b/changelog/8976.breaking.rst new file mode 100644 index 000000000..bd9a63982 --- /dev/null +++ b/changelog/8976.breaking.rst @@ -0,0 +1,5 @@ +Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only. +Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself +(unless :confval:`python_files` was changed to allow `__init__.py` file). + +To collect the entire package, specify just the directory: `pytest pkg`. diff --git a/changelog/9036.bugfix.rst b/changelog/9036.bugfix.rst new file mode 100644 index 000000000..4f25f82e2 --- /dev/null +++ b/changelog/9036.bugfix.rst @@ -0,0 +1 @@ +``pytest.warns`` and similar functions now capture warnings when an exception is raised inside a ``with`` block. diff --git a/changelog/9288.breaking.rst b/changelog/9288.breaking.rst new file mode 100644 index 000000000..053af8013 --- /dev/null +++ b/changelog/9288.breaking.rst @@ -0,0 +1,7 @@ +:func:`pytest.warns ` now re-emits unmatched warnings when the context +closes -- previously it would consume all warnings, hiding those that were not +matched by the function. + +While this is a new feature, we decided to announce this as a breaking change +because many test suites are configured to error-out on warnings, and will +therefore fail on the newly-re-emitted warnings. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index e3919f88e..914e763bd 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,8 @@ Release announcements :maxdepth: 2 + release-7.4.0 + release-7.3.2 release-7.3.1 release-7.3.0 release-7.2.2 diff --git a/doc/en/announce/release-7.3.2.rst b/doc/en/announce/release-7.3.2.rst new file mode 100644 index 000000000..b3b112f0d --- /dev/null +++ b/doc/en/announce/release-7.3.2.rst @@ -0,0 +1,21 @@ +pytest-7.3.2 +======================================= + +pytest 7.3.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Adam J. Stewart +* Alessio Izzo +* Bruno Oliveira +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/announce/release-7.4.0.rst b/doc/en/announce/release-7.4.0.rst new file mode 100644 index 000000000..5a0d18267 --- /dev/null +++ b/doc/en/announce/release-7.4.0.rst @@ -0,0 +1,49 @@ +pytest-7.4.0 +======================================= + +The pytest team is proud to announce the 7.4.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: + +* Adam J. Stewart +* Alessio Izzo +* Alex +* Alex Lambson +* Brian Larsen +* Bruno Oliveira +* Bryan Ricker +* Chris Mahoney +* Facundo Batista +* Florian Bruhin +* Jarrett Keifer +* Kenny Y +* Miro Hrončok +* Ran Benita +* Roberto Aldera +* Ronny Pfannschmidt +* Sergey Kim +* Stefanie Molin +* Vijay Arora +* Ville Skyttä +* Zac Hatfield-Dodds +* bzoracler +* leeyueh +* nondescryptid +* theirix + + +Happy testing, +The pytest Development Team diff --git a/doc/en/backwards-compatibility.rst b/doc/en/backwards-compatibility.rst index ea0c6a71a..4ffb9fe97 100644 --- a/doc/en/backwards-compatibility.rst +++ b/doc/en/backwards-compatibility.rst @@ -87,6 +87,7 @@ Released pytest versions support all Python versions that are actively maintaine ============== =================== pytest version min. Python version ============== =================== +8.0+ 3.8+ 7.1+ 3.7+ 6.2 - 7.0 3.6+ 5.0 - 6.1 3.5+ diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 7e9b51d00..53305eecd 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:510 + cache -- .../_pytest/cacheprovider.py:528 Return a cache object that can persist state between testing sessions. cache.get(key, default) @@ -119,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:1360 + pytestconfig [session scope] -- .../_pytest/fixtures.py:1353 Session-scoped fixture that returns the session's :class:`pytest.Config` object. @@ -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:498 + caplog -- .../_pytest/logging.py:570 Access and control log capturing. Captured logs are available through the following properties/methods:: @@ -207,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a * caplog.record_tuples -> list of (logger_name, level, message) tuples * caplog.clear() -> clear captured records and formatted log output string - monkeypatch -- .../_pytest/monkeypatch.py:29 + monkeypatch -- .../_pytest/monkeypatch.py:30 A convenient fixture for monkey-patching. The fixture provides these methods to modify objects, dictionaries, or diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index 7d6eb1ffa..391721df3 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -28,6 +28,122 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 7.4.0 (2023-06-23) +========================= + +Features +-------- + +- `#10901 `_: Added :func:`ExceptionInfo.from_exception() `, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception. + This can replace :func:`ExceptionInfo.from_exc_info() ` for most uses. + + + +Improvements +------------ + +- `#10872 `_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook. + + +- `#10907 `_: When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown: + + "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.". + + Previously, the last frame of the traceback was shown, even though it was hidden. + + +- `#10940 `_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output. + + Added ``TerminalReporter.wrap_write()`` as a helper for that. + + +- `#10991 `_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``. + + +- `#11005 `_: Added the underlying exception to the cache provider's path creation and write warning messages. + + +- `#11013 `_: Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory. + + +- `#11043 `_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir `. + Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem. + If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`. + + +- `#11081 `_: The :confval:`norecursedirs` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it. + + If after updating to this version you see that your `norecursedirs` setting is not being respected, + it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation. + Most likely, your hook returns `False` for paths it does not want to ignore, + which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path. + The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore. + + +- `#8711 `_: :func:`caplog.set_level() ` and :func:`caplog.at_level() ` + will temporarily enable the requested ``level`` if ``level`` was disabled globally via + ``logging.disable(LEVEL)``. + + + +Bug Fixes +--------- + +- `#10831 `_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``. + + +- `#11068 `_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files `. + + +- `#11104 `_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests, + even when it was not utilized (e.g. when explicit paths were given on the command line). + Now the ``testpaths`` are only considered when they are in use. + + +- `#1904 `_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message). + + +- `#7781 `_: Fix writing non-encodable text to log file when using ``--debug``. + + + +Improved Documentation +---------------------- + +- `#9146 `_: Improved documentation for :func:`caplog.set_level() `. + + + +Trivial/Internal Changes +------------------------ + +- `#11031 `_: Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file. + + +pytest 7.3.2 (2023-06-10) +========================= + +Bug Fixes +--------- + +- `#10169 `_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems. + + +- `#10894 `_: Support for Python 3.12 (beta at the time of writing). + + +- `#10987 `_: :confval:`testpaths` is now honored to load root ``conftests``. + + +- `#10999 `_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments. + + +- `#11028 `_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call. + + +- `#11054 `_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files). + + pytest 7.3.1 (2023-04-14) ========================= diff --git a/doc/en/conf.py b/doc/en/conf.py index 32f508219..92607a15a 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -15,12 +15,10 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -import ast import os import shutil import sys from textwrap import dedent -from typing import List from typing import TYPE_CHECKING from _pytest import __version__ as version @@ -451,25 +449,6 @@ def setup(app: "sphinx.application.Sphinx") -> None: configure_logging(app) - # Make Sphinx mark classes with "final" when decorated with @final. - # We need this because we import final from pytest._compat, not from - # typing (for Python < 3.8 compat), so Sphinx doesn't detect it. - # To keep things simple we accept any `@final` decorator. - # Ref: https://github.com/pytest-dev/pytest/pull/7780 - import sphinx.pycode.ast - import sphinx.pycode.parser - - original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final - - def patched_is_final(self, decorators: List[ast.expr]) -> bool: - if original_is_final(self, decorators): - return True - return any( - sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators - ) - - sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final - # legacypath.py monkey-patches pytest.Testdir in. Import the file so # that autodoc can discover references to it. import _pytest.legacypath # noqa: F401 diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 4f7830a27..e73c1a18e 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -380,6 +380,25 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of ``fspath``, as :ref:`outlined above `), a deprecation warning is now raised. +Applying a mark to a fixture function +------------------------------------- + +.. deprecated:: 7.4 + +Applying a mark to a fixture function never had any effect, but it is a common user error. + +.. code-block:: python + + @pytest.mark.usefixtures("clean_database") + @pytest.fixture + def user() -> User: + ... + +Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all. + +Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions. + + Backward compatibilities in ``Parser.addoption`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -467,12 +486,26 @@ The ``yield_fixture`` function/decorator It has been so for a very long time, so can be search/replaced safely. -Removed Features ----------------- +Removed Features and Breaking Changes +------------------------------------- As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after an appropriate period of deprecation has passed. +Some breaking changes which could not be deprecated are also listed. + + +Collecting ``__init__.py`` files no longer collects package +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 8.0 + +Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only. +Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself +(unless :confval:`python_files` was changed to allow `__init__.py` file). + +To collect the entire package, specify just the directory: `pytest pkg`. + The ``pytest.collect`` module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py index dd1ebe88d..e969e3e25 100644 --- a/doc/en/example/nonpython/conftest.py +++ b/doc/en/example/nonpython/conftest.py @@ -12,7 +12,7 @@ class YamlFile(pytest.File): # We need a yaml parser, e.g. PyYAML. import yaml - raw = yaml.safe_load(self.path.open()) + raw = yaml.safe_load(self.path.open(encoding="utf-8")) for name, spec in sorted(raw.items()): yield YamlItem.from_parent(self, name=name, spec=spec) diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index fa3e68ce9..5648aa383 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -691,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory: pass - @pytest.fixture(scope="session") + @pytest.fixture(scope="package") def db(): return DB() @@ -808,16 +808,15 @@ case we just write some information out to a ``failures`` file: import pytest - @pytest.hookimpl(tryfirst=True, hookwrapper=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object - outcome = yield - rep = outcome.get_result() + rep = yield # we only look at actual failing test calls, not setup/teardown if rep.when == "call" and rep.failed: mode = "a" if os.path.exists("failures") else "w" - with open("failures", mode) as f: + with open("failures", mode, encoding="utf-8") as f: # let's also access a fixture for the fun of it if "tmp_path" in item.fixturenames: extra = " ({})".format(item.funcargs["tmp_path"]) @@ -826,6 +825,8 @@ case we just write some information out to a ``failures`` file: f.write(rep.nodeid + extra + "\n") + return rep + if you then have failing tests: @@ -899,16 +900,17 @@ here is a little example implemented via a local plugin: phase_report_key = StashKey[Dict[str, CollectReport]]() - @pytest.hookimpl(tryfirst=True, hookwrapper=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_makereport(item, call): # execute all other hooks to obtain the report object - outcome = yield - rep = outcome.get_result() + rep = yield # store test results for each phase of a call, which can # be "setup", "call", "teardown" item.stash.setdefault(phase_report_key, {})[rep.when] = rep + return rep + @pytest.fixture def something(request): diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index f41571141..4a9dc4522 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -9,7 +9,7 @@ Get Started Install ``pytest`` ---------------------------------------- -``pytest`` requires: Python 3.7+ or PyPy3. +``pytest`` requires: Python 3.8+ or PyPy3. 1. Run the following command in your command line: @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 7.3.1 + pytest 7.4.0 .. _`simpletest`: diff --git a/doc/en/how-to/failures.rst b/doc/en/how-to/failures.rst index ef8755091..b3d0c155b 100644 --- a/doc/en/how-to/failures.rst +++ b/doc/en/how-to/failures.rst @@ -135,10 +135,6 @@ Warning about unraisable exceptions and unhandled thread exceptions .. versionadded:: 6.2 -.. note:: - - These features only work on Python>=3.8. - Unhandled exceptions are exceptions that are raised in a situation in which they cannot propagate to a caller. The most common case is an exception raised in a :meth:`__del__ ` implementation. diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index d8517c2c8..b2fb24b3f 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker: class TestDirectoryInit: def test_cwd_starts_empty(self): assert os.listdir(os.getcwd()) == [] - with open("myfile", "w") as f: + with open("myfile", "w", encoding="utf-8") as f: f.write("hello") def test_cwd_again_starts_empty(self): @@ -1752,8 +1752,7 @@ into an ini-file: def my_fixture_that_sadly_wont_use_my_other_fixture(): ... - Currently this will not generate any error or warning, but this is intended - to be handled by :issue:`3664`. + This generates a deprecation warning, and will become an error in Pytest 8. .. _`override fixtures`: diff --git a/doc/en/how-to/logging.rst b/doc/en/how-to/logging.rst index 9957a9bb8..b9f522fa4 100644 --- a/doc/en/how-to/logging.rst +++ b/doc/en/how-to/logging.rst @@ -172,6 +172,13 @@ the records for the ``setup`` and ``call`` stages during teardown like so: The full API is available at :class:`pytest.LogCaptureFixture`. +.. warning:: + + The ``caplog`` fixture adds a handler to the root logger to capture logs. If the root logger is + modified during a test, for example with ``logging.config.dictConfig``, this handler may be + removed and cause no logs to be captured. To avoid this, ensure that any root logger configuration + only adds to the existing handlers. + .. _live_logs: diff --git a/doc/en/how-to/nose.rst b/doc/en/how-to/nose.rst index a736dfa55..45d3357cf 100644 --- a/doc/en/how-to/nose.rst +++ b/doc/en/how-to/nose.rst @@ -47,8 +47,7 @@ Unsupported idioms / known issues - nose imports test modules with the same import path (e.g. ``tests.test_mode``) but different file system paths (e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``) - by extending sys.path/import semantics. pytest does not do that - but there is discussion in :issue:`268` for adding some support. Note that + by extending sys.path/import semantics. pytest does not do that. Note that `nose2 choose to avoid this sys.path/import hackery `_. If you place a conftest.py file in the root directory of your project @@ -66,16 +65,34 @@ Unsupported idioms / known issues - no nose-configuration is recognized. -- ``yield``-based methods are unsupported as of pytest 4.1.0. They are +- ``yield``-based methods are fundamentally incompatible with pytest because they don't support fixtures properly since collection and test execution are separated. +Here is a table comparing the default supported naming conventions for both +nose and pytest. + +========= ========================== ======= ===== +what default naming convention pytest nose +========= ========================== ======= ===== +module ``test*.py`` ✅ +module ``test_*.py`` ✅ ✅ +module ``*_test.py`` ✅ +module ``*_tests.py`` +class ``*(unittest.TestCase)`` ✅ ✅ +method ``test_*`` ✅ ✅ +class ``Test*`` ✅ +method ``test_*`` ✅ +function ``test_*`` ✅ +========= ========================== ======= ===== + + Migrating from nose to pytest ------------------------------ `nose2pytest `_ is a Python script and pytest plugin to help convert Nose-based tests into pytest-based tests. -Specifically, the script transforms nose.tools.assert_* function calls into +Specifically, the script transforms ``nose.tools.assert_*`` function calls into raw assert statements, while preserving format of original arguments as much as possible. diff --git a/doc/en/how-to/tmp_path.rst b/doc/en/how-to/tmp_path.rst index 792933dd8..d5573f584 100644 --- a/doc/en/how-to/tmp_path.rst +++ b/doc/en/how-to/tmp_path.rst @@ -24,8 +24,8 @@ created in the `base temporary directory`_. d = tmp_path / "sub" d.mkdir() p = d / "hello.txt" - p.write_text(CONTENT) - assert p.read_text() == CONTENT + p.write_text(CONTENT, encoding="utf-8") + assert p.read_text(encoding="utf-8") == CONTENT assert len(list(tmp_path.iterdir())) == 1 assert 0 diff --git a/doc/en/how-to/unittest.rst b/doc/en/how-to/unittest.rst index 37caf6e9f..7856c1a49 100644 --- a/doc/en/how-to/unittest.rst +++ b/doc/en/how-to/unittest.rst @@ -207,10 +207,10 @@ creation of a per-test temporary directory: @pytest.fixture(autouse=True) def initdir(self, tmp_path, monkeypatch): monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory - tmp_path.joinpath("samplefile.ini").write_text("# testdata") + tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8") def test_method(self): - with open("samplefile.ini") as f: + with open("samplefile.ini", encoding="utf-8") as f: s = f.read() assert "testdata" in s diff --git a/doc/en/how-to/usage.rst b/doc/en/how-to/usage.rst index 8e2dd8673..4ec4459a2 100644 --- a/doc/en/how-to/usage.rst +++ b/doc/en/how-to/usage.rst @@ -173,7 +173,8 @@ You can invoke ``pytest`` from Python code directly: this acts as if you would call "pytest" from the command line. It will not raise :class:`SystemExit` but return the :ref:`exit code ` instead. -You can pass in options and arguments: +If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable. +You can pass in options and arguments explicitly: .. code-block:: python diff --git a/doc/en/how-to/writing_hook_functions.rst b/doc/en/how-to/writing_hook_functions.rst index 71379016f..527aeec81 100644 --- a/doc/en/how-to/writing_hook_functions.rst +++ b/doc/en/how-to/writing_hook_functions.rst @@ -56,7 +56,7 @@ The remaining hook functions will not be called in this case. .. _`hookwrapper`: -hookwrapper: executing around other hooks +hook wrappers: executing around other hooks ------------------------------------------------- .. currentmodule:: _pytest.core @@ -69,10 +69,8 @@ which yields exactly once. When pytest invokes hooks it first executes hook wrappers and passes the same arguments as to the regular hooks. At the yield point of the hook wrapper pytest will execute the next hook -implementations and return their result to the yield point in the form of -a :py:class:`Result ` instance which encapsulates a result or -exception info. The yield point itself will thus typically not raise -exceptions (unless there are bugs). +implementations and return their result to the yield point, or will +propagate an exception if they raised. Here is an example definition of a hook wrapper: @@ -81,26 +79,35 @@ Here is an example definition of a hook wrapper: import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pyfunc_call(pyfuncitem): do_something_before_next_hook_executes() - outcome = yield - # outcome.excinfo may be None or a (cls, val, tb) tuple + # If the outcome is an exception, will raise the exception. + res = yield - res = outcome.get_result() # will raise if outcome was exception + new_res = post_process_result(res) - post_process_result(res) + # Override the return value to the plugin system. + return new_res - outcome.force_result(new_res) # to override the return value to the plugin system +The hook wrapper needs to return a result for the hook, or raise an exception. -Note that hook wrappers don't return results themselves, they merely -perform tracing or other side effects around the actual hook implementations. -If the result of the underlying hook is a mutable object, they may modify -that result but it's probably better to avoid it. +In many cases, the wrapper only needs to perform tracing or other side effects +around the actual hook implementations, in which case it can return the result +value of the ``yield``. The simplest (though useless) hook wrapper is +``return (yield)``. + +In other cases, the wrapper wants the adjust or adapt the result, in which case +it can return a new value. If the result of the underlying hook is a mutable +object, the wrapper may modify that result, but it's probably better to avoid it. + +If the hook implementation failed with an exception, the wrapper can handle that +exception using a ``try-catch-finally`` around the ``yield``, by propagating it, +supressing it, or raising a different exception entirely. For more information, consult the -:ref:`pluggy documentation about hookwrappers `. +:ref:`pluggy documentation about hook wrappers `. .. _plugin-hookorder: @@ -130,11 +137,14 @@ after others, i.e. the position in the ``N``-sized list of functions: # Plugin 3 - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_collection_modifyitems(items): # will execute even before the tryfirst one above! - outcome = yield - # will execute after all non-hookwrappers executed + try: + return (yield) + finally: + # will execute after all non-wrappers executed + ... Here is the order of execution: @@ -149,12 +159,11 @@ Here is the order of execution: Plugin1). 4. Plugin3's pytest_collection_modifyitems then executing the code after the yield - point. The yield receives a :py:class:`Result ` instance which encapsulates - the result from calling the non-wrappers. Wrappers shall not modify the result. + point. The yield receives the result from calling the non-wrappers, or raises + an exception if the non-wrappers raised. -It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with -``hookwrapper=True`` in which case it will influence the ordering of hookwrappers -among each other. +It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers +in which case it will influence the ordering of hook wrappers among each other. Declaring new hooks diff --git a/doc/en/index.rst b/doc/en/index.rst index 872138788..e64b5b311 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -2,7 +2,9 @@ .. sidebar:: Next Open Trainings - - `Professional Testing with Python `_, via `Python Academy `_, March 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote + - `pytest tips and tricks for a better testsuite `_, at `Europython 2023 `_, **July 18th** (3h), **Prague, Czech Republic / Remote** + - `pytest: Professionelles Testen (nicht nur) für Python `_, at `Workshoptage 2023 `_, **September 5th**, `OST `_ Campus **Rapperswil, Switzerland** + - `Professional Testing with Python `_, via `Python Academy `_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote** Also see :doc:`previous talks and blogposts `. @@ -17,7 +19,7 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can scale to support complex functional testing for applications and libraries. -``pytest`` requires: Python 3.7+ or PyPy3. +``pytest`` requires: Python 3.8+ or PyPy3. **PyPI package name**: :pypi:`pytest` @@ -76,7 +78,7 @@ Features - Can run :ref:`unittest ` (including trial) and :ref:`nose ` test suites out of the box -- Python 3.7+ or PyPy 3 +- Python 3.8+ or PyPy 3 - Rich plugin architecture, with over 800+ :ref:`external plugins ` and thriving community diff --git a/doc/en/reference/plugin_list.rst b/doc/en/reference/plugin_list.rst index fafc8c876..82b8286da 100644 --- a/doc/en/reference/plugin_list.rst +++ b/doc/en/reference/plugin_list.rst @@ -5,19 +5,22 @@ Plugin List =========== PyPI projects that match "pytest-\*" are considered plugins and are listed -automatically. Packages classified as inactive are excluded. +automatically together with a manually-maintained list in `the source +code `_. +Packages classified as inactive are excluded. .. The following conditional uses a different format for this list when creating a PDF, because otherwise the table gets far too wide for the page. -This list contains 1259 plugins. +This list contains 1283 plugins. .. only:: not latex =============================================== ======================================================================================================================================================================================================== ============== ===================== ================================================ name summary last release status requires =============================================== ======================================================================================================================================================================================================== ============== ===================== ================================================ + :pypi:`logassert` Simple but powerful assertion and verification of logged lines. May 20, 2022 5 - Production/Stable N/A :pypi:`pytest-abq` Pytest integration for the ABQ universal test runner. Apr 07, 2023 N/A N/A :pypi:`pytest-abstracts` A contextmanager pytest fixture for handling multiple mock abstracts May 25, 2022 N/A N/A :pypi:`pytest-accept` A pytest-plugin for updating doctest outputs Dec 21, 2022 N/A pytest (>=6,<8) @@ -33,12 +36,12 @@ This list contains 1259 plugins. :pypi:`pytest-aiogram` May 06, 2023 N/A N/A :pypi:`pytest-aiohttp` Pytest plugin for aiohttp support Feb 12, 2022 4 - Beta pytest (>=6.1.0) :pypi:`pytest-aiohttp-client` Pytest \`client\` fixture for the Aiohttp Jan 10, 2023 N/A pytest (>=7.2.0,<8.0.0) - :pypi:`pytest-aiomoto` pytest-aiomoto Nov 09, 2022 N/A pytest (>=7.0,<8.0) + :pypi:`pytest-aiomoto` pytest-aiomoto Jun 24, 2023 N/A pytest (>=7.0,<8.0) :pypi:`pytest-aioresponses` py.test integration for aioresponses Jul 29, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-aioworkers` A plugin to test aioworkers project with pytest May 01, 2023 5 - Production/Stable pytest>=6.1.0 :pypi:`pytest-airflow` pytest support for airflow. Apr 03, 2019 3 - Alpha pytest (>=4.4.0) :pypi:`pytest-airflow-utils` Nov 15, 2021 N/A N/A - :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. May 23, 2023 N/A pytest (>=6.0) + :pypi:`pytest-alembic` A pytest plugin for verifying alembic migrations. Jul 06, 2023 N/A pytest (>=6.0) :pypi:`pytest-allclose` Pytest fixture extending Numpy's allclose function Jul 30, 2019 5 - Production/Stable pytest :pypi:`pytest-allure-adaptor` Plugin for py.test to generate allure xml reports Jan 10, 2018 N/A pytest (>=2.7.3) :pypi:`pytest-allure-adaptor2` Plugin for py.test to generate allure xml reports Oct 14, 2020 N/A pytest (>=2.7.3) @@ -66,7 +69,7 @@ This list contains 1259 plugins. :pypi:`pytest-appium` Pytest plugin for appium Dec 05, 2019 N/A N/A :pypi:`pytest-approvaltests` A plugin to use approvaltests with pytest May 08, 2022 4 - Beta pytest (>=7.0.1) :pypi:`pytest-approvaltests-geo` Extension for ApprovalTests.Python specific to geo data verification Mar 04, 2023 5 - Production/Stable pytest - :pypi:`pytest-archon` Rule your architecture like a real developer Jan 31, 2023 5 - Production/Stable pytest (>=7.2) + :pypi:`pytest-archon` Rule your architecture like a real developer Jul 11, 2023 5 - Production/Stable pytest (>=7.2) :pypi:`pytest-argus` pyest results colection plugin Jun 24, 2021 5 - Production/Stable pytest (>=6.2.4) :pypi:`pytest-arraydiff` pytest plugin to help with comparing array output from tests Jan 13, 2022 4 - Beta pytest (>=4.6) :pypi:`pytest-asgi-server` Convenient ASGI client/server fixtures for Pytest Dec 12, 2020 N/A pytest (>=5.4.1) @@ -82,8 +85,9 @@ This list contains 1259 plugins. :pypi:`pytest-astropy` Meta-package containing dependencies for testing Apr 12, 2022 5 - Production/Stable pytest (>=4.6) :pypi:`pytest-astropy-header` pytest plugin to add diagnostic information to the header of the test output Sep 06, 2022 3 - Alpha pytest (>=4.6) :pypi:`pytest-ast-transformer` May 04, 2019 3 - Alpha pytest - :pypi:`pytest-asyncio` Pytest support for asyncio Mar 19, 2023 4 - Beta pytest (>=7.0.0) - :pypi:`pytest-asyncio-cooperative` Run all your asynchronous tests cooperatively. Feb 10, 2023 N/A N/A + :pypi:`pytest-async-generators` Pytest fixtures for async generators Jul 05, 2023 N/A N/A + :pypi:`pytest-asyncio` Pytest support for asyncio Jul 12, 2023 4 - Beta pytest (>=7.0.0) + :pypi:`pytest-asyncio-cooperative` Run all your asynchronous tests cooperatively. May 31, 2023 N/A N/A :pypi:`pytest-asyncio-network-simulator` pytest-asyncio-network-simulator: Plugin for pytest for simulator the network in tests Jul 31, 2018 3 - Alpha pytest (<3.7.0,>=3.3.2) :pypi:`pytest-async-mongodb` pytest plugin for async MongoDB Oct 18, 2017 5 - Production/Stable pytest (>=2.5.2) :pypi:`pytest-async-sqlalchemy` Database testing fixtures using the SQLAlchemy asyncio API Oct 07, 2021 4 - Beta pytest (>=6.0.0) @@ -109,12 +113,13 @@ This list contains 1259 plugins. :pypi:`pytest-base-url` pytest plugin for URL based testing Mar 27, 2022 5 - Production/Stable pytest (>=3.0.0,<8.0.0) :pypi:`pytest-bdd` BDD for pytest Nov 08, 2022 6 - Mature pytest (>=6.2.0) :pypi:`pytest-bdd-html` pytest plugin to display BDD info in HTML test report Nov 22, 2022 3 - Alpha pytest (!=6.0.0,>=5.0) - :pypi:`pytest-bdd-ng` BDD for pytest Oct 06, 2022 4 - Beta pytest (>=5.0) + :pypi:`pytest-bdd-ng` BDD for pytest Jul 01, 2023 4 - Beta pytest (>=5.0) :pypi:`pytest-bdd-splinter` Common steps for pytest bdd and splinter integration Aug 12, 2019 5 - Production/Stable pytest (>=4.0.0) :pypi:`pytest-bdd-web` A simple plugin to use with pytest Jan 02, 2020 4 - Beta pytest (>=3.5.0) :pypi:`pytest-bdd-wrappers` Feb 11, 2020 2 - Pre-Alpha N/A :pypi:`pytest-beakerlib` A pytest plugin that reports test results to the BeakerLib framework Mar 17, 2017 5 - Production/Stable pytest :pypi:`pytest-beds` Fixtures for testing Google Appengine (GAE) apps Jun 07, 2016 4 - Beta N/A + :pypi:`pytest-beeprint` use icdiff for better error messages in pytest assertions Jul 04, 2023 4 - Beta N/A :pypi:`pytest-bench` Benchmark utility that plugs into pytest. Jul 21, 2014 3 - Alpha N/A :pypi:`pytest-benchmark` A \`\`pytest\`\` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer. Oct 25, 2022 5 - Production/Stable pytest (>=3.8) :pypi:`pytest-better-datadir` A small example package Mar 13, 2023 N/A N/A @@ -142,7 +147,7 @@ This list contains 1259 plugins. :pypi:`pytest-browsermob-proxy` BrowserMob proxy plugin for py.test. Jun 11, 2013 4 - Beta N/A :pypi:`pytest-browserstack-local` \`\`py.test\`\` plugin to run \`\`BrowserStackLocal\`\` in background. Feb 09, 2018 N/A N/A :pypi:`pytest-budosystems` Budo Systems is a martial arts school management system. This module is the Budo Systems Pytest Plugin. May 07, 2023 3 - Alpha pytest - :pypi:`pytest-bug` Pytest plugin for marking tests as a bug Jan 29, 2023 5 - Production/Stable pytest (>=6.2.0) + :pypi:`pytest-bug` Pytest plugin for marking tests as a bug Jun 23, 2023 5 - Production/Stable pytest (>=7.1.0) :pypi:`pytest-bugtong-tag` pytest-bugtong-tag is a plugin for pytest Jan 16, 2022 N/A N/A :pypi:`pytest-bugzilla` py.test bugzilla integration plugin May 05, 2010 4 - Beta N/A :pypi:`pytest-bugzilla-notifier` A plugin that allows you to execute create, update, and read information from BugZilla bugs Jun 15, 2018 4 - Beta pytest (>=2.9.2) @@ -171,8 +176,8 @@ This list contains 1259 plugins. :pypi:`pytest-change-report` turn . into √,turn F into x Sep 14, 2020 N/A pytest :pypi:`pytest-change-xds` turn . into √,turn F into x Apr 16, 2022 N/A pytest :pypi:`pytest-chdir` A pytest fixture for changing current working directory Jan 28, 2020 N/A pytest (>=5.0.0,<6.0.0) - :pypi:`pytest-check` A pytest plugin that allows multiple failures per test. Feb 13, 2023 5 - Production/Stable pytest - :pypi:`pytest-checkdocs` check the README when running tests Oct 09, 2022 5 - Production/Stable pytest (>=6) ; extra == 'testing' + :pypi:`pytest-check` A pytest plugin that allows multiple failures per test. Jul 14, 2023 N/A pytest + :pypi:`pytest-checkdocs` check the README when running tests Jul 09, 2023 5 - Production/Stable pytest (>=6) ; extra == 'testing' :pypi:`pytest-checkipdb` plugin to check if there are ipdb debugs left Jul 22, 2020 5 - Production/Stable pytest (>=2.9.2) :pypi:`pytest-check-library` check your missing library Jul 17, 2022 N/A N/A :pypi:`pytest-check-libs` check your missing library Jul 17, 2022 N/A N/A @@ -202,33 +207,34 @@ This list contains 1259 plugins. :pypi:`pytest-codegen` Automatically create pytest test signatures Aug 23, 2020 2 - Pre-Alpha N/A :pypi:`pytest-codeowners` Pytest plugin for selecting tests by GitHub CODEOWNERS. Mar 30, 2022 4 - Beta pytest (>=6.0.0) :pypi:`pytest-codestyle` pytest plugin to run pycodestyle Mar 23, 2020 3 - Alpha N/A - :pypi:`pytest-codspeed` Pytest plugin to create CodSpeed benchmarks Dec 02, 2022 5 - Production/Stable pytest>=3.8 + :pypi:`pytest-codspeed` Pytest plugin to create CodSpeed benchmarks Jul 04, 2023 5 - Production/Stable pytest>=3.8 :pypi:`pytest-collect-formatter` Formatter for pytest collect output Mar 29, 2021 5 - Production/Stable N/A :pypi:`pytest-collect-formatter2` Formatter for pytest collect output May 31, 2021 5 - Production/Stable N/A :pypi:`pytest-collector` Python package for collecting pytest. Aug 02, 2022 N/A pytest (>=7.0,<8.0) :pypi:`pytest-colordots` Colorizes the progress indicators Oct 06, 2017 5 - Production/Stable N/A :pypi:`pytest-commander` An interactive GUI test runner for PyTest Aug 17, 2021 N/A pytest (<7.0.0,>=6.2.4) :pypi:`pytest-common-subject` pytest framework for testing different aspects of a common method May 15, 2022 N/A pytest (>=3.6,<8) - :pypi:`pytest-compare` pytest plugin for comparing call arguments. Mar 30, 2023 5 - Production/Stable N/A + :pypi:`pytest-compare` pytest plugin for comparing call arguments. Jun 22, 2023 5 - Production/Stable N/A :pypi:`pytest-concurrent` Concurrently execute test cases with multithread, multiprocess and gevent Jan 12, 2019 4 - Beta pytest (>=3.1.1) :pypi:`pytest-config` Base configurations and utilities for developing your Python project test suite with pytest. Nov 07, 2014 5 - Production/Stable N/A :pypi:`pytest-confluence-report` Package stands for pytest plugin to upload results into Confluence page. Apr 17, 2022 N/A N/A - :pypi:`pytest-console-scripts` Pytest plugin for testing console scripts May 22, 2023 4 - Beta N/A + :pypi:`pytest-console-scripts` Pytest plugin for testing console scripts May 31, 2023 4 - Beta pytest (>=4.0.0) :pypi:`pytest-consul` pytest plugin with fixtures for testing consul aware apps Nov 24, 2018 3 - Alpha pytest - :pypi:`pytest-container` Pytest fixtures for writing container based tests Mar 21, 2023 4 - Beta pytest (>=3.10) + :pypi:`pytest-container` Pytest fixtures for writing container based tests Jun 19, 2023 4 - Beta pytest (>=3.10) :pypi:`pytest-contextfixture` Define pytest fixtures as context managers. Mar 12, 2013 4 - Beta N/A :pypi:`pytest-contexts` A plugin to run tests written with the Contexts framework using pytest May 19, 2021 4 - Beta N/A :pypi:`pytest-cookies` The pytest plugin for your Cookiecutter templates. 🍪 Mar 22, 2023 5 - Production/Stable pytest (>=3.9.0) + :pypi:`pytest-copier` A pytest plugin to help testing Copier templates Jun 23, 2023 4 - Beta pytest>=7.1.2 :pypi:`pytest-couchdbkit` py.test extension for per-test couchdb databases using couchdbkit Apr 17, 2012 N/A N/A :pypi:`pytest-count` count erros and send email Jan 12, 2018 4 - Beta N/A :pypi:`pytest-cov` Pytest plugin for measuring coverage. May 24, 2023 5 - Production/Stable pytest (>=4.6) :pypi:`pytest-cover` Pytest plugin for measuring coverage. Forked from \`pytest-cov\`. Aug 01, 2015 5 - Production/Stable N/A :pypi:`pytest-coverage` Jun 17, 2015 N/A N/A - :pypi:`pytest-coverage-context` Coverage dynamic context support for PyTest, including sub-processes Jan 04, 2021 4 - Beta pytest (>=6.1.0) + :pypi:`pytest-coverage-context` Coverage dynamic context support for PyTest, including sub-processes Jun 28, 2023 4 - Beta N/A :pypi:`pytest-coveragemarkers` Using pytest markers to track functional coverage and filtering of tests Nov 29, 2022 N/A pytest (>=7.1.2,<8.0.0) :pypi:`pytest-cov-exclude` Pytest plugin for excluding tests based on coverage data Apr 29, 2016 4 - Beta pytest (>=2.8.0,<2.9.0); extra == 'dev' :pypi:`pytest-cpp` Use pytest's runner to discover and execute C++ tests Jan 30, 2023 5 - Production/Stable pytest (>=7.0) - :pypi:`pytest-cppython` A pytest plugin that imports CPPython testing types May 25, 2023 N/A N/A + :pypi:`pytest-cppython` A pytest plugin that imports CPPython testing types Jun 19, 2023 N/A N/A :pypi:`pytest-cqase` Custom qase pytest plugin Aug 22, 2022 N/A pytest (>=7.1.2,<8.0.0) :pypi:`pytest-cram` Run cram tests with pytest. Aug 08, 2020 N/A N/A :pypi:`pytest-crate` Manages CrateDB instances during your integration tests May 28, 2019 3 - Alpha pytest (>=4.0) @@ -237,7 +243,7 @@ This list contains 1259 plugins. :pypi:`pytest-cricri` A Cricri plugin for pytest. Jan 27, 2018 N/A pytest :pypi:`pytest-crontab` add crontab task in crontab Dec 09, 2019 N/A N/A :pypi:`pytest-csv` CSV output for pytest. Apr 22, 2021 N/A pytest (>=6.0) - :pypi:`pytest-csv-params` Pytest plugin for Test Case Parametrization with CSV files Aug 28, 2022 5 - Production/Stable pytest (>=7.1.2,<8.0.0) + :pypi:`pytest-csv-params` Pytest plugin for Test Case Parametrization with CSV files Jul 01, 2023 5 - Production/Stable pytest (>=7.4.0,<8.0.0) :pypi:`pytest-curio` Pytest support for curio. Oct 07, 2020 N/A N/A :pypi:`pytest-curl-report` pytest plugin to generate curl command line report Dec 11, 2016 4 - Beta N/A :pypi:`pytest-custom-concurrency` Custom grouping concurrence for pytest Feb 08, 2021 N/A N/A @@ -268,6 +274,7 @@ This list contains 1259 plugins. :pypi:`pytest-db` Session scope fixture "db" for mysql query or change Dec 04, 2019 N/A N/A :pypi:`pytest-dbfixtures` Databases fixtures plugin for py.test. Dec 07, 2016 4 - Beta N/A :pypi:`pytest-db-plugin` Nov 27, 2021 N/A pytest (>=5.0) + :pypi:`pytest-dbt` Unit test dbt models with standard python tooling Jun 08, 2023 2 - Pre-Alpha pytest (>=7.0.0,<8.0.0) :pypi:`pytest-dbt-adapter` A pytest plugin for testing dbt adapter plugins Nov 24, 2021 N/A pytest (<7,>=6) :pypi:`pytest-dbt-conventions` A pytest plugin for linting a dbt project's conventions Mar 02, 2022 N/A pytest (>=6.2.5,<7.0.0) :pypi:`pytest-dbt-core` Pytest extension for dbt. May 03, 2023 N/A pytest (>=6.2.5) ; extra == 'test' @@ -291,9 +298,10 @@ This list contains 1259 plugins. :pypi:`pytest-diffeo` A package to prevent Dependency Confusion attacks against Yandex. Feb 10, 2023 N/A N/A :pypi:`pytest-diff-selector` Get tests affected by code changes (using git) Feb 24, 2022 4 - Beta pytest (>=6.2.2) ; extra == 'all' :pypi:`pytest-difido` PyTest plugin for generating Difido reports Oct 23, 2022 4 - Beta pytest (>=4.0.0) + :pypi:`pytest-dir-equal` pytest-dir-equals is a pytest plugin providing helpers to assert directories equality allowing golden testing Jun 23, 2023 4 - Beta pytest>=7.1.2 :pypi:`pytest-disable` pytest plugin to disable a test and skip it from testrun Sep 10, 2015 4 - Beta N/A :pypi:`pytest-disable-plugin` Disable plugins per test Feb 28, 2019 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-discord` A pytest plugin to notify test results to a Discord channel. Feb 05, 2023 4 - Beta pytest (!=6.0.0,<8,>=3.3.2) + :pypi:`pytest-discord` A pytest plugin to notify test results to a Discord channel. Jul 15, 2023 4 - Beta pytest (!=6.0.0,<8,>=3.3.2) :pypi:`pytest-django` A Django plugin for pytest. Dec 07, 2021 5 - Production/Stable pytest (>=5.4.0) :pypi:`pytest-django-ahead` A Django plugin for pytest. Oct 27, 2016 5 - Production/Stable pytest (>=2.9) :pypi:`pytest-djangoapp` Nice pytest plugin to help you with Django pluggable application testing. May 19, 2023 4 - Beta pytest @@ -304,7 +312,7 @@ This list contains 1259 plugins. :pypi:`pytest-django-filefield` Replaces FileField.storage with something you can patch globally. May 09, 2022 5 - Production/Stable pytest >= 5.2 :pypi:`pytest-django-gcir` A Django plugin for pytest. Mar 06, 2018 5 - Production/Stable N/A :pypi:`pytest-django-haystack` Cleanup your Haystack indexes between tests Sep 03, 2017 5 - Production/Stable pytest (>=2.3.4) - :pypi:`pytest-django-ifactory` A model instance factory for pytest-django May 21, 2023 5 - Production/Stable N/A + :pypi:`pytest-django-ifactory` A model instance factory for pytest-django Jun 06, 2023 5 - Production/Stable N/A :pypi:`pytest-django-lite` The bare minimum to integrate py.test with Django. Jan 30, 2014 N/A N/A :pypi:`pytest-django-liveserver-ssl` Jan 20, 2022 3 - Alpha N/A :pypi:`pytest-django-model` A Simple Way to Test your Django Models Feb 14, 2019 4 - Beta N/A @@ -338,7 +346,8 @@ This list contains 1259 plugins. :pypi:`pytest-doctest-custom` A py.test plugin for customizing string representations of doctest results. Jul 25, 2016 4 - Beta N/A :pypi:`pytest-doctest-ellipsis-markers` Setup additional values for ELLIPSIS_MARKER for doctests Jan 12, 2018 4 - Beta N/A :pypi:`pytest-doctest-import` A simple pytest plugin to import names and add them to the doctest namespace. Nov 13, 2018 4 - Beta pytest (>=3.3.0) - :pypi:`pytest-doctestplus` Pytest plugin with advanced doctest features. Sep 26, 2022 3 - Alpha pytest (>=4.6) + :pypi:`pytest-doctestplus` Pytest plugin with advanced doctest features. Jun 08, 2023 3 - Alpha pytest (>=4.6) + :pypi:`pytest-dogu-report` pytest plugin for dogu report Jul 07, 2023 N/A N/A :pypi:`pytest-dolphin` Some extra stuff that we use ininternally Nov 30, 2016 4 - Beta pytest (==3.0.4) :pypi:`pytest-doorstop` A pytest plugin for adding test results into doorstop items. Jun 09, 2020 4 - Beta pytest (>=3.5.0) :pypi:`pytest-dotenv` A py.test plugin that parses environment files before running tests Jun 16, 2020 4 - Beta pytest (>=5.0.0) @@ -346,12 +355,13 @@ This list contains 1259 plugins. :pypi:`pytest-drf` A Django REST framework plugin for pytest. Jul 12, 2022 5 - Production/Stable pytest (>=3.7) :pypi:`pytest-drivings` Tool to allow webdriver automation to be ran locally or remotely Jan 13, 2021 N/A N/A :pypi:`pytest-drop-dup-tests` A Pytest plugin to drop duplicated tests during collection May 23, 2020 4 - Beta pytest (>=2.7) + :pypi:`pytest-dryrun` A Pytest plugin to ignore tests during collection without reporting them in the test summary. Jul 11, 2023 N/A pytest (>=7.4.0,<8.0.0) :pypi:`pytest-dummynet` A py.test plugin providing access to a dummynet. Dec 15, 2021 5 - Production/Stable pytest :pypi:`pytest-dump2json` A pytest plugin for dumping test results to json. Jun 29, 2015 N/A N/A :pypi:`pytest-duration-insights` Jun 25, 2021 N/A N/A :pypi:`pytest-durations` Pytest plugin reporting fixtures and test functions execution time. Apr 22, 2022 5 - Production/Stable pytest (>=4.6) :pypi:`pytest-dynamicrerun` A pytest plugin to rerun tests dynamically based off of test outcome and output. Aug 15, 2020 4 - Beta N/A - :pypi:`pytest-dynamodb` DynamoDB fixtures for pytest Mar 27, 2023 5 - Production/Stable pytest + :pypi:`pytest-dynamodb` DynamoDB fixtures for pytest Jun 12, 2023 5 - Production/Stable pytest :pypi:`pytest-easy-addoption` pytest-easy-addoption: Easy way to work with pytest addoption Jan 22, 2020 N/A N/A :pypi:`pytest-easy-api` Simple API testing with pytest Mar 26, 2018 N/A N/A :pypi:`pytest-easyMPI` Package that supports mpi tests in pytest Oct 21, 2020 N/A N/A @@ -366,22 +376,22 @@ This list contains 1259 plugins. :pypi:`pytest-eliot` An eliot plugin for pytest. Aug 31, 2022 1 - Planning pytest (>=5.4.0) :pypi:`pytest-elk-reporter` A simple plugin to use with pytest Jan 24, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-email` Send execution result email Jul 08, 2020 N/A pytest - :pypi:`pytest-embedded` pytest embedded plugin Apr 11, 2023 N/A pytest (>=7.0) - :pypi:`pytest-embedded-arduino` pytest embedded plugin for Arduino projects Apr 11, 2023 N/A N/A - :pypi:`pytest-embedded-idf` pytest embedded plugin for esp-idf project Apr 11, 2023 N/A N/A - :pypi:`pytest-embedded-jtag` pytest embedded plugin for testing with jtag Apr 11, 2023 N/A N/A - :pypi:`pytest-embedded-qemu` pytest embedded plugin for qemu, not target chip Apr 11, 2023 N/A N/A - :pypi:`pytest-embedded-serial` pytest embedded plugin for testing serial ports Apr 11, 2023 N/A N/A - :pypi:`pytest-embedded-serial-esp` pytest embedded plugin for testing espressif boards via serial ports Apr 11, 2023 N/A N/A + :pypi:`pytest-embedded` A pytest plugin that designed for embedded testing. Jul 09, 2023 5 - Production/Stable pytest>=7.0 + :pypi:`pytest-embedded-arduino` Make pytest-embedded plugin work with Arduino. Jul 09, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded-idf` Make pytest-embedded plugin work with ESP-IDF. Jul 09, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded-jtag` Make pytest-embedded plugin work with JTAG. Jul 09, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded-qemu` Make pytest-embedded plugin work with QEMU. Jul 09, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded-serial` Make pytest-embedded plugin work with Serial. Jul 09, 2023 5 - Production/Stable N/A + :pypi:`pytest-embedded-serial-esp` Make pytest-embedded plugin work with Espressif target boards. Jul 09, 2023 5 - Production/Stable N/A :pypi:`pytest-embrace` 💝 Dataclasses-as-tests. Describe the runtime once and multiply coverage with no boilerplate. Mar 25, 2023 N/A pytest (>=7.0,<8.0) :pypi:`pytest-emoji` A pytest plugin that adds emojis to your test result report Feb 19, 2019 4 - Beta pytest (>=4.2.1) :pypi:`pytest-emoji-output` Pytest plugin to represent test output with emoji support Apr 09, 2023 4 - Beta pytest (==7.0.1) - :pypi:`pytest-enabler` Enable installed pytest plugins May 12, 2023 5 - Production/Stable pytest (>=6) ; extra == 'testing' + :pypi:`pytest-enabler` Enable installed pytest plugins Jul 14, 2023 5 - Production/Stable pytest (>=6) ; extra == 'testing' :pypi:`pytest-encode` set your encoding and logger Nov 06, 2021 N/A N/A :pypi:`pytest-encode-kane` set your encoding and logger Nov 16, 2021 N/A pytest :pypi:`pytest-enhanced-reports` Enhanced test reports for pytest Dec 15, 2022 N/A N/A :pypi:`pytest-enhancements` Improvements for pytest (rejected upstream) Oct 30, 2019 4 - Beta N/A - :pypi:`pytest-env` py.test plugin that allows you to add environment variables. Oct 23, 2022 5 - Production/Stable pytest>=7.1.3 + :pypi:`pytest-env` py.test plugin that allows you to add environment variables. Jun 15, 2023 5 - Production/Stable pytest>=7.3.1 :pypi:`pytest-envfiles` A py.test plugin that parses environment files before running tests Oct 08, 2015 3 - Alpha N/A :pypi:`pytest-env-info` Push information about the running pytest into envvars Nov 25, 2017 4 - Beta pytest (>=3.1.1) :pypi:`pytest-envraw` py.test plugin that allows you to add environment variables. Aug 27, 2020 4 - Beta pytest (>=2.6.0) @@ -393,7 +403,7 @@ This list contains 1259 plugins. :pypi:`pytest-ethereum` pytest-ethereum: Pytest library for ethereum projects. Jun 24, 2019 3 - Alpha pytest (==3.3.2); extra == 'dev' :pypi:`pytest-eucalyptus` Pytest Plugin for BDD Jun 28, 2022 N/A pytest (>=4.2.0) :pypi:`pytest-eventlet` Applies eventlet monkey-patch as a pytest plugin. Oct 04, 2021 N/A pytest ; extra == 'dev' - :pypi:`pytest-examples` Pytest plugin for testing examples in docstrings and markdown files. May 05, 2023 4 - Beta pytest>=7 + :pypi:`pytest-examples` Pytest plugin for testing examples in docstrings and markdown files. Jul 11, 2023 4 - Beta pytest>=7 :pypi:`pytest-excel` pytest plugin for generating excel reports Jan 31, 2022 5 - Production/Stable N/A :pypi:`pytest-exceptional` Better exceptions Mar 16, 2017 4 - Beta N/A :pypi:`pytest-exception-script` Walk your code through exception script to check it's resiliency to failures. Aug 04, 2020 3 - Alpha pytest @@ -427,7 +437,7 @@ This list contains 1259 plugins. :pypi:`pytest-fantasy` Pytest plugin for Flask Fantasy Framework Mar 14, 2019 N/A N/A :pypi:`pytest-fastapi` Dec 27, 2020 N/A N/A :pypi:`pytest-fastapi-deps` A fixture which allows easy replacement of fastapi dependencies for testing Jul 20, 2022 5 - Production/Stable pytest - :pypi:`pytest-fastest` Use SCM and coverage to run only needed tests Mar 05, 2020 N/A N/A + :pypi:`pytest-fastest` Use SCM and coverage to run only needed tests Jun 15, 2023 4 - Beta pytest (>=4.4) :pypi:`pytest-fast-first` Pytest plugin that runs fast tests first Jan 19, 2023 3 - Alpha pytest :pypi:`pytest-faulthandler` py.test plugin that activates the fault handler module for tests (dummy package) Jul 04, 2019 6 - Mature pytest (>=5.0) :pypi:`pytest-fauxfactory` Integration of fauxfactory into pytest. Dec 06, 2017 5 - Production/Stable pytest (>=3.2) @@ -452,7 +462,7 @@ This list contains 1259 plugins. :pypi:`pytest-fixture-tools` Plugin for pytest which provides tools for fixtures Aug 18, 2020 6 - Mature pytest :pypi:`pytest-fixture-typecheck` A pytest plugin to assert type annotations at runtime. Aug 24, 2021 N/A pytest :pypi:`pytest-flake8` pytest plugin to check FLAKE8 requirements Mar 18, 2022 4 - Beta pytest (>=7.0) - :pypi:`pytest-flake8-path` A pytest fixture for testing flake8 plugins. May 11, 2022 5 - Production/Stable pytest + :pypi:`pytest-flake8-path` A pytest fixture for testing flake8 plugins. Jul 10, 2023 5 - Production/Stable pytest :pypi:`pytest-flake8-v2` pytest plugin to check FLAKE8 requirements Mar 01, 2022 5 - Production/Stable pytest (>=7.0) :pypi:`pytest-flakefinder` Runs tests multiple times to expose flakiness. Oct 26, 2022 4 - Beta pytest (>=2.7.1) :pypi:`pytest-flakes` pytest plugin to check source code with pyflakes Dec 02, 2021 5 - Production/Stable pytest (>=5) @@ -462,7 +472,8 @@ This list contains 1259 plugins. :pypi:`pytest-flask-sqlalchemy` A pytest plugin for preserving test isolation in Flask-SQlAlchemy using database transactions. Apr 30, 2022 4 - Beta pytest (>=3.2.1) :pypi:`pytest-flask-sqlalchemy-transactions` Run tests in transactions using pytest, Flask, and SQLalchemy. Aug 02, 2018 4 - Beta pytest (>=3.2.1) :pypi:`pytest-flexreport` Apr 15, 2023 4 - Beta pytest - :pypi:`pytest-fluent` A pytest plugin in order to provide logs via fluentd Jul 12, 2022 4 - Beta pytest + :pypi:`pytest-fluent` A pytest plugin in order to provide logs via fluentd Jun 26, 2023 4 - Beta pytest (>=7.0.0) + :pypi:`pytest-fluentbit` A pytest plugin in order to provide logs via fluentbit Jun 16, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-flyte` Pytest fixtures for simplifying Flyte integration testing May 03, 2021 N/A pytest :pypi:`pytest-focus` A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest :pypi:`pytest-forbid` Mar 07, 2023 N/A pytest (>=7.2.2,<8.0.0) @@ -471,7 +482,7 @@ This list contains 1259 plugins. :pypi:`pytest-forward-compatibility` A pytest plugin to shim pytest commandline options for fowards compatibility Sep 29, 2020 N/A N/A :pypi:`pytest-frappe` Pytest Frappe Plugin - A set of pytest fixtures to test Frappe applications May 03, 2023 4 - Beta pytest>=7.0.0 :pypi:`pytest-freezegun` Wrap tests with fixtures in freeze_time Jul 19, 2020 4 - Beta pytest (>=3.0.0) - :pypi:`pytest-freezer` Pytest plugin providing a fixture interface for spulec/freezegun Oct 20, 2022 N/A pytest>=3.6 + :pypi:`pytest-freezer` Pytest plugin providing a fixture interface for spulec/freezegun Jun 21, 2023 N/A pytest >= 3.6 :pypi:`pytest-freeze-reqs` Check if requirement files are frozen Apr 29, 2021 N/A N/A :pypi:`pytest-frozen-uuids` Deterministically frozen UUID's for your tests Apr 17, 2022 N/A pytest (>=3.0) :pypi:`pytest-func-cov` Pytest plugin for measuring function coverage Apr 15, 2021 3 - Alpha pytest (>=5) @@ -486,8 +497,9 @@ This list contains 1259 plugins. :pypi:`pytest-gherkin` A flexible framework for executing BDD gherkin tests Jul 27, 2019 3 - Alpha pytest (>=5.0.0) :pypi:`pytest-gh-log-group` pytest plugin for gh actions Jan 11, 2022 3 - Alpha pytest :pypi:`pytest-ghostinspector` For finding/executing Ghost Inspector tests May 17, 2016 3 - Alpha N/A - :pypi:`pytest-girder` A set of pytest fixtures for testing Girder applications. May 22, 2023 N/A N/A + :pypi:`pytest-girder` A set of pytest fixtures for testing Girder applications. Jun 28, 2023 N/A N/A :pypi:`pytest-git` Git repository fixture for py.test May 28, 2019 5 - Production/Stable pytest + :pypi:`pytest-gitconfig` Provide a gitconfig sandbox for testing Jun 22, 2023 4 - Beta pytest>=7.1.2 :pypi:`pytest-gitcov` Pytest plugin for reporting on coverage of the last git commit. Jan 11, 2020 2 - Pre-Alpha N/A :pypi:`pytest-git-fixtures` Pytest fixtures for testing with git. Mar 11, 2021 4 - Beta pytest :pypi:`pytest-github` Plugin for py.test that associates tests with github issues using a marker. Mar 07, 2019 5 - Production/Stable N/A @@ -503,6 +515,7 @@ This list contains 1259 plugins. :pypi:`pytest-google-chat` Notify google chat channel for test results Mar 27, 2022 4 - Beta pytest :pypi:`pytest-graphql-schema` Get graphql schema as fixture for pytest Oct 18, 2019 N/A N/A :pypi:`pytest-greendots` Green progress dots Feb 08, 2014 3 - Alpha N/A + :pypi:`pytest-group-by-class` A Pytest plugin for running a subset of your tests by splitting them in to groups of classes. Jun 27, 2023 5 - Production/Stable pytest (>=2.5) :pypi:`pytest-growl` Growl notifications for pytest results. Jan 13, 2014 5 - Production/Stable N/A :pypi:`pytest-grpc` pytest plugin for grpc May 01, 2020 N/A pytest (>=3.6.0) :pypi:`pytest-grunnur` Py.Test plugin for Grunnur-based packages. Feb 05, 2023 N/A N/A @@ -518,10 +531,10 @@ This list contains 1259 plugins. :pypi:`pytest-historic` Custom report to display pytest historical execution records Apr 08, 2020 N/A pytest :pypi:`pytest-historic-hook` Custom listener to store execution results into MYSQL DB, which is used for pytest-historic report Apr 08, 2020 N/A pytest :pypi:`pytest-homeassistant` A pytest plugin for use with homeassistant custom components. Aug 12, 2020 4 - Beta N/A - :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components May 24, 2023 3 - Alpha pytest (==7.3.1) + :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Jul 14, 2023 3 - Alpha pytest (==7.3.1) :pypi:`pytest-honey` A simple plugin to use with pytest Jan 07, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-honors` Report on tests that honor constraints, and guard against regressions Mar 06, 2020 4 - Beta N/A - :pypi:`pytest-hot-reloading` May 18, 2023 N/A N/A + :pypi:`pytest-hot-reloading` Jun 23, 2023 N/A N/A :pypi:`pytest-hot-test` A plugin that tracks test changes Dec 10, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-hoverfly` Simplify working with Hoverfly from pytest Jan 30, 2023 N/A pytest (>=5.0) :pypi:`pytest-hoverfly-wrapper` Integrates the Hoverfly HTTP proxy into Pytest Feb 27, 2023 5 - Production/Stable pytest (>=3.7.0) @@ -540,7 +553,7 @@ This list contains 1259 plugins. :pypi:`pytest-http-mocker` Pytest plugin for http mocking (via https://github.com/vilus/mocker) Oct 20, 2019 N/A N/A :pypi:`pytest-httpretty` A thin wrapper of HTTPretty for pytest Feb 16, 2014 3 - Alpha N/A :pypi:`pytest-httpserver` pytest-httpserver is a httpserver for pytest May 22, 2023 3 - Alpha N/A - :pypi:`pytest-httptesting` http_testing framework on top of pytest Apr 19, 2023 N/A pytest (>=7.2.0,<8.0.0) + :pypi:`pytest-httptesting` http_testing framework on top of pytest Jul 09, 2023 N/A pytest (>=7.2.0,<8.0.0) :pypi:`pytest-httpx` Send responses to httpx. Apr 12, 2023 5 - Production/Stable pytest (<8.0,>=6.0) :pypi:`pytest-httpx-blockage` Disable httpx requests during a test run Feb 16, 2023 N/A pytest (>=7.2.1) :pypi:`pytest-hue` Visualise PyTest status via your Phillips Hue lights May 09, 2019 N/A N/A @@ -549,7 +562,7 @@ This list contains 1259 plugins. :pypi:`pytest-ibutsu` A plugin to sent pytest results to an Ibutsu server Aug 05, 2022 4 - Beta pytest>=7.1 :pypi:`pytest-icdiff` use icdiff for better error messages in pytest assertions Aug 09, 2022 4 - Beta N/A :pypi:`pytest-idapro` A pytest plugin for idapython. Allows a pytest setup to run tests outside and inside IDA in an automated manner by runnig pytest inside IDA and by mocking idapython api Nov 03, 2018 N/A N/A - :pypi:`pytest-idem` A pytest plugin to help with testing idem projects Sep 07, 2022 5 - Production/Stable N/A + :pypi:`pytest-idem` A pytest plugin to help with testing idem projects Jun 23, 2023 5 - Production/Stable N/A :pypi:`pytest-idempotent` Pytest plugin for testing function idempotence. Jul 25, 2022 N/A N/A :pypi:`pytest-ignore-flaky` ignore failures from flaky tests (pytest plugin) Apr 23, 2021 5 - Production/Stable N/A :pypi:`pytest-image-diff` Mar 09, 2023 3 - Alpha pytest @@ -561,7 +574,7 @@ This list contains 1259 plugins. :pypi:`pytest-ini` Reuse pytest.ini to store env variables Apr 26, 2022 N/A N/A :pypi:`pytest-inline` A pytest plugin for writing inline tests. Feb 08, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-inmanta` A py.test plugin providing fixtures to simplify inmanta modules testing. Feb 23, 2023 5 - Production/Stable N/A - :pypi:`pytest-inmanta-extensions` Inmanta tests package Apr 12, 2023 5 - Production/Stable N/A + :pypi:`pytest-inmanta-extensions` Inmanta tests package Jul 04, 2023 5 - Production/Stable N/A :pypi:`pytest-inmanta-lsm` Common fixtures for inmanta LSM related modules May 17, 2023 5 - Production/Stable N/A :pypi:`pytest-inmanta-yang` Common fixtures used in inmanta yang related modules Jun 16, 2022 4 - Beta N/A :pypi:`pytest-Inomaly` A simple image diff plugin for pytest Feb 13, 2018 4 - Beta N/A @@ -572,14 +585,14 @@ This list contains 1259 plugins. :pypi:`pytest-integration-mark` Automatic integration test marking and excluding plugin for pytest May 22, 2023 N/A pytest (>=5.2) :pypi:`pytest-interactive` A pytest plugin for console based interactive test selection just after the collection phase Nov 30, 2017 3 - Alpha N/A :pypi:`pytest-intercept-remote` Pytest plugin for intercepting outgoing connection requests during pytest run. May 24, 2021 4 - Beta pytest (>=4.6) - :pypi:`pytest-interface-tester` Pytest plugin for checking charm relation interface protocol compliance. May 09, 2023 4 - Beta pytest - :pypi:`pytest-invenio` Pytest fixtures for Invenio. Apr 13, 2023 5 - Production/Stable pytest (<7.2.0,>=6) + :pypi:`pytest-interface-tester` Pytest plugin for checking charm relation interface protocol compliance. Jul 14, 2023 4 - Beta pytest + :pypi:`pytest-invenio` Pytest fixtures for Invenio. Jun 02, 2023 5 - Production/Stable pytest (<7.2.0,>=6) :pypi:`pytest-involve` Run tests covering a specific file or changeset Feb 02, 2020 4 - Beta pytest (>=3.5.0) :pypi:`pytest-ipdb` A py.test plug-in to enable drop to ipdb debugger on test failure. Mar 20, 2013 2 - Pre-Alpha N/A :pypi:`pytest-ipynb` THIS PROJECT IS ABANDONED Jan 29, 2019 3 - Alpha N/A :pypi:`pytest-isolate` Feb 20, 2023 4 - Beta pytest :pypi:`pytest-isort` py.test plugin to check import ordering using isort Oct 31, 2022 5 - Production/Stable pytest (>=5.0) - :pypi:`pytest-is-running` pytest plugin providing a function to check if pytest is running. Aug 19, 2022 5 - Production/Stable N/A + :pypi:`pytest-is-running` pytest plugin providing a function to check if pytest is running. Jul 10, 2023 5 - Production/Stable N/A :pypi:`pytest-it` Pytest plugin to display test reports as a plaintext spec, inspired by Rspec: https://github.com/mattduck/pytest-it. Jan 22, 2020 4 - Beta N/A :pypi:`pytest-iterassert` Nicer list and iterable assertion messages for pytest May 11, 2020 3 - Alpha N/A :pypi:`pytest-iters` A contextmanager pytest fixture for handling multiple mock iters May 24, 2022 N/A N/A @@ -587,9 +600,9 @@ This list contains 1259 plugins. :pypi:`pytest-jelastic` Pytest plugin defining the necessary command-line options to pass to pytests testing a Jelastic environment. Nov 16, 2022 N/A pytest (>=7.2.0,<8.0.0) :pypi:`pytest-jest` A custom jest-pytest oriented Pytest reporter May 22, 2018 4 - Beta pytest (>=3.3.2) :pypi:`pytest-jinja` A plugin to generate customizable jinja-based HTML reports in pytest Oct 04, 2022 3 - Alpha pytest (>=6.2.5,<7.0.0) - :pypi:`pytest-jira` py.test JIRA integration plugin, using markers Apr 07, 2022 3 - Alpha N/A - :pypi:`pytest-jira-xfail` Plugin skips (xfail) tests if unresolved Jira issue(s) linked Dec 01, 2022 N/A pytest (~=7.2.0) - :pypi:`pytest-jira-xray` pytest plugin to integrate tests with JIRA XRAY May 15, 2023 4 - Beta pytest + :pypi:`pytest-jira` py.test JIRA integration plugin, using markers Jun 12, 2023 3 - Alpha N/A + :pypi:`pytest-jira-xfail` Plugin skips (xfail) tests if unresolved Jira issue(s) linked Jun 19, 2023 N/A pytest (>=7.2.0) + :pypi:`pytest-jira-xray` pytest plugin to integrate tests with JIRA XRAY Jul 11, 2023 4 - Beta pytest :pypi:`pytest-job-selection` A pytest plugin for load balancing test suites Jan 30, 2023 4 - Beta pytest (>=3.5.0) :pypi:`pytest-jobserver` Limit parallel tests with posix jobserver. May 15, 2019 5 - Production/Stable pytest :pypi:`pytest-joke` Test failures are better served with humor. Oct 08, 2019 4 - Beta pytest (>=4.2.1) @@ -600,7 +613,7 @@ This list contains 1259 plugins. :pypi:`pytest-jtr` pytest plugin supporting json test report output Nov 29, 2022 N/A pytest (>=7.1.2,<8.0.0) :pypi:`pytest-jupyter` A pytest plugin for testing Jupyter libraries and extensions. Mar 30, 2023 4 - Beta pytest :pypi:`pytest-jupyterhub` A reusable JupyterHub pytest plugin Apr 25, 2023 5 - Production/Stable pytest - :pypi:`pytest-kafka` Zookeeper, Kafka server, and Kafka consumer fixtures for Pytest Oct 01, 2022 N/A pytest + :pypi:`pytest-kafka` Zookeeper, Kafka server, and Kafka consumer fixtures for Pytest Jun 14, 2023 N/A pytest :pypi:`pytest-kafkavents` A plugin to send pytest events to Kafka Sep 08, 2021 4 - Beta pytest :pypi:`pytest-kasima` Display horizontal lines above and below the captured standard output for easy viewing. Jan 26, 2023 5 - Production/Stable pytest (>=7.2.1,<8.0.0) :pypi:`pytest-keep-together` Pytest plugin to customize test ordering by running all 'related' tests together Dec 07, 2022 5 - Production/Stable pytest @@ -620,7 +633,7 @@ This list contains 1259 plugins. :pypi:`pytest-launchable` Launchable Pytest Plugin Apr 05, 2023 N/A pytest (>=4.2.0) :pypi:`pytest-layab` Pytest fixtures for layab. Oct 05, 2020 5 - Production/Stable N/A :pypi:`pytest-lazy-fixture` It helps to use fixtures in pytest.mark.parametrize Feb 01, 2020 4 - Beta pytest (>=3.2.5) - :pypi:`pytest-lazy-fixtures` Allows you to use fixtures in @pytest.mark.parametrize. Mar 11, 2023 N/A pytest (>=7.2.1,<8.0.0) + :pypi:`pytest-lazy-fixtures` Allows you to use fixtures in @pytest.mark.parametrize. May 28, 2023 N/A pytest (>=7.2.1,<8.0.0) :pypi:`pytest-ldap` python-ldap fixtures for pytest Aug 18, 2020 N/A pytest :pypi:`pytest-leak-finder` Find the test that's leaking before the one that fails Feb 15, 2023 4 - Beta pytest (>=3.5.0) :pypi:`pytest-leaks` A pytest plugin to trace resource leaks. Nov 27, 2019 1 - Planning N/A @@ -639,7 +652,7 @@ This list contains 1259 plugins. :pypi:`pytest-local-badge` Generate local badges (shields) reporting your test suite status. Jan 15, 2023 N/A pytest (>=6.1.0) :pypi:`pytest-localftpserver` A PyTest plugin which provides an FTP fixture for your tests Oct 04, 2022 5 - Production/Stable pytest :pypi:`pytest-localserver` pytest plugin to test server connections locally. Jan 30, 2023 4 - Beta N/A - :pypi:`pytest-localstack` Pytest plugin for AWS integration tests Oct 17, 2022 4 - Beta pytest (>=6.0.0,<7.0.0) + :pypi:`pytest-localstack` Pytest plugin for AWS integration tests Jun 07, 2023 4 - Beta pytest (>=6.0.0,<7.0.0) :pypi:`pytest-lockable` lockable resource plugin for pytest Jul 20, 2022 5 - Production/Stable pytest :pypi:`pytest-locker` Used to lock object during testing. Essentially changing assertions from being hard coded to asserting that nothing changed Oct 29, 2021 N/A pytest (>=5.4) :pypi:`pytest-log` print log Aug 15, 2021 N/A pytest (>=3.8) @@ -649,7 +662,7 @@ This list contains 1259 plugins. :pypi:`pytest-logger` Plugin configuring handlers for loggers from Python logging module. Jul 25, 2019 4 - Beta pytest (>=3.2) :pypi:`pytest-logging` Configures logging and allows tweaking the log level with a py.test flag Nov 04, 2015 4 - Beta N/A :pypi:`pytest-logging-end-to-end-test-tool` Sep 23, 2022 N/A pytest (>=7.1.2,<8.0.0) - :pypi:`pytest-logikal` Common testing environment May 27, 2023 5 - Production/Stable pytest (==7.3.1) + :pypi:`pytest-logikal` Common testing environment Jun 22, 2023 5 - Production/Stable pytest (==7.3.1) :pypi:`pytest-log-report` Package for creating a pytest test run reprot Dec 26, 2019 N/A N/A :pypi:`pytest-loguru` Pytest Loguru Apr 12, 2022 5 - Production/Stable N/A :pypi:`pytest-loop` pytest plugin for looping tests Jul 22, 2022 5 - Production/Stable pytest (>=6) @@ -671,10 +684,10 @@ This list contains 1259 plugins. :pypi:`pytest-maybe-raises` Pytest fixture for optional exception testing. May 27, 2022 N/A pytest ; extra == 'dev' :pypi:`pytest-mccabe` pytest plugin to run the mccabe code complexity checker. Jul 22, 2020 3 - Alpha pytest (>=5.4.0) :pypi:`pytest-md` Plugin for generating Markdown reports for pytest results Jul 11, 2019 3 - Alpha pytest (>=4.2.1) - :pypi:`pytest-md-report` A pytest plugin to make a test results report with Markdown table format. Aug 06, 2022 4 - Beta pytest (!=6.0.0,<8,>=3.3.2) + :pypi:`pytest-md-report` A pytest plugin to make a test results report with Markdown table format. Jun 25, 2023 4 - Beta pytest (!=6.0.0,<8,>=3.3.2) :pypi:`pytest-memlog` Log memory usage during tests May 03, 2023 N/A pytest (>=7.3.0,<8.0.0) :pypi:`pytest-memprof` Estimates memory consumption of test functions Mar 29, 2019 4 - Beta N/A - :pypi:`pytest-memray` A simple plugin to use with pytest Dec 02, 2022 N/A pytest>=7.2 + :pypi:`pytest-memray` A simple plugin to use with pytest Jun 06, 2023 N/A pytest>=7.2 :pypi:`pytest-menu` A pytest plugin for console based interactive test selection just after the collection phase Oct 04, 2017 3 - Alpha pytest (>=2.4.2) :pypi:`pytest-mercurial` pytest plugin to write integration tests for projects using Mercurial Python internals Nov 21, 2020 1 - Planning N/A :pypi:`pytest-mesh` pytest_mesh插件 Aug 05, 2022 N/A pytest (==7.1.2) @@ -682,20 +695,20 @@ This list contains 1259 plugins. :pypi:`pytest-messenger` Pytest to Slack reporting plugin Nov 24, 2022 5 - Production/Stable N/A :pypi:`pytest-metadata` pytest plugin for test session metadata May 27, 2023 5 - Production/Stable pytest>=7.0.0 :pypi:`pytest-metrics` Custom metrics report for pytest Apr 04, 2020 N/A pytest - :pypi:`pytest-mh` Pytest multihost plugin May 25, 2023 N/A pytest + :pypi:`pytest-mh` Pytest multihost plugin Jun 08, 2023 N/A pytest :pypi:`pytest-mimesis` Mimesis integration with the pytest test runner Mar 21, 2020 5 - Production/Stable pytest (>=4.2) :pypi:`pytest-minecraft` A pytest plugin for running tests against Minecraft releases Apr 06, 2022 N/A pytest (>=6.0.1) :pypi:`pytest-mini` A plugin to test mp Feb 06, 2023 N/A pytest (>=7.2.0,<8.0.0) :pypi:`pytest-missing-fixtures` Pytest plugin that creates missing fixtures Oct 14, 2020 4 - Beta pytest (>=3.5.0) :pypi:`pytest-ml` Test your machine learning! May 04, 2019 4 - Beta N/A :pypi:`pytest-mocha` pytest plugin to display test execution output like a mochajs Apr 02, 2020 4 - Beta pytest (>=5.4.0) - :pypi:`pytest-mock` Thin-wrapper around the mock package for easier use with pytest Oct 05, 2022 5 - Production/Stable pytest (>=5.0) + :pypi:`pytest-mock` Thin-wrapper around the mock package for easier use with pytest Jun 15, 2023 5 - Production/Stable pytest (>=5.0) :pypi:`pytest-mock-api` A mock API server with configurable routes and responses available as a fixture. Feb 13, 2019 1 - Planning pytest (>=4.0.0) :pypi:`pytest-mock-generator` A pytest fixture wrapper for https://pypi.org/project/mock-generator May 16, 2022 5 - Production/Stable N/A :pypi:`pytest-mock-helper` Help you mock HTTP call and generate mock code Jan 24, 2018 N/A pytest :pypi:`pytest-mockito` Base fixtures for mockito Jul 11, 2018 4 - Beta N/A :pypi:`pytest-mockredis` An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. Jan 02, 2018 2 - Pre-Alpha N/A - :pypi:`pytest-mock-resources` A pytest plugin for easily instantiating reproducible mock resources. May 23, 2023 N/A pytest (>=1.0) + :pypi:`pytest-mock-resources` A pytest plugin for easily instantiating reproducible mock resources. Jul 12, 2023 N/A pytest (>=1.0) :pypi:`pytest-mock-server` Mock server plugin for pytest Jan 09, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-mockservers` A set of fixtures to test your requests to HTTP/UDP servers Mar 31, 2020 N/A pytest (>=4.3.0) :pypi:`pytest-mocktcp` A pytest plugin for testing TCP clients Oct 11, 2022 N/A pytest @@ -705,7 +718,7 @@ This list contains 1259 plugins. :pypi:`pytest-molecule` PyTest Molecule Plugin :: discover and run molecule tests Mar 29, 2022 5 - Production/Stable pytest (>=7.0.0) :pypi:`pytest-mongo` MongoDB process and client fixtures plugin for Pytest. Jun 07, 2021 5 - Production/Stable pytest :pypi:`pytest-mongodb` pytest plugin for MongoDB fixtures May 16, 2023 5 - Production/Stable N/A - :pypi:`pytest-monitor` Pytest plugin for analyzing resource usage. Oct 22, 2022 5 - Production/Stable pytest + :pypi:`pytest-monitor` Pytest plugin for analyzing resource usage. Jun 25, 2023 5 - Production/Stable pytest :pypi:`pytest-monkeyplus` pytest's monkeypatch subclass with extra functionalities Sep 18, 2012 5 - Production/Stable N/A :pypi:`pytest-monkeytype` pytest-monkeytype: Generate Monkeytype annotations from your pytest tests. Jul 29, 2020 4 - Beta N/A :pypi:`pytest-moto` Fixtures for integration tests of AWS services,uses moto mocking library. Aug 28, 2015 1 - Planning N/A @@ -723,13 +736,13 @@ This list contains 1259 plugins. :pypi:`pytest-mutagen` Add the mutation testing feature to pytest Jul 24, 2020 N/A pytest (>=5.4) :pypi:`pytest-mypy` Mypy static type checker plugin for Pytest Dec 18, 2022 4 - Beta pytest (>=6.2) ; python_version >= "3.10" :pypi:`pytest-mypyd` Mypy static type checker plugin for Pytest Aug 20, 2019 4 - Beta pytest (<4.7,>=2.8) ; python_version < "3.5" - :pypi:`pytest-mypy-plugins` pytest plugin for writing tests for mypy plugins May 05, 2023 4 - Beta pytest (>=6.2.0) + :pypi:`pytest-mypy-plugins` pytest plugin for writing tests for mypy plugins Jun 29, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-mypy-plugins-shim` Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. Apr 12, 2021 N/A pytest>=6.0.0 :pypi:`pytest-mypy-testing` Pytest plugin to check mypy output. Feb 25, 2023 N/A pytest>=7,<8 :pypi:`pytest-mysql` MySQL process and client fixtures for pytest Mar 27, 2023 5 - Production/Stable pytest (>=6.2) :pypi:`pytest-needle` pytest plugin for visual testing websites using selenium Dec 10, 2018 4 - Beta pytest (<5.0.0,>=3.0.0) :pypi:`pytest-neo` pytest-neo is a plugin for pytest that shows tests like screen of Matrix. Jan 08, 2022 3 - Alpha pytest (>=6.2.0) - :pypi:`pytest-netdut` "Automated software testing for switches using pytest" Apr 18, 2023 N/A pytest (>=3.5.0) + :pypi:`pytest-netdut` "Automated software testing for switches using pytest" Jul 06, 2023 N/A pytest (>=3.5.0) :pypi:`pytest-network` A simple plugin to disable network on socket level. May 07, 2020 N/A N/A :pypi:`pytest-network-endpoints` Network endpoints plugin for pytest Mar 06, 2022 N/A pytest :pypi:`pytest-never-sleep` pytest plugin helps to avoid adding tests without mock \`time.sleep\` May 05, 2021 3 - Alpha pytest (>=3.5.1) @@ -737,7 +750,7 @@ This list contains 1259 plugins. :pypi:`pytest-nginx-iplweb` nginx fixture for pytest - iplweb temporary fork Mar 01, 2019 5 - Production/Stable N/A :pypi:`pytest-ngrok` Jan 20, 2022 3 - Alpha pytest :pypi:`pytest-ngsfixtures` pytest ngs fixtures Sep 06, 2019 2 - Pre-Alpha pytest (>=5.0.0) - :pypi:`pytest-nhsd-apim` Pytest plugin accessing NHSDigital's APIM proxies Mar 06, 2023 N/A pytest (==6.2.5) + :pypi:`pytest-nhsd-apim` Pytest plugin accessing NHSDigital's APIM proxies Jul 11, 2023 N/A pytest (==6.2.5) :pypi:`pytest-nice` A pytest plugin that alerts user of failed test cases with screen notifications May 04, 2019 4 - Beta pytest :pypi:`pytest-nice-parametrize` A small snippet for nicer PyTest's Parametrize Apr 17, 2021 5 - Production/Stable N/A :pypi:`pytest-nlcov` Pytest plugin to get the coverage of the new lines (based on git diff) only Jul 07, 2021 N/A N/A @@ -753,7 +766,7 @@ This list contains 1259 plugins. :pypi:`pytest-oar` PyTest plugin for the OAR testing framework May 02, 2023 N/A pytest>=6.0.1 :pypi:`pytest-object-getter` Import any object from a 3rd party module while mocking its namespace on demand. Jul 31, 2022 5 - Production/Stable pytest :pypi:`pytest-ochrus` pytest results data-base and HTML reporter Feb 21, 2018 4 - Beta N/A - :pypi:`pytest-odoo` py.test plugin to run Odoo tests Nov 17, 2022 4 - Beta pytest (>=7.2.0) + :pypi:`pytest-odoo` py.test plugin to run Odoo tests Jul 06, 2023 4 - Beta pytest (>=7.2.0) :pypi:`pytest-odoo-fixtures` Project description Jun 25, 2019 N/A N/A :pypi:`pytest-oerp` pytest plugin to test OpenERP modules Feb 28, 2012 3 - Alpha N/A :pypi:`pytest-offline` Mar 09, 2023 1 - Planning pytest (>=7.0.0,<8.0.0) @@ -797,9 +810,9 @@ This list contains 1259 plugins. :pypi:`pytest-pep257` py.test plugin for pep257 Jul 09, 2016 N/A N/A :pypi:`pytest-pep8` pytest plugin to check PEP8 requirements Apr 27, 2014 N/A N/A :pypi:`pytest-percent` Change the exit code of pytest test sessions when a required percent of tests pass. May 21, 2020 N/A pytest (>=5.2.0) - :pypi:`pytest-perf` pytest-perf Jun 23, 2022 5 - Production/Stable pytest (>=6) ; extra == 'testing' + :pypi:`pytest-perf` Run performance tests against the mainline code. Jun 02, 2023 5 - Production/Stable pytest (>=6) ; extra == 'testing' :pypi:`pytest-performance` A simple plugin to ensure the execution of critical sections of code has not been impacted Sep 11, 2020 5 - Production/Stable pytest (>=3.7.0) - :pypi:`pytest-persistence` Pytest tool for persistent objects May 16, 2023 N/A N/A + :pypi:`pytest-persistence` Pytest tool for persistent objects Jul 04, 2023 N/A N/A :pypi:`pytest-pg` A tiny plugin for pytest which runs PostgreSQL in Docker May 04, 2023 5 - Production/Stable pytest (>=6.0.0) :pypi:`pytest-pgsql` Pytest plugins and helpers for tests using a Postgres database. May 13, 2020 5 - Production/Stable pytest (>=3.0.0) :pypi:`pytest-phmdoctest` pytest plugin to test Python examples in Markdown using phmdoctest. Apr 15, 2022 4 - Beta pytest (>=5.4.3) @@ -816,6 +829,7 @@ This list contains 1259 plugins. :pypi:`pytest-play` pytest plugin that let you automate actions and assertions with test metrics reporting executing plain YAML files Jun 12, 2019 5 - Production/Stable N/A :pypi:`pytest-playbook` Pytest plugin for reading playbooks. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) :pypi:`pytest-playwright` A pytest wrapper with fixtures for Playwright to automate web browsers Apr 24, 2023 N/A pytest (<8.0.0,>=6.2.4) + :pypi:`pytest-playwright-async` ASYNC Pytest plugin for Playwright Jul 03, 2023 N/A N/A :pypi:`pytest-playwrights` A pytest wrapper with fixtures for Playwright to automate web browsers Dec 02, 2021 N/A N/A :pypi:`pytest-playwright-snapshot` A pytest wrapper for snapshot testing with playwright Aug 19, 2021 N/A N/A :pypi:`pytest-playwright-visual` A pytest fixture for visual testing with Playwright Apr 28, 2022 N/A N/A @@ -833,6 +847,7 @@ This list contains 1259 plugins. :pypi:`pytest-poo` Visualize your crappy tests Mar 25, 2021 5 - Production/Stable pytest (>=2.3.4) :pypi:`pytest-poo-fail` Visualize your failed tests with poo Feb 12, 2015 5 - Production/Stable N/A :pypi:`pytest-pop` A pytest plugin to help with testing pop projects May 09, 2023 5 - Production/Stable pytest + :pypi:`pytest-porringer` Jun 24, 2023 N/A pytest>=7.1.2 :pypi:`pytest-portion` Select a portion of the collected tests Jan 28, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-postgres` Run PostgreSQL in Docker container in Pytest. Mar 22, 2020 N/A pytest :pypi:`pytest-postgresql` Postgresql fixtures and fixture factories for Pytest. May 20, 2023 5 - Production/Stable pytest (>=6.2) @@ -842,7 +857,7 @@ This list contains 1259 plugins. :pypi:`pytest-pretty` pytest plugin for printing summary data as I want it Apr 05, 2023 5 - Production/Stable pytest>=7 :pypi:`pytest-pretty-terminal` pytest plugin for generating prettier terminal output Jan 31, 2022 N/A pytest (>=3.4.1) :pypi:`pytest-pride` Minitest-style test colors Apr 02, 2016 3 - Alpha N/A - :pypi:`pytest-print` pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Dec 28, 2021 5 - Production/Stable pytest (>=6) + :pypi:`pytest-print` pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) Jun 28, 2023 5 - Production/Stable pytest>=7.3.2 :pypi:`pytest-profiles` pytest plugin for configuration profiles Dec 09, 2021 4 - Beta pytest (>=3.7.0) :pypi:`pytest-profiling` Profiling plugin for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-progress` pytest plugin for instant test progress status Jan 31, 2022 5 - Production/Stable N/A @@ -863,14 +878,14 @@ This list contains 1259 plugins. :pypi:`pytest-pydocstyle` pytest plugin to run pydocstyle Jan 05, 2023 3 - Alpha N/A :pypi:`pytest-pylint` pytest plugin to check source code with pylint Sep 10, 2022 5 - Production/Stable pytest (>=5.4) :pypi:`pytest-pymysql-autorecord` Record PyMySQL queries and mock with the stored data. Sep 02, 2022 N/A N/A - :pypi:`pytest-pyodide` "Pytest plugin for testing applications that use Pyodide" May 10, 2023 N/A pytest + :pypi:`pytest-pyodide` "Pytest plugin for testing applications that use Pyodide" Jun 19, 2023 N/A pytest :pypi:`pytest-pypi` Easily test your HTTP library against a local copy of pypi Mar 04, 2018 3 - Alpha N/A :pypi:`pytest-pypom-navigation` Core engine for cookiecutter-qa and pytest-play packages Feb 18, 2019 4 - Beta pytest (>=3.0.7) :pypi:`pytest-pyppeteer` A plugin to run pyppeteer in pytest Apr 28, 2022 N/A pytest (>=6.2.5,<7.0.0) :pypi:`pytest-pyq` Pytest fixture "q" for pyq Mar 10, 2020 5 - Production/Stable N/A :pypi:`pytest-pyramid` pytest_pyramid - provides fixtures for testing pyramid applications with pytest test suite Dec 13, 2022 5 - Production/Stable pytest :pypi:`pytest-pyramid-server` Pyramid server fixture for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-pyreport` PyReport is a lightweight reporting plugin for Pytest that provides concise HTML report May 08, 2023 N/A pytest (>=7.3.1) + :pypi:`pytest-pyreport` PyReport is a lightweight reporting plugin for Pytest that provides concise HTML report Jul 02, 2023 N/A pytest (>=7.3.1) :pypi:`pytest-pyright` Pytest plugin for type checking code with Pyright Nov 20, 2022 4 - Beta pytest (>=7.0.0) :pypi:`pytest-pyspec` A plugin that transforms the pytest output into a result similar to the RSpec. It enables the use of docstrings to display results and also enables the use of the prefixes "describe", "with" and "it". Mar 12, 2023 5 - Production/Stable pytest (>=7.2.1,<8.0.0) :pypi:`pytest-pystack` Plugin to run pystack after a timeout for a test suite. May 07, 2023 N/A pytest (>=3.5.0) @@ -881,14 +896,14 @@ This list contains 1259 plugins. :pypi:`pytest-qaseio` Pytest plugin for Qase.io integration May 11, 2023 4 - Beta pytest (>=7.2.2,<8.0.0) :pypi:`pytest-qasync` Pytest support for qasync. Jul 12, 2021 4 - Beta pytest (>=5.4.0) :pypi:`pytest-qatouch` Pytest plugin for uploading test results to your QA Touch Testrun. Feb 14, 2023 4 - Beta pytest (>=6.2.0) - :pypi:`pytest-qgis` A pytest plugin for testing QGIS python plugins Jun 26, 2022 5 - Production/Stable pytest (>=6.2.3) + :pypi:`pytest-qgis` A pytest plugin for testing QGIS python plugins Jun 30, 2023 5 - Production/Stable pytest (>=6.2.5) :pypi:`pytest-qml` Run QML Tests with pytest Dec 02, 2020 4 - Beta pytest (>=6.0.0) :pypi:`pytest-qr` pytest plugin to generate test result QR codes Nov 25, 2021 4 - Beta N/A :pypi:`pytest-qt` pytest support for PyQt and PySide applications Oct 25, 2022 5 - Production/Stable pytest (>=3.0.0) :pypi:`pytest-qt-app` QT app fixture for py.test Dec 23, 2015 5 - Production/Stable N/A :pypi:`pytest-quarantine` A plugin for pytest to manage expected test failures Nov 24, 2019 5 - Production/Stable pytest (>=4.6) :pypi:`pytest-quickcheck` pytest plugin to generate random data inspired by QuickCheck Nov 05, 2022 4 - Beta pytest (>=4.0) - :pypi:`pytest-rabbitmq` RabbitMQ process and client fixtures for pytest Feb 11, 2022 5 - Production/Stable pytest (>=3.0.0) + :pypi:`pytest-rabbitmq` RabbitMQ process and client fixtures for pytest Jul 05, 2023 5 - Production/Stable pytest (>=6.2) :pypi:`pytest-race` Race conditions tester for pytest Jun 07, 2022 4 - Beta N/A :pypi:`pytest-rage` pytest plugin to implement PEP712 Oct 21, 2011 3 - Alpha N/A :pypi:`pytest-rail` pytest plugin for creating TestRail runs and adding results May 02, 2022 N/A pytest (>=3.6) @@ -897,7 +912,7 @@ This list contains 1259 plugins. :pypi:`pytest-raisesregexp` Simple pytest plugin to look for regex in Exceptions Dec 18, 2015 N/A N/A :pypi:`pytest-raisin` Plugin enabling the use of exception instances with pytest.raises Feb 06, 2022 N/A pytest :pypi:`pytest-random` py.test plugin to randomize tests Apr 28, 2013 3 - Alpha N/A - :pypi:`pytest-randomly` Pytest plugin to randomly order tests and control random.seed. May 11, 2022 5 - Production/Stable pytest + :pypi:`pytest-randomly` Pytest plugin to randomly order tests and control random.seed. Jul 10, 2023 5 - Production/Stable pytest :pypi:`pytest-randomness` Pytest plugin about random seed management May 30, 2019 3 - Alpha N/A :pypi:`pytest-random-num` Randomise the order in which pytest tests are run with some control over the randomness Oct 19, 2020 5 - Production/Stable N/A :pypi:`pytest-random-order` Randomise the order in which pytest tests are run with some control over the randomness Dec 03, 2022 5 - Production/Stable pytest (>=3.0.0) @@ -911,7 +926,7 @@ This list contains 1259 plugins. :pypi:`pytest-redmine` Pytest plugin for redmine Mar 19, 2018 1 - Planning N/A :pypi:`pytest-ref` A plugin to store reference files to ease regression testing Nov 23, 2019 4 - Beta pytest (>=3.5.0) :pypi:`pytest-reference-formatter` Conveniently run pytest with a dot-formatted test reference. Oct 01, 2019 4 - Beta N/A - :pypi:`pytest-regex` Select pytest tests with regular expressions May 23, 2023 4 - Beta pytest (>=3.5.0) + :pypi:`pytest-regex` Select pytest tests with regular expressions May 29, 2023 4 - Beta pytest (>=3.5.0) :pypi:`pytest-regex-dependency` Management of Pytest dependencies via regex patterns Jun 12, 2022 N/A pytest :pypi:`pytest-regressions` Easy to use fixtures to write regression tests. Jan 13, 2023 5 - Production/Stable pytest (>=6.2.0) :pypi:`pytest-regtest` pytest plugin for regression tests Jul 08, 2022 N/A N/A @@ -920,21 +935,22 @@ This list contains 1259 plugins. :pypi:`pytest-remfiles` Pytest plugin to create a temporary directory with remote files Jul 01, 2019 5 - Production/Stable N/A :pypi:`pytest-remotedata` Pytest plugin for controlling remote data access. Dec 12, 2022 3 - Alpha pytest (>=4.6) :pypi:`pytest-remote-response` Pytest plugin for capturing and mocking connection requests. Apr 26, 2023 5 - Production/Stable pytest (>=4.6) - :pypi:`pytest-remove-stale-bytecode` py.test plugin to remove stale byte code files. Mar 04, 2020 4 - Beta pytest + :pypi:`pytest-remove-stale-bytecode` py.test plugin to remove stale byte code files. Jul 07, 2023 4 - Beta pytest :pypi:`pytest-reorder` Reorder tests depending on their paths and names. May 31, 2018 4 - Beta pytest :pypi:`pytest-repeat` pytest plugin for repeating tests Oct 31, 2020 5 - Production/Stable pytest (>=3.6) :pypi:`pytest-replay` Saves previous test runs and allow re-execute previous pytest runs to reproduce crashes or flaky tests Jun 09, 2021 4 - Beta pytest (>=3.0.0) :pypi:`pytest-repo-health` A pytest plugin to report on repository standards conformance Apr 17, 2023 3 - Alpha pytest :pypi:`pytest-report` Creates json report that is compatible with atom.io's linter message format May 11, 2016 4 - Beta N/A :pypi:`pytest-reporter` Generate Pytest reports with templates Jul 22, 2021 4 - Beta pytest - :pypi:`pytest-reporter-html1` A basic HTML report template for Pytest Jun 08, 2021 4 - Beta N/A + :pypi:`pytest-reporter-html1` A basic HTML report template for Pytest Jun 05, 2023 4 - Beta N/A :pypi:`pytest-reporter-html-dots` A basic HTML report for pytest using Jinja2 template engine. Jan 22, 2023 N/A N/A :pypi:`pytest-reportinfra` Pytest plugin for reportinfra Aug 11, 2019 3 - Alpha N/A :pypi:`pytest-reporting` A plugin to report summarized results in a table format Oct 25, 2019 4 - Beta pytest (>=3.5.0) :pypi:`pytest-reportlog` Replacement for the --resultlog option, focused in simplicity and extensibility May 22, 2023 3 - Alpha pytest :pypi:`pytest-report-me` A pytest plugin to generate report. Dec 31, 2020 N/A pytest :pypi:`pytest-report-parameters` pytest plugin for adding tests' parameters to junit report Jun 18, 2020 3 - Alpha pytest (>=2.4.2) - :pypi:`pytest-reportportal` Agent for Reporting results of tests to the Report Portal May 24, 2023 N/A pytest (>=3.8.0) + :pypi:`pytest-reportportal` Agent for Reporting results of tests to the Report Portal Jun 30, 2023 N/A pytest (>=3.8.0) + :pypi:`pytest-reports` An interesting python package Jun 07, 2023 N/A N/A :pypi:`pytest-reqs` pytest plugin to check pinned requirements May 12, 2019 N/A pytest (>=2.4.2) :pypi:`pytest-requests` A simple plugin to use with pytest Jun 24, 2019 4 - Beta pytest (>=3.5.0) :pypi:`pytest-requestselapsed` collect and show http requests elapsed time Aug 14, 2022 N/A N/A @@ -942,17 +958,17 @@ This list contains 1259 plugins. :pypi:`pytest-requires` A pytest plugin to elegantly skip tests with optional requirements Dec 21, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-reraise` Make multi-threaded pytest test cases fail when they should Sep 20, 2022 5 - Production/Stable pytest (>=4.6) :pypi:`pytest-rerun` Re-run only changed files in specified branch Jul 08, 2019 N/A pytest (>=3.6) - :pypi:`pytest-rerunfailures` pytest plugin to re-run tests to eliminate flaky failures Mar 09, 2023 5 - Production/Stable pytest (>=5.3) + :pypi:`pytest-rerunfailures` pytest plugin to re-run tests to eliminate flaky failures Jul 05, 2023 5 - Production/Stable pytest (>=6.2) :pypi:`pytest-rerunfailures-all-logs` pytest plugin to re-run tests to eliminate flaky failures Mar 07, 2022 5 - Production/Stable N/A :pypi:`pytest-reserial` Pytest fixture for recording and replaying serial port traffic. Apr 26, 2023 4 - Beta pytest - :pypi:`pytest-resilient-circuits` Resilient Circuits fixtures for PyTest May 05, 2023 N/A pytest (~=4.6) ; python_version == "2.7" + :pypi:`pytest-resilient-circuits` Resilient Circuits fixtures for PyTest Jul 11, 2023 N/A pytest (~=4.6) ; python_version == "2.7" :pypi:`pytest-resource` Load resource fixture plugin to use with pytest Nov 14, 2018 4 - Beta N/A :pypi:`pytest-resource-path` Provides path for uniform access to test resources in isolated directory May 01, 2021 5 - Production/Stable pytest (>=3.5.0) :pypi:`pytest-resource-usage` Pytest plugin for reporting running time and peak memory usage Nov 06, 2022 5 - Production/Stable pytest>=7.0.0 :pypi:`pytest-responsemock` Simplified requests calls mocking for pytest Mar 10, 2022 5 - Production/Stable N/A :pypi:`pytest-responses` py.test integration for responses Oct 11, 2022 N/A pytest (>=2.5) :pypi:`pytest-rest-api` Aug 08, 2022 N/A pytest (>=7.1.2,<8.0.0) - :pypi:`pytest-restrict` Pytest plugin to restrict the test types allowed May 11, 2022 5 - Production/Stable pytest + :pypi:`pytest-restrict` Pytest plugin to restrict the test types allowed Jul 10, 2023 5 - Production/Stable pytest :pypi:`pytest-result-log` Write the execution result of the case to the log Apr 17, 2023 N/A pytest>=7.2.0 :pypi:`pytest-result-sender` Apr 20, 2023 N/A pytest>=7.3.1 :pypi:`pytest-resume` A Pytest plugin to resuming from the last run test Apr 22, 2023 4 - Beta pytest (>=7.0) @@ -960,10 +976,10 @@ This list contains 1259 plugins. :pypi:`pytest-retry` Adds the ability to retry flaky tests in CI environments Aug 16, 2022 N/A pytest (>=7.0.0) :pypi:`pytest-retry-class` A pytest plugin to rerun entire class on failure Mar 25, 2023 N/A pytest (>=5.3) :pypi:`pytest-reusable-testcases` Apr 28, 2023 N/A N/A - :pypi:`pytest-reverse` Pytest plugin to reverse test order. May 11, 2022 5 - Production/Stable pytest + :pypi:`pytest-reverse` Pytest plugin to reverse test order. Jul 10, 2023 5 - Production/Stable pytest :pypi:`pytest-rich` Leverage rich for richer test session output Mar 03, 2022 4 - Beta pytest (>=7.0) :pypi:`pytest-rich-reporter` A pytest plugin using Rich for beautiful test result formatting. Feb 17, 2022 1 - Planning pytest (>=5.0.0) - :pypi:`pytest-richtrace` Nov 05, 2022 N/A pytest (>=7.2.0,<8.0.0) + :pypi:`pytest-richtrace` A pytest plugin that displays the names and information of the pytest hook functions as they are executed. Jun 20, 2023 N/A N/A :pypi:`pytest-ringo` pytest plugin to test webapplications using the Ringo webframework Sep 27, 2017 3 - Alpha N/A :pypi:`pytest-rmsis` Sycronise pytest results to Jira RMsis Aug 10, 2022 N/A pytest (>=5.3.5) :pypi:`pytest-rng` Fixtures for seeding tests and making randomness reproducible Aug 08, 2019 5 - Production/Stable pytest @@ -974,13 +990,14 @@ This list contains 1259 plugins. :pypi:`pytest-rst` Test code from RST documents with pytest Jan 26, 2023 N/A N/A :pypi:`pytest-rt` pytest data collector plugin for Testgr May 05, 2022 N/A N/A :pypi:`pytest-rts` Coverage-based regression test selection (RTS) plugin for pytest May 17, 2021 N/A pytest - :pypi:`pytest-ruff` pytest plugin to check ruff requirements. Apr 28, 2023 4 - Beta N/A + :pypi:`pytest-ruff` pytest plugin to check ruff requirements. Jun 08, 2023 4 - Beta N/A :pypi:`pytest-run-changed` Pytest plugin that runs changed tests only Apr 02, 2021 3 - Alpha pytest :pypi:`pytest-runfailed` implement a --failed option for pytest Mar 24, 2016 N/A N/A :pypi:`pytest-runner` Invoke py.test as distutils command with dependency resolution Feb 25, 2022 5 - Production/Stable pytest (>=6) ; extra == 'testing' :pypi:`pytest-run-subprocess` Pytest Plugin for running and testing subprocesses. Nov 12, 2022 5 - Production/Stable pytest :pypi:`pytest-runtime-types` Checks type annotations on runtime while running tests. Feb 09, 2023 N/A pytest :pypi:`pytest-runtime-xfail` Call runtime_xfail() to mark running test as xfail. Aug 26, 2021 N/A pytest>=5.0.0 + :pypi:`pytest-runtime-yoyo` run case mark timeout Jun 12, 2023 N/A pytest (>=7.2.0) :pypi:`pytest-ry-demo1` 测试 Mar 26, 2023 N/A N/A :pypi:`pytest-saccharin` pytest-saccharin is a updated fork of pytest-sugar, a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). Oct 31, 2022 3 - Alpha N/A :pypi:`pytest-salt` Pytest Salt Plugin Jan 27, 2020 4 - Beta N/A @@ -991,14 +1008,14 @@ This list contains 1259 plugins. :pypi:`pytest-sanic` a pytest plugin for Sanic Oct 25, 2021 N/A pytest (>=5.2) :pypi:`pytest-sanity` Dec 07, 2020 N/A N/A :pypi:`pytest-sa-pg` May 14, 2019 N/A N/A - :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. May 25, 2023 5 - Production/Stable N/A + :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Jul 13, 2023 5 - Production/Stable N/A :pypi:`pytest-scenario` pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A :pypi:`pytest-schedule` The job of test scheduling for humans. Jan 07, 2023 5 - Production/Stable N/A :pypi:`pytest-schema` 👍 Validate return values against a schema-like object in testing Mar 14, 2022 5 - Production/Stable pytest (>=3.5.0) :pypi:`pytest-securestore` An encrypted password store for use within pytest cases Nov 08, 2021 4 - Beta N/A :pypi:`pytest-select` A pytest plugin which allows to (de-)select tests from a file. Jan 18, 2019 3 - Alpha pytest (>=3.0) - :pypi:`pytest-selenium` pytest plugin for Selenium Sep 21, 2022 5 - Production/Stable pytest (>=6.0.0,<7.0.0) - :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. May 25, 2023 5 - Production/Stable N/A + :pypi:`pytest-selenium` pytest plugin for Selenium May 28, 2023 5 - Production/Stable pytest>=6.0.0 + :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Jul 13, 2023 5 - Production/Stable N/A :pypi:`pytest-selenium-enhancer` pytest plugin for Selenium Apr 29, 2022 5 - Production/Stable N/A :pypi:`pytest-selenium-pdiff` A pytest package implementing perceptualdiff for Selenium tests. Apr 06, 2017 2 - Pre-Alpha N/A :pypi:`pytest-send-email` Send pytest execution result email Dec 04, 2019 N/A N/A @@ -1006,7 +1023,7 @@ This list contains 1259 plugins. :pypi:`pytest-sequence-markers` Pytest plugin for sequencing markers for execution of tests May 23, 2023 5 - Production/Stable N/A :pypi:`pytest-server-fixtures` Extensible server fixures for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-serverless` Automatically mocks resources from serverless.yml in pytest using moto. May 09, 2022 4 - Beta N/A - :pypi:`pytest-servers` pytest servers Apr 15, 2023 3 - Alpha pytest (>=6.2) + :pypi:`pytest-servers` pytest servers Jul 13, 2023 3 - Alpha pytest (>=6.2) :pypi:`pytest-services` Services plugin for pytest testing framework Oct 30, 2020 6 - Mature N/A :pypi:`pytest-session2file` pytest-session2file (aka: pytest-session_to_file for v0.1.0 - v0.1.2) is a py.test plugin for capturing and saving to file the stdout of py.test. Jan 26, 2021 3 - Alpha pytest :pypi:`pytest-session-fixture-globalize` py.test plugin to make session fixtures behave as if written in conftest, even if it is written in some modules May 15, 2018 4 - Beta N/A @@ -1017,7 +1034,7 @@ This list contains 1259 plugins. :pypi:`pytest-share-hdf` Plugin to save test data in HDF files and retrieve them for comparison Sep 21, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-sharkreport` this is pytest report plugin. Jul 11, 2022 N/A pytest (>=3.5) :pypi:`pytest-shell` A pytest plugin to help with testing shell scripts / black box commands Mar 27, 2022 N/A N/A - :pypi:`pytest-shell-utilities` Pytest plugin to simplify running shell commands against the system Sep 23, 2022 4 - Beta pytest (>=6.0.0) + :pypi:`pytest-shell-utilities` Pytest plugin to simplify running shell commands against the system Jul 02, 2023 5 - Production/Stable pytest (>=7.1.0) :pypi:`pytest-sheraf` Versatile ZODB abstraction layer - pytest fixtures Feb 11, 2020 N/A pytest :pypi:`pytest-sherlock` pytest plugin help to find coupled tests Jan 16, 2023 5 - Production/Stable pytest (>=3.5.1) :pypi:`pytest-shortcuts` Expand command-line shortcuts listed in pytest configuration Oct 29, 2020 4 - Beta pytest (>=3.5.0) @@ -1038,6 +1055,7 @@ This list contains 1259 plugins. :pypi:`pytest-smartcov` Smart coverage plugin for pytest. Sep 30, 2017 3 - Alpha N/A :pypi:`pytest-smell` Automated bad smell detection tool for Pytest Jun 26, 2022 N/A N/A :pypi:`pytest-smtp` Send email with pytest execution result Feb 20, 2021 N/A pytest + :pypi:`pytest-smtp4dev` Plugin for smtp4dev API Jun 27, 2023 5 - Production/Stable N/A :pypi:`pytest-smtpd` An SMTP server for testing built on aiosmtpd May 15, 2023 N/A pytest :pypi:`pytest-snail` Plugin for adding a marker to slow running tests. 🐌 Nov 04, 2019 3 - Alpha pytest (>=5.0.1) :pypi:`pytest-snapci` py.test plugin for Snap-CI Nov 12, 2015 N/A N/A @@ -1065,7 +1083,7 @@ This list contains 1259 plugins. :pypi:`pytest-splitio` Split.io SDK integration for e2e tests Sep 22, 2020 N/A pytest (<7,>=5.0) :pypi:`pytest-split-tests` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Forked from Mark Adams' original project pytest-test-groups. Jul 30, 2021 5 - Production/Stable pytest (>=2.5) :pypi:`pytest-split-tests-tresorit` Feb 22, 2021 1 - Planning N/A - :pypi:`pytest-splunk-addon` A Dynamic test tool for Splunk Apps and Add-ons Feb 22, 2023 N/A pytest (>5.4.0,<7.3) + :pypi:`pytest-splunk-addon` A Dynamic test tool for Splunk Apps and Add-ons Jun 30, 2023 N/A pytest (>5.4.0,<8) :pypi:`pytest-splunk-addon-ui-smartx` Library to support testing Splunk Add-on UX Mar 07, 2023 N/A N/A :pypi:`pytest-splunk-env` pytest fixtures for interaction with Splunk Enterprise and Splunk Cloud Oct 22, 2020 N/A pytest (>=6.1.1,<7.0.0) :pypi:`pytest-sqitch` sqitch for pytest Apr 06, 2020 4 - Beta N/A @@ -1098,6 +1116,7 @@ This list contains 1259 plugins. :pypi:`pytest-subunit` pytest-subunit is a plugin for py.test which outputs testsresult in subunit format. Aug 29, 2017 N/A N/A :pypi:`pytest-sugar` pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly). Apr 10, 2023 4 - Beta pytest (>=6.2.0) :pypi:`pytest-suitemanager` A simple plugin to use with pytest Apr 28, 2023 4 - Beta N/A + :pypi:`pytest-supercov` Pytest plugin for measuring explicit test-file to source-file coverage Jul 02, 2023 N/A N/A :pypi:`pytest-svn` SVN repository fixture for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-symbols` pytest-symbols is a pytest plugin that adds support for passing test environment symbols into pytest tests. Nov 20, 2017 3 - Alpha N/A :pypi:`pytest-system-statistics` Pytest plugin to track and report system usage statistics Feb 16, 2022 5 - Production/Stable pytest (>=6.0.0) @@ -1106,17 +1125,17 @@ This list contains 1259 plugins. :pypi:`pytest-takeltest` Fixtures for ansible, testinfra and molecule Feb 15, 2023 N/A N/A :pypi:`pytest-talisker` Nov 28, 2021 N/A N/A :pypi:`pytest-tally` A Pytest plugin to generate realtime summary stats, and display them in-console using a text-based dashboard. May 22, 2023 4 - Beta pytest (>=6.2.5) - :pypi:`pytest-tap` Test Anything Protocol (TAP) reporting plugin for pytest Oct 27, 2021 5 - Production/Stable pytest (>=3.0) + :pypi:`pytest-tap` Test Anything Protocol (TAP) reporting plugin for pytest Jul 15, 2023 5 - Production/Stable pytest (>=3.0) :pypi:`pytest-tape` easy assertion with expected results saved to yaml files Mar 17, 2021 4 - Beta N/A :pypi:`pytest-target` Pytest plugin for remote target orchestration. Jan 21, 2021 3 - Alpha pytest (>=6.1.2,<7.0.0) :pypi:`pytest-tblineinfo` tblineinfo is a py.test plugin that insert the node id in the final py.test report when --tb=line option is used Dec 01, 2015 3 - Alpha pytest (>=2.0) :pypi:`pytest-tcpclient` A pytest plugin for testing TCP clients Nov 16, 2022 N/A pytest (<8,>=7.1.3) :pypi:`pytest-teamcity-logblock` py.test plugin to introduce block structure in teamcity build log, if output is not captured May 15, 2018 4 - Beta N/A :pypi:`pytest-telegram` Pytest to Telegram reporting plugin Dec 10, 2020 5 - Production/Stable N/A - :pypi:`pytest-telegram-notifier` Telegram notification plugin for Pytest Mar 17, 2023 5 - Production/Stable N/A + :pypi:`pytest-telegram-notifier` Telegram notification plugin for Pytest Jun 27, 2023 5 - Production/Stable N/A :pypi:`pytest-tempdir` Predictable and repeatable tempdir support. Oct 11, 2019 4 - Beta pytest (>=2.8.1) :pypi:`pytest-terra-fixt` Terraform and Terragrunt fixtures for pytest Sep 15, 2022 N/A pytest (==6.2.5) - :pypi:`pytest-terraform` A pytest plugin for using terraform fixtures Sep 01, 2022 N/A pytest (>=6.0) + :pypi:`pytest-terraform` A pytest plugin for using terraform fixtures Jun 20, 2023 N/A pytest (>=6.0) :pypi:`pytest-terraform-fixture` generate terraform resources to use with pytest Nov 14, 2018 4 - Beta N/A :pypi:`pytest-testbook` A plugin to run tests written in Jupyter notebook Dec 11, 2016 3 - Alpha N/A :pypi:`pytest-testconfig` Test configuration plugin for pytest. Jan 11, 2020 4 - Beta pytest (>=3.5.0) @@ -1126,7 +1145,7 @@ This list contains 1259 plugins. :pypi:`pytest-test-groups` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Oct 25, 2016 5 - Production/Stable N/A :pypi:`pytest-testinfra` Test infrastructures May 21, 2023 5 - Production/Stable pytest (!=3.0.2) :pypi:`pytest-testlink-adaptor` pytest reporting plugin for testlink Dec 20, 2018 4 - Beta pytest (>=2.6) - :pypi:`pytest-testmon` selects tests affected by changed files and methods May 18, 2023 4 - Beta pytest (<8,>=5) + :pypi:`pytest-testmon` selects tests affected by changed files and methods Jul 13, 2023 4 - Beta pytest (<8,>=5) :pypi:`pytest-testmon-dev` selects tests affected by changed files and methods Mar 30, 2023 4 - Beta pytest (<8,>=5) :pypi:`pytest-testmon-oc` nOly selects tests affected by changed files and methods Jun 01, 2022 4 - Beta pytest (<8,>=5) :pypi:`pytest-testmon-skip-libraries` selects tests affected by changed files and methods Mar 03, 2023 4 - Beta pytest (<8,>=5) @@ -1148,17 +1167,21 @@ This list contains 1259 plugins. :pypi:`pytest-test-this` Plugin for py.test to run relevant tests, based on naively checking if a test contains a reference to the symbol you supply Sep 15, 2019 2 - Pre-Alpha pytest (>=2.3) :pypi:`pytest-test-utils` Jul 14, 2022 N/A pytest (>=5) :pypi:`pytest-tesults` Tesults plugin for pytest Dec 23, 2022 5 - Production/Stable pytest (>=3.5.0) + :pypi:`pytest-textual-snapshot` Snapshot testing for Textual apps Jul 12, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-tezos` pytest-ligo Jan 16, 2020 4 - Beta N/A :pypi:`pytest-th2-bdd` pytest_th2_bdd May 13, 2022 N/A N/A :pypi:`pytest-thawgun` Pytest plugin for time travel May 26, 2020 3 - Alpha N/A + :pypi:`pytest-thread` Jul 07, 2023 N/A N/A :pypi:`pytest-threadleak` Detects thread leaks Jul 03, 2022 4 - Beta pytest (>=3.1.1) :pypi:`pytest-tick` Ticking on tests Aug 31, 2021 5 - Production/Stable pytest (>=6.2.5,<7.0.0) + :pypi:`pytest-time` Jun 24, 2023 3 - Alpha pytest :pypi:`pytest-timeit` A pytest plugin to time test function runs Oct 13, 2016 4 - Beta N/A :pypi:`pytest-timeout` pytest plugin to abort hanging tests Jan 18, 2022 5 - Production/Stable pytest (>=5.0.0) :pypi:`pytest-timeouts` Linux-only Pytest plugin to control durations of various test case execution phases Sep 21, 2019 5 - Production/Stable N/A :pypi:`pytest-timer` A timer plugin for pytest Jun 02, 2021 N/A N/A :pypi:`pytest-timestamper` Pytest plugin to add a timestamp prefix to the pytest output Jun 06, 2021 N/A N/A :pypi:`pytest-timestamps` A simple plugin to view timestamps for each test Apr 01, 2023 N/A pytest (>=5.2) + :pypi:`pytest-tinybird` A pytest plugin to report test results to tinybird Jun 26, 2023 4 - Beta pytest (>=3.8.0) :pypi:`pytest-tipsi-django` Nov 17, 2021 4 - Beta pytest (>=6.0.0) :pypi:`pytest-tipsi-testing` Better fixtures management. Various helpers Nov 04, 2020 4 - Beta pytest (>=3.3.0) :pypi:`pytest-tldr` A pytest plugin that limits the output to just the things you need. Oct 26, 2022 4 - Beta pytest (>=3.5.0) @@ -1189,13 +1212,13 @@ This list contains 1259 plugins. :pypi:`pytest-tspwplib` A simple plugin to use with tspwplib Jan 08, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-tst` Customize pytest options, output and exit code to make it compatible with tst Apr 27, 2022 N/A pytest (>=5.0.0) :pypi:`pytest-tstcls` Test Class Base Mar 23, 2020 5 - Production/Stable N/A - :pypi:`pytest-tui` Text User Interface (TUI) and HTML report for Pytest test runs Apr 06, 2023 4 - Beta N/A + :pypi:`pytest-tui` Text User Interface (TUI) and HTML report for Pytest test runs Jun 12, 2023 4 - Beta N/A :pypi:`pytest-tutorials` Mar 11, 2023 N/A N/A :pypi:`pytest-twilio-conversations-client-mock` Aug 02, 2022 N/A N/A :pypi:`pytest-twisted` A twisted plugin for pytest. Oct 16, 2022 5 - Production/Stable pytest (>=2.3) :pypi:`pytest-typechecker` Run type checkers on specified test files Feb 04, 2022 N/A pytest (>=6.2.5,<7.0.0) :pypi:`pytest-typhoon-config` A Typhoon HIL plugin that facilitates test parameter configuration at runtime Apr 07, 2022 5 - Production/Stable N/A - :pypi:`pytest-typhoon-xray` Typhoon HIL plugin for pytest Nov 04, 2022 4 - Beta N/A + :pypi:`pytest-typhoon-xray` Typhoon HIL plugin for pytest Jun 10, 2023 4 - Beta N/A :pypi:`pytest-tytest` Typhoon HIL plugin for pytest May 25, 2020 4 - Beta pytest (>=5.4.2) :pypi:`pytest-ubersmith` Easily mock calls to ubersmith at the \`requests\` level. Apr 13, 2015 N/A N/A :pypi:`pytest-ui` Text User Interface for running python tests Jul 05, 2021 4 - Beta pytest @@ -1207,6 +1230,7 @@ This list contains 1259 plugins. :pypi:`pytest-unmarked` Run only unmarked tests Aug 27, 2019 5 - Production/Stable N/A :pypi:`pytest-unordered` Test equality of unordered collections in pytest Nov 28, 2022 4 - Beta pytest (>=6.0.0) :pypi:`pytest-unstable` Set a test as unstable to return 0 even if it failed Sep 27, 2022 4 - Beta N/A + :pypi:`pytest-unused-fixtures` A pytest plugin to list unused fixtures after a test run. Jun 30, 2023 4 - Beta pytest (>=7.3.2,<8.0.0) :pypi:`pytest-upload-report` pytest-upload-report is a plugin for pytest that upload your test report for test results. Jun 18, 2021 5 - Production/Stable N/A :pypi:`pytest-utils` Some helpers for pytest. Feb 02, 2023 4 - Beta pytest (>=7.0.0,<8.0.0) :pypi:`pytest-vagrant` A py.test plugin providing access to vagrant. Sep 07, 2021 5 - Production/Stable pytest @@ -1227,18 +1251,18 @@ This list contains 1259 plugins. :pypi:`pytest-vscodedebug` A pytest plugin to easily enable debugging tests within Visual Studio Code Dec 04, 2020 4 - Beta N/A :pypi:`pytest-vscode-pycharm-cls` A PyTest helper to enable start remote debugger on test start or failure or when pytest.set_trace is used. Feb 01, 2023 N/A pytest :pypi:`pytest-vts` pytest plugin for automatic recording of http stubbed tests Jun 05, 2019 N/A pytest (>=2.3) - :pypi:`pytest-vulture` A pytest plugin to checks dead code with vulture May 19, 2023 N/A pytest (>=7.0.0) + :pypi:`pytest-vulture` A pytest plugin to checks dead code with vulture Jun 01, 2023 N/A pytest (>=7.0.0) :pypi:`pytest-vw` pytest-vw makes your failing test cases succeed under CI tools scrutiny Oct 07, 2015 4 - Beta N/A :pypi:`pytest-vyper` Plugin for the vyper smart contract language. May 28, 2020 2 - Pre-Alpha N/A :pypi:`pytest-wa-e2e-plugin` Pytest plugin for testing whatsapp bots with end to end tests Feb 18, 2020 4 - Beta pytest (>=3.5.0) :pypi:`pytest-wake` May 11, 2023 N/A pytest :pypi:`pytest-watch` Local continuous test runner with pytest and watchdog. May 20, 2018 N/A N/A - :pypi:`pytest-watcher` Continiously runs pytest on changes in \*.py files Dec 11, 2022 3 - Alpha N/A + :pypi:`pytest-watcher` Automatically rerun your tests on file modifications Jun 24, 2023 4 - Beta N/A :pypi:`pytest-wdl` Pytest plugin for testing WDL workflows. Nov 17, 2020 5 - Production/Stable N/A :pypi:`pytest-web3-data` Sep 15, 2022 4 - Beta pytest :pypi:`pytest-webdriver` Selenium webdriver fixture for py.test May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-wetest` Welian API Automation test framework pytest plugin Nov 10, 2018 4 - Beta N/A - :pypi:`pytest-when` Utility which makes mocking more readable and controllable May 22, 2023 N/A pytest>=7.3.1 + :pypi:`pytest-when` Utility which makes mocking more readable and controllable Jun 05, 2023 N/A pytest>=7.3.1 :pypi:`pytest-whirlwind` Testing Tornado. Jun 12, 2020 N/A N/A :pypi:`pytest-wholenodeid` pytest addon for displaying the whole node id for failures Aug 26, 2015 4 - Beta pytest (>=2.0) :pypi:`pytest-win32consoletitle` Pytest progress in console title (Win32 only) Aug 08, 2021 N/A N/A @@ -1250,26 +1274,28 @@ This list contains 1259 plugins. :pypi:`pytest-xdist-debug-for-graingert` pytest xdist plugin for distributed testing and loop-on-failing modes Jul 24, 2019 5 - Production/Stable pytest (>=4.4.0) :pypi:`pytest-xdist-forked` forked from pytest-xdist Feb 10, 2020 5 - Production/Stable pytest (>=4.4.0) :pypi:`pytest-xdist-tracker` pytest plugin helps to reproduce failures for particular xdist node Nov 18, 2021 3 - Alpha pytest (>=3.5.1) + :pypi:`pytest-xdist-worker-stats` A pytest plugin to list worker statistics after a xdist run. Jun 19, 2023 4 - Beta pytest (>=7.3.2,<8.0.0) :pypi:`pytest-xfaillist` Maintain a xfaillist in an additional file to avoid merge-conflicts. Sep 17, 2021 N/A pytest (>=6.2.2,<7.0.0) :pypi:`pytest-xfiles` Pytest fixtures providing data read from function, module or package related (x)files. Feb 27, 2018 N/A N/A :pypi:`pytest-xlog` Extended logging for test and decorators May 31, 2020 4 - Beta N/A - :pypi:`pytest-xlsx` pytest plugin for generating test cases by xlsx(excel) Mar 01, 2023 N/A pytest>=7.2.0 + :pypi:`pytest-xlsx` pytest plugin for generating test cases by xlsx(excel) Jul 03, 2023 N/A pytest<8,>=7.4.0 :pypi:`pytest-xpara` An extended parametrizing plugin of pytest. Oct 30, 2017 3 - Alpha pytest :pypi:`pytest-xprocess` A pytest plugin for managing processes across test runs. Jan 05, 2023 4 - Beta pytest (>=2.8) :pypi:`pytest-xray` May 30, 2019 3 - Alpha N/A :pypi:`pytest-xrayjira` Mar 17, 2020 3 - Alpha pytest (==4.3.1) :pypi:`pytest-xray-server` May 03, 2022 3 - Alpha pytest (>=5.3.1) :pypi:`pytest-xskynet` A package to prevent Dependency Confusion attacks against Yandex. Feb 10, 2023 N/A N/A - :pypi:`pytest-xvfb` A pytest plugin to run Xvfb for tests. Jun 09, 2020 4 - Beta pytest (>=2.8.1) + :pypi:`pytest-xvfb` A pytest plugin to run Xvfb (or Xephyr/Xvnc) for tests. May 29, 2023 4 - Beta pytest (>=2.8.1) + :pypi:`pytest-xvirt` A pytest plugin to virtualize test. For example to transparently running them on a remote box. Jun 18, 2023 4 - Beta pytest (>=7.1.0) :pypi:`pytest-yaml` This plugin is used to load yaml output to your test using pytest framework. Oct 05, 2018 N/A pytest - :pypi:`pytest-yaml-sanmu` pytest plugin for generating test cases by yaml Mar 17, 2023 N/A pytest>=7.2.0 + :pypi:`pytest-yaml-sanmu` pytest plugin for generating test cases by yaml Jul 03, 2023 N/A pytest>=7.4.0 :pypi:`pytest-yamltree` Create or check file/directory trees described by YAML Mar 02, 2020 4 - Beta pytest (>=3.1.1) :pypi:`pytest-yamlwsgi` Run tests against wsgi apps defined in yaml May 11, 2010 N/A N/A - :pypi:`pytest-yaml-yoyo` http/https API run by yaml May 22, 2023 N/A pytest (>=7.2.0) + :pypi:`pytest-yaml-yoyo` http/https API run by yaml Jun 19, 2023 N/A pytest (>=7.2.0) :pypi:`pytest-yapf` Run yapf Jul 06, 2017 4 - Beta pytest (>=3.1.1) :pypi:`pytest-yapf3` Validate your Python file format with yapf Mar 29, 2023 5 - Production/Stable pytest (>=7) :pypi:`pytest-yield` PyTest plugin to run tests concurrently, each \`yield\` switch context to other one Jan 23, 2019 N/A N/A - :pypi:`pytest-yls` Pytest plugin to test the YLS as a whole. Mar 29, 2023 N/A pytest (>=7.2.2,<8.0.0) + :pypi:`pytest-yls` Pytest plugin to test the YLS as a whole. Jun 21, 2023 N/A pytest (>=7.2.2,<8.0.0) :pypi:`pytest-yuk` Display tests you are uneasy with, using 🤢/🤮 for pass/fail of tests marked with yuk. Mar 26, 2021 N/A pytest>=5.0.0 :pypi:`pytest-zafira` A Zafira plugin for pytest Sep 18, 2019 5 - Production/Stable pytest (==4.1.1) :pypi:`pytest-zap` OWASP ZAP plugin for py.test. May 12, 2014 4 - Beta N/A @@ -1282,6 +1308,13 @@ This list contains 1259 plugins. .. only:: latex + :pypi:`logassert` + *last release*: May 20, 2022, + *status*: 5 - Production/Stable, + *requires*: N/A + + Simple but powerful assertion and verification of logged lines. + :pypi:`pytest-abq` *last release*: Apr 07, 2023, *status*: N/A, @@ -1388,7 +1421,7 @@ This list contains 1259 plugins. Pytest \`client\` fixture for the Aiohttp :pypi:`pytest-aiomoto` - *last release*: Nov 09, 2022, + *last release*: Jun 24, 2023, *status*: N/A, *requires*: pytest (>=7.0,<8.0) @@ -1423,7 +1456,7 @@ This list contains 1259 plugins. :pypi:`pytest-alembic` - *last release*: May 23, 2023, + *last release*: Jul 06, 2023, *status*: N/A, *requires*: pytest (>=6.0) @@ -1619,7 +1652,7 @@ This list contains 1259 plugins. Extension for ApprovalTests.Python specific to geo data verification :pypi:`pytest-archon` - *last release*: Jan 31, 2023, + *last release*: Jul 11, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=7.2) @@ -1730,15 +1763,22 @@ This list contains 1259 plugins. + :pypi:`pytest-async-generators` + *last release*: Jul 05, 2023, + *status*: N/A, + *requires*: N/A + + Pytest fixtures for async generators + :pypi:`pytest-asyncio` - *last release*: Mar 19, 2023, + *last release*: Jul 12, 2023, *status*: 4 - Beta, *requires*: pytest (>=7.0.0) Pytest support for asyncio :pypi:`pytest-asyncio-cooperative` - *last release*: Feb 10, 2023, + *last release*: May 31, 2023, *status*: N/A, *requires*: N/A @@ -1920,7 +1960,7 @@ This list contains 1259 plugins. pytest plugin to display BDD info in HTML test report :pypi:`pytest-bdd-ng` - *last release*: Oct 06, 2022, + *last release*: Jul 01, 2023, *status*: 4 - Beta, *requires*: pytest (>=5.0) @@ -1961,6 +2001,13 @@ This list contains 1259 plugins. Fixtures for testing Google Appengine (GAE) apps + :pypi:`pytest-beeprint` + *last release*: Jul 04, 2023, + *status*: 4 - Beta, + *requires*: N/A + + use icdiff for better error messages in pytest assertions + :pypi:`pytest-bench` *last release*: Jul 21, 2014, *status*: 3 - Alpha, @@ -2151,9 +2198,9 @@ This list contains 1259 plugins. Budo Systems is a martial arts school management system. This module is the Budo Systems Pytest Plugin. :pypi:`pytest-bug` - *last release*: Jan 29, 2023, + *last release*: Jun 23, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=6.2.0) + *requires*: pytest (>=7.1.0) Pytest plugin for marking tests as a bug @@ -2354,14 +2401,14 @@ This list contains 1259 plugins. A pytest fixture for changing current working directory :pypi:`pytest-check` - *last release*: Feb 13, 2023, - *status*: 5 - Production/Stable, + *last release*: Jul 14, 2023, + *status*: N/A, *requires*: pytest A pytest plugin that allows multiple failures per test. :pypi:`pytest-checkdocs` - *last release*: Oct 09, 2022, + *last release*: Jul 09, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=6) ; extra == 'testing' @@ -2571,7 +2618,7 @@ This list contains 1259 plugins. pytest plugin to run pycodestyle :pypi:`pytest-codspeed` - *last release*: Dec 02, 2022, + *last release*: Jul 04, 2023, *status*: 5 - Production/Stable, *requires*: pytest>=3.8 @@ -2620,7 +2667,7 @@ This list contains 1259 plugins. pytest framework for testing different aspects of a common method :pypi:`pytest-compare` - *last release*: Mar 30, 2023, + *last release*: Jun 22, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -2648,9 +2695,9 @@ This list contains 1259 plugins. Package stands for pytest plugin to upload results into Confluence page. :pypi:`pytest-console-scripts` - *last release*: May 22, 2023, + *last release*: May 31, 2023, *status*: 4 - Beta, - *requires*: N/A + *requires*: pytest (>=4.0.0) Pytest plugin for testing console scripts @@ -2662,7 +2709,7 @@ This list contains 1259 plugins. pytest plugin with fixtures for testing consul aware apps :pypi:`pytest-container` - *last release*: Mar 21, 2023, + *last release*: Jun 19, 2023, *status*: 4 - Beta, *requires*: pytest (>=3.10) @@ -2689,6 +2736,13 @@ This list contains 1259 plugins. The pytest plugin for your Cookiecutter templates. 🍪 + :pypi:`pytest-copier` + *last release*: Jun 23, 2023, + *status*: 4 - Beta, + *requires*: pytest>=7.1.2 + + A pytest plugin to help testing Copier templates + :pypi:`pytest-couchdbkit` *last release*: Apr 17, 2012, *status*: N/A, @@ -2725,9 +2779,9 @@ This list contains 1259 plugins. :pypi:`pytest-coverage-context` - *last release*: Jan 04, 2021, + *last release*: Jun 28, 2023, *status*: 4 - Beta, - *requires*: pytest (>=6.1.0) + *requires*: N/A Coverage dynamic context support for PyTest, including sub-processes @@ -2753,7 +2807,7 @@ This list contains 1259 plugins. Use pytest's runner to discover and execute C++ tests :pypi:`pytest-cppython` - *last release*: May 25, 2023, + *last release*: Jun 19, 2023, *status*: N/A, *requires*: N/A @@ -2816,9 +2870,9 @@ This list contains 1259 plugins. CSV output for pytest. :pypi:`pytest-csv-params` - *last release*: Aug 28, 2022, + *last release*: Jul 01, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=7.1.2,<8.0.0) + *requires*: pytest (>=7.4.0,<8.0.0) Pytest plugin for Test Case Parametrization with CSV files @@ -3032,6 +3086,13 @@ This list contains 1259 plugins. + :pypi:`pytest-dbt` + *last release*: Jun 08, 2023, + *status*: 2 - Pre-Alpha, + *requires*: pytest (>=7.0.0,<8.0.0) + + Unit test dbt models with standard python tooling + :pypi:`pytest-dbt-adapter` *last release*: Nov 24, 2021, *status*: N/A, @@ -3193,6 +3254,13 @@ This list contains 1259 plugins. PyTest plugin for generating Difido reports + :pypi:`pytest-dir-equal` + *last release*: Jun 23, 2023, + *status*: 4 - Beta, + *requires*: pytest>=7.1.2 + + pytest-dir-equals is a pytest plugin providing helpers to assert directories equality allowing golden testing + :pypi:`pytest-disable` *last release*: Sep 10, 2015, *status*: 4 - Beta, @@ -3208,7 +3276,7 @@ This list contains 1259 plugins. Disable plugins per test :pypi:`pytest-discord` - *last release*: Feb 05, 2023, + *last release*: Jul 15, 2023, *status*: 4 - Beta, *requires*: pytest (!=6.0.0,<8,>=3.3.2) @@ -3285,7 +3353,7 @@ This list contains 1259 plugins. Cleanup your Haystack indexes between tests :pypi:`pytest-django-ifactory` - *last release*: May 21, 2023, + *last release*: Jun 06, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -3523,12 +3591,19 @@ This list contains 1259 plugins. A simple pytest plugin to import names and add them to the doctest namespace. :pypi:`pytest-doctestplus` - *last release*: Sep 26, 2022, + *last release*: Jun 08, 2023, *status*: 3 - Alpha, *requires*: pytest (>=4.6) Pytest plugin with advanced doctest features. + :pypi:`pytest-dogu-report` + *last release*: Jul 07, 2023, + *status*: N/A, + *requires*: N/A + + pytest plugin for dogu report + :pypi:`pytest-dolphin` *last release*: Nov 30, 2016, *status*: 4 - Beta, @@ -3578,6 +3653,13 @@ This list contains 1259 plugins. A Pytest plugin to drop duplicated tests during collection + :pypi:`pytest-dryrun` + *last release*: Jul 11, 2023, + *status*: N/A, + *requires*: pytest (>=7.4.0,<8.0.0) + + A Pytest plugin to ignore tests during collection without reporting them in the test summary. + :pypi:`pytest-dummynet` *last release*: Dec 15, 2021, *status*: 5 - Production/Stable, @@ -3614,7 +3696,7 @@ This list contains 1259 plugins. A pytest plugin to rerun tests dynamically based off of test outcome and output. :pypi:`pytest-dynamodb` - *last release*: Mar 27, 2023, + *last release*: Jun 12, 2023, *status*: 5 - Production/Stable, *requires*: pytest @@ -3719,53 +3801,53 @@ This list contains 1259 plugins. Send execution result email :pypi:`pytest-embedded` - *last release*: Apr 11, 2023, - *status*: N/A, - *requires*: pytest (>=7.0) + *last release*: Jul 09, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest>=7.0 - pytest embedded plugin + A pytest plugin that designed for embedded testing. :pypi:`pytest-embedded-arduino` - *last release*: Apr 11, 2023, - *status*: N/A, + *last release*: Jul 09, 2023, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for Arduino projects + Make pytest-embedded plugin work with Arduino. :pypi:`pytest-embedded-idf` - *last release*: Apr 11, 2023, - *status*: N/A, + *last release*: Jul 09, 2023, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for esp-idf project + Make pytest-embedded plugin work with ESP-IDF. :pypi:`pytest-embedded-jtag` - *last release*: Apr 11, 2023, - *status*: N/A, + *last release*: Jul 09, 2023, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for testing with jtag + Make pytest-embedded plugin work with JTAG. :pypi:`pytest-embedded-qemu` - *last release*: Apr 11, 2023, - *status*: N/A, + *last release*: Jul 09, 2023, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for qemu, not target chip + Make pytest-embedded plugin work with QEMU. :pypi:`pytest-embedded-serial` - *last release*: Apr 11, 2023, - *status*: N/A, + *last release*: Jul 09, 2023, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for testing serial ports + Make pytest-embedded plugin work with Serial. :pypi:`pytest-embedded-serial-esp` - *last release*: Apr 11, 2023, - *status*: N/A, + *last release*: Jul 09, 2023, + *status*: 5 - Production/Stable, *requires*: N/A - pytest embedded plugin for testing espressif boards via serial ports + Make pytest-embedded plugin work with Espressif target boards. :pypi:`pytest-embrace` *last release*: Mar 25, 2023, @@ -3789,7 +3871,7 @@ This list contains 1259 plugins. Pytest plugin to represent test output with emoji support :pypi:`pytest-enabler` - *last release*: May 12, 2023, + *last release*: Jul 14, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=6) ; extra == 'testing' @@ -3824,9 +3906,9 @@ This list contains 1259 plugins. Improvements for pytest (rejected upstream) :pypi:`pytest-env` - *last release*: Oct 23, 2022, + *last release*: Jun 15, 2023, *status*: 5 - Production/Stable, - *requires*: pytest>=7.1.3 + *requires*: pytest>=7.3.1 py.test plugin that allows you to add environment variables. @@ -3908,7 +3990,7 @@ This list contains 1259 plugins. Applies eventlet monkey-patch as a pytest plugin. :pypi:`pytest-examples` - *last release*: May 05, 2023, + *last release*: Jul 11, 2023, *status*: 4 - Beta, *requires*: pytest>=7 @@ -4146,9 +4228,9 @@ This list contains 1259 plugins. A fixture which allows easy replacement of fastapi dependencies for testing :pypi:`pytest-fastest` - *last release*: Mar 05, 2020, - *status*: N/A, - *requires*: N/A + *last release*: Jun 15, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=4.4) Use SCM and coverage to run only needed tests @@ -4321,7 +4403,7 @@ This list contains 1259 plugins. pytest plugin to check FLAKE8 requirements :pypi:`pytest-flake8-path` - *last release*: May 11, 2022, + *last release*: Jul 10, 2023, *status*: 5 - Production/Stable, *requires*: pytest @@ -4391,12 +4473,19 @@ This list contains 1259 plugins. :pypi:`pytest-fluent` - *last release*: Jul 12, 2022, + *last release*: Jun 26, 2023, *status*: 4 - Beta, - *requires*: pytest + *requires*: pytest (>=7.0.0) A pytest plugin in order to provide logs via fluentd + :pypi:`pytest-fluentbit` + *last release*: Jun 16, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.0.0) + + A pytest plugin in order to provide logs via fluentbit + :pypi:`pytest-flyte` *last release*: May 03, 2021, *status*: N/A, @@ -4454,9 +4543,9 @@ This list contains 1259 plugins. Wrap tests with fixtures in freeze_time :pypi:`pytest-freezer` - *last release*: Oct 20, 2022, + *last release*: Jun 21, 2023, *status*: N/A, - *requires*: pytest>=3.6 + *requires*: pytest >= 3.6 Pytest plugin providing a fixture interface for spulec/freezegun @@ -4559,7 +4648,7 @@ This list contains 1259 plugins. For finding/executing Ghost Inspector tests :pypi:`pytest-girder` - *last release*: May 22, 2023, + *last release*: Jun 28, 2023, *status*: N/A, *requires*: N/A @@ -4572,6 +4661,13 @@ This list contains 1259 plugins. Git repository fixture for py.test + :pypi:`pytest-gitconfig` + *last release*: Jun 22, 2023, + *status*: 4 - Beta, + *requires*: pytest>=7.1.2 + + Provide a gitconfig sandbox for testing + :pypi:`pytest-gitcov` *last release*: Jan 11, 2020, *status*: 2 - Pre-Alpha, @@ -4677,6 +4773,13 @@ This list contains 1259 plugins. Green progress dots + :pypi:`pytest-group-by-class` + *last release*: Jun 27, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=2.5) + + A Pytest plugin for running a subset of your tests by splitting them in to groups of classes. + :pypi:`pytest-growl` *last release*: Jan 13, 2014, *status*: 5 - Production/Stable, @@ -4783,7 +4886,7 @@ This list contains 1259 plugins. A pytest plugin for use with homeassistant custom components. :pypi:`pytest-homeassistant-custom-component` - *last release*: May 24, 2023, + *last release*: Jul 14, 2023, *status*: 3 - Alpha, *requires*: pytest (==7.3.1) @@ -4804,7 +4907,7 @@ This list contains 1259 plugins. Report on tests that honor constraints, and guard against regressions :pypi:`pytest-hot-reloading` - *last release*: May 18, 2023, + *last release*: Jun 23, 2023, *status*: N/A, *requires*: N/A @@ -4937,7 +5040,7 @@ This list contains 1259 plugins. pytest-httpserver is a httpserver for pytest :pypi:`pytest-httptesting` - *last release*: Apr 19, 2023, + *last release*: Jul 09, 2023, *status*: N/A, *requires*: pytest (>=7.2.0,<8.0.0) @@ -5000,7 +5103,7 @@ This list contains 1259 plugins. A pytest plugin for idapython. Allows a pytest setup to run tests outside and inside IDA in an automated manner by runnig pytest inside IDA and by mocking idapython api :pypi:`pytest-idem` - *last release*: Sep 07, 2022, + *last release*: Jun 23, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -5084,7 +5187,7 @@ This list contains 1259 plugins. A py.test plugin providing fixtures to simplify inmanta modules testing. :pypi:`pytest-inmanta-extensions` - *last release*: Apr 12, 2023, + *last release*: Jul 04, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -5161,14 +5264,14 @@ This list contains 1259 plugins. Pytest plugin for intercepting outgoing connection requests during pytest run. :pypi:`pytest-interface-tester` - *last release*: May 09, 2023, + *last release*: Jul 14, 2023, *status*: 4 - Beta, *requires*: pytest Pytest plugin for checking charm relation interface protocol compliance. :pypi:`pytest-invenio` - *last release*: Apr 13, 2023, + *last release*: Jun 02, 2023, *status*: 5 - Production/Stable, *requires*: pytest (<7.2.0,>=6) @@ -5210,7 +5313,7 @@ This list contains 1259 plugins. py.test plugin to check import ordering using isort :pypi:`pytest-is-running` - *last release*: Aug 19, 2022, + *last release*: Jul 10, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -5266,21 +5369,21 @@ This list contains 1259 plugins. A plugin to generate customizable jinja-based HTML reports in pytest :pypi:`pytest-jira` - *last release*: Apr 07, 2022, + *last release*: Jun 12, 2023, *status*: 3 - Alpha, *requires*: N/A py.test JIRA integration plugin, using markers :pypi:`pytest-jira-xfail` - *last release*: Dec 01, 2022, + *last release*: Jun 19, 2023, *status*: N/A, - *requires*: pytest (~=7.2.0) + *requires*: pytest (>=7.2.0) Plugin skips (xfail) tests if unresolved Jira issue(s) linked :pypi:`pytest-jira-xray` - *last release*: May 15, 2023, + *last release*: Jul 11, 2023, *status*: 4 - Beta, *requires*: pytest @@ -5357,7 +5460,7 @@ This list contains 1259 plugins. A reusable JupyterHub pytest plugin :pypi:`pytest-kafka` - *last release*: Oct 01, 2022, + *last release*: Jun 14, 2023, *status*: N/A, *requires*: pytest @@ -5497,7 +5600,7 @@ This list contains 1259 plugins. It helps to use fixtures in pytest.mark.parametrize :pypi:`pytest-lazy-fixtures` - *last release*: Mar 11, 2023, + *last release*: May 28, 2023, *status*: N/A, *requires*: pytest (>=7.2.1,<8.0.0) @@ -5630,7 +5733,7 @@ This list contains 1259 plugins. pytest plugin to test server connections locally. :pypi:`pytest-localstack` - *last release*: Oct 17, 2022, + *last release*: Jun 07, 2023, *status*: 4 - Beta, *requires*: pytest (>=6.0.0,<7.0.0) @@ -5700,7 +5803,7 @@ This list contains 1259 plugins. :pypi:`pytest-logikal` - *last release*: May 27, 2023, + *last release*: Jun 22, 2023, *status*: 5 - Production/Stable, *requires*: pytest (==7.3.1) @@ -5854,7 +5957,7 @@ This list contains 1259 plugins. Plugin for generating Markdown reports for pytest results :pypi:`pytest-md-report` - *last release*: Aug 06, 2022, + *last release*: Jun 25, 2023, *status*: 4 - Beta, *requires*: pytest (!=6.0.0,<8,>=3.3.2) @@ -5875,7 +5978,7 @@ This list contains 1259 plugins. Estimates memory consumption of test functions :pypi:`pytest-memray` - *last release*: Dec 02, 2022, + *last release*: Jun 06, 2023, *status*: N/A, *requires*: pytest>=7.2 @@ -5931,7 +6034,7 @@ This list contains 1259 plugins. Custom metrics report for pytest :pypi:`pytest-mh` - *last release*: May 25, 2023, + *last release*: Jun 08, 2023, *status*: N/A, *requires*: pytest @@ -5980,7 +6083,7 @@ This list contains 1259 plugins. pytest plugin to display test execution output like a mochajs :pypi:`pytest-mock` - *last release*: Oct 05, 2022, + *last release*: Jun 15, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=5.0) @@ -6022,7 +6125,7 @@ This list contains 1259 plugins. An in-memory mock of a Redis server that runs in a separate thread. This is to be used for unit-tests that require a Redis database. :pypi:`pytest-mock-resources` - *last release*: May 23, 2023, + *last release*: Jul 12, 2023, *status*: N/A, *requires*: pytest (>=1.0) @@ -6092,7 +6195,7 @@ This list contains 1259 plugins. pytest plugin for MongoDB fixtures :pypi:`pytest-monitor` - *last release*: Oct 22, 2022, + *last release*: Jun 25, 2023, *status*: 5 - Production/Stable, *requires*: pytest @@ -6218,9 +6321,9 @@ This list contains 1259 plugins. Mypy static type checker plugin for Pytest :pypi:`pytest-mypy-plugins` - *last release*: May 05, 2023, + *last release*: Jun 29, 2023, *status*: 4 - Beta, - *requires*: pytest (>=6.2.0) + *requires*: pytest (>=7.0.0) pytest plugin for writing tests for mypy plugins @@ -6260,7 +6363,7 @@ This list contains 1259 plugins. pytest-neo is a plugin for pytest that shows tests like screen of Matrix. :pypi:`pytest-netdut` - *last release*: Apr 18, 2023, + *last release*: Jul 06, 2023, *status*: N/A, *requires*: pytest (>=3.5.0) @@ -6316,7 +6419,7 @@ This list contains 1259 plugins. pytest ngs fixtures :pypi:`pytest-nhsd-apim` - *last release*: Mar 06, 2023, + *last release*: Jul 11, 2023, *status*: N/A, *requires*: pytest (==6.2.5) @@ -6428,7 +6531,7 @@ This list contains 1259 plugins. pytest results data-base and HTML reporter :pypi:`pytest-odoo` - *last release*: Nov 17, 2022, + *last release*: Jul 06, 2023, *status*: 4 - Beta, *requires*: pytest (>=7.2.0) @@ -6736,11 +6839,11 @@ This list contains 1259 plugins. Change the exit code of pytest test sessions when a required percent of tests pass. :pypi:`pytest-perf` - *last release*: Jun 23, 2022, + *last release*: Jun 02, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=6) ; extra == 'testing' - pytest-perf + Run performance tests against the mainline code. :pypi:`pytest-performance` *last release*: Sep 11, 2020, @@ -6750,7 +6853,7 @@ This list contains 1259 plugins. A simple plugin to ensure the execution of critical sections of code has not been impacted :pypi:`pytest-persistence` - *last release*: May 16, 2023, + *last release*: Jul 04, 2023, *status*: N/A, *requires*: N/A @@ -6868,6 +6971,13 @@ This list contains 1259 plugins. A pytest wrapper with fixtures for Playwright to automate web browsers + :pypi:`pytest-playwright-async` + *last release*: Jul 03, 2023, + *status*: N/A, + *requires*: N/A + + ASYNC Pytest plugin for Playwright + :pypi:`pytest-playwrights` *last release*: Dec 02, 2021, *status*: N/A, @@ -6987,6 +7097,13 @@ This list contains 1259 plugins. A pytest plugin to help with testing pop projects + :pypi:`pytest-porringer` + *last release*: Jun 24, 2023, + *status*: N/A, + *requires*: pytest>=7.1.2 + + + :pypi:`pytest-portion` *last release*: Jan 28, 2021, *status*: 4 - Beta, @@ -7051,9 +7168,9 @@ This list contains 1259 plugins. Minitest-style test colors :pypi:`pytest-print` - *last release*: Dec 28, 2021, + *last release*: Jun 28, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=6) + *requires*: pytest>=7.3.2 pytest-print adds the printer fixture you can use to print messages to the user (directly to the pytest runner, not stdout) @@ -7198,7 +7315,7 @@ This list contains 1259 plugins. Record PyMySQL queries and mock with the stored data. :pypi:`pytest-pyodide` - *last release*: May 10, 2023, + *last release*: Jun 19, 2023, *status*: N/A, *requires*: pytest @@ -7247,7 +7364,7 @@ This list contains 1259 plugins. Pyramid server fixture for py.test :pypi:`pytest-pyreport` - *last release*: May 08, 2023, + *last release*: Jul 02, 2023, *status*: N/A, *requires*: pytest (>=7.3.1) @@ -7324,9 +7441,9 @@ This list contains 1259 plugins. Pytest plugin for uploading test results to your QA Touch Testrun. :pypi:`pytest-qgis` - *last release*: Jun 26, 2022, + *last release*: Jun 30, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=6.2.3) + *requires*: pytest (>=6.2.5) A pytest plugin for testing QGIS python plugins @@ -7373,9 +7490,9 @@ This list contains 1259 plugins. pytest plugin to generate random data inspired by QuickCheck :pypi:`pytest-rabbitmq` - *last release*: Feb 11, 2022, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=3.0.0) + *requires*: pytest (>=6.2) RabbitMQ process and client fixtures for pytest @@ -7436,7 +7553,7 @@ This list contains 1259 plugins. py.test plugin to randomize tests :pypi:`pytest-randomly` - *last release*: May 11, 2022, + *last release*: Jul 10, 2023, *status*: 5 - Production/Stable, *requires*: pytest @@ -7534,7 +7651,7 @@ This list contains 1259 plugins. Conveniently run pytest with a dot-formatted test reference. :pypi:`pytest-regex` - *last release*: May 23, 2023, + *last release*: May 29, 2023, *status*: 4 - Beta, *requires*: pytest (>=3.5.0) @@ -7597,7 +7714,7 @@ This list contains 1259 plugins. Pytest plugin for capturing and mocking connection requests. :pypi:`pytest-remove-stale-bytecode` - *last release*: Mar 04, 2020, + *last release*: Jul 07, 2023, *status*: 4 - Beta, *requires*: pytest @@ -7646,7 +7763,7 @@ This list contains 1259 plugins. Generate Pytest reports with templates :pypi:`pytest-reporter-html1` - *last release*: Jun 08, 2021, + *last release*: Jun 05, 2023, *status*: 4 - Beta, *requires*: N/A @@ -7695,12 +7812,19 @@ This list contains 1259 plugins. pytest plugin for adding tests' parameters to junit report :pypi:`pytest-reportportal` - *last release*: May 24, 2023, + *last release*: Jun 30, 2023, *status*: N/A, *requires*: pytest (>=3.8.0) Agent for Reporting results of tests to the Report Portal + :pypi:`pytest-reports` + *last release*: Jun 07, 2023, + *status*: N/A, + *requires*: N/A + + An interesting python package + :pypi:`pytest-reqs` *last release*: May 12, 2019, *status*: N/A, @@ -7751,9 +7875,9 @@ This list contains 1259 plugins. Re-run only changed files in specified branch :pypi:`pytest-rerunfailures` - *last release*: Mar 09, 2023, + *last release*: Jul 05, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=5.3) + *requires*: pytest (>=6.2) pytest plugin to re-run tests to eliminate flaky failures @@ -7772,7 +7896,7 @@ This list contains 1259 plugins. Pytest fixture for recording and replaying serial port traffic. :pypi:`pytest-resilient-circuits` - *last release*: May 05, 2023, + *last release*: Jul 11, 2023, *status*: N/A, *requires*: pytest (~=4.6) ; python_version == "2.7" @@ -7821,7 +7945,7 @@ This list contains 1259 plugins. :pypi:`pytest-restrict` - *last release*: May 11, 2022, + *last release*: Jul 10, 2023, *status*: 5 - Production/Stable, *requires*: pytest @@ -7877,7 +8001,7 @@ This list contains 1259 plugins. :pypi:`pytest-reverse` - *last release*: May 11, 2022, + *last release*: Jul 10, 2023, *status*: 5 - Production/Stable, *requires*: pytest @@ -7898,11 +8022,11 @@ This list contains 1259 plugins. A pytest plugin using Rich for beautiful test result formatting. :pypi:`pytest-richtrace` - *last release*: Nov 05, 2022, + *last release*: Jun 20, 2023, *status*: N/A, - *requires*: pytest (>=7.2.0,<8.0.0) - + *requires*: N/A + A pytest plugin that displays the names and information of the pytest hook functions as they are executed. :pypi:`pytest-ringo` *last release*: Sep 27, 2017, @@ -7975,7 +8099,7 @@ This list contains 1259 plugins. Coverage-based regression test selection (RTS) plugin for pytest :pypi:`pytest-ruff` - *last release*: Apr 28, 2023, + *last release*: Jun 08, 2023, *status*: 4 - Beta, *requires*: N/A @@ -8023,6 +8147,13 @@ This list contains 1259 plugins. Call runtime_xfail() to mark running test as xfail. + :pypi:`pytest-runtime-yoyo` + *last release*: Jun 12, 2023, + *status*: N/A, + *requires*: pytest (>=7.2.0) + + run case mark timeout + :pypi:`pytest-ry-demo1` *last release*: Mar 26, 2023, *status*: N/A, @@ -8094,7 +8225,7 @@ This list contains 1259 plugins. :pypi:`pytest-sbase` - *last release*: May 25, 2023, + *last release*: Jul 13, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -8136,14 +8267,14 @@ This list contains 1259 plugins. A pytest plugin which allows to (de-)select tests from a file. :pypi:`pytest-selenium` - *last release*: Sep 21, 2022, + *last release*: May 28, 2023, *status*: 5 - Production/Stable, - *requires*: pytest (>=6.0.0,<7.0.0) + *requires*: pytest>=6.0.0 pytest plugin for Selenium :pypi:`pytest-seleniumbase` - *last release*: May 25, 2023, + *last release*: Jul 13, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -8199,7 +8330,7 @@ This list contains 1259 plugins. Automatically mocks resources from serverless.yml in pytest using moto. :pypi:`pytest-servers` - *last release*: Apr 15, 2023, + *last release*: Jul 13, 2023, *status*: 3 - Alpha, *requires*: pytest (>=6.2) @@ -8276,9 +8407,9 @@ This list contains 1259 plugins. A pytest plugin to help with testing shell scripts / black box commands :pypi:`pytest-shell-utilities` - *last release*: Sep 23, 2022, - *status*: 4 - Beta, - *requires*: pytest (>=6.0.0) + *last release*: Jul 02, 2023, + *status*: 5 - Production/Stable, + *requires*: pytest (>=7.1.0) Pytest plugin to simplify running shell commands against the system @@ -8422,6 +8553,13 @@ This list contains 1259 plugins. Send email with pytest execution result + :pypi:`pytest-smtp4dev` + *last release*: Jun 27, 2023, + *status*: 5 - Production/Stable, + *requires*: N/A + + Plugin for smtp4dev API + :pypi:`pytest-smtpd` *last release*: May 15, 2023, *status*: N/A, @@ -8612,9 +8750,9 @@ This list contains 1259 plugins. :pypi:`pytest-splunk-addon` - *last release*: Feb 22, 2023, + *last release*: Jun 30, 2023, *status*: N/A, - *requires*: pytest (>5.4.0,<7.3) + *requires*: pytest (>5.4.0,<8) A Dynamic test tool for Splunk Apps and Add-ons @@ -8842,6 +8980,13 @@ This list contains 1259 plugins. A simple plugin to use with pytest + :pypi:`pytest-supercov` + *last release*: Jul 02, 2023, + *status*: N/A, + *requires*: N/A + + Pytest plugin for measuring explicit test-file to source-file coverage + :pypi:`pytest-svn` *last release*: May 28, 2019, *status*: 5 - Production/Stable, @@ -8899,7 +9044,7 @@ This list contains 1259 plugins. A Pytest plugin to generate realtime summary stats, and display them in-console using a text-based dashboard. :pypi:`pytest-tap` - *last release*: Oct 27, 2021, + *last release*: Jul 15, 2023, *status*: 5 - Production/Stable, *requires*: pytest (>=3.0) @@ -8948,7 +9093,7 @@ This list contains 1259 plugins. Pytest to Telegram reporting plugin :pypi:`pytest-telegram-notifier` - *last release*: Mar 17, 2023, + *last release*: Jun 27, 2023, *status*: 5 - Production/Stable, *requires*: N/A @@ -8969,7 +9114,7 @@ This list contains 1259 plugins. Terraform and Terragrunt fixtures for pytest :pypi:`pytest-terraform` - *last release*: Sep 01, 2022, + *last release*: Jun 20, 2023, *status*: N/A, *requires*: pytest (>=6.0) @@ -9039,7 +9184,7 @@ This list contains 1259 plugins. pytest reporting plugin for testlink :pypi:`pytest-testmon` - *last release*: May 18, 2023, + *last release*: Jul 13, 2023, *status*: 4 - Beta, *requires*: pytest (<8,>=5) @@ -9192,6 +9337,13 @@ This list contains 1259 plugins. Tesults plugin for pytest + :pypi:`pytest-textual-snapshot` + *last release*: Jul 12, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.0.0) + + Snapshot testing for Textual apps + :pypi:`pytest-tezos` *last release*: Jan 16, 2020, *status*: 4 - Beta, @@ -9213,6 +9365,13 @@ This list contains 1259 plugins. Pytest plugin for time travel + :pypi:`pytest-thread` + *last release*: Jul 07, 2023, + *status*: N/A, + *requires*: N/A + + + :pypi:`pytest-threadleak` *last release*: Jul 03, 2022, *status*: 4 - Beta, @@ -9227,6 +9386,13 @@ This list contains 1259 plugins. Ticking on tests + :pypi:`pytest-time` + *last release*: Jun 24, 2023, + *status*: 3 - Alpha, + *requires*: pytest + + + :pypi:`pytest-timeit` *last release*: Oct 13, 2016, *status*: 4 - Beta, @@ -9269,6 +9435,13 @@ This list contains 1259 plugins. A simple plugin to view timestamps for each test + :pypi:`pytest-tinybird` + *last release*: Jun 26, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=3.8.0) + + A pytest plugin to report test results to tinybird + :pypi:`pytest-tipsi-django` *last release*: Nov 17, 2021, *status*: 4 - Beta, @@ -9480,7 +9653,7 @@ This list contains 1259 plugins. Test Class Base :pypi:`pytest-tui` - *last release*: Apr 06, 2023, + *last release*: Jun 12, 2023, *status*: 4 - Beta, *requires*: N/A @@ -9522,7 +9695,7 @@ This list contains 1259 plugins. A Typhoon HIL plugin that facilitates test parameter configuration at runtime :pypi:`pytest-typhoon-xray` - *last release*: Nov 04, 2022, + *last release*: Jun 10, 2023, *status*: 4 - Beta, *requires*: N/A @@ -9605,6 +9778,13 @@ This list contains 1259 plugins. Set a test as unstable to return 0 even if it failed + :pypi:`pytest-unused-fixtures` + *last release*: Jun 30, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.3.2,<8.0.0) + + A pytest plugin to list unused fixtures after a test run. + :pypi:`pytest-upload-report` *last release*: Jun 18, 2021, *status*: 5 - Production/Stable, @@ -9746,7 +9926,7 @@ This list contains 1259 plugins. pytest plugin for automatic recording of http stubbed tests :pypi:`pytest-vulture` - *last release*: May 19, 2023, + *last release*: Jun 01, 2023, *status*: N/A, *requires*: pytest (>=7.0.0) @@ -9788,11 +9968,11 @@ This list contains 1259 plugins. Local continuous test runner with pytest and watchdog. :pypi:`pytest-watcher` - *last release*: Dec 11, 2022, - *status*: 3 - Alpha, + *last release*: Jun 24, 2023, + *status*: 4 - Beta, *requires*: N/A - Continiously runs pytest on changes in \*.py files + Automatically rerun your tests on file modifications :pypi:`pytest-wdl` *last release*: Nov 17, 2020, @@ -9823,7 +10003,7 @@ This list contains 1259 plugins. Welian API Automation test framework pytest plugin :pypi:`pytest-when` - *last release*: May 22, 2023, + *last release*: Jun 05, 2023, *status*: N/A, *requires*: pytest>=7.3.1 @@ -9906,6 +10086,13 @@ This list contains 1259 plugins. pytest plugin helps to reproduce failures for particular xdist node + :pypi:`pytest-xdist-worker-stats` + *last release*: Jun 19, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.3.2,<8.0.0) + + A pytest plugin to list worker statistics after a xdist run. + :pypi:`pytest-xfaillist` *last release*: Sep 17, 2021, *status*: N/A, @@ -9928,9 +10115,9 @@ This list contains 1259 plugins. Extended logging for test and decorators :pypi:`pytest-xlsx` - *last release*: Mar 01, 2023, + *last release*: Jul 03, 2023, *status*: N/A, - *requires*: pytest>=7.2.0 + *requires*: pytest<8,>=7.4.0 pytest plugin for generating test cases by xlsx(excel) @@ -9977,11 +10164,18 @@ This list contains 1259 plugins. A package to prevent Dependency Confusion attacks against Yandex. :pypi:`pytest-xvfb` - *last release*: Jun 09, 2020, + *last release*: May 29, 2023, *status*: 4 - Beta, *requires*: pytest (>=2.8.1) - A pytest plugin to run Xvfb for tests. + A pytest plugin to run Xvfb (or Xephyr/Xvnc) for tests. + + :pypi:`pytest-xvirt` + *last release*: Jun 18, 2023, + *status*: 4 - Beta, + *requires*: pytest (>=7.1.0) + + A pytest plugin to virtualize test. For example to transparently running them on a remote box. :pypi:`pytest-yaml` *last release*: Oct 05, 2018, @@ -9991,9 +10185,9 @@ This list contains 1259 plugins. This plugin is used to load yaml output to your test using pytest framework. :pypi:`pytest-yaml-sanmu` - *last release*: Mar 17, 2023, + *last release*: Jul 03, 2023, *status*: N/A, - *requires*: pytest>=7.2.0 + *requires*: pytest>=7.4.0 pytest plugin for generating test cases by yaml @@ -10012,7 +10206,7 @@ This list contains 1259 plugins. Run tests against wsgi apps defined in yaml :pypi:`pytest-yaml-yoyo` - *last release*: May 22, 2023, + *last release*: Jun 19, 2023, *status*: N/A, *requires*: pytest (>=7.2.0) @@ -10040,7 +10234,7 @@ This list contains 1259 plugins. PyTest plugin to run tests concurrently, each \`yield\` switch context to other one :pypi:`pytest-yls` - *last release*: Mar 29, 2023, + *last release*: Jun 21, 2023, *status*: N/A, *requires*: pytest (>=7.2.2,<8.0.0) diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 7107218b3..ea008110e 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -783,18 +783,66 @@ reporting or interaction with exceptions: .. autofunction:: pytest_leave_pdb -Objects -------- +Collection tree objects +----------------------- -Full reference to objects accessible from :ref:`fixtures ` or :ref:`hooks `. +These are the collector and item classes (collectively called "nodes") which +make up the collection tree. +Node +~~~~ -CallInfo -~~~~~~~~ - -.. autoclass:: pytest.CallInfo() +.. autoclass:: _pytest.nodes.Node() :members: +Collector +~~~~~~~~~ + +.. autoclass:: pytest.Collector() + :members: + :show-inheritance: + +Item +~~~~ + +.. autoclass:: pytest.Item() + :members: + :show-inheritance: + +File +~~~~ + +.. autoclass:: pytest.File() + :members: + :show-inheritance: + +FSCollector +~~~~~~~~~~~ + +.. autoclass:: _pytest.nodes.FSCollector() + :members: + :show-inheritance: + +Session +~~~~~~~ + +.. autoclass:: pytest.Session() + :members: + :show-inheritance: + +Package +~~~~~~~ + +.. autoclass:: pytest.Package() + :members: + :show-inheritance: + +Module +~~~~~~ + +.. autoclass:: pytest.Module() + :members: + :show-inheritance: Class ~~~~~ @@ -803,13 +851,34 @@ Class :members: :show-inheritance: -Collector -~~~~~~~~~ +Function +~~~~~~~~ -.. autoclass:: pytest.Collector() +.. autoclass:: pytest.Function() :members: :show-inheritance: +FunctionDefinition +~~~~~~~~~~~~~~~~~~ + +.. autoclass:: _pytest.python.FunctionDefinition() + :members: + :show-inheritance: + + +Objects +------- + +Objects accessible from :ref:`fixtures ` or :ref:`hooks ` +or importable from ``pytest``. + + +CallInfo +~~~~~~~~ + +.. autoclass:: pytest.CallInfo() + :members: + CollectReport ~~~~~~~~~~~~~ @@ -837,46 +906,11 @@ ExitCode .. autoclass:: pytest.ExitCode :members: -File -~~~~ - -.. autoclass:: pytest.File() - :members: - :show-inheritance: - FixtureDef ~~~~~~~~~~ -.. autoclass:: _pytest.fixtures.FixtureDef() - :members: - :show-inheritance: - -FSCollector -~~~~~~~~~~~ - -.. autoclass:: _pytest.nodes.FSCollector() - :members: - :show-inheritance: - -Function -~~~~~~~~ - -.. autoclass:: pytest.Function() - :members: - :show-inheritance: - -FunctionDefinition -~~~~~~~~~~~~~~~~~~ - -.. autoclass:: _pytest.python.FunctionDefinition() - :members: - :show-inheritance: - -Item -~~~~ - -.. autoclass:: pytest.Item() +.. autoclass:: pytest.FixtureDef() :members: :show-inheritance: @@ -907,19 +941,6 @@ Metafunc .. autoclass:: pytest.Metafunc() :members: -Module -~~~~~~ - -.. autoclass:: pytest.Module() - :members: - :show-inheritance: - -Node -~~~~ - -.. autoclass:: _pytest.nodes.Node() - :members: - Parser ~~~~~~ @@ -941,13 +962,6 @@ PytestPluginManager :inherited-members: :show-inheritance: -Session -~~~~~~~ - -.. autoclass:: pytest.Session() - :members: - :show-inheritance: - TestReport ~~~~~~~~~~ @@ -1153,6 +1167,9 @@ Custom warnings generated in some situations such as improper usage or deprecate .. autoclass:: pytest.PytestRemovedIn8Warning :show-inheritance: +.. autoclass:: pytest.PytestRemovedIn9Warning + :show-inheritance: + .. autoclass:: pytest.PytestUnhandledCoroutineWarning :show-inheritance: @@ -1703,6 +1720,11 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] pythonpath = src1 src2 + .. note:: + + ``pythonpath`` does not affect some imports that happen very early, + most notably plugins loaded using the ``-p`` command line option. + .. confval:: required_plugins @@ -1918,9 +1940,9 @@ 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, --config-file FILE + -c FILE, --config-file=FILE Load configuration from `FILE` instead of trying to - locate one of the implicit configuration files + 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 diff --git a/doc/en/requirements.txt b/doc/en/requirements.txt index b6059723c..0ee999e0f 100644 --- a/doc/en/requirements.txt +++ b/doc/en/requirements.txt @@ -1,5 +1,5 @@ pallets-sphinx-themes -pluggy>=1.0 +pluggy>=1.2.0 pygments-pytest>=2.3.0 sphinx-removed-in>=0.2.0 sphinx>=5,<6 diff --git a/pyproject.toml b/pyproject.toml index a4139a5c0..d540773c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -113,7 +113,7 @@ template = "changelog/_template.rst" showcontent = true [tool.black] -target-version = ['py37'] +target-version = ['py38'] # check-wheel-contents is executed by the build-and-inspect-python-package action. [tool.check-wheel-contents] diff --git a/scripts/towncrier-draft-to-file.py b/scripts/towncrier-draft-to-file.py index 81507b40b..1f1068689 100644 --- a/scripts/towncrier-draft-to-file.py +++ b/scripts/towncrier-draft-to-file.py @@ -7,7 +7,9 @@ def main(): Platform agnostic wrapper script for towncrier. Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs. """ - with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file: + with open( + "doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8" + ) as draft_file: return call(("towncrier", "--draft"), stdout=draft_file) diff --git a/scripts/update-plugin-list.py b/scripts/update-plugin-list.py index 34d1c8bb6..ea7e7986e 100644 --- a/scripts/update-plugin-list.py +++ b/scripts/update-plugin-list.py @@ -17,7 +17,9 @@ Plugin List =========== PyPI projects that match "pytest-\*" are considered plugins and are listed -automatically. Packages classified as inactive are excluded. +automatically together with a manually-maintained list in `the source +code `_. +Packages classified as inactive are excluded. .. The following conditional uses a different format for this list when creating a PDF, because otherwise the table gets far too wide for the @@ -33,6 +35,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = ( "Development Status :: 6 - Mature", "Development Status :: 7 - Inactive", ) +ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins + "logassert", +} def escape_rst(text: str) -> str: @@ -52,18 +57,18 @@ def iter_plugins(): regex = r">([\d\w-]*)" response = requests.get("https://pypi.org/simple") - matches = list( - match - for match in re.finditer(regex, response.text) - if match.groups()[0].startswith("pytest-") - ) + match_names = (match.groups()[0] for match in re.finditer(regex, response.text)) + plugin_names = [ + name + for name in match_names + if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS + ] - for match in tqdm(matches, smoothing=0): - name = match.groups()[0] + for name in tqdm(plugin_names, smoothing=0): response = requests.get(f"https://pypi.org/pypi/{name}/json") if response.status_code == 404: - # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but - # return 404 on the JSON API. Skip. + # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple + # but return 404 on the JSON API. Skip. continue response.raise_for_status() info = response.json()["info"] diff --git a/setup.cfg b/setup.cfg index 56dadae7b..945635369 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,7 @@ long_description_content_type = text/x-rst url = https://docs.pytest.org/en/latest/ author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others license = MIT -license_file = LICENSE +license_files = LICENSE platforms = unix, linux, osx, cygwin, win32 classifiers = Development Status :: 6 - Mature @@ -17,11 +17,11 @@ classifiers = Operating System :: POSIX Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 + Programming Language :: Python :: 3.12 Topic :: Software Development :: Libraries Topic :: Software Development :: Testing Topic :: Utilities @@ -46,12 +46,11 @@ py_modules = py install_requires = iniconfig packaging - pluggy>=0.12,<2.0 + pluggy>=1.2.0,<2.0 colorama;sys_platform=="win32" exceptiongroup>=1.0.0rc8;python_version<"3.11" - importlib-metadata>=0.12;python_version<"3.8" tomli>=1.0.0;python_version<"3.11" -python_requires = >=3.7 +python_requires = >=3.8 package_dir = =src setup_requires = @@ -73,6 +72,7 @@ testing = nose pygments>=2.7.2 requests + setuptools xmlschema [options.package_data] diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 43c979d70..7a16cc5e6 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -17,18 +17,21 @@ from typing import Any from typing import Callable from typing import ClassVar from typing import Dict +from typing import Final +from typing import final from typing import Generic from typing import Iterable from typing import List +from typing import Literal from typing import Mapping from typing import Optional from typing import overload from typing import Pattern from typing import Sequence from typing import Set +from typing import SupportsIndex from typing import Tuple from typing import Type -from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -42,22 +45,16 @@ from _pytest._code.source import Source from _pytest._io import TerminalWriter from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr -from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.deprecated import check_ispytest from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath -if TYPE_CHECKING: - from typing_extensions import Final - from typing_extensions import Literal - from typing_extensions import SupportsIndex - - _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] - if sys.version_info[:2] < (3, 11): from exceptiongroup import BaseExceptionGroup +_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] + class Code: """Wrapper around Python code objects.""" @@ -396,11 +393,11 @@ class Traceback(List[TracebackEntry]): def filter( self, - # TODO(py38): change to positional only. - _excinfo_or_fn: Union[ + excinfo_or_fn: Union[ "ExceptionInfo[BaseException]", Callable[[TracebackEntry], bool], ], + /, ) -> "Traceback": """Return a Traceback instance with certain items removed. @@ -411,10 +408,10 @@ class Traceback(List[TracebackEntry]): ``TracebackEntry`` instance, and should return True when the item should be added to the ``Traceback``, False when not. """ - if isinstance(_excinfo_or_fn, ExceptionInfo): - fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731 + if isinstance(excinfo_or_fn, ExceptionInfo): + fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731 else: - fn = _excinfo_or_fn + fn = excinfo_or_fn return Traceback(filter(fn, self)) def recursionindex(self) -> Optional[int]: @@ -633,7 +630,7 @@ class ExceptionInfo(Generic[E]): def getrepr( self, showlocals: bool = False, - style: "_TracebackStyle" = "long", + style: _TracebackStyle = "long", abspath: bool = False, tbfilter: Union[ bool, Callable[["ExceptionInfo[BaseException]"], Traceback] @@ -707,7 +704,12 @@ class ExceptionInfo(Generic[E]): If it matches `True` is returned, otherwise an `AssertionError` is raised. """ __tracebackhide__ = True - value = str(self.value) + value = "\n".join( + [ + str(self.value), + *getattr(self.value, "__notes__", []), + ] + ) msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n str(exception): {value!r}" if regexp == value: msg += "\n Did you mean to `re.escape()` the regex?" @@ -725,7 +727,7 @@ class FormattedExcinfo: fail_marker: ClassVar = "E" showlocals: bool = False - style: "_TracebackStyle" = "long" + style: _TracebackStyle = "long" abspath: bool = True tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True funcargs: bool = False @@ -1090,7 +1092,7 @@ class ReprExceptionInfo(ExceptionRepr): class ReprTraceback(TerminalRepr): reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] extraline: Optional[str] - style: "_TracebackStyle" + style: _TracebackStyle entrysep: ClassVar = "_ " @@ -1124,7 +1126,7 @@ class ReprTracebackNative(ReprTraceback): class ReprEntryNative(TerminalRepr): lines: Sequence[str] - style: ClassVar["_TracebackStyle"] = "native" + style: ClassVar[_TracebackStyle] = "native" def toterminal(self, tw: TerminalWriter) -> None: tw.write("".join(self.lines)) @@ -1136,7 +1138,7 @@ class ReprEntry(TerminalRepr): reprfuncargs: Optional["ReprFuncArgs"] reprlocals: Optional["ReprLocals"] reprfileloc: Optional["ReprFileLocation"] - style: "_TracebackStyle" + style: _TracebackStyle def _write_entry_lines(self, tw: TerminalWriter) -> None: """Write the source code portions of a list of traceback entries with syntax highlighting. diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 208cfb800..cc7ac407e 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -149,8 +149,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i values: List[int] = [] for x in ast.walk(node): if isinstance(x, (ast.stmt, ast.ExceptHandler)): - # Before Python 3.8, the lineno of a decorated class or function pointed at the decorator. - # Since Python 3.8, the lineno points to the class/def, so need to include the decorators. + # The lineno points to the class/def, so need to include the decorators. if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)): for d in x.decorator_list: values.append(d.lineno - 1) diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index 379035d85..eb1b46939 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -2,12 +2,12 @@ import os import shutil import sys +from typing import final from typing import Optional from typing import Sequence from typing import TextIO from .wcwidth import wcswidth -from _pytest.compat import final # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index 73a070d19..41a7926c5 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -25,14 +25,12 @@ from stat import S_ISREG from typing import Any from typing import Callable from typing import cast +from typing import Literal from typing import overload from typing import TYPE_CHECKING from . import error -if TYPE_CHECKING: - from typing import Literal - # Moved from local.py. iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt") diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index a46e58136..64ad4b0e6 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -112,8 +112,8 @@ def pytest_collection(session: "Session") -> None: assertstate.hook.set_session(session) -@hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: """Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks. The rewrite module will use util._reprcompare if it exists to use custom @@ -162,10 +162,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: util._assertion_pass = call_assertion_pass_hook - yield - - util._reprcompare, util._assertion_pass = saved_assert_hooks - util._config = None + try: + return (yield) + finally: + util._reprcompare, util._assertion_pass = saved_assert_hooks + util._config = None def pytest_sessionfinish(session: "Session") -> None: diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index ab8169da2..9bf79f1e1 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -44,11 +44,6 @@ from _pytest.stash import StashKey if TYPE_CHECKING: from _pytest.assertion import AssertionState -if sys.version_info >= (3, 8): - namedExpr = ast.NamedExpr -else: - namedExpr = ast.Expr - assertstate_key = StashKey["AssertionState"]() @@ -680,9 +675,10 @@ class AssertionRewriter(ast.NodeVisitor): if ( expect_docstring and isinstance(item, ast.Expr) - and isinstance(item.value, ast.Str) + and isinstance(item.value, ast.Constant) + and isinstance(item.value.value, str) ): - doc = item.value.s + doc = item.value.value if self.is_rewrite_disabled(doc): return expect_docstring = False @@ -814,7 +810,7 @@ class AssertionRewriter(ast.NodeVisitor): current = self.stack.pop() if self.stack: self.explanation_specifiers = self.stack[-1] - keys = [ast.Str(key) for key in current.keys()] + keys = [ast.Constant(key) for key in current.keys()] format_dict = ast.Dict(keys, list(current.values())) form = ast.BinOp(expl_expr, ast.Mod(), format_dict) name = "@py_format" + str(next(self.variable_counter)) @@ -868,16 +864,16 @@ class AssertionRewriter(ast.NodeVisitor): negation = ast.UnaryOp(ast.Not(), top_condition) if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook - msg = self.pop_format_context(ast.Str(explanation)) + msg = self.pop_format_context(ast.Constant(explanation)) # Failed if assert_.msg: assertmsg = self.helper("_format_assertmsg", assert_.msg) gluestr = "\n>assert " else: - assertmsg = ast.Str("") + assertmsg = ast.Constant("") gluestr = "assert " - err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg) + err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg) err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation) err_name = ast.Name("AssertionError", ast.Load()) fmt = self.helper("_format_explanation", err_msg) @@ -893,8 +889,8 @@ class AssertionRewriter(ast.NodeVisitor): hook_call_pass = ast.Expr( self.helper( "_call_assertion_pass", - ast.Num(assert_.lineno), - ast.Str(orig), + ast.Constant(assert_.lineno), + ast.Constant(orig), fmt_pass, ) ) @@ -913,7 +909,7 @@ class AssertionRewriter(ast.NodeVisitor): variables = [ ast.Name(name, ast.Store()) for name in self.format_variables ] - clear_format = ast.Assign(variables, ast.NameConstant(None)) + clear_format = ast.Assign(variables, ast.Constant(None)) self.statements.append(clear_format) else: # Original assertion rewriting @@ -924,9 +920,9 @@ class AssertionRewriter(ast.NodeVisitor): assertmsg = self.helper("_format_assertmsg", assert_.msg) explanation = "\n>assert " + explanation else: - assertmsg = ast.Str("") + assertmsg = ast.Constant("") explanation = "assert " + explanation - template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation)) + template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation)) msg = self.pop_format_context(template) fmt = self.helper("_format_explanation", msg) err_name = ast.Name("AssertionError", ast.Load()) @@ -938,7 +934,7 @@ class AssertionRewriter(ast.NodeVisitor): # Clear temporary variables by setting them to None. if self.variables: variables = [ast.Name(name, ast.Store()) for name in self.variables] - clear = ast.Assign(variables, ast.NameConstant(None)) + clear = ast.Assign(variables, ast.Constant(None)) self.statements.append(clear) # Fix locations (line numbers/column offsets). for stmt in self.statements: @@ -946,26 +942,26 @@ class AssertionRewriter(ast.NodeVisitor): ast.copy_location(node, assert_) return self.statements - def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]: + def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.NamedExpr, str]: # This method handles the 'walrus operator' repr of the target # name if it's a local variable or _should_repr_global_name() # thinks it's acceptable. locs = ast.Call(self.builtin("locals"), [], []) target_id = name.target.id # type: ignore[attr-defined] - inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs]) + inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs]) dorepr = self.helper("_should_repr_global_name", name) test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) - expr = ast.IfExp(test, self.display(name), ast.Str(target_id)) + expr = ast.IfExp(test, self.display(name), ast.Constant(target_id)) return name, self.explanation_param(expr) def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]: # Display the repr of the name if it's a local variable or # _should_repr_global_name() thinks it's acceptable. locs = ast.Call(self.builtin("locals"), [], []) - inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs]) + inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs]) dorepr = self.helper("_should_repr_global_name", name) test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) - expr = ast.IfExp(test, self.display(name), ast.Str(name.id)) + expr = ast.IfExp(test, self.display(name), ast.Constant(name.id)) return name, self.explanation_param(expr) def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: @@ -984,10 +980,10 @@ class AssertionRewriter(ast.NodeVisitor): # cond is set in a prior loop iteration below self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa self.expl_stmts = fail_inner - # Check if the left operand is a namedExpr and the value has already been visited + # Check if the left operand is a ast.NamedExpr and the value has already been visited if ( isinstance(v, ast.Compare) - and isinstance(v.left, namedExpr) + and isinstance(v.left, ast.NamedExpr) and v.left.target.id in [ ast_expr.id @@ -1003,7 +999,7 @@ class AssertionRewriter(ast.NodeVisitor): self.push_format_context() res, expl = self.visit(v) body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) - expl_format = self.pop_format_context(ast.Str(expl)) + expl_format = self.pop_format_context(ast.Constant(expl)) call = ast.Call(app, [expl_format], []) self.expl_stmts.append(ast.Expr(call)) if i < levels: @@ -1015,7 +1011,7 @@ class AssertionRewriter(ast.NodeVisitor): self.statements = body = inner self.statements = save self.expl_stmts = fail_save - expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or)) + expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or)) expl = self.pop_format_context(expl_template) return ast.Name(res_var, ast.Load()), self.explanation_param(expl) @@ -1089,7 +1085,7 @@ class AssertionRewriter(ast.NodeVisitor): comp.left = self.variables_overwrite[ comp.left.id ] # type:ignore[assignment] - if isinstance(comp.left, namedExpr): + if isinstance(comp.left, ast.NamedExpr): self.variables_overwrite[ comp.left.target.id ] = comp.left # type:ignore[assignment] @@ -1105,7 +1101,7 @@ class AssertionRewriter(ast.NodeVisitor): results = [left_res] for i, op, next_operand in it: if ( - isinstance(next_operand, namedExpr) + isinstance(next_operand, ast.NamedExpr) and isinstance(left_res, ast.Name) and next_operand.target.id == left_res.id ): @@ -1118,9 +1114,9 @@ class AssertionRewriter(ast.NodeVisitor): next_expl = f"({next_expl})" results.append(next_res) sym = BINOP_MAP[op.__class__] - syms.append(ast.Str(sym)) + syms.append(ast.Constant(sym)) expl = f"{left_expl} {sym} {next_expl}" - expls.append(ast.Str(expl)) + expls.append(ast.Constant(expl)) res_expr = ast.Compare(left_res, [op], [next_res]) self.statements.append(ast.Assign([store_names[i]], res_expr)) left_res, left_expl = next_res, next_expl @@ -1164,7 +1160,7 @@ def try_makedirs(cache_dir: Path) -> bool: def get_cache_dir(file_path: Path) -> Path: """Return the cache directory to write .pyc files for the given .py file path.""" - if sys.version_info >= (3, 8) and sys.pycache_prefix: + if sys.pycache_prefix: # given: # prefix = '/tmp/pycs' # path = '/home/user/proj/test_app.py' diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 3d4fb1d6c..f519c974b 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -6,6 +6,7 @@ import json import os from pathlib import Path from typing import Dict +from typing import final from typing import Generator from typing import Iterable from typing import List @@ -18,7 +19,6 @@ from .pathlib import rm_rf from .reports import CollectReport from _pytest import nodes from _pytest._io import TerminalWriter -from _pytest.compat import final from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import hookimpl @@ -27,7 +27,7 @@ from _pytest.deprecated import check_ispytest from _pytest.fixtures import fixture from _pytest.fixtures import FixtureRequest from _pytest.main import Session -from _pytest.python import Module +from _pytest.nodes import File from _pytest.python import Package from _pytest.reports import TestReport @@ -217,12 +217,12 @@ class LFPluginCollWrapper: self.lfplugin = lfplugin self._collected_at_least_one_failure = False - @hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector: nodes.Collector): + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: nodes.Collector + ) -> Generator[None, CollectReport, CollectReport]: + res = yield if isinstance(collector, (Session, Package)): - out = yield - res: CollectReport = out.get_result() - # Sort any lf-paths to the beginning. lf_paths = self.lfplugin._last_failed_paths @@ -240,19 +240,16 @@ class LFPluginCollWrapper: key=sort_key, reverse=True, ) - return - elif isinstance(collector, Module): + elif isinstance(collector, File): if collector.path in self.lfplugin._last_failed_paths: - out = yield - res = out.get_result() result = res.result lastfailed = self.lfplugin.lastfailed # Only filter with known failures. if not self._collected_at_least_one_failure: if not any(x.nodeid in lastfailed for x in result): - return + return res self.lfplugin.config.pluginmanager.register( LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip" ) @@ -268,8 +265,8 @@ class LFPluginCollWrapper: # Keep all sub-collectors. or isinstance(x, nodes.Collector) ] - return - yield + + return res class LFPluginCollSkipfiles: @@ -280,9 +277,9 @@ class LFPluginCollSkipfiles: def pytest_make_collect_report( self, collector: nodes.Collector ) -> Optional[CollectReport]: - # Packages are Modules, but we only want to skip test-bearing Modules, + # Packages are Files, but we only want to skip test-bearing Files, # so don't filter Packages. - if isinstance(collector, Module) and not isinstance(collector, Package): + if isinstance(collector, File) and not isinstance(collector, Package): if collector.path not in self.lfplugin._last_failed_paths: self.lfplugin._skipped_files += 1 @@ -342,14 +339,14 @@ class LFPlugin: else: self.lastfailed[report.nodeid] = True - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, config: Config, items: List[nodes.Item] ) -> Generator[None, None, None]: - yield + res = yield if not self.active: - return + return res if self.lastfailed: previously_failed = [] @@ -394,6 +391,8 @@ class LFPlugin: else: self._report_status += "not deselecting items." + return res + def pytest_sessionfinish(self, session: Session) -> None: config = self.config if config.getoption("cacheshow") or hasattr(config, "workerinput"): @@ -414,11 +413,11 @@ class NFPlugin: assert config.cache is not None self.cached_nodeids = set(config.cache.get("cache/nodeids", [])) - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_collection_modifyitems( self, items: List[nodes.Item] ) -> Generator[None, None, None]: - yield + res = yield if self.active: new_items: Dict[str, nodes.Item] = {} @@ -436,6 +435,8 @@ class NFPlugin: else: self.cached_nodeids.update(item.nodeid for item in items) + return res + def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return] diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index a8ca0869f..81b8bffbc 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -11,11 +11,14 @@ from types import TracebackType from typing import Any from typing import AnyStr from typing import BinaryIO +from typing import Final +from typing import final from typing import Generator from typing import Generic from typing import Iterable from typing import Iterator from typing import List +from typing import Literal from typing import NamedTuple from typing import Optional from typing import TextIO @@ -24,7 +27,6 @@ from typing import Type from typing import TYPE_CHECKING from typing import Union -from _pytest.compat import final from _pytest.config import Config from _pytest.config import hookimpl from _pytest.config.argparsing import Parser @@ -34,12 +36,9 @@ from _pytest.fixtures import SubRequest from _pytest.nodes import Collector from _pytest.nodes import File from _pytest.nodes import Item +from _pytest.reports import CollectReport -if TYPE_CHECKING: - from typing_extensions import Final - from typing_extensions import Literal - - _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] +_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] def pytest_addoption(parser: Parser) -> None: @@ -132,8 +131,8 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None: sys.stderr = _reopen_stdio(sys.stderr, "wb") -@hookimpl(hookwrapper=True) -def pytest_load_initial_conftests(early_config: Config): +@hookimpl(wrapper=True) +def pytest_load_initial_conftests(early_config: Config) -> Generator[None, None, None]: ns = early_config.known_args_namespace if ns.capture == "fd": _windowsconsoleio_workaround(sys.stdout) @@ -147,12 +146,16 @@ def pytest_load_initial_conftests(early_config: Config): # Finally trigger conftest loading but while capturing (issue #93). capman.start_global_capturing() - outcome = yield - capman.suspend_global_capture() - if outcome.excinfo is not None: + try: + try: + yield + finally: + capman.suspend_global_capture() + except BaseException: out, err = capman.read_global_capture() sys.stdout.write(out) sys.stderr.write(err) + raise # IO Helpers. @@ -687,7 +690,7 @@ class MultiCapture(Generic[AnyStr]): return CaptureResult(out, err) # type: ignore[arg-type] -def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]: +def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]: if method == "fd": return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2)) elif method == "sys": @@ -723,7 +726,7 @@ class CaptureManager: needed to ensure the fixtures take precedence over the global capture. """ - def __init__(self, method: "_CaptureMethod") -> None: + def __init__(self, method: _CaptureMethod) -> None: self._method: Final = method self._global_capturing: Optional[MultiCapture[str]] = None self._capture_fixture: Optional[CaptureFixture[Any]] = None @@ -843,41 +846,45 @@ class CaptureManager: self.deactivate_fixture() self.suspend_global_capture(in_=False) - out, err = self.read_global_capture() - item.add_report_section(when, "stdout", out) - item.add_report_section(when, "stderr", err) + out, err = self.read_global_capture() + item.add_report_section(when, "stdout", out) + item.add_report_section(when, "stderr", err) # Hooks - @hookimpl(hookwrapper=True) - def pytest_make_collect_report(self, collector: Collector): + @hookimpl(wrapper=True) + def pytest_make_collect_report( + self, collector: Collector + ) -> Generator[None, CollectReport, CollectReport]: if isinstance(collector, File): self.resume_global_capture() - outcome = yield - self.suspend_global_capture() + try: + rep = yield + finally: + self.suspend_global_capture() out, err = self.read_global_capture() - rep = outcome.get_result() if out: rep.sections.append(("Captured stdout", out)) if err: rep.sections.append(("Captured stderr", err)) else: - yield + rep = yield + return rep - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]: with self.item_capture("setup", item): - yield + return (yield) - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]: with self.item_capture("call", item): - yield + return (yield) - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]: with self.item_capture("teardown", item): - yield + return (yield) @hookimpl(tryfirst=True) def pytest_keyboard_interrupt(self) -> None: diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 352211de8..cead6c311 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -12,26 +12,12 @@ from inspect import signature from pathlib import Path from typing import Any from typing import Callable -from typing import Generic +from typing import Final from typing import NoReturn -from typing import TYPE_CHECKING from typing import TypeVar import py -# fmt: off -# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351. -# If `overload` is imported from `compat` instead of from `typing`, -# Sphinx doesn't recognize it as `overload` and the API docs for -# overloaded functions look good again. But type checkers handle -# it fine. -# fmt: on -if True: - from typing import overload as overload - -if TYPE_CHECKING: - from typing_extensions import Final - _T = TypeVar("_T") _S = TypeVar("_S") @@ -58,17 +44,6 @@ class NotSetType(enum.Enum): NOTSET: Final = NotSetType.token # noqa: E305 # fmt: on -if sys.version_info >= (3, 8): - import importlib.metadata - - importlib_metadata = importlib.metadata -else: - import importlib_metadata as importlib_metadata # noqa: F401 - - -def _format_args(func: Callable[..., Any]) -> str: - return str(signature(func)) - def is_generator(func: object) -> bool: genfunc = inspect.isgeneratorfunction(func) @@ -93,7 +68,7 @@ def is_async_function(func: object) -> bool: return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) -def getlocation(function, curdir: str | None = None) -> str: +def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str: function = get_real_func(function) fn = Path(inspect.getfile(function)) lineno = function.__code__.co_firstlineno @@ -127,7 +102,7 @@ def num_mock_patch_args(function) -> int: def getfuncargnames( - function: Callable[..., Any], + function: Callable[..., object], *, name: str = "", is_method: bool = False, @@ -338,47 +313,6 @@ def safe_isclass(obj: object) -> bool: return False -if TYPE_CHECKING: - if sys.version_info >= (3, 8): - from typing import final as final - else: - from typing_extensions import final as final -elif sys.version_info >= (3, 8): - from typing import final as final -else: - - def final(f): - return f - - -if sys.version_info >= (3, 8): - from functools import cached_property as cached_property -else: - - class cached_property(Generic[_S, _T]): - __slots__ = ("func", "__doc__") - - def __init__(self, func: Callable[[_S], _T]) -> None: - self.func = func - self.__doc__ = func.__doc__ - - @overload - def __get__( - self, instance: None, owner: type[_S] | None = ... - ) -> cached_property[_S, _T]: - ... - - @overload - def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T: - ... - - def __get__(self, instance, owner=None): - if instance is None: - return self - value = instance.__dict__[self.func.__name__] = self.func(instance) - return value - - def get_user_id() -> int | None: """Return the current user id, or None if we cannot get it reliably on the current platform.""" # win32 does not have a getuid() function. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 85d8830e7..1f4670a56 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -5,6 +5,7 @@ import copy import dataclasses import enum import glob +import importlib.metadata import inspect import os import re @@ -21,6 +22,7 @@ from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import Generator from typing import IO from typing import Iterable @@ -48,8 +50,6 @@ from .findpaths import determine_setup from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback from _pytest._io import TerminalWriter -from _pytest.compat import final -from _pytest.compat import importlib_metadata # type: ignore[attr-defined] from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.pathlib import absolutepath @@ -137,7 +137,9 @@ def main( ) -> Union[int, ExitCode]: """Perform an in-process test run. - :param args: List of command line arguments. + :param args: + List of command line arguments. If `None` or not given, defaults to reading + arguments directly from the process command line (:data:`sys.argv`). :param plugins: List of plugin objects to be auto-registered during initialization. :returns: An exit code. @@ -257,7 +259,8 @@ default_plugins = essential_plugins + ( "logging", "reports", "python_path", - *(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []), + "unraisableexception", + "threadexception", "faulthandler", ) @@ -527,9 +530,12 @@ class PytestPluginManager(PluginManager): # def _set_initial_conftests( self, - namespace: argparse.Namespace, + args: Sequence[Union[str, Path]], + pyargs: bool, + noconftest: bool, rootpath: Path, - testpaths_ini: Sequence[str], + confcutdir: Optional[Path], + importmode: Union[ImportMode, str], ) -> None: """Load initial conftest files given a preparsed "namespace". @@ -539,17 +545,12 @@ class PytestPluginManager(PluginManager): common options will not confuse our logic here. """ current = Path.cwd() - self._confcutdir = ( - absolutepath(current / namespace.confcutdir) - if namespace.confcutdir - else None - ) - self._noconftest = namespace.noconftest - self._using_pyargs = namespace.pyargs - testpaths = namespace.file_or_dir + testpaths_ini + self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None + self._noconftest = noconftest + self._using_pyargs = pyargs foundanchor = False - for testpath in testpaths: - path = str(testpath) + for intitial_path in args: + path = str(intitial_path) # remove node-id syntax i = path.find("::") if i != -1: @@ -563,10 +564,10 @@ class PytestPluginManager(PluginManager): except OSError: # pragma: no cover anchor_exists = False if anchor_exists: - self._try_load_conftest(anchor, namespace.importmode, rootpath) + self._try_load_conftest(anchor, importmode, rootpath) foundanchor = True if not foundanchor: - self._try_load_conftest(current, namespace.importmode, rootpath) + self._try_load_conftest(current, importmode, rootpath) def _is_in_confcutdir(self, path: Path) -> bool: """Whether a path is within the confcutdir. @@ -1140,10 +1141,25 @@ class Config: @hookimpl(trylast=True) def pytest_load_initial_conftests(self, early_config: "Config") -> None: - self.pluginmanager._set_initial_conftests( - early_config.known_args_namespace, + # We haven't fully parsed the command line arguments yet, so + # early_config.args it not set yet. But we need it for + # discovering the initial conftests. So "pre-run" the logic here. + # It will be done for real in `parse()`. + args, args_source = early_config._decide_args( + args=early_config.known_args_namespace.file_or_dir, + pyargs=early_config.known_args_namespace.pyargs, + testpaths=early_config.getini("testpaths"), + invocation_dir=early_config.invocation_params.dir, rootpath=early_config.rootpath, - testpaths_ini=self.getini("testpaths"), + warn=False, + ) + self.pluginmanager._set_initial_conftests( + args=args, + pyargs=early_config.known_args_namespace.pyargs, + noconftest=early_config.known_args_namespace.noconftest, + rootpath=early_config.rootpath, + confcutdir=early_config.known_args_namespace.confcutdir, + importmode=early_config.known_args_namespace.importmode, ) def _initini(self, args: Sequence[str]) -> None: @@ -1203,7 +1219,7 @@ class Config: package_files = ( str(file) - for dist in importlib_metadata.distributions() + for dist in importlib.metadata.distributions() if any(ep.group == "pytest11" for ep in dist.entry_points) for file in dist.files or [] ) @@ -1223,6 +1239,49 @@ class Config: return args + def _decide_args( + self, + *, + args: List[str], + pyargs: List[str], + testpaths: List[str], + invocation_dir: Path, + rootpath: Path, + warn: bool, + ) -> Tuple[List[str], ArgsSource]: + """Decide the args (initial paths/nodeids) to use given the relevant inputs. + + :param warn: Whether can issue warnings. + """ + if args: + source = Config.ArgsSource.ARGS + result = args + else: + if invocation_dir == rootpath: + source = Config.ArgsSource.TESTPATHS + if pyargs: + result = testpaths + else: + result = [] + for path in testpaths: + result.extend(sorted(glob.iglob(path, recursive=True))) + if testpaths and not result: + if warn: + warning_text = ( + "No files were found in testpaths; " + "consider removing or adjusting your testpaths configuration. " + "Searching recursively from the current directory instead." + ) + self.issue_config_time_warning( + PytestConfigWarning(warning_text), stacklevel=3 + ) + else: + result = [] + if not result: + source = Config.ArgsSource.INCOVATION_DIR + result = [str(invocation_dir)] + return result, source + def _preparse(self, args: List[str], addopts: bool = True) -> None: if addopts: env_addopts = os.environ.get("PYTEST_ADDOPTS", "") @@ -1282,12 +1341,14 @@ class Config: else: raise - @hookimpl(hookwrapper=True) - def pytest_collection(self) -> Generator[None, None, None]: + @hookimpl(wrapper=True) + def pytest_collection(self) -> Generator[None, object, object]: # Validate invalid ini keys after collection is done so we take in account # options added by late-loading conftest files. - yield - self._validate_config_options() + try: + return (yield) + finally: + self._validate_config_options() def _checkversion(self) -> None: import pytest @@ -1371,34 +1432,17 @@ class Config: self.hook.pytest_cmdline_preparse(config=self, args=args) self._parser.after_preparse = True # type: ignore try: - source = Config.ArgsSource.ARGS args = self._parser.parse_setoption( args, self.option, namespace=self.option ) - if not args: - if self.invocation_params.dir == self.rootpath: - source = Config.ArgsSource.TESTPATHS - testpaths: List[str] = self.getini("testpaths") - if self.known_args_namespace.pyargs: - args = testpaths - else: - args = [] - for path in testpaths: - args.extend(sorted(glob.iglob(path, recursive=True))) - if testpaths and not args: - warning_text = ( - "No files were found in testpaths; " - "consider removing or adjusting your testpaths configuration. " - "Searching recursively from the current directory instead." - ) - self.issue_config_time_warning( - PytestConfigWarning(warning_text), stacklevel=3 - ) - if not args: - source = Config.ArgsSource.INCOVATION_DIR - args = [str(self.invocation_params.dir)] - self.args = args - self.args_source = source + self.args, self.args_source = self._decide_args( + args=args, + pyargs=self.known_args_namespace.pyargs, + testpaths=self.getini("testpaths"), + invocation_dir=self.invocation_params.dir, + rootpath=self.rootpath, + warn=True, + ) except PrintHelp: pass @@ -1406,7 +1450,7 @@ class Config: """Issue and handle a warning during the "configure" stage. During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item`` - function because it is not possible to have hookwrappers around ``pytest_configure``. + function because it is not possible to have hook wrappers around ``pytest_configure``. This function is mainly intended for plugins that need to issue warnings during ``pytest_configure`` (or similar stages). diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index d3f01916b..e345de016 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -7,26 +7,23 @@ from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import List +from typing import Literal from typing import Mapping from typing import NoReturn from typing import Optional from typing import Sequence from typing import Tuple -from typing import TYPE_CHECKING from typing import Union import _pytest._io -from _pytest.compat import final from _pytest.config.exceptions import UsageError from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT from _pytest.deprecated import ARGUMENT_TYPE_STR from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE from _pytest.deprecated import check_ispytest -if TYPE_CHECKING: - from typing_extensions import Literal - FILE_OR_DIR = "file_or_dir" @@ -177,7 +174,7 @@ class Parser: name: str, help: str, type: Optional[ - "Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']" + Literal["string", "paths", "pathlist", "args", "linelist", "bool"] ] = None, default: Any = None, ) -> None: diff --git a/src/_pytest/config/exceptions.py b/src/_pytest/config/exceptions.py index 4f1320e75..4031ea732 100644 --- a/src/_pytest/config/exceptions.py +++ b/src/_pytest/config/exceptions.py @@ -1,4 +1,4 @@ -from _pytest.compat import final +from typing import final @final diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index a3f80802c..69ec58c5b 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -304,10 +304,10 @@ class PdbInvoke: class PdbTrace: - @hookimpl(hookwrapper=True) - def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]: + @hookimpl(wrapper=True) + def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]: wrap_pytest_function_for_tracing(pyfuncitem) - yield + return (yield) def wrap_pytest_function_for_tracing(pyfuncitem): diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index b9c10df7a..3fcf99ba4 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -122,6 +122,11 @@ HOOK_LEGACY_MARKING = UnformattedWarning( "#configuring-hook-specs-impls-using-markers", ) +MARKED_FIXTURE = PytestRemovedIn8Warning( + "Marks applied to fixtures have no effect\n" + "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function" +) + # You want to make some `__init__` or function "private". # # def my_private_function(some, args): diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 455ad62cc..a37f1d2ac 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -582,7 +582,7 @@ def _setup_fixtures(doctest_item: DoctestItem) -> FixtureRequest: doctest_item._fixtureinfo = fm.getfixtureinfo( # type: ignore[attr-defined] node=doctest_item, func=func, cls=None, funcargs=False ) - fixture_request = FixtureRequest(doctest_item, _ispytest=True) + fixture_request = FixtureRequest(doctest_item, _ispytest=True) # type: ignore[arg-type] fixture_request._fillfixtures() return fixture_request diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index af879aa44..2dc672c8d 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -62,8 +62,8 @@ def get_timeout_config_value(config: Config) -> float: return float(config.getini("faulthandler_timeout") or 0.0) -@pytest.hookimpl(hookwrapper=True, trylast=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@pytest.hookimpl(wrapper=True, trylast=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: timeout = get_timeout_config_value(item.config) if timeout > 0: import faulthandler @@ -71,11 +71,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: stderr = item.config.stash[fault_handler_stderr_fd_key] faulthandler.dump_traceback_later(timeout, file=stderr) try: - yield + return (yield) finally: faulthandler.cancel_dump_traceback_later() else: - yield + return (yield) @pytest.hookimpl(tryfirst=True) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 0462504ef..650e934b3 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2,17 +2,17 @@ import dataclasses import functools import inspect import os -import sys import warnings from collections import defaultdict from collections import deque from contextlib import suppress from pathlib import Path -from types import TracebackType from typing import Any from typing import Callable from typing import cast from typing import Dict +from typing import Final +from typing import final from typing import Generator from typing import Generic from typing import Iterable @@ -21,10 +21,10 @@ from typing import List from typing import MutableMapping from typing import NoReturn from typing import Optional +from typing import overload from typing import Sequence from typing import Set from typing import Tuple -from typing import Type from typing import TYPE_CHECKING from typing import TypeVar from typing import Union @@ -35,10 +35,8 @@ from _pytest._code import getfslineno from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never -from _pytest.compat import final from _pytest.compat import get_real_func from _pytest.compat import get_real_method from _pytest.compat import getfuncargnames @@ -47,12 +45,12 @@ from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import NotSetType -from _pytest.compat import overload from _pytest.compat import safe_getattr from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest +from _pytest.deprecated import MARKED_FIXTURE from _pytest.deprecated import YIELD_FIXTURE from _pytest.mark import Mark from _pytest.mark import ParameterSet @@ -62,6 +60,7 @@ from _pytest.outcomes import skip from _pytest.outcomes import TEST_OUTCOME from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath +from _pytest.scope import _ScopeName from _pytest.scope import HIGH_SCOPES from _pytest.scope import Scope from _pytest.stash import StashKey @@ -70,9 +69,9 @@ from _pytest.stash import StashKey if TYPE_CHECKING: from typing import Deque - from _pytest.scope import _ScopeName from _pytest.main import Session from _pytest.python import CallSpec2 + from _pytest.python import Function from _pytest.python import Metafunc @@ -97,8 +96,8 @@ _FixtureCachedResult = Union[ None, # Cache key. object, - # Exc info if raised. - Tuple[Type[BaseException], BaseException, TracebackType], + # Exception if raised. + BaseException, ], ] @@ -217,6 +216,7 @@ def add_funcarg_pseudo_fixture_def( params=valuelist, unittest=False, ids=None, + _ispytest=True, ) arg2fixturedefs[argname] = [fixturedef] if name2pseudofixturedef is not None: @@ -352,17 +352,35 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any: return request.param -@dataclasses.dataclass +@dataclasses.dataclass(frozen=True) class FuncFixtureInfo: + """Fixture-related information for a fixture-requesting item (e.g. test + function). + + This is used to examine the fixtures which an item requests statically + (known during collection). This includes autouse fixtures, fixtures + requested by the `usefixtures` marker, fixtures requested in the function + parameters, and the transitive closure of these. + + An item may also request fixtures dynamically (using `request.getfixturevalue`); + these are not reflected here. + """ + __slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs") - # Original function argument names. + # Fixture names that the item requests directly by function parameters. argnames: Tuple[str, ...] - # Argnames that function immediately requires. These include argnames + - # fixture names specified via usefixtures and via autouse=True in fixture - # definitions. + # Fixture names that the item immediately requires. These include + # argnames + fixture names specified via usefixtures and via autouse=True in + # fixture definitions. initialnames: Tuple[str, ...] + # The transitive closure of the fixture names that the item requires. + # Note: can't include dynamic dependencies (`request.getfixturevalue` calls). names_closure: List[str] + # A map from a fixture name in the transitive closure to the FixtureDefs + # matching the name which are applicable to this function. + # There may be multiple overriding fixtures with the same name. The + # sequence is ordered from furthest to closes to the function. name2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]] def prune_dependency_tree(self) -> None: @@ -401,17 +419,31 @@ class FixtureRequest: indirectly. """ - def __init__(self, pyfuncitem, *, _ispytest: bool = False) -> None: + def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) - self._pyfuncitem = pyfuncitem #: Fixture for which this request is being performed. self.fixturename: Optional[str] = None + self._pyfuncitem = pyfuncitem + self._fixturemanager = pyfuncitem.session._fixturemanager self._scope = Scope.Function - self._fixture_defs: Dict[str, FixtureDef[Any]] = {} - fixtureinfo: FuncFixtureInfo = pyfuncitem._fixtureinfo - self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy() + # The FixtureDefs for each fixture name requested by this item. + # Starts from the statically-known fixturedefs resolved during + # collection. Dynamically requested fixtures (using + # `request.getfixturevalue("foo")`) are added dynamically. + self._arg2fixturedefs = pyfuncitem._fixtureinfo.name2fixturedefs.copy() + # A fixture may override another fixture with the same name, e.g. a fixture + # in a module can override a fixture in a conftest, a fixture in a class can + # override a fixture in the module, and so on. + # An overriding fixture can request its own name; in this case it gets + # the value of the fixture it overrides, one level up. + # The _arg2index state keeps the current depth in the overriding chain. + # The fixturedefs list in _arg2fixturedefs for a given name is ordered from + # furthest to closest, so we use negative indexing -1, -2, ... to go from + # last to first. self._arg2index: Dict[str, int] = {} - self._fixturemanager: FixtureManager = pyfuncitem.session._fixturemanager + # The evaluated argnames so far, mapping to the FixtureDef they resolved + # to. + self._fixture_defs: Dict[str, FixtureDef[Any]] = {} # 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 @@ -423,7 +455,7 @@ class FixtureRequest: self.param: Any @property - def scope(self) -> "_ScopeName": + def scope(self) -> _ScopeName: """Scope string, one of "function", "class", "module", "package", "session".""" return self._scope.value @@ -464,12 +496,17 @@ class FixtureRequest: assert self._pyfuncitem.parent is not None parentid = self._pyfuncitem.parent.nodeid fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid) - # TODO: Fix this type ignore. Either add assert or adjust types. - # Can this be None here? - self._arg2fixturedefs[argname] = fixturedefs # type: ignore[assignment] - # fixturedefs list is immutable so we maintain a decreasing index. + if fixturedefs is not None: + self._arg2fixturedefs[argname] = fixturedefs + # No fixtures defined with this name. + if fixturedefs is None: + raise FixtureLookupError(argname, self) + # The are no fixtures with this name applicable for the function. + if not fixturedefs: + raise FixtureLookupError(argname, self) index = self._arg2index.get(argname, 0) - 1 - if fixturedefs is None or (-index > len(fixturedefs)): + # The fixture requested its own name, but no remaining to override. + if -index > len(fixturedefs): raise FixtureLookupError(argname, self) self._arg2index[argname] = index return fixturedefs[index] @@ -502,7 +539,7 @@ class FixtureRequest: """Instance (can be None) on which test function was collected.""" # unittest support hack, see _pytest.unittest.TestCaseFunction. try: - return self._pyfuncitem._testcase + return self._pyfuncitem._testcase # type: ignore[attr-defined] except AttributeError: function = getattr(self, "function", None) return getattr(function, "__self__", None) @@ -512,15 +549,16 @@ class FixtureRequest: """Python module object where the test function was collected.""" if self.scope not in ("function", "class", "module"): raise AttributeError(f"module not available in {self.scope}-scoped context") - return self._pyfuncitem.getparent(_pytest.python.Module).obj + mod = self._pyfuncitem.getparent(_pytest.python.Module) + assert mod is not None + return mod.obj @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. - return self._pyfuncitem.path # type: ignore + return self._pyfuncitem.path @property def keywords(self) -> MutableMapping[str, Any]: @@ -592,9 +630,8 @@ class FixtureRequest: def _get_active_fixturedef( self, argname: str ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]: - try: - return self._fixture_defs[argname] - except KeyError: + fixturedef = self._fixture_defs.get(argname) + if fixturedef is None: try: fixturedef = self._getnextfixturedef(argname) except FixtureLookupError: @@ -602,10 +639,8 @@ class FixtureRequest: cached_result = (self, [0], None) return PseudoFixtureDef(cached_result, Scope.Function) raise - # Remove indent to prevent the python3 exception - # from leaking into the call. - self._compute_fixture_value(fixturedef) - self._fixture_defs[argname] = fixturedef + self._compute_fixture_value(fixturedef) + self._fixture_defs[argname] = fixturedef return fixturedef def _get_fixturestack(self) -> List["FixtureDef[Any]"]: @@ -698,7 +733,8 @@ class FixtureRequest: self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest" ) -> None: # If fixture function failed it might have registered finalizers. - subrequest.node.addfinalizer(lambda: fixturedef.finish(request=subrequest)) + finalizer = functools.partial(fixturedef.finish, request=subrequest) + subrequest.node.addfinalizer(finalizer) def _check_scope( self, @@ -728,8 +764,10 @@ class FixtureRequest: p = bestrelpath(session.path, fs) else: p = fs - args = _format_args(factory) - lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args)) + lines.append( + "%s:%d: def %s%s" + % (p, lineno + 1, factory.__name__, inspect.signature(factory)) + ) return lines def __repr__(self) -> str: @@ -825,7 +863,9 @@ class FixtureLookupError(LookupError): if msg is None: fm = self.request._fixturemanager available = set() - parentid = self.request._pyfuncitem.parent.nodeid + parent = self.request._pyfuncitem.parent + assert parent is not None + parentid = parent.nodeid for name, fixturedefs in fm._arg2fixturedefs.items(): faclist = list(fm._matchfactories(fixturedefs, parentid)) if faclist: @@ -916,10 +956,10 @@ def _teardown_yield_fixture(fixturefunc, it) -> None: def _eval_scope_callable( - scope_callable: "Callable[[str, Config], _ScopeName]", + scope_callable: Callable[[str, Config], _ScopeName], fixture_name: str, config: Config, -) -> "_ScopeName": +) -> _ScopeName: try: # Type ignored because there is no typing mechanism to specify # keyword arguments, currently. @@ -942,7 +982,11 @@ def _eval_scope_callable( @final class FixtureDef(Generic[FixtureValue]): - """A container for a fixture definition.""" + """A container for a fixture definition. + + Note: At this time, only explicitly documented fields and methods are + considered public stable API. + """ def __init__( self, @@ -950,13 +994,16 @@ class FixtureDef(Generic[FixtureValue]): baseid: Optional[str], argname: str, func: "_FixtureFunc[FixtureValue]", - scope: Union[Scope, "_ScopeName", Callable[[str, Config], "_ScopeName"], None], + scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None], params: Optional[Sequence[object]], unittest: bool = False, ids: Optional[ Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] ] = None, + *, + _ispytest: bool = False, ) -> None: + check_ispytest(_ispytest) self._fixturemanager = fixturemanager # The "base" node ID for the fixture. # @@ -972,15 +1019,15 @@ class FixtureDef(Generic[FixtureValue]): # directory path relative to the rootdir. # # For other plugins, the baseid is the empty string (always matches). - self.baseid = baseid or "" + self.baseid: Final = baseid or "" # Whether the fixture was found from a node or a conftest in the # collection tree. Will be false for fixtures defined in non-conftest # plugins. - self.has_location = baseid is not None + self.has_location: Final = baseid is not None # The fixture factory function. - self.func = func + self.func: Final = func # The name by which the fixture may be requested. - self.argname = argname + self.argname: Final = argname if scope is None: scope = Scope.Function elif callable(scope): @@ -989,26 +1036,24 @@ class FixtureDef(Generic[FixtureValue]): scope = Scope.from_user( scope, descr=f"Fixture '{func.__name__}'", where=baseid ) - self._scope = scope + self._scope: Final = scope # If the fixture is directly parametrized, the parameter values. - self.params: Optional[Sequence[object]] = params + self.params: Final = params # If the fixture is directly parametrized, a tuple of explicit IDs to # assign to the parameter values, or a callable to generate an ID given # a parameter value. - self.ids = ids + self.ids: Final = ids # The names requested by the fixtures. - self.argnames = getfuncargnames(func, name=argname, is_method=unittest) + self.argnames: Final = getfuncargnames(func, name=argname, is_method=unittest) # Whether the fixture was collected from a unittest TestCase class. - # Note that it really only makes sense to define autouse fixtures in - # unittest TestCases. - self.unittest = unittest + self.unittest: Final = unittest # If the fixture was executed, the current value of the fixture. # Can change if the fixture is executed with different parameters. self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None - self._finalizers: List[Callable[[], object]] = [] + self._finalizers: Final[List[Callable[[], object]]] = [] @property - def scope(self) -> "_ScopeName": + def scope(self) -> _ScopeName: """Scope string, one of "function", "class", "module", "package", "session".""" return self._scope.value @@ -1036,7 +1081,7 @@ class FixtureDef(Generic[FixtureValue]): # value and remove all finalizers because they may be bound methods # which will keep instances alive. self.cached_result = None - self._finalizers = [] + self._finalizers.clear() def execute(self, request: SubRequest) -> FixtureValue: # Get required arguments and register our own finish() @@ -1050,13 +1095,13 @@ class FixtureDef(Generic[FixtureValue]): my_cache_key = self.cache_key(request) if self.cached_result is not None: + cache_key = self.cached_result[1] # note: comparison with `==` can fail (or be expensive) for e.g. # numpy arrays (#6497). - cache_key = self.cached_result[1] if my_cache_key is cache_key: if self.cached_result[2] is not None: - _, val, tb = self.cached_result[2] - raise val.with_traceback(tb) + exc = self.cached_result[2] + raise exc else: result = self.cached_result[0] return result @@ -1121,35 +1166,18 @@ def pytest_fixture_setup( my_cache_key = fixturedef.cache_key(request) try: result = call_fixture_func(fixturefunc, request, kwargs) - except TEST_OUTCOME: - exc_info = sys.exc_info() - assert exc_info[0] is not None - if isinstance( - exc_info[1], skip.Exception - ) and not fixturefunc.__name__.startswith("xunit_setup"): - exc_info[1]._use_item_location = True # type: ignore[attr-defined] - fixturedef.cached_result = (None, my_cache_key, exc_info) + except TEST_OUTCOME as e: + if isinstance(e, skip.Exception): + # The test requested a fixture which caused a skip. + # Don't show the fixture as the skip location, as then the user + # wouldn't know which test skipped. + e._use_item_location = True + fixturedef.cached_result = (None, my_cache_key, e) raise fixturedef.cached_result = (result, my_cache_key, None) return result -def _ensure_immutable_ids( - ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]] -) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]: - if ids is None: - return None - if callable(ids): - return ids - return tuple(ids) - - -def _params_converter( - params: Optional[Iterable[object]], -) -> Optional[Tuple[object, ...]]: - return tuple(params) if params is not None else None - - def wrap_function_to_error_out_if_called_directly( function: FixtureFunction, fixture_marker: "FixtureFunctionMarker", @@ -1199,6 +1227,9 @@ class FixtureFunctionMarker: "fixture is being applied more than once to the same function" ) + if hasattr(function, "pytestmark"): + warnings.warn(MARKED_FIXTURE, stacklevel=2) + function = wrap_function_to_error_out_if_called_directly(function, self) name = self.name or function.__name__ @@ -1410,10 +1441,14 @@ class FixtureManager: def __init__(self, session: "Session") -> None: self.session = session self.config: Config = session.config - self._arg2fixturedefs: Dict[str, List[FixtureDef[Any]]] = {} - self._holderobjseen: Set[object] = set() + # Maps a fixture name (argname) to all of the FixtureDefs in the test + # suite/plugins defined with this name. Populated by parsefactories(). + # TODO: The order of the FixtureDefs list of each arg is significant, + # explain. + self._arg2fixturedefs: Final[Dict[str, List[FixtureDef[Any]]]] = {} + self._holderobjseen: Final[Set[object]] = set() # A mapping from a nodeid to a list of autouse fixtures it defines. - self._nodeid_autousenames: Dict[str, List[str]] = { + self._nodeid_autousenames: Final[Dict[str, List[str]]] = { "": self.config.getini("usefixtures"), } session.config.pluginmanager.register(self, "funcmanage") @@ -1438,8 +1473,26 @@ class FixtureManager: return parametrize_argnames def getfixtureinfo( - self, node: nodes.Node, func, cls, funcargs: bool = True + self, + node: nodes.Item, + func: Callable[..., object], + cls: Optional[type], + funcargs: bool = True, ) -> FuncFixtureInfo: + """Calculate the :class:`FuncFixtureInfo` for an item. + + If ``funcargs`` is false, or if the item sets an attribute + ``nofuncargs = True``, then ``func`` is not examined at all. + + :param node: + The item requesting the fixtures. + :param func: + The item's function. + :param cls: + If the function is a method, the method's class. + :param funcargs: + Whether to look into func's parameters as fixture requests. + """ if funcargs and not getattr(node, "nofuncargs", False): argnames = getfuncargnames(func, name=node.name, cls=cls) else: @@ -1449,8 +1502,7 @@ class FixtureManager: arg for mark in node.iter_markers(name="usefixtures") for arg in mark.args ) initialnames = usefixtures + argnames - fm = node.session._fixturemanager - initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure( + initialnames, names_closure, arg2fixturedefs = self.getfixtureclosure( initialnames, node, ignore_args=self._get_direct_parametrize_args(node) ) return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs) @@ -1465,7 +1517,7 @@ class FixtureManager: # Construct the base nodeid which is later used to check # what fixtures are visible for particular tests (as denoted # by their test id). - if p.name.startswith("conftest.py"): + if p.name == "conftest.py": try: nodeid = str(p.parent.relative_to(self.config.rootpath)) except ValueError: @@ -1671,6 +1723,7 @@ class FixtureManager: params=marker.params, unittest=unittest, ids=marker.ids, + _ispytest=True, ) faclist = self._arg2fixturedefs.setdefault(name, []) @@ -1692,11 +1745,16 @@ class FixtureManager: def getfixturedefs( self, argname: str, nodeid: str ) -> Optional[Sequence[FixtureDef[Any]]]: - """Get a list of fixtures which are applicable to the given node id. + """Get FixtureDefs for a fixture name which are applicable + to a given node. - :param str argname: Name of the fixture to search for. - :param str nodeid: Full node id of the requesting test. - :rtype: Sequence[FixtureDef] + Returns None if there are no fixtures at all defined with the given + name. (This is different from the case in which there are fixtures + with the given name, but none applicable to the node. In this case, + an empty result is returned). + + :param argname: Name of the fixture to search for. + :param nodeid: Full node id of the requesting test. """ try: fixturedefs = self._arg2fixturedefs[argname] diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 6b6718a70..4122d6009 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -2,6 +2,7 @@ import os import sys from argparse import Action +from typing import Generator from typing import List from typing import Optional from typing import Union @@ -97,15 +98,14 @@ def pytest_addoption(parser: Parser) -> None: ) -@pytest.hookimpl(hookwrapper=True) -def pytest_cmdline_parse(): - outcome = yield - config: Config = outcome.get_result() +@pytest.hookimpl(wrapper=True) +def pytest_cmdline_parse() -> Generator[None, Config, Config]: + config = yield if config.option.debug: # --debug | --debug was provided. path = config.option.debug - debugfile = open(path, "w") + debugfile = open(path, "w", encoding="utf-8") debugfile.write( "versions pytest-%s, " "python-%s\ncwd=%s\nargs=%s\n\n" @@ -128,6 +128,8 @@ def pytest_cmdline_parse(): config.add_cleanup(unset_tracing) + return config + def showversion(config: Config) -> None: if config.option.version > 1: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 1f7c368f7..11878d1b0 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -18,7 +18,7 @@ from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK if TYPE_CHECKING: import pdb import warnings - from typing_extensions import Literal + from typing import Literal from _pytest._code.code import ExceptionRepr from _pytest._code.code import ExceptionInfo @@ -60,7 +60,7 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: :param pytest.PytestPluginManager pluginmanager: The pytest plugin manager. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. """ @@ -74,7 +74,7 @@ def pytest_plugin_registered( :param pytest.PytestPluginManager manager: pytest plugin manager. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. """ @@ -113,7 +113,7 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> attribute or can be retrieved as the ``pytestconfig`` fixture. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. """ @@ -128,7 +128,7 @@ def pytest_configure(config: "Config") -> None: imported. .. note:: - This hook is incompatible with ``hookwrapper=True``. + This hook is incompatible with hook wrappers. :param pytest.Config config: The pytest config object. """ diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index af1d0c07e..8df0a5163 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -3,6 +3,8 @@ import dataclasses import shlex import subprocess from pathlib import Path +from typing import Final +from typing import final from typing import List from typing import Optional from typing import TYPE_CHECKING @@ -11,7 +13,6 @@ from typing import Union from iniconfig import SectionWrapper from _pytest.cacheprovider import Cache -from _pytest.compat import final from _pytest.compat import LEGACY_PATH from _pytest.compat import legacy_path from _pytest.config import Config @@ -32,8 +33,6 @@ from _pytest.terminal import TerminalReporter from _pytest.tmpdir import TempPathFactory if TYPE_CHECKING: - from typing_extensions import Final - import pexpect diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 6381e86b0..8de690d9b 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -13,8 +13,10 @@ from logging import LogRecord from pathlib import Path from typing import AbstractSet from typing import Dict +from typing import final from typing import Generator from typing import List +from typing import Literal from typing import Mapping from typing import Optional from typing import Tuple @@ -25,7 +27,6 @@ from typing import Union from _pytest import nodes from _pytest._io import TerminalWriter from _pytest.capture import CaptureManager -from _pytest.compat import final from _pytest.config import _strtobool from _pytest.config import Config from _pytest.config import create_terminal_writer @@ -41,8 +42,6 @@ from _pytest.terminal import TerminalReporter if TYPE_CHECKING: logging_StreamHandler = logging.StreamHandler[StringIO] - - from typing_extensions import Literal else: logging_StreamHandler = logging.StreamHandler @@ -515,7 +514,9 @@ class LogCaptureFixture: return original_disable_level def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None: - """Set the level of a logger for the duration of a test. + """Set the threshold level of a logger for the duration of a test. + + Logging messages which are less severe than this level will not be captured. .. versionchanged:: 3.4 The levels of the loggers changed by this function will be @@ -736,27 +737,26 @@ class LoggingPlugin: return True - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_sessionstart(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("sessionstart") with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield + return (yield) - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_collection(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("collection") with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield + return (yield) - @hookimpl(hookwrapper=True) - def pytest_runtestloop(self, session: Session) -> Generator[None, None, None]: + @hookimpl(wrapper=True) + def pytest_runtestloop(self, session: Session) -> Generator[None, object, object]: if session.config.option.collectonly: - yield - return + return (yield) if self._log_cli_enabled() and self._config.getoption("verbose") < 1: # The verbose flag is needed to avoid messy test progress output. @@ -764,7 +764,7 @@ class LoggingPlugin: with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield # Run all the tests. + return (yield) # Run all the tests. @hookimpl def pytest_runtest_logstart(self) -> None: @@ -789,12 +789,13 @@ class LoggingPlugin: item.stash[caplog_records_key][when] = caplog_handler.records item.stash[caplog_handler_key] = caplog_handler - yield + try: + yield + finally: + log = report_handler.stream.getvalue().strip() + item.add_report_section(when, "log", log) - log = report_handler.stream.getvalue().strip() - item.add_report_section(when, "log", log) - - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("setup") @@ -802,31 +803,33 @@ class LoggingPlugin: item.stash[caplog_records_key] = empty yield from self._runtest_for(item, "setup") - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_call(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("call") yield from self._runtest_for(item, "call") - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("teardown") - yield from self._runtest_for(item, "teardown") - del item.stash[caplog_records_key] - del item.stash[caplog_handler_key] + try: + yield from self._runtest_for(item, "teardown") + finally: + del item.stash[caplog_records_key] + del item.stash[caplog_handler_key] @hookimpl def pytest_runtest_logfinish(self) -> None: self.log_cli_handler.set_when("finish") - @hookimpl(hookwrapper=True, tryfirst=True) + @hookimpl(wrapper=True, tryfirst=True) def pytest_sessionfinish(self) -> Generator[None, None, None]: self.log_cli_handler.set_when("sessionfinish") with catching_logs(self.log_cli_handler, level=self.log_cli_level): with catching_logs(self.log_file_handler, level=self.log_file_level): - yield + return (yield) @hookimpl def pytest_unconfigure(self) -> None: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index c9a495380..f27767cf1 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -9,21 +9,21 @@ import sys from pathlib import Path from typing import Callable from typing import Dict +from typing import final from typing import FrozenSet from typing import Iterator from typing import List +from typing import Literal from typing import Optional +from typing import overload from typing import Sequence from typing import Set from typing import Tuple from typing import Type -from typing import TYPE_CHECKING from typing import Union import _pytest._code from _pytest import nodes -from _pytest.compat import final -from _pytest.compat import overload from _pytest.config import Config from _pytest.config import directory_arg from _pytest.config import ExitCode @@ -43,10 +43,6 @@ from _pytest.runner import collect_one_node from _pytest.runner import SetupState -if TYPE_CHECKING: - from typing_extensions import Literal - - def pytest_addoption(parser: Parser) -> None: parser.addini( "norecursedirs", @@ -400,6 +396,12 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo allow_in_venv = config.getoption("collect_in_virtualenv") if not allow_in_venv and _in_venv(collection_path): return True + + if collection_path.is_dir(): + norecursepatterns = config.getini("norecursedirs") + if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns): + return True + return None @@ -456,6 +458,11 @@ class _bestrelpath_cache(Dict[Path, str]): @final class Session(nodes.FSCollector): + """The root of the collection tree. + + ``Session`` collects the initial paths given as arguments to pytest. + """ + Interrupted = Interrupted Failed = Failed # Set on the session by runner.pytest_sessionstart. @@ -563,9 +570,6 @@ class Session(nodes.FSCollector): ihook = self.gethookproxy(fspath.parent) if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): return False - norecursepatterns = self.config.getini("norecursedirs") - if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): - return False return True def _collectfile( @@ -686,8 +690,8 @@ class Session(nodes.FSCollector): # are not collected more than once. matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {} - # Dirnames of pkgs with dunder-init files. - pkg_roots: Dict[str, Package] = {} + # Directories of pkgs with dunder-init files. + pkg_roots: Dict[Path, Package] = {} for argpath, names in self._initial_parts: self.trace("processing argument", (argpath, names)) @@ -708,7 +712,7 @@ class Session(nodes.FSCollector): col = self._collectfile(pkginit, handle_dupes=False) if col: if isinstance(col[0], Package): - pkg_roots[str(parent)] = col[0] + pkg_roots[parent] = col[0] node_cache1[col[0].path] = [col[0]] # If it's a directory argument, recurse and look for any Subpackages. @@ -717,7 +721,7 @@ class Session(nodes.FSCollector): assert not names, f"invalid arg {(argpath, names)!r}" seen_dirs: Set[Path] = set() - for direntry in visit(str(argpath), self._recurse): + for direntry in visit(argpath, self._recurse): if not direntry.is_file(): continue @@ -732,8 +736,8 @@ class Session(nodes.FSCollector): for x in self._collectfile(pkginit): yield x if isinstance(x, Package): - pkg_roots[str(dirpath)] = x - if str(dirpath) in pkg_roots: + pkg_roots[dirpath] = x + if dirpath in pkg_roots: # Do not collect packages here. continue @@ -750,7 +754,7 @@ class Session(nodes.FSCollector): if argpath in node_cache1: col = node_cache1[argpath] else: - collect_root = pkg_roots.get(str(argpath.parent), self) + collect_root = pkg_roots.get(argpath.parent, self) col = collect_root._collectfile(argpath, handle_dupes=False) if col: node_cache1[argpath] = col diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index f82a81d44..b995518bf 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -26,7 +26,6 @@ from typing import NoReturn from typing import Optional from typing import Sequence - __all__ = [ "Expression", "ParseError", @@ -132,7 +131,7 @@ IDENT_PREFIX = "$" def expression(s: Scanner) -> ast.Expression: if s.accept(TokenType.EOF): - ret: ast.expr = ast.NameConstant(False) + ret: ast.expr = ast.Constant(False) else: ret = expr(s) s.accept(TokenType.EOF, reject=True) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 8dbff1dc9..d6e426567 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -5,6 +5,7 @@ import warnings from typing import Any from typing import Callable from typing import Collection +from typing import final from typing import Iterable from typing import Iterator from typing import List @@ -23,11 +24,11 @@ from typing import Union from .._code import getfslineno from ..compat import ascii_escaped -from ..compat import final from ..compat import NOTSET from ..compat import NotSetType from _pytest.config import Config from _pytest.deprecated import check_ispytest +from _pytest.deprecated import MARKED_FIXTURE from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning @@ -373,7 +374,9 @@ def get_unpacked_marks( if not consider_mro: mark_lists = [obj.__dict__.get("pytestmark", [])] else: - mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__] + mark_lists = [ + x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__) + ] mark_list = [] for item in mark_lists: if isinstance(item, list): @@ -412,6 +415,12 @@ def store_mark(obj, mark: Mark) -> None: This is used to implement the Mark declarations/decorators correctly. """ assert isinstance(mark, Mark), mark + + from ..fixtures import getfixturemarker + + if getfixturemarker(obj) is not None: + warnings.warn(MARKED_FIXTURE, stacklevel=2) + # Always reassign name to avoid updating pytestmark in a reference that # was only borrowed. obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark] diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 9e51ff335..834700b1b 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -5,6 +5,7 @@ import sys import warnings from contextlib import contextmanager from typing import Any +from typing import final from typing import Generator from typing import List from typing import Mapping @@ -15,7 +16,6 @@ from typing import Tuple from typing import TypeVar from typing import Union -from _pytest.compat import final from _pytest.fixtures import fixture from _pytest.warning_types import PytestWarning diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index dbd6b0a42..cb8907fe8 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,5 +1,6 @@ import os import warnings +from functools import cached_property from inspect import signature from pathlib import Path from typing import Any @@ -23,7 +24,6 @@ from _pytest._code import getfslineno from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest._code.code import Traceback -from _pytest.compat import cached_property from _pytest.compat import LEGACY_PATH from _pytest.config import Config from _pytest.config import ConftestImportFailure @@ -157,10 +157,11 @@ class NodeMeta(type): class Node(metaclass=NodeMeta): - """Base class for Collector and Item, the components of the test - collection tree. + r"""Base class of :class:`Collector` and :class:`Item`, the components of + the test collection tree. - Collector subclasses have children; Items are leaf nodes. + ``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the + leaf nodes. """ # Implemented in the legacypath plugin. @@ -525,15 +526,17 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i class Collector(Node): - """Collector instances create children through collect() and thus - iteratively build a tree.""" + """Base class of all collectors. + + Collector create children through `collect()` and thus iteratively build + the collection tree. + """ class CollectError(Exception): """An error during collection, contains a custom message.""" def collect(self) -> Iterable[Union["Item", "Collector"]]: - """Return a list of children (items and collectors) for this - collection node.""" + """Collect children (items and collectors) for this collector.""" raise NotImplementedError("abstract") # TODO: This omits the style= parameter which breaks Liskov Substitution. @@ -577,6 +580,8 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[ class FSCollector(Collector): + """Base class for filesystem collectors.""" + def __init__( self, fspath: Optional[LEGACY_PATH] = None, @@ -660,7 +665,7 @@ class File(FSCollector): class Item(Node): - """A basic test invocation item. + """Base class of all test invocation items. Note that for a single function there might be multiple test invocation items. """ diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 1be97dda4..7dab4499b 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -7,23 +7,12 @@ from typing import Callable from typing import cast from typing import NoReturn from typing import Optional +from typing import Protocol from typing import Type from typing import TypeVar from _pytest.deprecated import KEYWORD_MSG_ARG -TYPE_CHECKING = False # Avoid circular import through compat. - -if TYPE_CHECKING: - from typing_extensions import Protocol -else: - # typing.Protocol is only available starting from Python 3.8. It is also - # available from typing_extensions, but we don't want a runtime dependency - # on that. So use a dummy runtime implementation. - from typing import Generic - - Protocol = Generic - class OutcomeException(BaseException): """OutcomeException and its subclass instances indicate and contain info diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 70383e4b5..138a0bdb2 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -523,6 +523,8 @@ def import_path( if mode is ImportMode.importlib: module_name = module_name_from_path(path, root) + with contextlib.suppress(KeyError): + return sys.modules[module_name] for meta_importer in sys.meta_path: spec = meta_importer.find_spec(module_name, [str(path.parent)]) @@ -633,6 +635,9 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> otherwise "src.tests.test_foo" is not importable by ``__import__``. """ module_parts = module_name.split(".") + child_module: Union[ModuleType, None] = None + module: Union[ModuleType, None] = None + child_name: str = "" while module_name: if module_name not in modules: try: @@ -642,13 +647,22 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> # ourselves to fall back to creating a dummy module. if not sys.meta_path: raise ModuleNotFoundError - importlib.import_module(module_name) + module = importlib.import_module(module_name) except ModuleNotFoundError: module = ModuleType( module_name, doc="Empty module created by pytest's importmode=importlib.", ) + else: + module = modules[module_name] + if child_module: + # Add child attribute to the parent that can reference the child + # modules. + if not hasattr(module, child_name): + setattr(module, child_name, child_module) modules[module_name] = module + # Keep track of the child module while moving up the tree. + child_module, child_name = module, module_name.rpartition(".")[-1] module_parts.pop(-1) module_name = ".".join(module_parts) @@ -755,21 +769,3 @@ def bestrelpath(directory: Path, dest: Path) -> str: # Forward from base to dest. *reldest.parts, ) - - -# Originates from py. path.local.copy(), with siginficant trims and adjustments. -# TODO(py38): Replace with shutil.copytree(..., symlinks=True, dirs_exist_ok=True) -def copytree(source: Path, target: Path) -> None: - """Recursively copy a source directory to target.""" - assert source.is_dir() - for entry in visit(source, recurse=lambda entry: not entry.is_symlink()): - x = Path(entry) - relpath = x.relative_to(source) - newx = target / relpath - newx.parent.mkdir(exist_ok=True) - if x.is_symlink(): - newx.symlink_to(os.readlink(x)) - elif x.is_file(): - shutil.copyfile(x, newx) - elif x.is_dir(): - newx.mkdir(exist_ok=True) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a9299944d..b112e6e70 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -6,6 +6,7 @@ import collections.abc import contextlib import gc import importlib +import locale import os import platform import re @@ -19,10 +20,13 @@ from pathlib import Path from typing import Any from typing import Callable from typing import Dict +from typing import Final +from typing import final from typing import Generator from typing import IO from typing import Iterable from typing import List +from typing import Literal from typing import Optional from typing import overload from typing import Sequence @@ -39,7 +43,6 @@ from iniconfig import SectionWrapper from _pytest import timing from _pytest._code import Source from _pytest.capture import _get_multicapture -from _pytest.compat import final from _pytest.compat import NOTSET from _pytest.compat import NotSetType from _pytest.config import _PluggyPlugin @@ -60,18 +63,13 @@ from _pytest.outcomes import fail from _pytest.outcomes import importorskip from _pytest.outcomes import skip from _pytest.pathlib import bestrelpath -from _pytest.pathlib import copytree from _pytest.pathlib import make_numbered_dir from _pytest.reports import CollectReport from _pytest.reports import TestReport from _pytest.tmpdir import TempPathFactory from _pytest.warning_types import PytestWarning - if TYPE_CHECKING: - from typing_extensions import Final - from typing_extensions import Literal - import pexpect @@ -129,6 +127,7 @@ class LsofFdLeakChecker: stderr=subprocess.DEVNULL, check=True, text=True, + encoding=locale.getpreferredencoding(False), ).stdout def isopen(line: str) -> bool: @@ -161,29 +160,31 @@ class LsofFdLeakChecker: else: return True - @hookimpl(hookwrapper=True, tryfirst=True) - def pytest_runtest_protocol(self, item: Item) -> Generator[None, None, None]: + @hookimpl(wrapper=True, tryfirst=True) + def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object]: lines1 = self.get_open_files() - yield - if hasattr(sys, "pypy_version_info"): - gc.collect() - lines2 = self.get_open_files() + try: + return (yield) + finally: + if hasattr(sys, "pypy_version_info"): + gc.collect() + lines2 = self.get_open_files() - new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} - leaked_files = [t for t in lines2 if t[0] in new_fds] - if leaked_files: - error = [ - "***** %s FD leakage detected" % len(leaked_files), - *(str(f) for f in leaked_files), - "*** Before:", - *(str(f) for f in lines1), - "*** After:", - *(str(f) for f in lines2), - "***** %s FD leakage detected" % len(leaked_files), - "*** function %s:%s: %s " % item.location, - "See issue #2366", - ] - item.warn(PytestWarning("\n".join(error))) + new_fds = {t[0] for t in lines2} - {t[0] for t in lines1} + leaked_files = [t for t in lines2 if t[0] in new_fds] + if leaked_files: + error = [ + "***** %s FD leakage detected" % len(leaked_files), + *(str(f) for f in leaked_files), + "*** Before:", + *(str(f) for f in lines1), + "*** After:", + *(str(f) for f in lines2), + "***** %s FD leakage detected" % len(leaked_files), + "*** function %s:%s: %s " % item.location, + "See issue #2366", + ] + item.warn(PytestWarning("\n".join(error))) # used at least by pytest-xdist plugin @@ -971,7 +972,7 @@ class Pytester: example_path = example_dir.joinpath(name) if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): - copytree(example_path, self.path) + shutil.copytree(example_path, self.path, symlinks=True, dirs_exist_ok=True) return self.path elif example_path.is_file(): result = self.path.joinpath(example_path.name) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 3b1253cf4..cf7575336 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -15,17 +15,18 @@ from pathlib import Path from typing import Any from typing import Callable from typing import Dict +from typing import final from typing import Generator from typing import Iterable from typing import Iterator from typing import List +from typing import Literal from typing import Mapping from typing import Optional from typing import Pattern from typing import Sequence from typing import Set from typing import Tuple -from typing import TYPE_CHECKING from typing import Union import _pytest @@ -40,7 +41,6 @@ from _pytest._io import TerminalWriter from _pytest._io.saferepr import saferepr from _pytest.compat import ascii_escaped from _pytest.compat import assert_never -from _pytest.compat import final from _pytest.compat import get_default_arg_names from _pytest.compat import get_real_func from _pytest.compat import getimfunc @@ -75,16 +75,12 @@ from _pytest.pathlib import import_path from _pytest.pathlib import ImportPathMismatchError from _pytest.pathlib import parts from _pytest.pathlib import visit +from _pytest.scope import _ScopeName from _pytest.scope import Scope from _pytest.warning_types import PytestCollectionWarning from _pytest.warning_types import PytestReturnNotNoneWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning -if TYPE_CHECKING: - from typing_extensions import Literal - - from _pytest.scope import _ScopeName - _PYTEST_DIR = Path(_pytest.__file__).parent @@ -522,7 +518,7 @@ class PyCollector(PyobjMixin, nodes.Collector): class Module(nodes.File, PyCollector): - """Collector for test classes and functions.""" + """Collector for test classes and functions in a Python module.""" def _getobj(self): return self._importtestmodule() @@ -659,6 +655,9 @@ class Module(nodes.File, PyCollector): class Package(Module): + """Collector for files and directories in a Python packages -- directories + with an `__init__.py` file.""" + def __init__( self, fspath: Optional[LEGACY_PATH], @@ -706,9 +705,6 @@ class Package(Module): ihook = self.session.gethookproxy(fspath.parent) if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): return False - norecursepatterns = self.config.getini("norecursedirs") - if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns): - return False return True def _collectfile( @@ -739,7 +735,9 @@ class Package(Module): this_path = self.path.parent # Always collect the __init__ first. - if path_matches_patterns(self.path, self.config.getini("python_files")): + if self.session.isinitpath(self.path) or path_matches_patterns( + self.path, self.config.getini("python_files") + ): yield Module.from_parent(self, path=self.path) pkg_prefixes: Set[Path] = set() @@ -791,7 +789,7 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[o class Class(PyCollector): - """Collector for test methods.""" + """Collector for test methods (and nested classes) in a Python class.""" @classmethod def from_parent(cls, parent, *, name, obj=None, **kw): @@ -1234,7 +1232,7 @@ class Metafunc: ids: Optional[ Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]] ] = None, - scope: "Optional[_ScopeName]" = None, + scope: Optional[_ScopeName] = None, *, _param_mark: Optional[Mark] = None, ) -> None: @@ -1676,7 +1674,7 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: class Function(PyobjMixin, nodes.Item): - """An Item responsible for setting up and executing a Python test function. + """Item responsible for setting up and executing a Python test function. :param name: The full function name, including any decorations like those @@ -1833,10 +1831,8 @@ class Function(PyobjMixin, nodes.Item): class FunctionDefinition(Function): - """ - This class is a step gap solution until we evolve to have actual function definition nodes - and manage to get rid of ``metafunc``. - """ + """This class is a stop gap solution until we evolve to have actual function + definition nodes and manage to get rid of ``metafunc``.""" def runtest(self) -> None: raise RuntimeError("function definitions are not supposed to be run as tests") diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 4213bd098..a045da220 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -9,9 +9,11 @@ from typing import Any from typing import Callable from typing import cast from typing import ContextManager +from typing import final from typing import List from typing import Mapping from typing import Optional +from typing import overload from typing import Pattern from typing import Sequence from typing import Tuple @@ -20,17 +22,14 @@ from typing import TYPE_CHECKING from typing import TypeVar from typing import Union +import _pytest._code +from _pytest.compat import STRING_TYPES +from _pytest.outcomes import fail + if TYPE_CHECKING: from numpy import ndarray -import _pytest._code -from _pytest.compat import final -from _pytest.compat import STRING_TYPES -from _pytest.compat import overload -from _pytest.outcomes import fail - - def _non_numeric_type_error(value, at: Optional[str]) -> TypeError: at_str = f" at {at}" if at else "" return TypeError( @@ -266,19 +265,20 @@ class ApproxMapping(ApproxBase): approx_side_as_map.items(), other_side.values() ): if approx_value != other_value: - 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 - ), + 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 + ), + ) different_ids.append(approx_key) message_data = [ @@ -843,6 +843,14 @@ def raises( # noqa: F811 >>> with pytest.raises(ValueError, match=r'must be \d+$'): ... raise ValueError("value must be 42") + The ``match`` argument searches the formatted exception string, which includes any + `PEP-678 ` ``__notes__``: + + >>> with pytest.raises(ValueError, match=r'had a note added'): # doctest: +SKIP + ... e = ValueError("value must be 42") + ... e.add_note("had a note added") + ... raise e + The context manager produces an :class:`ExceptionInfo` object which can be used to inspect the details of the captured exception:: diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index d76ea020f..5484d6f3b 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -5,18 +5,18 @@ from pprint import pformat from types import TracebackType from typing import Any from typing import Callable +from typing import final from typing import Generator from typing import Iterator from typing import List from typing import Optional +from typing import overload from typing import Pattern from typing import Tuple from typing import Type from typing import TypeVar from typing import Union -from _pytest.compat import final -from _pytest.compat import overload from _pytest.deprecated import check_ispytest from _pytest.deprecated import WARNS_NONE_ARG from _pytest.fixtures import fixture @@ -117,10 +117,10 @@ def warns( # noqa: F811 warning of that class or classes. This helper produces a list of :class:`warnings.WarningMessage` objects, one for - each warning raised (regardless of whether it is an ``expected_warning`` or not). + each warning emitted (regardless of whether it is an ``expected_warning`` or not). + Since pytest 8.0, unmatched warnings are also re-emitted when the context closes. - This function can be used as a context manager, which will capture all the raised - warnings inside it:: + This function can be used as a context manager:: >>> import pytest >>> with pytest.warns(RuntimeWarning): @@ -135,8 +135,9 @@ def warns( # noqa: F811 >>> with pytest.warns(UserWarning, match=r'must be \d+$'): ... warnings.warn("value must be 42", UserWarning) - >>> with pytest.warns(UserWarning, match=r'must be \d+$'): - ... warnings.warn("this is not here", UserWarning) + >>> with pytest.warns(UserWarning): # catch re-emitted warning + ... with pytest.warns(UserWarning, match=r'must be \d+$'): + ... warnings.warn("this is not here", UserWarning) Traceback (most recent call last): ... Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted... @@ -205,10 +206,21 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] return len(self._list) def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage": - """Pop the first recorded warning, raise exception if not exists.""" + """Pop the first recorded warning which is an instance of ``cls``, + but not an instance of a child class of any other match. + Raises ``AssertionError`` if there is no match. + """ + best_idx: Optional[int] = None for i, w in enumerate(self._list): - if issubclass(w.category, cls): - return self._list.pop(i) + if w.category == cls: + return self._list.pop(i) # exact match, stop looking + if issubclass(w.category, cls) and ( + best_idx is None + or not issubclass(w.category, self._list[best_idx].category) + ): + best_idx = i + if best_idx is not None: + return self._list.pop(best_idx) __tracebackhide__ = True raise AssertionError(f"{cls!r} not found in warning list") @@ -277,6 +289,12 @@ class WarningsChecker(WarningsRecorder): self.expected_warning = expected_warning_tup self.match_expr = match_expr + def matches(self, warning: warnings.WarningMessage) -> bool: + assert self.expected_warning is not None + return issubclass(warning.category, self.expected_warning) and bool( + self.match_expr is None or re.search(self.match_expr, str(warning.message)) + ) + def __exit__( self, exc_type: Optional[Type[BaseException]], @@ -287,27 +305,34 @@ class WarningsChecker(WarningsRecorder): __tracebackhide__ = True + if self.expected_warning is None: + # nothing to do in this deprecated case, see WARNS_NONE_ARG above + return + def found_str(): return pformat([record.message for record in self], indent=2) - # only check if we're not currently handling an exception - if exc_type is None and exc_val is None and exc_tb is None: - if self.expected_warning is not None: - if not any(issubclass(r.category, self.expected_warning) for r in self): - __tracebackhide__ = True - fail( - f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n" - f"The list of emitted warnings is: {found_str()}." + try: + if not any(issubclass(w.category, self.expected_warning) for w in self): + fail( + f"DID NOT WARN. No warnings of type {self.expected_warning} were emitted.\n" + f" Emitted warnings: {found_str()}." + ) + elif not any(self.matches(w) for w in self): + fail( + f"DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted.\n" + f" Regex: {self.match_expr}\n" + f" Emitted warnings: {found_str()}." + ) + finally: + # Whether or not any warnings matched, we want to re-emit all unmatched warnings. + for w in self: + if not self.matches(w): + warnings.warn_explicit( + str(w.message), + w.message.__class__, # type: ignore[arg-type] + w.filename, + w.lineno, + module=w.__module__, + source=w.source, ) - elif self.match_expr is not None: - for r in self: - if issubclass(r.category, self.expected_warning): - if re.compile(self.match_expr).search(str(r.message)): - break - else: - fail( - f"""\ -DID NOT WARN. No warnings of type {self.expected_warning} matching the regex were emitted. - Regex: {self.match_expr} - Emitted warnings: {found_str()}""" - ) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 74e8794b2..18f1c948a 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -5,9 +5,11 @@ from pprint import pprint from typing import Any from typing import cast from typing import Dict +from typing import final from typing import Iterable from typing import Iterator from typing import List +from typing import Literal from typing import Mapping from typing import NoReturn from typing import Optional @@ -29,15 +31,12 @@ from _pytest._code.code import ReprLocals from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr from _pytest._io import TerminalWriter -from _pytest.compat import final from _pytest.config import Config from _pytest.nodes import Collector from _pytest.nodes import Item from _pytest.outcomes import skip if TYPE_CHECKING: - from typing_extensions import Literal - from _pytest.runner import CallInfo @@ -64,7 +63,7 @@ class BaseReport: ] sections: List[Tuple[str, str]] nodeid: str - outcome: "Literal['passed', 'failed', 'skipped']" + outcome: Literal["passed", "failed", "skipped"] def __init__(self, **kw: Any) -> None: self.__dict__.update(kw) @@ -249,17 +248,20 @@ class TestReport(BaseReport): """ __test__ = False + # Defined by skipping plugin. + # xfail reason if xfailed, otherwise not defined. Use hasattr to distinguish. + wasxfail: str def __init__( self, nodeid: str, location: Tuple[str, Optional[int], str], keywords: Mapping[str, Any], - outcome: "Literal['passed', 'failed', 'skipped']", + outcome: Literal["passed", "failed", "skipped"], longrepr: Union[ None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr ], - when: "Literal['setup', 'call', 'teardown']", + when: Literal["setup", "call", "teardown"], sections: Iterable[Tuple[str, str]] = (), duration: float = 0, start: float = 0, diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index f861c05a4..1b39f93cf 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -6,8 +6,10 @@ import sys from typing import Callable from typing import cast from typing import Dict +from typing import final from typing import Generic from typing import List +from typing import Literal from typing import Optional from typing import Tuple from typing import Type @@ -23,7 +25,6 @@ from _pytest import timing from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr -from _pytest.compat import final from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest from _pytest.nodes import Collector @@ -38,8 +39,6 @@ if sys.version_info[:2] < (3, 11): from exceptiongroup import BaseExceptionGroup if TYPE_CHECKING: - from typing_extensions import Literal - from _pytest.main import Session from _pytest.terminal import TerminalReporter @@ -184,7 +183,7 @@ def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None: def _update_current_test_var( - item: Item, when: Optional["Literal['setup', 'call', 'teardown']"] + item: Item, when: Optional[Literal["setup", "call", "teardown"]] ) -> None: """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. @@ -217,7 +216,7 @@ def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str def call_and_report( - item: Item, when: "Literal['setup', 'call', 'teardown']", log: bool = True, **kwds + item: Item, when: Literal["setup", "call", "teardown"], log: bool = True, **kwds ) -> TestReport: call = call_runtest_hook(item, when, **kwds) hook = item.ihook @@ -245,7 +244,7 @@ def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> def call_runtest_hook( - item: Item, when: "Literal['setup', 'call', 'teardown']", **kwds + item: Item, when: Literal["setup", "call", "teardown"], **kwds ) -> "CallInfo[None]": if when == "setup": ihook: Callable[..., None] = item.ihook.pytest_runtest_setup @@ -281,7 +280,7 @@ class CallInfo(Generic[TResult]): #: The call duration, in seconds. duration: float #: The context of invocation: "collect", "setup", "call" or "teardown". - when: "Literal['collect', 'setup', 'call', 'teardown']" + when: Literal["collect", "setup", "call", "teardown"] def __init__( self, @@ -290,7 +289,7 @@ class CallInfo(Generic[TResult]): start: float, stop: float, duration: float, - when: "Literal['collect', 'setup', 'call', 'teardown']", + when: Literal["collect", "setup", "call", "teardown"], *, _ispytest: bool = False, ) -> None: @@ -319,7 +318,7 @@ class CallInfo(Generic[TResult]): def from_call( cls, func: "Callable[[], TResult]", - when: "Literal['collect', 'setup', 'call', 'teardown']", + when: Literal["collect", "setup", "call", "teardown"], reraise: Optional[ Union[Type[BaseException], Tuple[Type[BaseException], ...]] ] = None, diff --git a/src/_pytest/scope.py b/src/_pytest/scope.py index 7a746fb9f..98edaf402 100644 --- a/src/_pytest/scope.py +++ b/src/_pytest/scope.py @@ -9,13 +9,11 @@ Also this makes the module light to import, as it should. """ from enum import Enum from functools import total_ordering +from typing import Literal from typing import Optional -from typing import TYPE_CHECKING -if TYPE_CHECKING: - from typing_extensions import Literal - _ScopeName = Literal["session", "package", "module", "class", "function"] +_ScopeName = Literal["session", "package", "module", "class", "function"] @total_ordering @@ -33,11 +31,11 @@ class Scope(Enum): """ # Scopes need to be listed from lower to higher. - Function: "_ScopeName" = "function" - Class: "_ScopeName" = "class" - Module: "_ScopeName" = "module" - Package: "_ScopeName" = "package" - Session: "_ScopeName" = "session" + Function: _ScopeName = "function" + Class: _ScopeName = "class" + Module: _ScopeName = "module" + Package: _ScopeName = "package" + Session: _ScopeName = "session" def next_lower(self) -> "Scope": """Return the next lower scope.""" @@ -60,7 +58,7 @@ class Scope(Enum): @classmethod def from_user( - cls, scope_name: "_ScopeName", descr: str, where: Optional[str] = None + cls, scope_name: _ScopeName, descr: str, where: Optional[str] = None ) -> "Scope": """ Given a scope name from the user, return the equivalent Scope enum. Should be used diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 583590d6b..0f8be899a 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -28,24 +28,26 @@ def pytest_addoption(parser: Parser) -> None: ) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_fixture_setup( fixturedef: FixtureDef[object], request: SubRequest -) -> Generator[None, None, None]: - yield - if request.config.option.setupshow: - if hasattr(request, "param"): - # Save the fixture parameter so ._show_fixture_action() can - # display it now and during the teardown (in .finish()). - if fixturedef.ids: - if callable(fixturedef.ids): - param = fixturedef.ids(request.param) +) -> Generator[None, object, object]: + try: + return (yield) + finally: + if request.config.option.setupshow: + if hasattr(request, "param"): + # Save the fixture parameter so ._show_fixture_action() can + # display it now and during the teardown (in .finish()). + if fixturedef.ids: + if callable(fixturedef.ids): + param = fixturedef.ids(request.param) + else: + param = fixturedef.ids[request.param_index] else: - param = fixturedef.ids[request.param_index] - else: - param = request.param - fixturedef.cached_param = param # type: ignore[attr-defined] - _show_fixture_action(fixturedef, "SETUP") + param = request.param + fixturedef.cached_param = param # type: ignore[attr-defined] + _show_fixture_action(fixturedef, "SETUP") def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None: diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 26ce73758..0c5c38f5f 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -19,6 +19,7 @@ from _pytest.outcomes import fail from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.reports import BaseReport +from _pytest.reports import TestReport from _pytest.runner import CallInfo from _pytest.stash import StashKey @@ -243,7 +244,7 @@ def pytest_runtest_setup(item: Item) -> None: xfail("[NOTRUN] " + xfailed.reason) -@hookimpl(hookwrapper=True) +@hookimpl(wrapper=True) def pytest_runtest_call(item: Item) -> Generator[None, None, None]: xfailed = item.stash.get(xfailed_key, None) if xfailed is None: @@ -252,18 +253,20 @@ def pytest_runtest_call(item: Item) -> Generator[None, None, None]: if xfailed and not item.config.option.runxfail and not xfailed.run: xfail("[NOTRUN] " + xfailed.reason) - yield - - # The test run may have added an xfail mark dynamically. - xfailed = item.stash.get(xfailed_key, None) - if xfailed is None: - item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) + try: + return (yield) + finally: + # The test run may have added an xfail mark dynamically. + xfailed = item.stash.get(xfailed_key, None) + if xfailed is None: + item.stash[xfailed_key] = xfailed = evaluate_xfail_marks(item) -@hookimpl(hookwrapper=True) -def pytest_runtest_makereport(item: Item, call: CallInfo[None]): - outcome = yield - rep = outcome.get_result() +@hookimpl(wrapper=True) +def pytest_runtest_makereport( + item: Item, call: CallInfo[None] +) -> Generator[None, TestReport, TestReport]: + rep = yield xfailed = item.stash.get(xfailed_key, None) if item.config.option.runxfail: pass # don't interfere @@ -286,6 +289,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]): else: rep.outcome = "passed" rep.wasxfail = xfailed.reason + return rep def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index b0cdb58ce..ea26d9368 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -15,11 +15,12 @@ from functools import partial from pathlib import Path from typing import Any from typing import Callable -from typing import cast from typing import ClassVar from typing import Dict +from typing import final from typing import Generator from typing import List +from typing import Literal from typing import Mapping from typing import NamedTuple from typing import Optional @@ -40,7 +41,6 @@ from _pytest._code.code import ExceptionRepr from _pytest._io import TerminalWriter from _pytest._io.wcwidth import wcswidth from _pytest.assertion.util import running_on_ci -from _pytest.compat import final from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode @@ -55,8 +55,6 @@ from _pytest.reports import CollectReport from _pytest.reports import TestReport if TYPE_CHECKING: - from typing_extensions import Literal - from _pytest.main import Session @@ -367,7 +365,7 @@ class TerminalReporter: self._already_displayed_warnings: Optional[int] = None self._keyboardinterrupt_memo: Optional[ExceptionRepr] = None - def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]": + def _determine_show_progress_info(self) -> Literal["progress", "count", False]: """Return whether we should display progress information based on the current config.""" # do not show progress if we are not capturing output (#3038) unless explicitly # overridden by progress-even-when-capture-no @@ -849,12 +847,11 @@ class TerminalReporter: for line in doc.splitlines(): self._tw.line("{}{}".format(indent + " ", line)) - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_sessionfinish( self, session: "Session", exitstatus: Union[int, ExitCode] - ): - outcome = yield - outcome.get_result() + ) -> Generator[None, None, None]: + result = yield self._tw.line("") summary_exit_codes = ( ExitCode.OK, @@ -875,17 +872,20 @@ class TerminalReporter: elif session.shouldstop: self.write_sep("!", str(session.shouldstop), red=True) self.summary_stats() + return result - @hookimpl(hookwrapper=True) + @hookimpl(wrapper=True) def pytest_terminal_summary(self) -> Generator[None, None, None]: self.summary_errors() self.summary_failures() self.summary_warnings() self.summary_passes() - yield - self.short_test_summary() - # Display any extra warnings from teardown here (if any). - self.summary_warnings() + try: + return (yield) + finally: + self.short_test_summary() + # Display any extra warnings from teardown here (if any). + self.summary_warnings() def pytest_keyboard_interrupt(self, excinfo: ExceptionInfo[BaseException]) -> None: self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) @@ -1466,7 +1466,7 @@ def _get_raw_skip_reason(report: TestReport) -> str: The string is just the part given by the user. """ if hasattr(report, "wasxfail"): - reason = cast(str, report.wasxfail) + reason = report.wasxfail if reason.startswith("reason: "): reason = reason[len("reason: ") :] return reason diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py index 43341e739..0b5902d66 100644 --- a/src/_pytest/threadexception.py +++ b/src/_pytest/threadexception.py @@ -59,30 +59,34 @@ class catch_threading_exception: def thread_exception_runtest_hook() -> Generator[None, None, None]: with catch_threading_exception() as cm: - yield - if cm.args: - thread_name = "" if cm.args.thread is None else cm.args.thread.name - msg = f"Exception in thread {thread_name}\n\n" - msg += "".join( - traceback.format_exception( - cm.args.exc_type, - cm.args.exc_value, - cm.args.exc_traceback, + try: + yield + finally: + if cm.args: + thread_name = ( + "" if cm.args.thread is None else cm.args.thread.name ) - ) - warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) + msg = f"Exception in thread {thread_name}\n\n" + msg += "".join( + traceback.format_exception( + cm.args.exc_type, + cm.args.exc_value, + cm.args.exc_traceback, + ) + ) + warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) -@pytest.hookimpl(hookwrapper=True, trylast=True) +@pytest.hookimpl(wrapper=True, trylast=True) def pytest_runtest_setup() -> Generator[None, None, None]: yield from thread_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_call() -> Generator[None, None, None]: yield from thread_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_teardown() -> Generator[None, None, None]: yield from thread_exception_runtest_hook() diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 3cc2bace5..6fa227760 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -7,38 +7,32 @@ from pathlib import Path from shutil import rmtree from typing import Any from typing import Dict +from typing import final from typing import Generator +from typing import Literal from typing import Optional -from typing import TYPE_CHECKING from typing import Union -from _pytest.nodes import Item -from _pytest.reports import CollectReport -from _pytest.stash import StashKey - -if TYPE_CHECKING: - from typing_extensions import Literal - - RetentionType = Literal["all", "failed", "none"] - - -from _pytest.config.argparsing import Parser - +from .pathlib import cleanup_dead_symlinks from .pathlib import LOCK_TIMEOUT from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from .pathlib import rm_rf -from .pathlib import cleanup_dead_symlinks -from _pytest.compat import final, get_user_id +from _pytest.compat import get_user_id from _pytest.config import Config from _pytest.config import ExitCode 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 FixtureRequest from _pytest.monkeypatch import MonkeyPatch +from _pytest.nodes import Item +from _pytest.reports import TestReport +from _pytest.stash import StashKey tmppath_result_key = StashKey[Dict[str, bool]]() +RetentionType = Literal["all", "failed", "none"] @final @@ -54,13 +48,13 @@ class TempPathFactory: _trace: Any _basetemp: Optional[Path] _retention_count: int - _retention_policy: "RetentionType" + _retention_policy: RetentionType def __init__( self, given_basetemp: Optional[Path], retention_count: int, - retention_policy: "RetentionType", + retention_policy: RetentionType, trace, basetemp: Optional[Path] = None, *, @@ -315,10 +309,12 @@ def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]): cleanup_dead_symlinks(basetemp) -@hookimpl(tryfirst=True, hookwrapper=True) -def pytest_runtest_makereport(item: Item, call): - outcome = yield - result: CollectReport = outcome.get_result() - +@hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_makereport( + item: Item, call +) -> Generator[None, TestReport, TestReport]: + rep = yield + assert rep.when is not None empty: Dict[str, bool] = {} - item.stash.setdefault(tmppath_result_key, empty)[result.when] = result.passed + item.stash.setdefault(tmppath_result_key, empty)[rep.when] = rep.passed + return rep diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index d42a12a3a..34845cec1 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -200,10 +200,10 @@ class TestCaseFunction(Function): assert self.parent is not None self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined] self._obj = getattr(self._testcase, self.name) - if hasattr(self, "_request"): - self._request._fillfixtures() + super().setup() def teardown(self) -> None: + super().teardown() if self._explicit_tearDown is not None: self._explicit_tearDown() self._explicit_tearDown = None @@ -376,8 +376,8 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None: # Twisted trial support. -@hookimpl(hookwrapper=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@hookimpl(wrapper=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: if isinstance(item, TestCaseFunction) and "twisted.trial.unittest" in sys.modules: ut: Any = sys.modules["twisted.python.failure"] Failure__init__ = ut.Failure.__init__ @@ -400,10 +400,13 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: Failure__init__(self, exc_value, exc_type, exc_tb) ut.Failure.__init__ = excstore - yield - ut.Failure.__init__ = Failure__init__ + try: + res = yield + finally: + ut.Failure.__init__ = Failure__init__ else: - yield + res = yield + return res def check_testcase_implements_trial_reporter(done: List[int] = []) -> None: diff --git a/src/_pytest/unraisableexception.py b/src/_pytest/unraisableexception.py index fcb5d8237..8c0a2d9ae 100644 --- a/src/_pytest/unraisableexception.py +++ b/src/_pytest/unraisableexception.py @@ -61,33 +61,35 @@ class catch_unraisable_exception: def unraisable_exception_runtest_hook() -> Generator[None, None, None]: with catch_unraisable_exception() as cm: - yield - if cm.unraisable: - if cm.unraisable.err_msg is not None: - err_msg = cm.unraisable.err_msg - else: - err_msg = "Exception ignored in" - msg = f"{err_msg}: {cm.unraisable.object!r}\n\n" - msg += "".join( - traceback.format_exception( - cm.unraisable.exc_type, - cm.unraisable.exc_value, - cm.unraisable.exc_traceback, + try: + yield + finally: + if cm.unraisable: + if cm.unraisable.err_msg is not None: + err_msg = cm.unraisable.err_msg + else: + err_msg = "Exception ignored in" + msg = f"{err_msg}: {cm.unraisable.object!r}\n\n" + msg += "".join( + traceback.format_exception( + cm.unraisable.exc_type, + cm.unraisable.exc_value, + cm.unraisable.exc_traceback, + ) ) - ) - warnings.warn(pytest.PytestUnraisableExceptionWarning(msg)) + warnings.warn(pytest.PytestUnraisableExceptionWarning(msg)) -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_setup() -> Generator[None, None, None]: yield from unraisable_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_call() -> Generator[None, None, None]: yield from unraisable_exception_runtest_hook() -@pytest.hookimpl(hookwrapper=True, tryfirst=True) +@pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_teardown() -> Generator[None, None, None]: yield from unraisable_exception_runtest_hook() diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index bd5f41873..31726e1ce 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -3,12 +3,11 @@ import inspect import warnings from types import FunctionType from typing import Any +from typing import final from typing import Generic from typing import Type from typing import TypeVar -from _pytest.compat import final - class PytestWarning(UserWarning): """Base class for all warnings emitted by pytest.""" @@ -56,6 +55,12 @@ class PytestRemovedIn8Warning(PytestDeprecationWarning): __module__ = "pytest" +class PytestRemovedIn9Warning(PytestDeprecationWarning): + """Warning class for features that will be removed in pytest 9.""" + + __module__ = "pytest" + + class PytestReturnNotNoneWarning(PytestRemovedIn8Warning): """Warning emitted when a test function is returning value other than None.""" diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 4aaa94452..6f20a872c 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -2,8 +2,8 @@ import sys import warnings from contextlib import contextmanager from typing import Generator +from typing import Literal from typing import Optional -from typing import TYPE_CHECKING import pytest from _pytest.config import apply_warning_filters @@ -13,9 +13,6 @@ from _pytest.main import Session from _pytest.nodes import Item from _pytest.terminal import TerminalReporter -if TYPE_CHECKING: - from typing_extensions import Literal - def pytest_configure(config: Config) -> None: config.addinivalue_line( @@ -29,7 +26,7 @@ def pytest_configure(config: Config) -> None: def catch_warnings_for_item( config: Config, ihook, - when: "Literal['config', 'collect', 'runtest']", + when: Literal["config", "collect", "runtest"], item: Optional[Item], ) -> Generator[None, None, None]: """Context manager that catches warnings generated in the contained execution block. @@ -49,6 +46,8 @@ def catch_warnings_for_item( warnings.filterwarnings("always", category=DeprecationWarning) warnings.filterwarnings("always", category=PendingDeprecationWarning) + warnings.filterwarnings("error", category=pytest.PytestRemovedIn8Warning) + apply_warning_filters(config_filters, cmdline_filters) # apply filters from "filterwarnings" marks @@ -58,17 +57,18 @@ def catch_warnings_for_item( for arg in mark.args: warnings.filterwarnings(*parse_warning_filter(arg, escape=False)) - yield - - for warning_message in log: - ihook.pytest_warning_recorded.call_historic( - kwargs=dict( - warning_message=warning_message, - nodeid=nodeid, - when=when, - location=None, + try: + yield + finally: + for warning_message in log: + ihook.pytest_warning_recorded.call_historic( + kwargs=dict( + warning_message=warning_message, + nodeid=nodeid, + when=when, + location=None, + ) ) - ) def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: @@ -101,24 +101,24 @@ def warning_record_to_str(warning_message: warnings.WarningMessage) -> str: return msg -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: with catch_warnings_for_item( config=item.config, ihook=item.ihook, when="runtest", item=item ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_collection(session: Session) -> Generator[None, None, None]: +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_collection(session: Session) -> Generator[None, object, object]: config = session.config with catch_warnings_for_item( config=config, ihook=config.hook, when="collect", item=None ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_terminal_summary( terminalreporter: TerminalReporter, ) -> Generator[None, None, None]: @@ -126,23 +126,23 @@ def pytest_terminal_summary( with catch_warnings_for_item( config=config, ihook=config.hook, when="config", item=None ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: config = session.config with catch_warnings_for_item( config=config, ihook=config.hook, when="config", item=None ): - yield + return (yield) -@pytest.hookimpl(hookwrapper=True) +@pytest.hookimpl(wrapper=True) def pytest_load_initial_conftests( early_config: "Config", ) -> Generator[None, None, None]: with catch_warnings_for_item( config=early_config, ihook=early_config.hook, when="config", item=None ): - yield + return (yield) diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index 9c1d5d204..831ede1fa 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -20,6 +20,7 @@ from _pytest.config.argparsing import Parser from _pytest.debugging import pytestPDB as __pytestPDB from _pytest.doctest import DoctestItem from _pytest.fixtures import fixture +from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureRequest from _pytest.fixtures import yield_fixture @@ -71,6 +72,7 @@ from _pytest.warning_types import PytestConfigWarning from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import PytestExperimentalApiWarning from _pytest.warning_types import PytestRemovedIn8Warning +from _pytest.warning_types import PytestRemovedIn9Warning from _pytest.warning_types import PytestReturnNotNoneWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnhandledThreadExceptionWarning @@ -101,6 +103,7 @@ __all__ = [ "fail", "File", "fixture", + "FixtureDef", "FixtureLookupError", "FixtureRequest", "freeze_includes", @@ -131,6 +134,7 @@ __all__ = [ "PytestDeprecationWarning", "PytestExperimentalApiWarning", "PytestRemovedIn8Warning", + "PytestRemovedIn9Warning", "PytestReturnNotNoneWarning", "Pytester", "PytestPluginManager", diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index b463d769d..895066a9f 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -1,7 +1,9 @@ +import contextlib import multiprocessing import os import sys import time +import warnings from unittest import mock import pytest @@ -9,6 +11,14 @@ from py import error from py.path import local +@contextlib.contextmanager +def ignore_encoding_warning(): + with warnings.catch_warnings(): + with contextlib.suppress(NameError): # new in 3.10 + warnings.simplefilter("ignore", EncodingWarning) + yield + + class CommonFSTests: def test_constructor_equality(self, path1): p = path1.__class__(path1) @@ -223,7 +233,8 @@ class CommonFSTests: assert not (path1 < path1) def test_simple_read(self, path1): - x = path1.join("samplefile").read("r") + with ignore_encoding_warning(): + x = path1.join("samplefile").read("r") assert x == "samplefile\n" def test_join_div_operator(self, path1): @@ -265,12 +276,14 @@ class CommonFSTests: def test_readlines(self, path1): fn = path1.join("samplefile") - contents = fn.readlines() + with ignore_encoding_warning(): + contents = fn.readlines() assert contents == ["samplefile\n"] def test_readlines_nocr(self, path1): fn = path1.join("samplefile") - contents = fn.readlines(cr=0) + with ignore_encoding_warning(): + contents = fn.readlines(cr=0) assert contents == ["samplefile", ""] def test_file(self, path1): @@ -362,8 +375,8 @@ class CommonFSTests: initpy.copy(copied) try: assert copied.check() - s1 = initpy.read() - s2 = copied.read() + s1 = initpy.read_text(encoding="utf-8") + s2 = copied.read_text(encoding="utf-8") assert s1 == s2 finally: if copied.check(): @@ -376,8 +389,8 @@ class CommonFSTests: otherdir.copy(copied) assert copied.check(dir=1) assert copied.join("__init__.py").check(file=1) - s1 = otherdir.join("__init__.py").read() - s2 = copied.join("__init__.py").read() + s1 = otherdir.join("__init__.py").read_text(encoding="utf-8") + s2 = copied.join("__init__.py").read_text(encoding="utf-8") assert s1 == s2 finally: if copied.check(dir=1): @@ -463,13 +476,13 @@ def setuptestfs(path): return # print "setting up test fs for", repr(path) samplefile = path.ensure("samplefile") - samplefile.write("samplefile\n") + samplefile.write_text("samplefile\n", encoding="utf-8") execfile = path.ensure("execfile") - execfile.write("x=42") + execfile.write_text("x=42", encoding="utf-8") execfilepy = path.ensure("execfile.py") - execfilepy.write("x=42") + execfilepy.write_text("x=42", encoding="utf-8") d = {1: 2, "hello": "world", "answer": 42} path.ensure("samplepickle").dump(d) @@ -481,22 +494,24 @@ def setuptestfs(path): otherdir.ensure("__init__.py") module_a = otherdir.ensure("a.py") - module_a.write("from .b import stuff as result\n") + module_a.write_text("from .b import stuff as result\n", encoding="utf-8") module_b = otherdir.ensure("b.py") - module_b.write('stuff="got it"\n') + module_b.write_text('stuff="got it"\n', encoding="utf-8") module_c = otherdir.ensure("c.py") - module_c.write( + module_c.write_text( """import py; import otherdir.a value = otherdir.a.result -""" +""", + encoding="utf-8", ) module_d = otherdir.ensure("d.py") - module_d.write( + module_d.write_text( """import py; from otherdir import a value2 = a.result -""" +""", + encoding="utf-8", ) @@ -534,9 +549,11 @@ def batch_make_numbered_dirs(rootdir, repeats): for i in range(repeats): dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir) file_ = dir_.join("foo") - file_.write("%s" % i) - actual = int(file_.read()) - assert actual == i, f"int(file_.read()) is {actual} instead of {i}" + file_.write_text("%s" % i, encoding="utf-8") + actual = int(file_.read_text(encoding="utf-8")) + assert ( + actual == i + ), f"int(file_.read_text(encoding='utf-8')) is {actual} instead of {i}" dir_.join(".lock").remove(ignore_errors=True) return True @@ -692,14 +709,14 @@ class TestLocalPath(CommonFSTests): def test_open_and_ensure(self, path1): p = path1.join("sub1", "sub2", "file") - with p.open("w", ensure=1) as f: + with p.open("w", ensure=1, encoding="utf-8") as f: f.write("hello") - assert p.read() == "hello" + assert p.read_text(encoding="utf-8") == "hello" def test_write_and_ensure(self, path1): p = path1.join("sub1", "sub2", "file") - p.write("hello", ensure=1) - assert p.read() == "hello" + p.write_text("hello", ensure=1, encoding="utf-8") + assert p.read_text(encoding="utf-8") == "hello" @pytest.mark.parametrize("bin", (False, True)) def test_dump(self, tmpdir, bin): @@ -770,9 +787,9 @@ class TestLocalPath(CommonFSTests): newfile = tmpdir.join("test1", "test") newfile.ensure() assert newfile.check(file=1) - newfile.write("42") + newfile.write_text("42", encoding="utf-8") newfile.ensure() - s = newfile.read() + s = newfile.read_text(encoding="utf-8") assert s == "42" def test_ensure_filepath_withoutdir(self, tmpdir): @@ -806,9 +823,9 @@ class TestLocalPath(CommonFSTests): newfilename = "/test" * 60 # type:ignore[unreachable] l1 = tmpdir.join(newfilename) l1.ensure(file=True) - l1.write("foo") + l1.write_text("foo", encoding="utf-8") l2 = tmpdir.join(newfilename) - assert l2.read() == "foo" + assert l2.read_text(encoding="utf-8") == "foo" def test_visit_depth_first(self, tmpdir): tmpdir.ensure("a", "1") @@ -1278,14 +1295,14 @@ class TestPOSIXLocalPath: def test_hardlink(self, tmpdir): linkpath = tmpdir.join("test") filepath = tmpdir.join("file") - filepath.write("Hello") + filepath.write_text("Hello", encoding="utf-8") nlink = filepath.stat().nlink linkpath.mklinkto(filepath) assert filepath.stat().nlink == nlink + 1 def test_symlink_are_identical(self, tmpdir): filepath = tmpdir.join("file") - filepath.write("Hello") + filepath.write_text("Hello", encoding="utf-8") linkpath = tmpdir.join("test") linkpath.mksymlinkto(filepath) assert linkpath.readlink() == str(filepath) @@ -1293,7 +1310,7 @@ class TestPOSIXLocalPath: def test_symlink_isfile(self, tmpdir): linkpath = tmpdir.join("test") filepath = tmpdir.join("file") - filepath.write("") + filepath.write_text("", encoding="utf-8") linkpath.mksymlinkto(filepath) assert linkpath.check(file=1) assert not linkpath.check(link=0, file=1) @@ -1302,10 +1319,12 @@ class TestPOSIXLocalPath: def test_symlink_relative(self, tmpdir): linkpath = tmpdir.join("test") filepath = tmpdir.join("file") - filepath.write("Hello") + filepath.write_text("Hello", encoding="utf-8") linkpath.mksymlinkto(filepath, absolute=False) assert linkpath.readlink() == "file" - assert filepath.read() == linkpath.read() + assert filepath.read_text(encoding="utf-8") == linkpath.read_text( + encoding="utf-8" + ) def test_symlink_not_existing(self, tmpdir): linkpath = tmpdir.join("testnotexisting") @@ -1338,7 +1357,7 @@ class TestPOSIXLocalPath: def test_realpath_file(self, tmpdir): linkpath = tmpdir.join("test") filepath = tmpdir.join("file") - filepath.write("") + filepath.write_text("", encoding="utf-8") linkpath.mksymlinkto(filepath) realpath = linkpath.realpath() assert realpath.basename == "file" @@ -1383,7 +1402,7 @@ class TestPOSIXLocalPath: atime1 = path.atime() # we could wait here but timer resolution is very # system dependent - path.read() + path.read_binary() time.sleep(ATIME_RESOLUTION) atime2 = path.atime() time.sleep(ATIME_RESOLUTION) @@ -1467,7 +1486,7 @@ class TestPOSIXLocalPath: test_files = ["a", "b", "c"] src = tmpdir.join("src") for f in test_files: - src.join(f).write(f, ensure=True) + src.join(f).write_text(f, ensure=True, encoding="utf-8") dst = tmpdir.join("dst") # a small delay before the copy time.sleep(ATIME_RESOLUTION) @@ -1500,9 +1519,9 @@ class TestPOSIXLocalPath: path1.chown(owner, group) -class TestUnicodePy2Py3: +class TestUnicode: def test_join_ensure(self, tmpdir, monkeypatch): - if sys.version_info >= (3, 0) and "LANG" not in os.environ: + if "LANG" not in os.environ: pytest.skip("cannot run test without locale") x = local(tmpdir.strpath) part = "hällo" @@ -1510,7 +1529,7 @@ class TestUnicodePy2Py3: assert x.join(part) == y def test_listdir(self, tmpdir): - if sys.version_info >= (3, 0) and "LANG" not in os.environ: + if "LANG" not in os.environ: pytest.skip("cannot run test without locale") x = local(tmpdir.strpath) part = "hällo" @@ -1521,10 +1540,11 @@ class TestUnicodePy2Py3: def test_read_write(self, tmpdir): x = tmpdir.join("hello") part = "hällo" - x.write(part) - assert x.read() == part - x.write(part.encode(sys.getdefaultencoding())) - assert x.read() == part.encode(sys.getdefaultencoding()) + with ignore_encoding_warning(): + x.write(part) + assert x.read() == part + x.write(part.encode(sys.getdefaultencoding())) + assert x.read() == part.encode(sys.getdefaultencoding()) class TestBinaryAndTextMethods: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 680afe133..429fb4e43 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,10 +1,10 @@ import dataclasses +import importlib.metadata import os import sys import types import pytest -from _pytest.compat import importlib_metadata from _pytest.config import ExitCode from _pytest.pathlib import symlink_or_skip from _pytest.pytester import Pytester @@ -139,7 +139,7 @@ class TestGeneralUsage: def my_dists(): return (DummyDist(entry_points),) - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) params = ("-p", "mycov") if load_cov_early else () pytester.runpytest_inprocess(*params) if load_cov_early: @@ -267,7 +267,7 @@ class TestGeneralUsage: def test_issue109_sibling_conftests_not_loaded(self, pytester: Pytester) -> None: sub1 = pytester.mkdir("sub1") sub2 = pytester.mkdir("sub2") - sub1.joinpath("conftest.py").write_text("assert 0") + sub1.joinpath("conftest.py").write_text("assert 0", encoding="utf-8") result = pytester.runpytest(sub2) assert result.ret == ExitCode.NO_TESTS_COLLECTED sub2.joinpath("__init__.py").touch() @@ -467,7 +467,7 @@ class TestGeneralUsage: assert "invalid" in str(excinfo.value) p = pytester.path.joinpath("test_test_plugins_given_as_strings.py") - p.write_text("def test_foo(): pass") + p.write_text("def test_foo(): pass", encoding="utf-8") mod = types.ModuleType("myplugin") monkeypatch.setitem(sys.modules, "myplugin", mod) assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0 @@ -587,7 +587,7 @@ class TestInvocationVariants: def test_pyargs_importerror(self, pytester: Pytester, monkeypatch) -> None: monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False) path = pytester.mkpydir("tpkg") - path.joinpath("test_hello.py").write_text("raise ImportError") + path.joinpath("test_hello.py").write_text("raise ImportError", encoding="utf-8") result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True) assert result.ret != 0 @@ -597,10 +597,10 @@ class TestInvocationVariants: def test_pyargs_only_imported_once(self, pytester: Pytester) -> None: pkg = pytester.mkpydir("foo") pkg.joinpath("test_foo.py").write_text( - "print('hello from test_foo')\ndef test(): pass" + "print('hello from test_foo')\ndef test(): pass", encoding="utf-8" ) pkg.joinpath("conftest.py").write_text( - "def pytest_configure(config): print('configuring')" + "def pytest_configure(config): print('configuring')", encoding="utf-8" ) result = pytester.runpytest( @@ -613,7 +613,7 @@ class TestInvocationVariants: def test_pyargs_filename_looks_like_module(self, pytester: Pytester) -> None: pytester.path.joinpath("conftest.py").touch() - pytester.path.joinpath("t.py").write_text("def test(): pass") + pytester.path.joinpath("t.py").write_text("def test(): pass", encoding="utf-8") result = pytester.runpytest("--pyargs", "t.py") assert result.ret == ExitCode.OK @@ -622,8 +622,12 @@ class TestInvocationVariants: monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False) path = pytester.mkpydir("tpkg") - path.joinpath("test_hello.py").write_text("def test_hello(): pass") - path.joinpath("test_world.py").write_text("def test_world(): pass") + path.joinpath("test_hello.py").write_text( + "def test_hello(): pass", encoding="utf-8" + ) + path.joinpath("test_world.py").write_text( + "def test_world(): pass", encoding="utf-8" + ) result = pytester.runpytest("--pyargs", "tpkg") assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) @@ -662,13 +666,15 @@ class TestInvocationVariants: ns = d.joinpath("ns_pkg") ns.mkdir() ns.joinpath("__init__.py").write_text( - "__import__('pkg_resources').declare_namespace(__name__)" + "__import__('pkg_resources').declare_namespace(__name__)", + encoding="utf-8", ) lib = ns.joinpath(dirname) lib.mkdir() lib.joinpath("__init__.py").touch() lib.joinpath(f"test_{dirname}.py").write_text( - f"def test_{dirname}(): pass\ndef test_other():pass" + f"def test_{dirname}(): pass\ndef test_other():pass", + encoding="utf-8", ) # The structure of the test directory is now: @@ -754,10 +760,10 @@ class TestInvocationVariants: lib.mkdir() lib.joinpath("__init__.py").touch() lib.joinpath("test_bar.py").write_text( - "def test_bar(): pass\ndef test_other(a_fixture):pass" + "def test_bar(): pass\ndef test_other(a_fixture):pass", encoding="utf-8" ) lib.joinpath("conftest.py").write_text( - "import pytest\n@pytest.fixture\ndef a_fixture():pass" + "import pytest\n@pytest.fixture\ndef a_fixture():pass", encoding="utf-8" ) d_local = pytester.mkdir("symlink_root") @@ -1158,7 +1164,6 @@ def test_usage_error_code(pytester: Pytester) -> None: assert result.ret == ExitCode.USAGE_ERROR -@pytest.mark.filterwarnings("default::pytest.PytestUnhandledCoroutineWarning") def test_warn_on_async_function(pytester: Pytester) -> None: # In the below we .close() the coroutine only to avoid # "RuntimeWarning: coroutine 'test_2' was never awaited" @@ -1175,7 +1180,7 @@ def test_warn_on_async_function(pytester: Pytester) -> None: return coro """ ) - result = pytester.runpytest() + result = pytester.runpytest("-Wdefault") result.stdout.fnmatch_lines( [ "test_async.py::test_1", @@ -1191,7 +1196,6 @@ def test_warn_on_async_function(pytester: Pytester) -> None: ) -@pytest.mark.filterwarnings("default::pytest.PytestUnhandledCoroutineWarning") def test_warn_on_async_gen_function(pytester: Pytester) -> None: pytester.makepyfile( test_async=""" @@ -1203,7 +1207,7 @@ def test_warn_on_async_gen_function(pytester: Pytester) -> None: return test_2() """ ) - result = pytester.runpytest() + result = pytester.runpytest("-Wdefault") result.stdout.fnmatch_lines( [ "test_async.py::test_1", @@ -1276,8 +1280,7 @@ def test_tee_stdio_captures_and_live_prints(pytester: Pytester) -> None: result.stderr.fnmatch_lines(["*@this is stderr@*"]) # now ensure the output is in the junitxml - with open(pytester.path.joinpath("output.xml")) as f: - fullXml = f.read() + fullXml = pytester.path.joinpath("output.xml").read_text(encoding="utf-8") assert "@this is stdout@\n" in fullXml assert "@this is stderr@\n" in fullXml @@ -1312,3 +1315,38 @@ def test_function_return_non_none_warning(pytester: Pytester) -> None: ) res = pytester.runpytest() res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"]) + + +def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None: + """ + Regression test for #10811: previously import_path with ImportMode.importlib would + not return a module if already in sys.modules, resulting in modules being imported + multiple times, which causes problems with modules that have import side effects. + """ + # Uses the exact reproducer form #10811, given it is very minimal + # and illustrates the problem well. + pytester.makepyfile( + **{ + "pmxbot/commands.py": "from . import logging", + "pmxbot/logging.py": "", + "tests/__init__.py": "", + "tests/test_commands.py": """ + import importlib + from pmxbot import logging + + class TestCommands: + def test_boo(self): + assert importlib.import_module('pmxbot.logging') is logging + """, + } + ) + pytester.makeini( + """ + [pytest] + addopts= + --doctest-modules + --import-mode importlib + """ + ) + result = pytester.runpytest_subprocess() + result.stdout.fnmatch_lines("*1 passed*") diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index d71591179..92ebfbc48 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,15 +1,15 @@ +from __future__ import annotations + import importlib import io import operator import queue +import re import sys import textwrap from pathlib import Path from typing import Any -from typing import Dict -from typing import Tuple from typing import TYPE_CHECKING -from typing import Union import _pytest._code import pytest @@ -374,7 +374,7 @@ def test_excinfo_no_sourcecode(): def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None: # XXX: simplified locally testable version - tmp_path.joinpath("test.txt").write_text("{{ h()}}:") + tmp_path.joinpath("test.txt").write_text("{{ h()}}:", encoding="utf-8") jinja2 = pytest.importorskip("jinja2") loader = jinja2.FileSystemLoader(str(tmp_path)) @@ -451,7 +451,7 @@ class TestFormattedExcinfo: source = textwrap.dedent(source) modpath = tmp_path.joinpath("mod.py") tmp_path.joinpath("__init__.py").touch() - modpath.write_text(source) + modpath.write_text(source, encoding="utf-8") importlib.invalidate_caches() return import_path(modpath, root=tmp_path) @@ -801,7 +801,7 @@ raise ValueError() ) excinfo = pytest.raises(ValueError, mod.entry) - styles: Tuple[_TracebackStyle, ...] = ("long", "short") + styles: tuple[_TracebackStyle, ...] = ("long", "short") for style in styles: p = FormattedExcinfo(style=style) reprtb = p.repr_traceback(excinfo) @@ -928,7 +928,7 @@ raise ValueError() ) excinfo = pytest.raises(ValueError, mod.entry) - styles: Tuple[_TracebackStyle, ...] = ("short", "long", "no") + styles: tuple[_TracebackStyle, ...] = ("short", "long", "no") for style in styles: for showlocals in (True, False): repr = excinfo.getrepr(style=style, showlocals=showlocals) @@ -1023,7 +1023,7 @@ raise ValueError() """ ) excinfo = pytest.raises(ValueError, mod.f) - tmp_path.joinpath("mod.py").write_text("asdf") + tmp_path.joinpath("mod.py").write_text("asdf", encoding="utf-8") excinfo.traceback = excinfo.traceback.filter(excinfo) repr = excinfo.getrepr() repr.toterminal(tw_mock) @@ -1090,7 +1090,7 @@ raise ValueError() for funcargs in (True, False) ], ) - def test_format_excinfo(self, reproptions: Dict[str, Any]) -> None: + def test_format_excinfo(self, reproptions: dict[str, Any]) -> None: def bar(): assert False, "some error" @@ -1398,7 +1398,7 @@ raise ValueError() @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"]) def test_repr_traceback_with_unicode(style, encoding): if encoding is None: - msg: Union[str, bytes] = "☹" + msg: str | bytes = "☹" else: msg = "☹".encode(encoding) try: @@ -1648,3 +1648,51 @@ def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester) ], consecutive=True, ) + + +def add_note(err: BaseException, msg: str) -> None: + """Adds a note to an exception inplace.""" + if sys.version_info < (3, 11): + err.__notes__ = getattr(err, "__notes__", []) + [msg] # type: ignore[attr-defined] + else: + err.add_note(msg) + + +@pytest.mark.parametrize( + "error,notes,match", + [ + (Exception("test"), [], "test"), + (AssertionError("foo"), ["bar"], "bar"), + (AssertionError("foo"), ["bar", "baz"], "bar"), + (AssertionError("foo"), ["bar", "baz"], "baz"), + (ValueError("foo"), ["bar", "baz"], re.compile(r"bar\nbaz", re.MULTILINE)), + (ValueError("foo"), ["bar", "baz"], re.compile(r"BAZ", re.IGNORECASE)), + ], +) +def test_check_error_notes_success( + error: Exception, notes: list[str], match: str +) -> None: + for note in notes: + add_note(error, note) + + with pytest.raises(Exception, match=match): + raise error + + +@pytest.mark.parametrize( + "error, notes, match", + [ + (Exception("test"), [], "foo"), + (AssertionError("foo"), ["bar"], "baz"), + (AssertionError("foo"), ["bar"], "foo\nbaz"), + ], +) +def test_check_error_notes_failure( + error: Exception, notes: list[str], match: str +) -> None: + for note in notes: + add_note(error, note) + + with pytest.raises(AssertionError): + with pytest.raises(type(error), match=match): + raise error diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 52417f2f8..260c80299 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -294,7 +294,7 @@ def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) - """ ) path = tmp_path.joinpath("a.py") - path.write_text(str(source)) + path.write_text(str(source), encoding="utf-8") mod: Any = import_path(path, root=tmp_path) s2 = Source(mod.A) assert str(source).strip() == str(s2).strip() @@ -439,14 +439,9 @@ comment 4 ''' for line in range(2, 6): assert str(getstatement(line, source)) == " x = 1" - if sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): - tqs_start = 8 - else: - tqs_start = 10 - assert str(getstatement(10, source)) == '"""' - for line in range(6, tqs_start): + for line in range(6, 8): assert str(getstatement(line, source)) == " assert False" - for line in range(tqs_start, 10): + for line in range(8, 10): assert str(getstatement(line, source)) == '"""\ncomment 4\n"""' diff --git a/testing/conftest.py b/testing/conftest.py index a83552fd2..926a1d5d3 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,6 +1,7 @@ import dataclasses import re import sys +from typing import Generator from typing import List import pytest @@ -21,11 +22,11 @@ if sys.gettrace(): sys.settrace(orig_trace) -@pytest.hookimpl(hookwrapper=True, tryfirst=True) -def pytest_collection_modifyitems(items): +@pytest.hookimpl(wrapper=True, tryfirst=True) +def pytest_collection_modifyitems(items) -> Generator[None, None, None]: """Prefer faster tests. - Use a hookwrapper to do this in the beginning, so e.g. --ff still works + Use a hook wrapper to do this in the beginning, so e.g. --ff still works correctly. """ fast_items = [] @@ -62,7 +63,7 @@ def pytest_collection_modifyitems(items): items[:] = fast_items + neutral_items + slow_items + slowest_items - yield + return (yield) @pytest.fixture @@ -105,7 +106,7 @@ def tw_mock(): @pytest.fixture -def dummy_yaml_custom_test(pytester: Pytester): +def dummy_yaml_custom_test(pytester: Pytester) -> None: """Writes a conftest file that collects and executes a dummy yaml test. Taken from the docs, but stripped down to the bare minimum, useful for diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 3ceed7f5a..f4197a1f6 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -103,7 +103,7 @@ def test_strict_option_is_deprecated(pytester: Pytester) -> None: def test_foo(): pass """ ) - result = pytester.runpytest("--strict") + result = pytester.runpytest("--strict", "-Wdefault::pytest.PytestRemovedIn8Warning") result.stdout.fnmatch_lines( [ "'unknown' not found in `markers` configuration option", @@ -189,7 +189,7 @@ class TestSkipMsgArgumentDeprecated: pytest.skip(msg="skippedmsg") """ ) - result = pytester.runpytest(p) + result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning") result.stdout.fnmatch_lines( [ "*PytestRemovedIn8Warning: pytest.skip(msg=...) is now deprecated, " @@ -208,7 +208,7 @@ class TestSkipMsgArgumentDeprecated: pytest.fail(msg="failedmsg") """ ) - result = pytester.runpytest(p) + result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning") result.stdout.fnmatch_lines( [ "*PytestRemovedIn8Warning: pytest.fail(msg=...) is now deprecated, " @@ -227,7 +227,7 @@ class TestSkipMsgArgumentDeprecated: pytest.exit(msg="exitmsg") """ ) - result = pytester.runpytest(p) + result = pytester.runpytest(p, "-Wdefault::pytest.PytestRemovedIn8Warning") result.stdout.fnmatch_lines( [ "*PytestRemovedIn8Warning: pytest.exit(msg=...) is now deprecated, " @@ -245,7 +245,7 @@ def test_deprecation_of_cmdline_preparse(pytester: Pytester) -> None: """ ) - result = pytester.runpytest() + result = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning") result.stdout.fnmatch_lines( [ "*PytestRemovedIn8Warning: The pytest_cmdline_preparse hook is deprecated*", @@ -281,6 +281,57 @@ def test_importing_instance_is_deprecated(pytester: Pytester) -> None: from _pytest.python import Instance # noqa: F401 +def test_fixture_disallow_on_marked_functions(): + """Test that applying @pytest.fixture to a marked function warns (#3364).""" + with pytest.warns( + pytest.PytestRemovedIn8Warning, + match=r"Marks applied to fixtures have no effect", + ) as record: + + @pytest.fixture + @pytest.mark.parametrize("example", ["hello"]) + @pytest.mark.usefixtures("tmp_path") + def foo(): + raise NotImplementedError() + + # it's only possible to get one warning here because you're already prevented + # from applying @fixture twice + # ValueError("fixture is being applied more than once to the same function") + assert len(record) == 1 + + +def test_fixture_disallow_marks_on_fixtures(): + """Test that applying a mark to a fixture warns (#3364).""" + with pytest.warns( + pytest.PytestRemovedIn8Warning, + match=r"Marks applied to fixtures have no effect", + ) as record: + + @pytest.mark.parametrize("example", ["hello"]) + @pytest.mark.usefixtures("tmp_path") + @pytest.fixture + def foo(): + raise NotImplementedError() + + assert len(record) == 2 # one for each mark decorator + + +def test_fixture_disallowed_between_marks(): + """Test that applying a mark to a fixture warns (#3364).""" + with pytest.warns( + pytest.PytestRemovedIn8Warning, + match=r"Marks applied to fixtures have no effect", + ) as record: + + @pytest.mark.parametrize("example", ["hello"]) + @pytest.fixture + @pytest.mark.usefixtures("tmp_path") + def foo(): + raise NotImplementedError() + + assert len(record) == 2 # one for each mark decorator + + @pytest.mark.filterwarnings("default") def test_nose_deprecated_with_setup(pytester: Pytester) -> None: pytest.importorskip("nose") @@ -299,7 +350,7 @@ def test_nose_deprecated_with_setup(pytester: Pytester) -> None: ... """ ) - output = pytester.runpytest() + output = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning") message = [ "*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.", "*test_nose_deprecated_with_setup.py::test_omits_warnings is using nose method: `setup_fn_no_op` (setup)", @@ -327,7 +378,7 @@ def test_nose_deprecated_setup_teardown(pytester: Pytester) -> None: ... """ ) - output = pytester.runpytest() + output = pytester.runpytest("-Wdefault::pytest.PytestRemovedIn8Warning") message = [ "*PytestRemovedIn8Warning: Support for nose tests is deprecated and will be removed in a future release.", "*test_nose_deprecated_setup_teardown.py::Test::test is using nose-specific method: `setup(self)`", diff --git a/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py b/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py index e69de29bb..9cd366295 100644 --- a/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py +++ b/testing/example_scripts/collect/package_init_given_as_arg/pkg/__init__.py @@ -0,0 +1,2 @@ +def test_init(): + pass diff --git a/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py b/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py index f17482385..8f2d73cfa 100644 --- a/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py +++ b/testing/example_scripts/collect/package_init_given_as_arg/pkg/test_foo.py @@ -1,2 +1,2 @@ -def test(): +def test_foo(): pass diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 14b77236a..8c1e4f8cc 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -81,7 +81,7 @@ def test_root_logger_affected(pytester: Pytester) -> None: # not the info one, because the default level of the root logger is # WARNING. assert os.path.isfile(log_file) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "info text going to logger" not in contents assert "warning text going to logger" in contents @@ -656,7 +656,7 @@ def test_log_file_cli(pytester: Pytester) -> None: # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 assert os.path.isfile(log_file) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "This log message will be shown" in contents assert "This log message won't be shown" not in contents @@ -687,7 +687,7 @@ def test_log_file_cli_level(pytester: Pytester) -> None: # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 assert os.path.isfile(log_file) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "This log message will be shown" in contents assert "This log message won't be shown" not in contents @@ -738,7 +738,7 @@ def test_log_file_ini(pytester: Pytester) -> None: # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 assert os.path.isfile(log_file) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "This log message will be shown" in contents assert "This log message won't be shown" not in contents @@ -777,7 +777,7 @@ def test_log_file_ini_level(pytester: Pytester) -> None: # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 assert os.path.isfile(log_file) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "This log message will be shown" in contents assert "This log message won't be shown" not in contents @@ -985,7 +985,7 @@ def test_log_in_hooks(pytester: Pytester) -> None: ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*sessionstart*", "*runtestloop*", "*sessionfinish*"]) - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert "sessionstart" in contents assert "runtestloop" in contents @@ -1021,7 +1021,7 @@ def test_log_in_runtest_logreport(pytester: Pytester) -> None: """ ) pytester.runpytest() - with open(log_file) as rfh: + with open(log_file, encoding="utf-8") as rfh: contents = rfh.read() assert contents.count("logreport") == 3 @@ -1040,13 +1040,13 @@ def test_log_set_path(pytester: Pytester) -> None: """ import os import pytest - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_setup(item): config = item.config logging_plugin = config.pluginmanager.get_plugin("logging-plugin") report_file = os.path.join({}, item._request.node.name) logging_plugin.set_log_path(report_file) - yield + return (yield) """.format( repr(report_dir_base) ) @@ -1065,11 +1065,11 @@ def test_log_set_path(pytester: Pytester) -> None: """ ) pytester.runpytest() - with open(os.path.join(report_dir_base, "test_first")) as rfh: + with open(os.path.join(report_dir_base, "test_first"), encoding="utf-8") as rfh: content = rfh.read() assert "message from test 1" in content - with open(os.path.join(report_dir_base, "test_second")) as rfh: + with open(os.path.join(report_dir_base, "test_second"), encoding="utf-8") as rfh: content = rfh.read() assert "message from test 2" in content diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt index 0d30ab96f..cf580b7cc 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,15 +1,15 @@ -anyio[curio,trio]==3.7.0 -django==4.2.1 -pytest-asyncio==0.21.0 +anyio[curio,trio]==3.7.1 +django==4.2.3 +pytest-asyncio==0.21.1 pytest-bdd==6.1.1 pytest-cov==4.1.0 pytest-django==4.5.2 pytest-flakes==4.0.5 pytest-html==3.2.0 -pytest-mock==3.10.0 -pytest-rerunfailures==11.1.2 +pytest-mock==3.11.1 +pytest-rerunfailures==12.0 pytest-sugar==0.9.7 pytest-trio==0.7.0 pytest-twisted==1.14.0 twisted==22.8.0 -pytest-xvfb==2.0.0 +pytest-xvfb==3.0.0 diff --git a/testing/python/approx.py b/testing/python/approx.py index 631e52b56..6ad411a3e 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -122,6 +122,23 @@ class TestApprox: ], ) + assert_approx_raises_regex( + {"a": 1.0, "b": None, "c": None}, + { + "a": None, + "b": 1000.0, + "c": None, + }, + [ + r" comparison failed. Mismatched elements: 2 / 3:", + r" Max absolute difference: -inf", + r" Max relative difference: -inf", + r" Index \| Obtained\s+\| Expected\s+", + rf" a \| {SOME_FLOAT} \| None", + rf" b \| None\s+\| {SOME_FLOAT} ± {SOME_FLOAT}", + ], + ) + assert_approx_raises_regex( [1.0, 2.0, 3.0, 4.0], [1.0, 3.0, 3.0, 5.0], diff --git a/testing/python/collect.py b/testing/python/collect.py index 41845517b..0415c3fbe 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -60,7 +60,8 @@ class TestModule: """.format( str(root2) ) - ) + ), + encoding="utf-8", ) with monkeypatch.context() as mp: mp.chdir(root2) @@ -826,13 +827,14 @@ class TestConftestCustomization: textwrap.dedent( """\ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pycollect_makemodule(): - outcome = yield - mod = outcome.get_result() + mod = yield mod.obj.hello = "world" + return mod """ - ) + ), + encoding="utf-8", ) b.joinpath("test_module.py").write_text( textwrap.dedent( @@ -840,7 +842,8 @@ class TestConftestCustomization: def test_hello(): assert hello == "world" """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -852,16 +855,16 @@ class TestConftestCustomization: textwrap.dedent( """\ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pycollect_makeitem(): - outcome = yield - if outcome.excinfo is None: - result = outcome.get_result() - if result: - for func in result: - func._some123 = "world" + result = yield + if result: + for func in result: + func._some123 = "world" + return result """ - ) + ), + encoding="utf-8", ) b.joinpath("test_module.py").write_text( textwrap.dedent( @@ -874,7 +877,8 @@ class TestConftestCustomization: def test_hello(obj): assert obj == "world" """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -897,25 +901,29 @@ class TestConftestCustomization: def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None: """Ensure we can collect files with weird file extensions as Python modules (#2369)""" - # We'll implement a little finder and loader to import files containing + # Implement a little meta path finder to import files containing # Python source code whose file extension is ".narf". pytester.makeconftest( """ - import sys, os, imp + import sys + import os.path + from importlib.util import spec_from_loader + from importlib.machinery import SourceFileLoader from _pytest.python import Module - class Loader(object): - def load_module(self, name): - return imp.load_source(name, name + ".narf") - class Finder(object): - def find_module(self, name, path=None): - if os.path.exists(name + ".narf"): - return Loader() - sys.meta_path.append(Finder()) + class MetaPathFinder: + def find_spec(self, fullname, path, target=None): + if os.path.exists(fullname + ".narf"): + return spec_from_loader( + fullname, + SourceFileLoader(fullname, fullname + ".narf"), + ) + sys.meta_path.append(MetaPathFinder()) def pytest_collect_file(file_path, parent): if file_path.suffix == ".narf": - return Module.from_parent(path=file_path, parent=parent)""" + return Module.from_parent(path=file_path, parent=parent) + """ ) pytester.makefile( ".narf", @@ -970,7 +978,8 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None: def pytest_runtest_teardown(item): assert item.path.stem == "test_in_sub1" """ - ) + ), + encoding="utf-8", ) sub2.joinpath("conftest.py").write_text( textwrap.dedent( @@ -983,10 +992,11 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None: def pytest_runtest_teardown(item): assert item.path.stem == "test_in_sub2" """ - ) + ), + encoding="utf-8", ) - sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass") - sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass") + sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass", encoding="utf-8") + sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass", encoding="utf-8") result = pytester.runpytest("-v", "-s") result.assert_outcomes(passed=2) @@ -1374,7 +1384,8 @@ def test_skip_duplicates_by_default(pytester: Pytester) -> None: def test_real(): pass """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest(str(a), str(a)) result.stdout.fnmatch_lines(["*collected 1 item*"]) @@ -1394,7 +1405,8 @@ def test_keep_duplicates(pytester: Pytester) -> None: def test_real(): pass """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("--keep-duplicates", str(a), str(a)) result.stdout.fnmatch_lines(["*collected 2 item*"]) @@ -1407,10 +1419,15 @@ def test_package_collection_infinite_recursion(pytester: Pytester) -> None: def test_package_collection_init_given_as_argument(pytester: Pytester) -> None: - """Regression test for #3749""" + """Regression test for #3749, #8976, #9263, #9313. + + Specifying an __init__.py file directly should collect only the __init__.py + Module, not the entire package. + """ p = pytester.copy_example("collect/package_init_given_as_arg") - result = pytester.runpytest(p / "pkg" / "__init__.py") - result.stdout.fnmatch_lines(["*1 passed*"]) + items, hookrecorder = pytester.inline_genitems(p / "pkg" / "__init__.py") + assert len(items) == 1 + assert items[0].name == "test_init" def test_package_with_modules(pytester: Pytester) -> None: @@ -1439,8 +1456,12 @@ def test_package_with_modules(pytester: Pytester) -> None: sub2_test = sub2.joinpath("test") sub2_test.mkdir(parents=True) - sub1_test.joinpath("test_in_sub1.py").write_text("def test_1(): pass") - sub2_test.joinpath("test_in_sub2.py").write_text("def test_2(): pass") + sub1_test.joinpath("test_in_sub1.py").write_text( + "def test_1(): pass", encoding="utf-8" + ) + sub2_test.joinpath("test_in_sub2.py").write_text( + "def test_2(): pass", encoding="utf-8" + ) # Execute from . result = pytester.runpytest("-v", "-s") @@ -1484,9 +1505,11 @@ def test_package_ordering(pytester: Pytester) -> None: sub2_test = sub2.joinpath("test") sub2_test.mkdir(parents=True) - root.joinpath("Test_root.py").write_text("def test_1(): pass") - sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass") - sub2_test.joinpath("test_sub2.py").write_text("def test_3(): pass") + root.joinpath("Test_root.py").write_text("def test_1(): pass", encoding="utf-8") + sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass", encoding="utf-8") + sub2_test.joinpath("test_sub2.py").write_text( + "def test_3(): pass", encoding="utf-8" + ) # Execute from . result = pytester.runpytest("-v", "-s") diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index d996f80bb..191689d1c 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -287,7 +287,8 @@ class TestFillFixtures: def spam(): return 'spam' """ - ) + ), + encoding="utf-8", ) testfile = subdir.joinpath("test_spam.py") testfile.write_text( @@ -296,7 +297,8 @@ class TestFillFixtures: def test_spam(spam): assert spam == "spam" """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) @@ -359,7 +361,8 @@ class TestFillFixtures: def spam(request): return request.param """ - ) + ), + encoding="utf-8", ) testfile = subdir.joinpath("test_spam.py") testfile.write_text( @@ -371,7 +374,8 @@ class TestFillFixtures: assert spam == params['spam'] params['spam'] += 1 """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) @@ -403,7 +407,8 @@ class TestFillFixtures: def spam(request): return request.param """ - ) + ), + encoding="utf-8", ) testfile = subdir.joinpath("test_spam.py") testfile.write_text( @@ -415,7 +420,8 @@ class TestFillFixtures: assert spam == params['spam'] params['spam'] += 1 """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest() result.stdout.fnmatch_lines(["*3 passed*"]) @@ -693,6 +699,7 @@ class TestRequestBasic: """ ) (item1,) = pytester.genitems([modcol]) + assert isinstance(item1, Function) assert item1.name == "test_method" arg2fixturedefs = fixtures.FixtureRequest( item1, _ispytest=True @@ -961,6 +968,7 @@ class TestRequestBasic: def test_request_getmodulepath(self, pytester: Pytester) -> None: modcol = pytester.getmodulecol("def test_somefunc(): pass") (item,) = pytester.genitems([modcol]) + assert isinstance(item, Function) req = fixtures.FixtureRequest(item, _ispytest=True) assert req.path == modcol.path @@ -1037,10 +1045,11 @@ class TestRequestBasic: def arg1(): pass """ - ) + ), + encoding="utf-8", ) p = b.joinpath("test_module.py") - p.write_text("def test_func(arg1): pass") + p.write_text("def test_func(arg1): pass", encoding="utf-8") result = pytester.runpytest(p, "--fixtures") assert result.ret == 0 result.stdout.fnmatch_lines( @@ -1118,6 +1127,7 @@ class TestRequestMarking: pass """ ) + assert isinstance(item1, Function) req1 = fixtures.FixtureRequest(item1, _ispytest=True) assert "xfail" not in item1.keywords req1.applymarker(pytest.mark.xfail) @@ -1617,7 +1627,8 @@ class TestFixtureManagerParseFactories: def one(): return 1 """ - ) + ), + encoding="utf-8", ) package.joinpath("test_x.py").write_text( textwrap.dedent( @@ -1625,7 +1636,8 @@ class TestFixtureManagerParseFactories: def test_x(one): assert one == 1 """ - ) + ), + encoding="utf-8", ) sub = package.joinpath("sub") sub.mkdir() @@ -1638,7 +1650,8 @@ class TestFixtureManagerParseFactories: def one(): return 2 """ - ) + ), + encoding="utf-8", ) sub.joinpath("test_y.py").write_text( textwrap.dedent( @@ -1646,7 +1659,8 @@ class TestFixtureManagerParseFactories: def test_x(one): assert one == 2 """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=2) @@ -1671,7 +1685,8 @@ class TestFixtureManagerParseFactories: def teardown_module(): values[:] = [] """ - ) + ), + encoding="utf-8", ) package.joinpath("test_x.py").write_text( textwrap.dedent( @@ -1680,7 +1695,8 @@ class TestFixtureManagerParseFactories: def test_x(): assert values == ["package"] """ - ) + ), + encoding="utf-8", ) package = pytester.mkdir("package2") package.joinpath("__init__.py").write_text( @@ -1692,7 +1708,8 @@ class TestFixtureManagerParseFactories: def teardown_module(): values[:] = [] """ - ) + ), + encoding="utf-8", ) package.joinpath("test_x.py").write_text( textwrap.dedent( @@ -1701,7 +1718,8 @@ class TestFixtureManagerParseFactories: def test_x(): assert values == ["package2"] """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=2) @@ -1714,7 +1732,7 @@ class TestFixtureManagerParseFactories: ) pytester.syspathinsert(pytester.path.name) package = pytester.mkdir("package") - package.joinpath("__init__.py").write_text("") + package.joinpath("__init__.py").write_text("", encoding="utf-8") package.joinpath("conftest.py").write_text( textwrap.dedent( """\ @@ -1731,7 +1749,8 @@ class TestFixtureManagerParseFactories: yield values values.pop() """ - ) + ), + encoding="utf-8", ) package.joinpath("test_x.py").write_text( textwrap.dedent( @@ -1742,7 +1761,8 @@ class TestFixtureManagerParseFactories: def test_package(one): assert values == ["package-auto", "package"] """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=2) @@ -1892,8 +1912,12 @@ class TestAutouseDiscovery: """ ) conftest.rename(a.joinpath(conftest.name)) - a.joinpath("test_something.py").write_text("def test_func(): pass") - b.joinpath("test_otherthing.py").write_text("def test_func(): pass") + a.joinpath("test_something.py").write_text( + "def test_func(): pass", encoding="utf-8" + ) + b.joinpath("test_otherthing.py").write_text( + "def test_func(): pass", encoding="utf-8" + ) result = pytester.runpytest() result.stdout.fnmatch_lines( """ @@ -1939,7 +1963,8 @@ class TestAutouseManagement: import sys sys._myapp = "hello" """ - ) + ), + encoding="utf-8", ) sub = pkgdir.joinpath("tests") sub.mkdir() @@ -1952,7 +1977,8 @@ class TestAutouseManagement: def test_app(): assert sys._myapp == "hello" """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=1) @@ -2438,6 +2464,31 @@ class TestFixtureMarker: ["*ScopeMismatch*You tried*function*session*request*"] ) + def test_scope_mismatch_already_computed_dynamic(self, pytester: Pytester) -> None: + pytester.makepyfile( + test_it=""" + import pytest + + @pytest.fixture(scope="function") + def fixfunc(): pass + + @pytest.fixture(scope="module") + def fixmod(fixfunc): pass + + def test_it(request, fixfunc): + request.getfixturevalue("fixmod") + """, + ) + + result = pytester.runpytest() + assert result.ret == ExitCode.TESTS_FAILED + result.stdout.fnmatch_lines( + [ + "*ScopeMismatch*involved factories*", + "test_it.py:6: def fixmod(fixfunc)", + ] + ) + def test_dynamic_scope(self, pytester: Pytester) -> None: pytester.makeconftest( """ @@ -2882,7 +2933,7 @@ class TestFixtureMarker: def browser(request): def finalize(): - sys.stdout.write_text('Finalized') + sys.stdout.write_text('Finalized', encoding='utf-8') request.addfinalizer(finalize) return {} """ @@ -2900,7 +2951,8 @@ class TestFixtureMarker: def test_browser(browser): assert browser['visited'] is True """ - ) + ), + encoding="utf-8", ) reprec = pytester.runpytest("-s") for test in ["test_browser"]: @@ -3855,7 +3907,8 @@ class TestParameterizedSubRequest: def fix_with_param(request): return request.param """ - ) + ), + encoding="utf-8", ) testfile = tests_dir.joinpath("test_foos.py") @@ -3867,7 +3920,8 @@ class TestParameterizedSubRequest: def test_foo(request): request.getfixturevalue('fix_with_param') """ - ) + ), + encoding="utf-8", ) os.chdir(tests_dir) @@ -3983,6 +4037,7 @@ class TestScopeOrdering: """ ) items, _ = pytester.inline_genitems() + assert isinstance(items[0], Function) request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "m1 f1".split() @@ -4031,6 +4086,7 @@ class TestScopeOrdering: """ ) items, _ = pytester.inline_genitems() + assert isinstance(items[0], Function) request = FixtureRequest(items[0], _ispytest=True) # order of fixtures based on their scope and position in the parameter list assert ( @@ -4058,6 +4114,7 @@ class TestScopeOrdering: """ ) items, _ = pytester.inline_genitems() + assert isinstance(items[0], Function) request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "m1 f1".split() @@ -4091,6 +4148,7 @@ class TestScopeOrdering: """ ) items, _ = pytester.inline_genitems() + assert isinstance(items[0], Function) request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "s1 m1 c1 f2 f1".split() @@ -4133,6 +4191,7 @@ class TestScopeOrdering: } ) items, _ = pytester.inline_genitems() + assert isinstance(items[0], Function) request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "p_sub m_conf m_sub m_test f1".split() @@ -4177,6 +4236,7 @@ class TestScopeOrdering: """ ) items, _ = pytester.inline_genitems() + assert isinstance(items[0], Function) request = FixtureRequest(items[0], _ispytest=True) assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split() @@ -4196,7 +4256,7 @@ class TestScopeOrdering: └── test_2.py """ root = pytester.mkdir("root") - root.joinpath("__init__.py").write_text("values = []") + root.joinpath("__init__.py").write_text("values = []", encoding="utf-8") sub1 = root.joinpath("sub1") sub1.mkdir() sub1.joinpath("__init__.py").touch() @@ -4211,7 +4271,8 @@ class TestScopeOrdering: yield values assert values.pop() == "pre-sub1" """ - ) + ), + encoding="utf-8", ) sub1.joinpath("test_1.py").write_text( textwrap.dedent( @@ -4220,7 +4281,8 @@ class TestScopeOrdering: def test_1(fix): assert values == ["pre-sub1"] """ - ) + ), + encoding="utf-8", ) sub2 = root.joinpath("sub2") sub2.mkdir() @@ -4236,7 +4298,8 @@ class TestScopeOrdering: yield values assert values.pop() == "pre-sub2" """ - ) + ), + encoding="utf-8", ) sub2.joinpath("test_2.py").write_text( textwrap.dedent( @@ -4245,7 +4308,8 @@ class TestScopeOrdering: def test_2(fix): assert values == ["pre-sub2"] """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run() reprec.assertoutcome(passed=2) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index c1cc9c3d3..08ea8f910 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -19,7 +19,6 @@ from hypothesis import strategies import pytest from _pytest import fixtures from _pytest import python -from _pytest.compat import _format_args from _pytest.compat import getfuncargnames from _pytest.compat import NOTSET from _pytest.outcomes import fail @@ -1036,27 +1035,6 @@ class TestMetafunc: """ ) - def test_format_args(self) -> None: - def function1(): - pass - - assert _format_args(function1) == "()" - - def function2(arg1): - pass - - assert _format_args(function2) == "(arg1)" - - def function3(arg1, arg2="qwe"): - pass - - assert _format_args(function3) == "(arg1, arg2='qwe')" - - def function4(arg1, *args, **kwargs): - pass - - assert _format_args(function4) == "(arg1, *args, **kwargs)" - class TestMetafuncFunctional: def test_attributes(self, pytester: Pytester) -> None: @@ -1443,7 +1421,8 @@ class TestMetafuncFunctional: def pytest_generate_tests(metafunc): assert metafunc.function.__name__ == "test_1" """ - ) + ), + encoding="utf-8", ) sub2.joinpath("conftest.py").write_text( textwrap.dedent( @@ -1451,10 +1430,15 @@ class TestMetafuncFunctional: def pytest_generate_tests(metafunc): assert metafunc.function.__name__ == "test_2" """ - ) + ), + encoding="utf-8", + ) + sub1.joinpath("test_in_sub1.py").write_text( + "def test_1(): pass", encoding="utf-8" + ) + sub2.joinpath("test_in_sub2.py").write_text( + "def test_2(): pass", encoding="utf-8" ) - sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass") - sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass") result = pytester.runpytest("--keep-duplicates", "-v", "-s", sub1, sub2, sub1) result.assert_outcomes(passed=3) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 473ae44d9..c04c31f31 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -199,8 +199,8 @@ class TestImportHookInstallation: return check """, "mainwrapper.py": """\ + import importlib.metadata import pytest - from _pytest.compat import importlib_metadata class DummyEntryPoint(object): name = 'spam' @@ -220,7 +220,7 @@ class TestImportHookInstallation: def distributions(): return (DummyDistInfo(),) - importlib_metadata.distributions = distributions + importlib.metadata.distributions = distributions pytest.main() """, "test_foo.py": """\ @@ -1392,14 +1392,14 @@ def test_sequence_comparison_uses_repr(pytester: Pytester) -> None: def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None: pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"]) a = pytester.mkdir("a") - a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2") + a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8") a.joinpath("conftest.py").write_text( - 'def pytest_assertrepr_compare(): return ["summary a"]' + 'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8" ) b = pytester.mkdir("b") - b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2") + b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8") b.joinpath("conftest.py").write_text( - 'def pytest_assertrepr_compare(): return ["summary b"]' + 'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8" ) result = pytester.runpytest() diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 245241af2..e1b71ded6 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -131,9 +131,8 @@ class TestAssertionRewrite: for n in [node, *ast.iter_child_nodes(node)]: assert n.lineno == 3 assert n.col_offset == 0 - if sys.version_info >= (3, 8): - assert n.end_lineno == 6 - assert n.end_col_offset == 3 + assert n.end_lineno == 6 + assert n.end_col_offset == 3 def test_dont_rewrite(self) -> None: s = """'PYTEST_DONT_REWRITE'\nassert 14""" @@ -160,7 +159,8 @@ class TestAssertionRewrite: "def special_asserter():\n" " def special_assert(x, y):\n" " assert x == y\n" - " return special_assert\n" + " return special_assert\n", + encoding="utf-8", ) pytester.makeconftest('pytest_plugins = ["plugin"]') pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n") @@ -173,7 +173,9 @@ class TestAssertionRewrite: pytester.makepyfile(test_y="x = 1") xdir = pytester.mkdir("x") pytester.mkpydir(str(xdir.joinpath("test_Y"))) - xdir.joinpath("test_Y").joinpath("__init__.py").write_text("x = 2") + xdir.joinpath("test_Y").joinpath("__init__.py").write_text( + "x = 2", encoding="utf-8" + ) pytester.makepyfile( "import test_y\n" "import test_Y\n" @@ -726,7 +728,7 @@ class TestAssertionRewrite: class TestRewriteOnImport: def test_pycache_is_a_file(self, pytester: Pytester) -> None: - pytester.path.joinpath("__pycache__").write_text("Hello") + pytester.path.joinpath("__pycache__").write_text("Hello", encoding="utf-8") pytester.makepyfile( """ def test_rewritten(): @@ -903,7 +905,8 @@ def test_rewritten(): pkg.joinpath("test_blah.py").write_text( """ def test_rewritten(): - assert "@py_builtins" in globals()""" + assert "@py_builtins" in globals()""", + encoding="utf-8", ) assert pytester.runpytest().ret == 0 @@ -1066,7 +1069,7 @@ class TestAssertionRewriteHookDetails: source = tmp_path / "source.py" pyc = Path(str(source) + "c") - source.write_text("def test(): pass") + source.write_text("def test(): pass", encoding="utf-8") py_compile.compile(str(source), str(pyc)) contents = pyc.read_bytes() @@ -1092,7 +1095,7 @@ class TestAssertionRewriteHookDetails: fn = tmp_path / "source.py" pyc = Path(str(fn) + "c") - fn.write_text("def test(): assert True") + fn.write_text("def test(): assert True", encoding="utf-8") source_stat, co = _rewrite_test(fn, config) _write_pyc(state, co, source_stat, pyc) @@ -1157,7 +1160,7 @@ class TestAssertionRewriteHookDetails: return False def rewrite_self(): - with open(__file__, 'w') as self: + with open(__file__, 'w', encoding='utf-8') as self: self.write('def reloaded(): return True') """, test_fun=""" @@ -1187,9 +1190,10 @@ class TestAssertionRewriteHookDetails: data = pkgutil.get_data('foo.test_foo', 'data.txt') assert data == b'Hey' """ - ) + ), + encoding="utf-8", ) - path.joinpath("data.txt").write_text("Hey") + path.joinpath("data.txt").write_text("Hey", encoding="utf-8") result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) @@ -1265,9 +1269,6 @@ class TestIssue2121: result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"]) -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="walrus operator not available in py<38" -) class TestIssue10743: def test_assertion_walrus_operator(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -1436,9 +1437,6 @@ class TestIssue10743: assert result.ret == 0 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="walrus operator not available in py<38" -) class TestIssue11028: def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -1952,16 +1950,10 @@ class TestPyCacheDir: ) def test_get_cache_dir(self, monkeypatch, prefix, source, expected) -> None: monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) - - if prefix is not None and sys.version_info < (3, 8): - pytest.skip("pycache_prefix not available in py<38") monkeypatch.setattr(sys, "pycache_prefix", prefix, raising=False) assert get_cache_dir(Path(source)) == Path(expected) - @pytest.mark.skipif( - sys.version_info < (3, 8), reason="pycache_prefix not available in py<38" - ) @pytest.mark.skipif( sys.version_info[:2] == (3, 9) and sys.platform.startswith("win"), reason="#9298", @@ -2050,3 +2042,17 @@ class TestReprSizeVerbosity: self.create_test_file(pytester, DEFAULT_REPR_MAX_SIZE * 10) result = pytester.runpytest("-vv") result.stdout.no_fnmatch_line("*xxx...xxx*") + + +class TestIssue11140: + def test_constant_not_picked_as_module_docstring(self, pytester: Pytester) -> None: + pytester.makepyfile( + """\ + 0 + + def test_foo(): + pass + """ + ) + result = pytester.runpytest() + assert result.ret == 0 diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index cb8011036..e2e195ca7 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -38,7 +38,9 @@ class TestNewAPI: @pytest.mark.filterwarnings("ignore:could not create cache path") def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None: pytester.makeini("[pytest]") - pytester.path.joinpath(".pytest_cache").write_text("gone wrong") + pytester.path.joinpath(".pytest_cache").write_text( + "gone wrong", encoding="utf-8" + ) config = pytester.parseconfigure() cache = config.cache assert cache is not None @@ -1085,6 +1087,28 @@ class TestLastFailed: result = pytester.runpytest("--lf") result.assert_outcomes(failed=3) + def test_non_python_file_skipped( + self, + pytester: Pytester, + dummy_yaml_custom_test: None, + ) -> None: + pytester.makepyfile( + **{ + "test_bad.py": """def test_bad(): assert False""", + }, + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines(["collected 2 items", "* 1 failed, 1 passed in *"]) + + result = pytester.runpytest("--lf") + result.stdout.fnmatch_lines( + [ + "collected 1 item", + "run-last-failure: rerun previous 1 failure (skipped 1 file)", + "* 1 failed in *", + ] + ) + class TestNewFirst: def test_newfirst_usecase(self, pytester: Pytester) -> None: @@ -1112,7 +1136,9 @@ class TestNewFirst: ["*test_2/test_2.py::test_1 PASSED*", "*test_1/test_1.py::test_1 PASSED*"] ) - p1.write_text("def test_1(): assert 1\n" "def test_2(): assert 1\n") + p1.write_text( + "def test_1(): assert 1\n" "def test_2(): assert 1\n", encoding="utf-8" + ) os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) result = pytester.runpytest("--nf", "--collect-only", "-q") @@ -1185,7 +1211,8 @@ class TestNewFirst: p1.write_text( "import pytest\n" "@pytest.mark.parametrize('num', [1, 2, 3])\n" - "def test_1(num): assert num\n" + "def test_1(num): assert num\n", + encoding="utf-8", ) os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) @@ -1237,7 +1264,7 @@ def test_gitignore(pytester: Pytester) -> None: assert gitignore_path.read_text(encoding="UTF-8") == msg # Does not overwrite existing/custom one. - gitignore_path.write_text("custom") + gitignore_path.write_text("custom", encoding="utf-8") cache.set("something", "else") assert gitignore_path.read_text(encoding="UTF-8") == "custom" diff --git a/testing/test_capture.py b/testing/test_capture.py index 5d6ef64ef..b6ea81613 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -750,9 +750,10 @@ def test_setup_failure_does_not_kill_capturing(pytester: Pytester) -> None: def pytest_runtest_setup(item): raise ValueError(42) """ - ) + ), + encoding="utf-8", ) - sub1.joinpath("test_mod.py").write_text("def test_func1(): pass") + sub1.joinpath("test_mod.py").write_text("def test_func1(): pass", encoding="utf-8") result = pytester.runpytest(pytester.path, "--traceconfig") result.stdout.fnmatch_lines(["*ValueError(42)*", "*1 error*"]) @@ -1523,9 +1524,9 @@ def test_global_capture_with_live_logging(pytester: Pytester) -> None: def pytest_runtest_logreport(report): if "test_global" in report.nodeid: if report.when == "teardown": - with open("caplog", "w") as f: + with open("caplog", "w", encoding="utf-8") as f: f.write(report.caplog) - with open("capstdout", "w") as f: + with open("capstdout", "w", encoding="utf-8") as f: f.write(report.capstdout) """ ) @@ -1555,14 +1556,14 @@ def test_global_capture_with_live_logging(pytester: Pytester) -> None: result = pytester.runpytest_subprocess("--log-cli-level=INFO") assert result.ret == 0 - with open("caplog") as f: + with open("caplog", encoding="utf-8") as f: caplog = f.read() assert "fix setup" in caplog assert "something in test" in caplog assert "fix teardown" in caplog - with open("capstdout") as f: + with open("capstdout", encoding="utf-8") as f: capstdout = f.read() assert "fix setup" in capstdout diff --git a/testing/test_collection.py b/testing/test_collection.py index bbcb358b6..ca2e2b731 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -140,7 +140,7 @@ class TestCollectFS: ensure_file(tmp_path / ".bzr" / "test_notfound.py") ensure_file(tmp_path / "normal" / "test_found.py") for x in tmp_path.rglob("test_*.py"): - x.write_text("def test_hello(): pass", "utf-8") + x.write_text("def test_hello(): pass", encoding="utf-8") result = pytester.runpytest("--collect-only") s = result.stdout.str() @@ -162,7 +162,7 @@ class TestCollectFS: bindir = "Scripts" if sys.platform.startswith("win") else "bin" ensure_file(pytester.path / "virtual" / bindir / fname) testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py") - testfile.write_text("def test_hello(): pass") + testfile.write_text("def test_hello(): pass", encoding="utf-8") # by default, ignore tests inside a virtualenv result = pytester.runpytest() @@ -192,7 +192,7 @@ class TestCollectFS: # norecursedirs takes priority ensure_file(pytester.path / ".virtual" / bindir / fname) testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py") - testfile.write_text("def test_hello(): pass") + testfile.write_text("def test_hello(): pass", encoding="utf-8") result = pytester.runpytest("--collect-in-virtualenv") result.stdout.no_fnmatch_line("*test_invenv*") # ...unless the virtualenv is explicitly given on the CLI @@ -231,10 +231,14 @@ class TestCollectFS: ) tmp_path = pytester.path ensure_file(tmp_path / "mydir" / "test_hello.py").write_text( - "def test_1(): pass" + "def test_1(): pass", encoding="utf-8" + ) + ensure_file(tmp_path / "xyz123" / "test_2.py").write_text( + "def test_2(): 0/0", encoding="utf-8" + ) + ensure_file(tmp_path / "xy" / "test_ok.py").write_text( + "def test_3(): pass", encoding="utf-8" ) - ensure_file(tmp_path / "xyz123" / "test_2.py").write_text("def test_2(): 0/0") - ensure_file(tmp_path / "xy" / "test_ok.py").write_text("def test_3(): pass") rec = pytester.inline_run() rec.assertoutcome(passed=1) rec = pytester.inline_run("xyz123/test_2.py") @@ -248,12 +252,14 @@ class TestCollectFS: """ ) tmp_path = pytester.path - ensure_file(tmp_path / "a" / "test_1.py").write_text("def test_a(): pass") + ensure_file(tmp_path / "a" / "test_1.py").write_text( + "def test_a(): pass", encoding="utf-8" + ) ensure_file(tmp_path / "b" / "tests" / "test_2.py").write_text( - "def test_b(): pass" + "def test_b(): pass", encoding="utf-8" ) ensure_file(tmp_path / "c" / "tests" / "test_3.py").write_text( - "def test_c(): pass" + "def test_c(): pass", encoding="utf-8" ) # executing from rootdir only tests from `testpaths` directories @@ -328,12 +334,11 @@ class TestPrunetraceback: pytester.makeconftest( """ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_make_collect_report(): - outcome = yield - rep = outcome.get_result() + rep = yield rep.headerlines += ["header1"] - outcome.force_result(rep) + return rep """ ) result = pytester.runpytest(p) @@ -349,8 +354,8 @@ class TestCustomConftests: """ ) sub = pytester.mkdir("xy123") - ensure_file(sub / "test_hello.py").write_text("syntax error") - sub.joinpath("conftest.py").write_text("syntax error") + ensure_file(sub / "test_hello.py").write_text("syntax error", encoding="utf-8") + sub.joinpath("conftest.py").write_text("syntax error", encoding="utf-8") pytester.makepyfile("def test_hello(): pass") pytester.makepyfile(test_one="syntax error") result = pytester.runpytest("--fulltrace") @@ -1060,13 +1065,18 @@ def test_fixture_scope_sibling_conftests(pytester: Pytester) -> None: def fix(): return 1 """ - ) + ), + encoding="utf-8", + ) + foo_path.joinpath("test_foo.py").write_text( + "def test_foo(fix): assert fix == 1", encoding="utf-8" ) - foo_path.joinpath("test_foo.py").write_text("def test_foo(fix): assert fix == 1") # Tests in `food/` should not see the conftest fixture from `foo/` food_path = pytester.mkpydir("food") - food_path.joinpath("test_food.py").write_text("def test_food(fix): assert fix == 1") + food_path.joinpath("test_food.py").write_text( + "def test_food(fix): assert fix == 1", encoding="utf-8" + ) res = pytester.runpytest() assert res.ret == 1 @@ -1197,7 +1207,8 @@ def test_collect_with_chdir_during_import(pytester: Pytester) -> None: os.chdir(%r) """ % (str(subdir),) - ) + ), + encoding="utf-8", ) pytester.makepyfile( """ @@ -1227,8 +1238,12 @@ def test_collect_pyargs_with_testpaths( ) -> None: testmod = pytester.mkdir("testmod") # NOTE: __init__.py is not collected since it does not match python_files. - testmod.joinpath("__init__.py").write_text("def test_func(): pass") - testmod.joinpath("test_file.py").write_text("def test_func(): pass") + testmod.joinpath("__init__.py").write_text( + "def test_func(): pass", encoding="utf-8" + ) + testmod.joinpath("test_file.py").write_text( + "def test_func(): pass", encoding="utf-8" + ) root = pytester.mkdir("root") root.joinpath("pytest.ini").write_text( @@ -1238,7 +1253,8 @@ def test_collect_pyargs_with_testpaths( addopts = --pyargs testpaths = testmod """ - ) + ), + encoding="utf-8", ) monkeypatch.setenv("PYTHONPATH", str(pytester.path), prepend=os.pathsep) with monkeypatch.context() as mp: @@ -1256,7 +1272,8 @@ def test_initial_conftests_with_testpaths(pytester: Pytester) -> None: def pytest_sessionstart(session): raise Exception("pytest_sessionstart hook successfully run") """ - ) + ), + encoding="utf-8", ) pytester.makeini( """ @@ -1264,11 +1281,18 @@ def test_initial_conftests_with_testpaths(pytester: Pytester) -> None: testpaths = some_path """ ) + + # No command line args - falls back to testpaths. result = pytester.runpytest() + assert result.ret == ExitCode.INTERNAL_ERROR result.stdout.fnmatch_lines( "INTERNALERROR* Exception: pytest_sessionstart hook successfully run" ) + # No fallback. + result = pytester.runpytest(".") + assert result.ret == ExitCode.NO_TESTS_COLLECTED + def test_large_option_breaks_initial_conftests(pytester: Pytester) -> None: """Long option values do not break initial conftests handling (#10169).""" @@ -1316,6 +1340,7 @@ def test_collect_symlink_out_of_tree(pytester: Pytester) -> None: assert request.node.nodeid == "test_real.py::test_nodeid" """ ), + encoding="utf-8", ) out_of_tree = pytester.mkdir("out_of_tree") @@ -1344,12 +1369,16 @@ def test_collect_symlink_dir(pytester: Pytester) -> None: def test_collectignore_via_conftest(pytester: Pytester) -> None: """collect_ignore in parent conftest skips importing child (issue #4592).""" tests = pytester.mkpydir("tests") - tests.joinpath("conftest.py").write_text("collect_ignore = ['ignore_me']") + tests.joinpath("conftest.py").write_text( + "collect_ignore = ['ignore_me']", encoding="utf-8" + ) ignore_me = tests.joinpath("ignore_me") ignore_me.mkdir() ignore_me.joinpath("__init__.py").touch() - ignore_me.joinpath("conftest.py").write_text("assert 0, 'should_not_be_called'") + ignore_me.joinpath("conftest.py").write_text( + "assert 0, 'should_not_be_called'", encoding="utf-8" + ) result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED @@ -1358,23 +1387,31 @@ def test_collectignore_via_conftest(pytester: Pytester) -> None: def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None: subdir = pytester.mkdir("sub") init = subdir.joinpath("__init__.py") - init.write_text("def test_init(): pass") + init.write_text("def test_init(): pass", encoding="utf-8") p = subdir.joinpath("test_file.py") - p.write_text("def test_file(): pass") + p.write_text("def test_file(): pass", encoding="utf-8") - # NOTE: without "-o python_files=*.py" this collects test_file.py twice. - # This changed/broke with "Add package scoped fixtures #2283" (2b1410895) - # initially (causing a RecursionError). - result = pytester.runpytest("-v", str(init), str(p)) + # Just the package directory, the __init__.py module is filtered out. + result = pytester.runpytest("-v", subdir) result.stdout.fnmatch_lines( [ "sub/test_file.py::test_file PASSED*", + "*1 passed in*", + ] + ) + + # But it's included if specified directly. + result = pytester.runpytest("-v", init, p) + result.stdout.fnmatch_lines( + [ + "sub/__init__.py::test_init PASSED*", "sub/test_file.py::test_file PASSED*", "*2 passed in*", ] ) - result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init), str(p)) + # Or if the pattern allows it. + result = pytester.runpytest("-v", "-o", "python_files=*.py", subdir) result.stdout.fnmatch_lines( [ "sub/__init__.py::test_init PASSED*", @@ -1387,12 +1424,15 @@ def test_collect_pkg_init_and_file_in_args(pytester: Pytester) -> None: def test_collect_pkg_init_only(pytester: Pytester) -> None: subdir = pytester.mkdir("sub") init = subdir.joinpath("__init__.py") - init.write_text("def test_init(): pass") + init.write_text("def test_init(): pass", encoding="utf-8") - result = pytester.runpytest(str(init)) + result = pytester.runpytest(subdir) result.stdout.fnmatch_lines(["*no tests ran in*"]) - result = pytester.runpytest("-v", "-o", "python_files=*.py", str(init)) + result = pytester.runpytest("-v", init) + result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) + + result = pytester.runpytest("-v", "-o", "python_files=*.py", subdir) result.stdout.fnmatch_lines(["sub/__init__.py::test_init PASSED*", "*1 passed in*"]) @@ -1402,7 +1442,7 @@ def test_collect_sub_with_symlinks(use_pkg: bool, pytester: Pytester) -> None: sub = pytester.mkdir("sub") if use_pkg: sub.joinpath("__init__.py").touch() - sub.joinpath("test_file.py").write_text("def test_file(): pass") + sub.joinpath("test_file.py").write_text("def test_file(): pass", encoding="utf-8") # Create a broken symlink. symlink_or_skip("test_doesnotexist.py", sub.joinpath("test_broken.py")) @@ -1440,7 +1480,7 @@ def test_collector_respects_tbstyle(pytester: Pytester) -> None: def test_does_not_eagerly_collect_packages(pytester: Pytester) -> None: pytester.makepyfile("def test(): pass") pydir = pytester.mkpydir("foopkg") - pydir.joinpath("__init__.py").write_text("assert False") + pydir.joinpath("__init__.py").write_text("assert False", encoding="utf-8") result = pytester.runpytest() assert result.ret == ExitCode.OK diff --git a/testing/test_compat.py b/testing/test_compat.py index 8a80fd625..27c6db95b 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,5 +1,6 @@ import enum import sys +from functools import cached_property from functools import partial from functools import wraps from typing import TYPE_CHECKING @@ -8,7 +9,6 @@ from typing import Union import pytest from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never -from _pytest.compat import cached_property from _pytest.compat import get_real_func from _pytest.compat import is_generator from _pytest.compat import safe_getattr diff --git a/testing/test_config.py b/testing/test_config.py index cdeb67ace..43561000c 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,4 +1,5 @@ import dataclasses +import importlib.metadata import os import re import sys @@ -13,7 +14,6 @@ from typing import Union import _pytest._code import pytest -from _pytest.compat import importlib_metadata from _pytest.config import _get_plugin_specs_as_list from _pytest.config import _iter_rewritable_modules from _pytest.config import _strtobool @@ -87,7 +87,8 @@ class TestParseIni: [pytest] addopts = --verbose """ - ) + ), + encoding="utf-8", ) config = pytester.parseconfig(tmp_path) assert config.option.color == "no" @@ -127,7 +128,8 @@ class TestParseIni: """.format( section=section ) - ) + ), + encoding="utf-8", ) config = pytester.parseconfig() assert config.getini("minversion") == "3.36" @@ -150,7 +152,8 @@ class TestParseIni: [pytest] minversion = 2.0 """ - ) + ), + encoding="utf-8", ) pytester.path.joinpath("pytest.ini").write_text( textwrap.dedent( @@ -158,13 +161,16 @@ class TestParseIni: [pytest] minversion = 1.5 """ - ) + ), + encoding="utf-8", ) config = pytester.parseconfigure(sub) assert config.getini("minversion") == "2.0" def test_ini_parse_error(self, pytester: Pytester) -> None: - pytester.path.joinpath("pytest.ini").write_text("addopts = -x") + pytester.path.joinpath("pytest.ini").write_text( + "addopts = -x", encoding="utf-8" + ) result = pytester.runpytest() assert result.ret != 0 result.stderr.fnmatch_lines("ERROR: *pytest.ini:1: no section header defined") @@ -469,7 +475,7 @@ class TestParseIni: pytester.makepyfile(myplugin1_module="# my plugin module") pytester.syspathinsert() - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) pytester.makeini(ini_file_text) @@ -634,7 +640,7 @@ class TestConfigAPI: def test_getconftest_pathlist(self, pytester: Pytester, tmp_path: Path) -> None: somepath = tmp_path.joinpath("x", "y", "z") p = tmp_path.joinpath("conftest.py") - p.write_text(f"mylist = {['.', str(somepath)]}") + p.write_text(f"mylist = {['.', str(somepath)]}", encoding="utf-8") config = pytester.parseconfigure(p) assert ( config._getconftest_pathlist("notexist", path=tmp_path, rootpath=tmp_path) @@ -910,7 +916,8 @@ class TestConfigFromdictargs: [pytest] name = value """ - ) + ), + encoding="utf-8", ) inifilename = "../../foo/bar.ini" @@ -927,7 +934,8 @@ class TestConfigFromdictargs: name = wrong-value should_not_be_set = true """ - ) + ), + encoding="utf-8", ) with MonkeyPatch.context() as mp: mp.chdir(cwd) @@ -995,7 +1003,7 @@ def test_preparse_ordering_with_setuptools( def my_dists(): return (Dist,) - monkeypatch.setattr(importlib_metadata, "distributions", my_dists) + monkeypatch.setattr(importlib.metadata, "distributions", my_dists) pytester.makeconftest( """ pytest_plugins = "mytestplugin", @@ -1028,7 +1036,7 @@ def test_setuptools_importerror_issue1479( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) with pytest.raises(ImportError): pytester.parseconfig() @@ -1055,7 +1063,7 @@ def test_importlib_metadata_broken_distribution( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) pytester.parseconfig() @@ -1083,7 +1091,7 @@ def test_plugin_preparse_prevents_setuptools_loading( def distributions(): return (Distribution(),) - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) args = ("-p", "no:mytestplugin") if block_it else () config = pytester.parseconfig(*args) config.pluginmanager.import_plugin("mytestplugin") @@ -1132,7 +1140,7 @@ def test_disable_plugin_autoload( return (Distribution(),) monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") - monkeypatch.setattr(importlib_metadata, "distributions", distributions) + monkeypatch.setattr(importlib.metadata, "distributions", distributions) monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) # type: ignore[misc] config = pytester.parseconfig(*parse_args) has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None @@ -1176,7 +1184,7 @@ def test_cmdline_processargs_simple(pytester: Pytester) -> None: args.append("-h") """ ) - result = pytester.runpytest() + result = pytester.runpytest("-Wignore::pytest.PytestRemovedIn8Warning") result.stdout.fnmatch_lines(["*pytest*", "*-h*"]) @@ -1309,7 +1317,7 @@ def test_load_initial_conftest_last_ordering(_config_for_test): hookimpls = [ ( hookimpl.function.__module__, - "wrapper" if hookimpl.hookwrapper else "nonwrapper", + "wrapper" if (hookimpl.wrapper or hookimpl.hookwrapper) else "nonwrapper", ) for hookimpl in hc.get_hookimpls() ] @@ -1387,7 +1395,7 @@ class TestRootdir: ) def test_with_ini(self, tmp_path: Path, name: str, contents: str) -> None: inipath = tmp_path / name - inipath.write_text(contents, "utf-8") + inipath.write_text(contents, encoding="utf-8") a = tmp_path / "a" a.mkdir() @@ -1446,7 +1454,7 @@ class TestRootdir: ) -> None: p = tmp_path / name p.touch() - p.write_text(contents, "utf-8") + p.write_text(contents, encoding="utf-8") rootpath, inipath, ini_config = determine_setup(str(p), [str(tmp_path)]) assert rootpath == tmp_path assert inipath == p @@ -1542,7 +1550,8 @@ class TestOverrideIniArgs: custom = 1.0""".format( section=section ) - ) + ), + encoding="utf-8", ) pytester.makeconftest( """ diff --git a/testing/test_conftest.py b/testing/test_conftest.py index c64bd11d4..427831507 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,4 +1,3 @@ -import argparse import os import textwrap from pathlib import Path @@ -7,6 +6,8 @@ from typing import Dict from typing import Generator from typing import List from typing import Optional +from typing import Sequence +from typing import Union import pytest from _pytest.config import ExitCode @@ -24,18 +25,18 @@ def ConftestWithSetinitial(path) -> PytestPluginManager: def conftest_setinitial( - conftest: PytestPluginManager, args, confcutdir: Optional["os.PathLike[str]"] = None + conftest: PytestPluginManager, + args: Sequence[Union[str, Path]], + confcutdir: Optional[Path] = None, ) -> None: - class Namespace: - def __init__(self) -> None: - self.file_or_dir = args - self.confcutdir = os.fspath(confcutdir) if confcutdir is not None else None - self.noconftest = False - self.pyargs = False - self.importmode = "prepend" - - namespace = cast(argparse.Namespace, Namespace()) - conftest._set_initial_conftests(namespace, rootpath=Path(args[0]), testpaths_ini=[]) + conftest._set_initial_conftests( + args=args, + pyargs=False, + noconftest=False, + rootpath=Path(args[0]), + confcutdir=confcutdir, + importmode="prepend", + ) @pytest.mark.usefixtures("_sys_snapshot") @@ -46,8 +47,12 @@ class TestConftestValueAccessGlobal: ) -> Generator[Path, None, None]: 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("a=1 ; Directory = 3") - tmp_path.joinpath("adir/b/conftest.py").write_text("b=2 ; a = 1.5") + tmp_path.joinpath("adir/conftest.py").write_text( + "a=1 ; Directory = 3", encoding="utf-8" + ) + tmp_path.joinpath("adir/b/conftest.py").write_text( + "b=2 ; a = 1.5", encoding="utf-8" + ) if request.param == "inpackage": tmp_path.joinpath("adir/__init__.py").touch() tmp_path.joinpath("adir/b/__init__.py").touch() @@ -122,8 +127,12 @@ class TestConftestValueAccessGlobal: def test_conftest_in_nonpkg_with_init(tmp_path: Path, _sys_snapshot) -> None: tmp_path.joinpath("adir-1.0/b").mkdir(parents=True) - tmp_path.joinpath("adir-1.0/conftest.py").write_text("a=1 ; Directory = 3") - tmp_path.joinpath("adir-1.0/b/conftest.py").write_text("b=2 ; a = 1.5") + tmp_path.joinpath("adir-1.0/conftest.py").write_text( + "a=1 ; Directory = 3", encoding="utf-8" + ) + tmp_path.joinpath("adir-1.0/b/conftest.py").write_text( + "b=2 ; a = 1.5", encoding="utf-8" + ) tmp_path.joinpath("adir-1.0/b/__init__.py").touch() tmp_path.joinpath("adir-1.0/__init__.py").touch() ConftestWithSetinitial(tmp_path.joinpath("adir-1.0", "b")) @@ -166,7 +175,7 @@ def test_conftest_global_import(pytester: Pytester) -> None: sub = Path("sub") sub.mkdir() subconf = sub / "conftest.py" - subconf.write_text("y=4") + subconf.write_text("y=4", encoding="utf-8") mod2 = conf._importconftest(subconf, importmode="prepend", rootpath=Path.cwd()) assert mod != mod2 assert mod2.y == 4 @@ -245,7 +254,8 @@ def test_conftest_confcutdir(pytester: Pytester) -> None: def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("-h", "--confcutdir=%s" % x, x) result.stdout.fnmatch_lines(["*--xyz*"]) @@ -273,9 +283,12 @@ def test_installed_conftest_is_picked_up(pytester: Pytester, tmp_path: Path) -> @pytest.fixture def fix(): return None """ - ) + ), + encoding="utf-8", + ) + tmp_path.joinpath("foo", "test_it.py").write_text( + "def test_it(fix): pass", encoding="utf-8" ) - tmp_path.joinpath("foo", "test_it.py").write_text("def test_it(fix): pass") result = pytester.runpytest("--pyargs", "foo") assert result.ret == 0 @@ -400,7 +413,8 @@ def test_conftest_existing_junitxml(pytester: Pytester) -> None: def pytest_addoption(parser): parser.addoption("--xyz", action="store_true") """ - ) + ), + encoding="utf-8", ) pytester.makefile(ext=".xml", junit="") # Writes junit.xml result = pytester.runpytest("-h", "--junitxml", "junit.xml") @@ -411,7 +425,7 @@ def test_conftest_import_order(pytester: Pytester, monkeypatch: MonkeyPatch) -> ct1 = pytester.makeconftest("") sub = pytester.mkdir("sub") ct2 = sub / "conftest.py" - ct2.write_text("") + ct2.write_text("", encoding="utf-8") def impct(p, importmode, root): return p @@ -449,7 +463,8 @@ def test_fixture_dependency(pytester: Pytester) -> None: def bar(foo): return 'bar' """ - ) + ), + encoding="utf-8", ) subsub = sub.joinpath("subsub") subsub.mkdir() @@ -466,7 +481,8 @@ def test_fixture_dependency(pytester: Pytester) -> None: def test_event_fixture(bar): assert bar == 'sub bar' """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("sub") result.stdout.fnmatch_lines(["*1 passed*"]) @@ -480,10 +496,11 @@ def test_conftest_found_with_double_dash(pytester: Pytester) -> None: def pytest_addoption(parser): parser.addoption("--hello-world", action="store_true") """ - ) + ), + encoding="utf-8", ) p = sub.joinpath("test_hello.py") - p.write_text("def test_hello(): pass") + p.write_text("def test_hello(): pass", encoding="utf-8") result = pytester.runpytest(str(p) + "::test_hello", "-h") result.stdout.fnmatch_lines( """ @@ -507,7 +524,8 @@ class TestConftestVisibility: def fxtr(): return "from-package" """ - ) + ), + encoding="utf-8", ) package.joinpath("test_pkgroot.py").write_text( textwrap.dedent( @@ -515,7 +533,8 @@ class TestConftestVisibility: def test_pkgroot(fxtr): assert fxtr == "from-package" """ - ) + ), + encoding="utf-8", ) swc = package.joinpath("swc") @@ -529,7 +548,8 @@ class TestConftestVisibility: def fxtr(): return "from-swc" """ - ) + ), + encoding="utf-8", ) swc.joinpath("test_with_conftest.py").write_text( textwrap.dedent( @@ -537,7 +557,8 @@ class TestConftestVisibility: def test_with_conftest(fxtr): assert fxtr == "from-swc" """ - ) + ), + encoding="utf-8", ) snc = package.joinpath("snc") @@ -550,7 +571,8 @@ class TestConftestVisibility: assert fxtr == "from-package" # No local conftest.py, so should # use value from parent dir's """ - ) + ), + encoding="utf-8", ) print("created directory structure:") for x in pytester.path.glob("**/"): @@ -616,7 +638,7 @@ def test_search_conftest_up_to_inifile( root = pytester.path src = root.joinpath("src") src.mkdir() - src.joinpath("pytest.ini").write_text("[pytest]") + src.joinpath("pytest.ini").write_text("[pytest]", encoding="utf-8") src.joinpath("conftest.py").write_text( textwrap.dedent( """\ @@ -624,7 +646,8 @@ def test_search_conftest_up_to_inifile( @pytest.fixture def fix1(): pass """ - ) + ), + encoding="utf-8", ) src.joinpath("test_foo.py").write_text( textwrap.dedent( @@ -634,7 +657,8 @@ def test_search_conftest_up_to_inifile( def test_2(out_of_reach): pass """ - ) + ), + encoding="utf-8", ) root.joinpath("conftest.py").write_text( textwrap.dedent( @@ -643,7 +667,8 @@ def test_search_conftest_up_to_inifile( @pytest.fixture def out_of_reach(): pass """ - ) + ), + encoding="utf-8", ) args = [str(src)] @@ -726,7 +751,8 @@ def test_required_option_help(pytester: Pytester) -> None: def pytest_addoption(parser): parser.addoption("--xyz", action="store_true", required=True) """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("-h", x) result.stdout.no_fnmatch_line("*argument --xyz is required*") diff --git a/testing/test_doctest.py b/testing/test_doctest.py index d2944fa2b..f189e8645 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -114,7 +114,7 @@ class TestDoctests: reprec.assertoutcome(failed=1) def test_importmode(self, pytester: Pytester): - p = pytester.makepyfile( + pytester.makepyfile( **{ "namespacepkg/innerpkg/__init__.py": "", "namespacepkg/innerpkg/a.py": """ @@ -132,7 +132,7 @@ class TestDoctests: """, } ) - reprec = pytester.inline_run(p, "--doctest-modules", "--import-mode=importlib") + reprec = pytester.inline_run("--doctest-modules", "--import-mode=importlib") reprec.assertoutcome(passed=1) def test_new_pattern(self, pytester: Pytester): @@ -357,7 +357,8 @@ class TestDoctests: >>> 1/0 ''' """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( @@ -448,7 +449,8 @@ class TestDoctests: """\ import asdalsdkjaslkdjasd """ - ) + ), + encoding="utf-8", ) pytester.maketxtfile( """ @@ -492,7 +494,8 @@ class TestDoctests: 2 ''' """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines( @@ -1566,7 +1569,9 @@ def test_warning_on_unwrap_of_broken_object( def test_is_setup_py_not_named_setup_py(tmp_path: Path) -> None: not_setup_py = tmp_path.joinpath("not_setup.py") - not_setup_py.write_text('from setuptools import setup; setup(name="foo")') + not_setup_py.write_text( + 'from setuptools import setup; setup(name="foo")', encoding="utf-8" + ) assert not _is_setup_py(not_setup_py) diff --git a/testing/test_entry_points.py b/testing/test_entry_points.py index 5d0031273..dfb3d57d2 100644 --- a/testing/test_entry_points.py +++ b/testing/test_entry_points.py @@ -1,7 +1,7 @@ -from _pytest.compat import importlib_metadata +import importlib.metadata def test_pytest_entry_points_are_identical(): - dist = importlib_metadata.distribution("pytest") + dist = importlib.metadata.distribution("pytest") entry_map = {ep.name: ep for ep in dist.entry_points} assert entry_map["pytest"].value == entry_map["py.test"].value diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 90804c619..690830329 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -28,7 +28,7 @@ from _pytest.stash import Stash def schema() -> xmlschema.XMLSchema: """Return an xmlschema.XMLSchema object for the junit-10.xsd file.""" fn = Path(__file__).parent / "example_scripts/junit-10.xsd" - with fn.open() as f: + with fn.open(encoding="utf-8") as f: return xmlschema.XMLSchema(f) @@ -45,7 +45,7 @@ class RunAndParse: xml_path = self.pytester.path.joinpath("junit.xml") result = self.pytester.runpytest("--junitxml=%s" % xml_path, *args) if family == "xunit2": - with xml_path.open() as f: + with xml_path.open(encoding="utf-8") as f: self.schema.validate(f) xmldoc = minidom.parse(str(xml_path)) return result, DomNode(xmldoc) @@ -469,7 +469,7 @@ class TestPython: self, pytester: Pytester, run_and_parse: RunAndParse, xunit_family: str ) -> None: p = pytester.mkdir("sub").joinpath("test_hello.py") - p.write_text("def test_func(): 0/0") + p.write_text("def test_func(): 0/0", encoding="utf-8") result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") @@ -987,7 +987,7 @@ class TestNonPython: return "custom item runtest failed" """ ) - pytester.path.joinpath("myfile.xyz").write_text("hello") + pytester.path.joinpath("myfile.xyz").write_text("hello", encoding="utf-8") result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") @@ -1013,7 +1013,7 @@ def test_nullbyte(pytester: Pytester, junit_logging: str) -> None: ) xmlf = pytester.path.joinpath("junit.xml") pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) - text = xmlf.read_text() + text = xmlf.read_text(encoding="utf-8") assert "\x00" not in text if junit_logging == "system-out": assert "#x00" in text @@ -1035,7 +1035,7 @@ def test_nullbyte_replace(pytester: Pytester, junit_logging: str) -> None: ) xmlf = pytester.path.joinpath("junit.xml") pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) - text = xmlf.read_text() + text = xmlf.read_text(encoding="utf-8") if junit_logging == "system-out": assert "#x0" in text if junit_logging == "no": diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index fbfd88b73..b32036d14 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -90,6 +90,7 @@ def test_cache_makedir(cache: pytest.Cache) -> None: def test_fixturerequest_getmodulepath(pytester: pytest.Pytester) -> None: modcol = pytester.getmodulecol("def test_somefunc(): pass") (item,) = pytester.genitems([modcol]) + assert isinstance(item, pytest.Function) req = pytest.FixtureRequest(item, _ispytest=True) assert req.path == modcol.path assert req.fspath == modcol.fspath # type: ignore[attr-defined] diff --git a/testing/test_link_resolve.py b/testing/test_link_resolve.py index 60a86ada3..1ac3afd09 100644 --- a/testing/test_link_resolve.py +++ b/testing/test_link_resolve.py @@ -59,7 +59,8 @@ def test_link_resolve(pytester: Pytester) -> None: def test_foo(): raise AssertionError() """ - ) + ), + encoding="utf-8", ) subst = subst_path_linux diff --git a/testing/test_mark.py b/testing/test_mark.py index e2d1a40c3..7415b393e 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -806,12 +806,12 @@ class TestKeywordSelection: pytester.makepyfile( conftest=""" import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_pycollect_makeitem(name): - outcome = yield + item = yield if name == "TestClass": - item = outcome.get_result() item.extra_keyword_matches.add("xxx") + return item """ ) reprec = pytester.inline_run(p.parent, "-s", "-k", keyword) @@ -1130,6 +1130,41 @@ def test_mark_mro() -> None: all_marks = get_unpacked_marks(C) - assert all_marks == [xfail("c").mark, xfail("a").mark, xfail("b").mark] + assert all_marks == [xfail("b").mark, xfail("a").mark, xfail("c").mark] assert get_unpacked_marks(C, consider_mro=False) == [xfail("c").mark] + + +# @pytest.mark.issue("https://github.com/pytest-dev/pytest/issues/10447") +def test_mark_fixture_order_mro(pytester: Pytester): + """This ensures we walk marks of the mro starting with the base classes + the action at a distance fixtures are taken as minimal example from a real project + + """ + foo = pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def add_attr1(request): + request.instance.attr1 = object() + + + @pytest.fixture + def add_attr2(request): + request.instance.attr2 = request.instance.attr1 + + + @pytest.mark.usefixtures('add_attr1') + class Parent: + pass + + + @pytest.mark.usefixtures('add_attr2') + class TestThings(Parent): + def test_attrs(self): + assert self.attr1 == self.attr2 + """ + ) + result = pytester.runpytest(foo) + result.assert_outcomes(passed=1) diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 500e24453..8175b5f0f 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -324,7 +324,8 @@ def test_importerror(pytester: Pytester) -> None: x = 1 """ - ) + ), + encoding="utf-8", ) pytester.path.joinpath("test_importerror.py").write_text( textwrap.dedent( @@ -332,7 +333,8 @@ def test_importerror(pytester: Pytester) -> None: def test_importerror(monkeypatch): monkeypatch.setattr('package.a.x', 2) """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest() result.stdout.fnmatch_lines( @@ -434,11 +436,13 @@ def test_syspath_prepend_with_namespace_packages( ns = d.joinpath("ns_pkg") ns.mkdir() ns.joinpath("__init__.py").write_text( - "__import__('pkg_resources').declare_namespace(__name__)" + "__import__('pkg_resources').declare_namespace(__name__)", encoding="utf-8" ) lib = ns.joinpath(dirname) lib.mkdir() - lib.joinpath("__init__.py").write_text("def check(): return %r" % dirname) + lib.joinpath("__init__.py").write_text( + "def check(): return %r" % dirname, encoding="utf-8" + ) monkeypatch.syspath_prepend("hello") import ns_pkg.hello @@ -457,5 +461,5 @@ def test_syspath_prepend_with_namespace_packages( # Should invalidate caches via importlib.invalidate_caches. modules_tmpdir = pytester.mkdir("modules_tmpdir") monkeypatch.syspath_prepend(str(modules_tmpdir)) - modules_tmpdir.joinpath("main_app.py").write_text("app = True") + modules_tmpdir.joinpath("main_app.py").write_text("app = True", encoding="utf-8") from main_app import app # noqa: F401 diff --git a/testing/test_nose.py b/testing/test_nose.py index e838e79dd..7ec4026f2 100644 --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -23,7 +23,9 @@ def test_nose_setup(pytester: Pytester) -> None: test_hello.teardown = lambda: values.append(2) """ ) - result = pytester.runpytest(p, "-p", "nose") + result = pytester.runpytest( + p, "-p", "nose", "-Wignore::pytest.PytestRemovedIn8Warning" + ) result.assert_outcomes(passed=2) @@ -76,7 +78,9 @@ def test_nose_setup_func(pytester: Pytester) -> None: """ ) - result = pytester.runpytest(p, "-p", "nose") + result = pytester.runpytest( + p, "-p", "nose", "-Wignore::pytest.PytestRemovedIn8Warning" + ) result.assert_outcomes(passed=2) @@ -100,7 +104,9 @@ def test_nose_setup_func_failure(pytester: Pytester) -> None: """ ) - result = pytester.runpytest(p, "-p", "nose") + result = pytester.runpytest( + p, "-p", "nose", "-Wignore::pytest.PytestRemovedIn8Warning" + ) result.stdout.fnmatch_lines(["*TypeError: ()*"]) @@ -154,7 +160,9 @@ def test_nose_setup_partial(pytester: Pytester) -> None: test_hello.teardown = my_teardown_partial """ ) - result = pytester.runpytest(p, "-p", "nose") + result = pytester.runpytest( + p, "-p", "nose", "-Wignore::pytest.PytestRemovedIn8Warning" + ) result.stdout.fnmatch_lines(["*2 passed*"]) @@ -193,7 +201,9 @@ def test_module_level_setup(pytester: Pytester) -> None: assert items["setup2"] == ["up", "down", "up"] """ ) - result = pytester.runpytest("-p", "nose") + result = pytester.runpytest( + "-p", "nose", "-Wignore::pytest.PytestRemovedIn8Warning" + ) result.stdout.fnmatch_lines(["*4 passed*"]) @@ -278,7 +288,7 @@ def test_nose_setup_ordering(pytester: Pytester) -> None: assert self.visited_cls """ ) - result = pytester.runpytest() + result = pytester.runpytest("-Wignore::pytest.PytestRemovedIn8Warning") result.stdout.fnmatch_lines(["*1 passed*"]) @@ -494,7 +504,7 @@ def test_nose_setup_skipped_if_non_callable(pytester: Pytester) -> None: pass """, ) - result = pytester.runpytest(p, "-p", "nose") + result = pytester.runpytest(p.parent, "-p", "nose") assert result.ret == 0 diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 992f49bc5..1899abe15 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -1,4 +1,5 @@ import argparse +import locale import os import shlex import subprocess @@ -289,6 +290,10 @@ class TestParser: def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + try: + encoding = locale.getencoding() # New in Python 3.11, ignores utf-8 mode + except AttributeError: + encoding = locale.getpreferredencoding(False) try: bash_version = subprocess.run( ["bash", "--version"], @@ -296,6 +301,7 @@ def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: stderr=subprocess.DEVNULL, check=True, text=True, + encoding=encoding, ).stdout except (OSError, subprocess.CalledProcessError): pytest.skip("bash is not available") @@ -305,7 +311,7 @@ def test_argcomplete(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: script = str(pytester.path.joinpath("test_argcomplete")) - with open(str(script), "w") as fp: + with open(str(script), "w", encoding="utf-8") as fp: # redirect output from argcomplete to stdin and stderr is not trivial # http://stackoverflow.com/q/12589419/1307905 # so we use bash diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 481d7a606..3d574e856 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -7,6 +7,7 @@ from textwrap import dedent from types import ModuleType from typing import Any from typing import Generator +from typing import Iterator import pytest from _pytest.monkeypatch import MonkeyPatch @@ -100,13 +101,13 @@ class TestImportPath: def setuptestfs(self, path: Path) -> None: # print "setting up test fs for", repr(path) samplefile = path / "samplefile" - samplefile.write_text("samplefile\n") + samplefile.write_text("samplefile\n", encoding="utf-8") execfile = path / "execfile" - execfile.write_text("x=42") + execfile.write_text("x=42", encoding="utf-8") execfilepy = path / "execfile.py" - execfilepy.write_text("x=42") + execfilepy.write_text("x=42", encoding="utf-8") d = {1: 2, "hello": "world", "answer": 42} path.joinpath("samplepickle").write_bytes(pickle.dumps(d, 1)) @@ -120,9 +121,9 @@ class TestImportPath: otherdir.joinpath("__init__.py").touch() module_a = otherdir / "a.py" - module_a.write_text("from .b import stuff as result\n") + module_a.write_text("from .b import stuff as result\n", encoding="utf-8") module_b = otherdir / "b.py" - module_b.write_text('stuff="got it"\n') + module_b.write_text('stuff="got it"\n', encoding="utf-8") module_c = otherdir / "c.py" module_c.write_text( dedent( @@ -131,7 +132,8 @@ class TestImportPath: import otherdir.a value = otherdir.a.result """ - ) + ), + encoding="utf-8", ) module_d = otherdir / "d.py" module_d.write_text( @@ -141,7 +143,8 @@ class TestImportPath: from otherdir import a value2 = a.result """ - ) + ), + encoding="utf-8", ) def test_smoke_test(self, path1: Path) -> None: @@ -280,29 +283,36 @@ class TestImportPath: import_path(tmp_path / "invalid.py", root=tmp_path) @pytest.fixture - def simple_module(self, tmp_path: Path) -> Path: - fn = tmp_path / "_src/tests/mymod.py" + def simple_module( + self, tmp_path: Path, request: pytest.FixtureRequest + ) -> Iterator[Path]: + name = f"mymod_{request.node.name}" + fn = tmp_path / f"_src/tests/{name}.py" fn.parent.mkdir(parents=True) - fn.write_text("def foo(x): return 40 + x") - return fn + fn.write_text("def foo(x): return 40 + x", encoding="utf-8") + module_name = module_name_from_path(fn, root=tmp_path) + yield fn + sys.modules.pop(module_name, None) - def test_importmode_importlib(self, simple_module: Path, tmp_path: Path) -> None: + def test_importmode_importlib( + self, simple_module: Path, tmp_path: Path, request: pytest.FixtureRequest + ) -> None: """`importlib` mode does not change sys.path.""" module = import_path(simple_module, mode="importlib", root=tmp_path) assert module.foo(2) == 42 # type: ignore[attr-defined] assert str(simple_module.parent) not in sys.path assert module.__name__ in sys.modules - assert module.__name__ == "_src.tests.mymod" + assert module.__name__ == f"_src.tests.mymod_{request.node.name}" assert "_src" in sys.modules assert "_src.tests" in sys.modules - def test_importmode_twice_is_different_module( + def test_remembers_previous_imports( self, simple_module: Path, tmp_path: Path ) -> None: - """`importlib` mode always returns a new module.""" + """`importlib` mode called remembers previous module (#10341, #10811).""" module1 = import_path(simple_module, mode="importlib", root=tmp_path) module2 = import_path(simple_module, mode="importlib", root=tmp_path) - assert module1 is not module2 + assert module1 is module2 def test_no_meta_path_found( self, simple_module: Path, monkeypatch: MonkeyPatch, tmp_path: Path @@ -315,6 +325,9 @@ class TestImportPath: # mode='importlib' fails if no spec is found to load the module import importlib.util + # Force module to be re-imported. + del sys.modules[module.__name__] + monkeypatch.setattr( importlib.util, "spec_from_file_location", lambda *args: None ) @@ -447,7 +460,7 @@ def test_samefile_false_negatives(tmp_path: Path, monkeypatch: MonkeyPatch) -> N return False, even when they are clearly equal. """ module_path = tmp_path.joinpath("my_module.py") - module_path.write_text("def foo(): return 42") + module_path.write_text("def foo(): return 42", encoding="utf-8") monkeypatch.syspath_prepend(tmp_path) with monkeypatch.context() as mp: @@ -473,7 +486,8 @@ class TestImportLibMode: class Data: value: str """ - ) + ), + encoding="utf-8", ) module = import_path(fn, mode="importlib", root=tmp_path) @@ -498,7 +512,8 @@ class TestImportLibMode: s = pickle.dumps(_action) return pickle.loads(s) """ - ) + ), + encoding="utf-8", ) module = import_path(fn, mode="importlib", root=tmp_path) @@ -525,7 +540,8 @@ class TestImportLibMode: class Data: x: int = 42 """ - ) + ), + encoding="utf-8", ) fn2 = tmp_path.joinpath("_src/m2/tests/test.py") @@ -540,7 +556,8 @@ class TestImportLibMode: class Data: x: str = "" """ - ) + ), + encoding="utf-8", ) import pickle @@ -586,3 +603,15 @@ class TestImportLibMode: modules = {} insert_missing_modules(modules, "") assert modules == {} + + def test_parent_contains_child_module_attribute( + self, monkeypatch: MonkeyPatch, tmp_path: Path + ): + monkeypatch.chdir(tmp_path) + # Use 'xxx' and 'xxy' as parent names as they are unlikely to exist and + # don't end up being imported. + modules = {"xxx.tests.foo": ModuleType("xxx.tests.foo")} + insert_missing_modules(modules, "xxx.tests.foo") + assert sorted(modules) == ["xxx", "xxx.tests", "xxx.tests.foo"] + assert modules["xxx"].tests is modules["xxx.tests"] + assert modules["xxx.tests"].foo is modules["xxx.tests.foo"] diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 9fe23d177..c6f518b1d 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -347,7 +347,7 @@ class TestPytestPluginManager: pytest.raises(ImportError, pytestpm.import_plugin, "pytest_qweqwex.y") pytester.syspathinsert() - pytester.mkpydir("pkg").joinpath("plug.py").write_text("x=3") + pytester.mkpydir("pkg").joinpath("plug.py").write_text("x=3", encoding="utf-8") pluginname = "pkg.plug" pytestpm.import_plugin(pluginname) mod = pytestpm.get_plugin("pkg.plug") diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 62dad9858..8f8b4d291 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -222,7 +222,7 @@ class TestInlineRunModulesCleanup: result = pytester.inline_run(str(test_mod)) assert result.ret == ExitCode.OK # rewrite module, now test should fail if module was re-imported - test_mod.write_text("def test_foo(): assert False") + test_mod.write_text("def test_foo(): assert False", encoding="utf-8") result2 = pytester.inline_run(str(test_mod)) assert result2.ret == ExitCode.TESTS_FAILED diff --git a/testing/test_python_path.py b/testing/test_python_path.py index e1628feb1..dfef0f3fe 100644 --- a/testing/test_python_path.py +++ b/testing/test_python_path.py @@ -85,8 +85,8 @@ def test_clean_up(pytester: Pytester) -> None: # This is tough to test behaviorally because the cleanup really runs last. # So the test make several implementation assumptions: # - Cleanup is done in pytest_unconfigure(). - # - Not a hookwrapper. - # So we can add a hookwrapper ourselves to test what it does. + # - Not a hook wrapper. + # So we can add a hook wrapper ourselves to test what it does. pytester.makefile(".ini", pytest="[pytest]\npythonpath=I_SHALL_BE_REMOVED\n") pytester.makepyfile(test_foo="""def test_foo(): pass""") @@ -94,12 +94,14 @@ def test_clean_up(pytester: Pytester) -> None: after: Optional[List[str]] = None class Plugin: - @pytest.hookimpl(hookwrapper=True, tryfirst=True) + @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_unconfigure(self) -> Generator[None, None, None]: nonlocal before, after before = sys.path.copy() - yield - after = sys.path.copy() + try: + return (yield) + finally: + after = sys.path.copy() result = pytester.runpytest_inprocess(plugins=[Plugin()]) assert result.ret == 0 diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 7e0f836a6..8b70c8aff 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -1,5 +1,7 @@ import warnings +from typing import List from typing import Optional +from typing import Type import pytest from _pytest.pytester import Pytester @@ -37,6 +39,47 @@ def test_recwarn_captures_deprecation_warning(recwarn: WarningsRecorder) -> None assert recwarn.pop(DeprecationWarning) +class TestSubclassWarningPop: + class ParentWarning(Warning): + pass + + class ChildWarning(ParentWarning): + pass + + class ChildOfChildWarning(ChildWarning): + pass + + @staticmethod + def raise_warnings_from_list(_warnings: List[Type[Warning]]): + for warn in _warnings: + warnings.warn(f"Warning {warn().__repr__()}", warn) + + def test_pop_finds_exact_match(self): + with pytest.warns((self.ParentWarning, self.ChildWarning)) as record: + self.raise_warnings_from_list( + [self.ChildWarning, self.ParentWarning, self.ChildOfChildWarning] + ) + + assert len(record) == 3 + _warn = record.pop(self.ParentWarning) + assert _warn.category is self.ParentWarning + + def test_pop_raises_if_no_match(self): + with pytest.raises(AssertionError): + with pytest.warns(self.ParentWarning) as record: + self.raise_warnings_from_list([self.ParentWarning]) + record.pop(self.ChildOfChildWarning) + + def test_pop_finds_best_inexact_match(self): + with pytest.warns(self.ParentWarning) as record: + self.raise_warnings_from_list( + [self.ChildOfChildWarning, self.ChildWarning, self.ChildOfChildWarning] + ) + + _warn = record.pop(self.ParentWarning) + assert _warn.category is self.ChildWarning + + class TestWarningsRecorderChecker: def test_recording(self) -> None: rec = WarningsRecorder(_ispytest=True) @@ -172,22 +215,6 @@ class TestDeprecatedCall: with pytest.deprecated_call(): assert f() == 10 - @pytest.mark.parametrize("mode", ["context_manager", "call"]) - def test_deprecated_call_exception_is_raised(self, mode) -> None: - """If the block of the code being tested by deprecated_call() raises an exception, - it must raise the exception undisturbed. - """ - - def f(): - raise ValueError("some exception") - - with pytest.raises(ValueError, match="some exception"): - if mode == "call": - pytest.deprecated_call(f) - else: - with pytest.deprecated_call(): - f() - def test_deprecated_call_specificity(self) -> None: other_warnings = [ Warning, @@ -203,19 +230,21 @@ class TestDeprecatedCall: def f(): warnings.warn(warning("hi")) - with pytest.raises(pytest.fail.Exception): - pytest.deprecated_call(f) - with pytest.raises(pytest.fail.Exception): - with pytest.deprecated_call(): - f() + with pytest.warns(warning): + with pytest.raises(pytest.fail.Exception): + pytest.deprecated_call(f) + with pytest.raises(pytest.fail.Exception): + with pytest.deprecated_call(): + f() def test_deprecated_call_supports_match(self) -> None: with pytest.deprecated_call(match=r"must be \d+$"): warnings.warn("value must be 42", DeprecationWarning) - with pytest.raises(pytest.fail.Exception): - with pytest.deprecated_call(match=r"must be \d+$"): - warnings.warn("this is not here", DeprecationWarning) + with pytest.deprecated_call(): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): + with pytest.deprecated_call(match=r"must be \d+$"): + warnings.warn("this is not here", DeprecationWarning) class TestWarns: @@ -227,8 +256,9 @@ class TestWarns: def test_several_messages(self) -> None: # different messages, b/c Python suppresses multiple identical warnings pytest.warns(RuntimeWarning, lambda: warnings.warn("w1", RuntimeWarning)) - with pytest.raises(pytest.fail.Exception): - pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning)) + with pytest.warns(RuntimeWarning): + with pytest.raises(pytest.fail.Exception): + pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning)) pytest.warns(RuntimeWarning, lambda: warnings.warn("w3", RuntimeWarning)) def test_function(self) -> None: @@ -243,13 +273,14 @@ class TestWarns: pytest.warns( (RuntimeWarning, SyntaxWarning), lambda: warnings.warn("w2", SyntaxWarning) ) - pytest.raises( - pytest.fail.Exception, - lambda: pytest.warns( - (RuntimeWarning, SyntaxWarning), - lambda: warnings.warn("w3", UserWarning), - ), - ) + with pytest.warns(): + pytest.raises( + pytest.fail.Exception, + lambda: pytest.warns( + (RuntimeWarning, SyntaxWarning), + lambda: warnings.warn("w3", UserWarning), + ), + ) def test_as_contextmanager(self) -> None: with pytest.warns(RuntimeWarning): @@ -258,20 +289,22 @@ class TestWarns: with pytest.warns(UserWarning): warnings.warn("user", UserWarning) - with pytest.raises(pytest.fail.Exception) as excinfo: - with pytest.warns(RuntimeWarning): - warnings.warn("user", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception) as excinfo: + with pytest.warns(RuntimeWarning): + warnings.warn("user", UserWarning) excinfo.match( r"DID NOT WARN. No warnings of type \(.+RuntimeWarning.+,\) were emitted.\n" - r"The list of emitted warnings is: \[UserWarning\('user',?\)\]." + r" Emitted warnings: \[UserWarning\('user',?\)\]." ) - with pytest.raises(pytest.fail.Exception) as excinfo: - with pytest.warns(UserWarning): - warnings.warn("runtime", RuntimeWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception) as excinfo: + with pytest.warns(UserWarning): + warnings.warn("runtime", RuntimeWarning) excinfo.match( r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n" - r"The list of emitted warnings is: \[RuntimeWarning\('runtime',?\)]." + r" Emitted warnings: \[RuntimeWarning\('runtime',?\)]." ) with pytest.raises(pytest.fail.Exception) as excinfo: @@ -279,19 +312,20 @@ class TestWarns: pass excinfo.match( r"DID NOT WARN. No warnings of type \(.+UserWarning.+,\) were emitted.\n" - r"The list of emitted warnings is: \[\]." + r" Emitted warnings: \[\]." ) warning_classes = (UserWarning, FutureWarning) - with pytest.raises(pytest.fail.Exception) as excinfo: - with pytest.warns(warning_classes) as warninfo: - warnings.warn("runtime", RuntimeWarning) - warnings.warn("import", ImportWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception) as excinfo: + with pytest.warns(warning_classes) as warninfo: + warnings.warn("runtime", RuntimeWarning) + warnings.warn("import", ImportWarning) messages = [each.message for each in warninfo] expected_str = ( f"DID NOT WARN. No warnings of type {warning_classes} were emitted.\n" - f"The list of emitted warnings is: {messages}." + f" Emitted warnings: {messages}." ) assert str(excinfo.value) == expected_str @@ -367,25 +401,31 @@ class TestWarns: with pytest.warns(UserWarning, match=r"must be \d+$"): warnings.warn("value must be 42", UserWarning) - with pytest.raises(pytest.fail.Exception): - with pytest.warns(UserWarning, match=r"must be \d+$"): - warnings.warn("this is not here", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception): + with pytest.warns(UserWarning, match=r"must be \d+$"): + warnings.warn("this is not here", UserWarning) - with pytest.raises(pytest.fail.Exception): - with pytest.warns(FutureWarning, match=r"must be \d+$"): - warnings.warn("value must be 42", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception): + with pytest.warns(FutureWarning, match=r"must be \d+$"): + warnings.warn("value must be 42", UserWarning) def test_one_from_multiple_warns(self) -> None: - with pytest.warns(UserWarning, match=r"aaa"): - warnings.warn("cccccccccc", UserWarning) - warnings.warn("bbbbbbbbbb", UserWarning) - warnings.warn("aaaaaaaaaa", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): + with pytest.warns(UserWarning, match=r"aaa"): + with pytest.warns(UserWarning, match=r"aaa"): + warnings.warn("cccccccccc", UserWarning) + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("aaaaaaaaaa", UserWarning) def test_none_of_multiple_warns(self) -> None: - with pytest.raises(pytest.fail.Exception): - with pytest.warns(UserWarning, match=r"aaa"): - warnings.warn("bbbbbbbbbb", UserWarning) - warnings.warn("cccccccccc", UserWarning) + with pytest.warns(): + with pytest.raises(pytest.fail.Exception, match="DID NOT WARN"): + with pytest.warns(UserWarning, match=r"aaa"): + warnings.warn("bbbbbbbbbb", UserWarning) + warnings.warn("cccccccccc", UserWarning) @pytest.mark.filterwarnings("ignore") def test_can_capture_previously_warned(self) -> None: @@ -403,3 +443,45 @@ class TestWarns: with pytest.warns(UserWarning, foo="bar"): # type: ignore pass assert "Unexpected keyword arguments" in str(excinfo.value) + + def test_re_emit_single(self) -> None: + with pytest.warns(DeprecationWarning): + with pytest.warns(UserWarning): + warnings.warn("user warning", UserWarning) + warnings.warn("some deprecation warning", DeprecationWarning) + + def test_re_emit_multiple(self) -> None: + with pytest.warns(UserWarning): + warnings.warn("first warning", UserWarning) + warnings.warn("second warning", UserWarning) + + def test_re_emit_match_single(self) -> None: + with pytest.warns(DeprecationWarning): + with pytest.warns(UserWarning, match="user warning"): + warnings.warn("user warning", UserWarning) + warnings.warn("some deprecation warning", DeprecationWarning) + + def test_re_emit_match_multiple(self) -> None: + with warnings.catch_warnings(): + warnings.simplefilter("error") # if anything is re-emitted + with pytest.warns(UserWarning, match="user warning"): + warnings.warn("first user warning", UserWarning) + warnings.warn("second user warning", UserWarning) + + def test_re_emit_non_match_single(self) -> None: + with pytest.warns(UserWarning, match="v2 warning"): + with pytest.warns(UserWarning, match="v1 warning"): + warnings.warn("v1 warning", UserWarning) + warnings.warn("non-matching v2 warning", UserWarning) + + def test_catch_warning_within_raise(self) -> None: + # warns-in-raises works since https://github.com/pytest-dev/pytest/pull/11129 + with pytest.raises(ValueError, match="some exception"): + with pytest.warns(FutureWarning, match="some warning"): + warnings.warn("some warning", category=FutureWarning) + raise ValueError("some exception") + # and raises-in-warns has always worked but we'll check for symmetry. + with pytest.warns(FutureWarning, match="some warning"): + with pytest.raises(ValueError, match="some exception"): + warnings.warn("some warning", category=FutureWarning) + raise ValueError("some exception") diff --git a/testing/test_reports.py b/testing/test_reports.py index e101b51da..387d2e807 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -410,7 +410,7 @@ class TestReportSerialization: ) -> None: sub_dir = pytester.path.joinpath("ns") sub_dir.mkdir() - sub_dir.joinpath("conftest.py").write_text("import unknown") + sub_dir.joinpath("conftest.py").write_text("import unknown", encoding="utf-8") result = pytester.runpytest_subprocess(".") result.stdout.fnmatch_lines(["E *Error: No module named 'unknown'"]) diff --git a/testing/test_runner.py b/testing/test_runner.py index de3e18184..cab631ee1 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -542,10 +542,10 @@ def test_runtest_in_module_ordering(pytester: Pytester) -> None: @pytest.fixture def mylist(self, request): return request.function.mylist - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtest_call(self, item): try: - (yield).get_result() + yield except ValueError: pass def test_hello1(self, mylist): @@ -826,12 +826,12 @@ def test_unicode_in_longrepr(pytester: Pytester) -> None: pytester.makeconftest( """\ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtest_makereport(): - outcome = yield - rep = outcome.get_result() + rep = yield if rep.when == "call": rep.longrepr = 'ä' + return rep """ ) pytester.makepyfile( diff --git a/testing/test_session.py b/testing/test_session.py index f73dc89ef..48dc08e8c 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -265,9 +265,9 @@ def test_plugin_already_exists(pytester: Pytester) -> None: def test_exclude(pytester: Pytester) -> None: hellodir = pytester.mkdir("hello") - hellodir.joinpath("test_hello.py").write_text("x y syntaxerror") + hellodir.joinpath("test_hello.py").write_text("x y syntaxerror", encoding="utf-8") hello2dir = pytester.mkdir("hello2") - hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror") + hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror", encoding="utf-8") pytester.makepyfile(test_ok="def test_pass(): pass") result = pytester.runpytest("--ignore=hello", "--ignore=hello2") assert result.ret == 0 @@ -276,13 +276,13 @@ def test_exclude(pytester: Pytester) -> None: def test_exclude_glob(pytester: Pytester) -> None: hellodir = pytester.mkdir("hello") - hellodir.joinpath("test_hello.py").write_text("x y syntaxerror") + hellodir.joinpath("test_hello.py").write_text("x y syntaxerror", encoding="utf-8") hello2dir = pytester.mkdir("hello2") - hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror") + hello2dir.joinpath("test_hello2.py").write_text("x y syntaxerror", encoding="utf-8") hello3dir = pytester.mkdir("hallo3") - hello3dir.joinpath("test_hello3.py").write_text("x y syntaxerror") + hello3dir.joinpath("test_hello3.py").write_text("x y syntaxerror", encoding="utf-8") subdir = pytester.mkdir("sub") - subdir.joinpath("test_hello4.py").write_text("x y syntaxerror") + subdir.joinpath("test_hello4.py").write_text("x y syntaxerror", encoding="utf-8") pytester.makepyfile(test_ok="def test_pass(): pass") result = pytester.runpytest("--ignore-glob=*h[ea]llo*") assert result.ret == 0 diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 892ed8547..b7e448df3 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -195,7 +195,8 @@ class TestEvaluation: def pytest_markeval_namespace(): return {"arg": "root"} """ - ) + ), + encoding="utf-8", ) root.joinpath("test_root.py").write_text( textwrap.dedent( @@ -206,7 +207,8 @@ class TestEvaluation: def test_root(): assert False """ - ) + ), + encoding="utf-8", ) foo = root.joinpath("foo") foo.mkdir() @@ -219,7 +221,8 @@ class TestEvaluation: def pytest_markeval_namespace(): return {"arg": "foo"} """ - ) + ), + encoding="utf-8", ) foo.joinpath("test_foo.py").write_text( textwrap.dedent( @@ -230,7 +233,8 @@ class TestEvaluation: def test_foo(): assert False """ - ) + ), + encoding="utf-8", ) bar = root.joinpath("bar") bar.mkdir() @@ -243,7 +247,8 @@ class TestEvaluation: def pytest_markeval_namespace(): return {"arg": "bar"} """ - ) + ), + encoding="utf-8", ) bar.joinpath("test_bar.py").write_text( textwrap.dedent( @@ -254,7 +259,8 @@ class TestEvaluation: def test_bar(): assert False """ - ) + ), + encoding="utf-8", ) reprec = pytester.inline_run("-vs", "--capture=no") @@ -629,7 +635,8 @@ class TestXFail: @pytest.mark.xfail(reason='unsupported feature', strict=%s) def test_foo(): - with open('foo_executed', 'w'): pass # make sure test executes + with open('foo_executed', 'w', encoding='utf-8'): + pass # make sure test executes """ % strict ) @@ -982,33 +989,34 @@ def test_skipped_reasons_functional(pytester: Pytester) -> None: pytester.makepyfile( test_one=""" import pytest - from conftest import doskip + from helpers import doskip - def setup_function(func): - doskip() + def setup_function(func): # LINE 4 + doskip("setup function") def test_func(): pass - class TestClass(object): + class TestClass: def test_method(self): - doskip() + doskip("test method") - @pytest.mark.skip("via_decorator") + @pytest.mark.skip("via_decorator") # LINE 14 def test_deco(self): assert 0 """, - conftest=""" + helpers=""" import pytest, sys - def doskip(): + def doskip(reason): assert sys._getframe().f_lineno == 3 - pytest.skip('test') + pytest.skip(reason) # LINE 4 """, ) result = pytester.runpytest("-rs") result.stdout.fnmatch_lines_random( [ - "SKIPPED [[]2[]] conftest.py:4: test", + "SKIPPED [[]1[]] test_one.py:7: setup function", + "SKIPPED [[]1[]] helpers.py:4: test method", "SKIPPED [[]1[]] test_one.py:14: via_decorator", ] ) @@ -1135,12 +1143,10 @@ def test_errors_in_xfail_skip_expressions(pytester: Pytester) -> None: """ ) result = pytester.runpytest() - markline = " ^" + markline = " ^" pypy_version_info = getattr(sys, "pypy_version_info", None) if pypy_version_info is not None and pypy_version_info < (6,): - markline = markline[5:] - elif sys.version_info >= (3, 8) or hasattr(sys, "pypy_version_info"): - markline = markline[4:] + markline = markline[1:] if sys.version_info[:2] >= (3, 10): expected = [ diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 2094abc4e..85e38c7d5 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -352,6 +352,6 @@ def test_one(): assert result.ret == 0 assert Path(stepwise_cache_file).exists() - with stepwise_cache_file.open() as file_handle: + with stepwise_cache_file.open(encoding="utf-8") as file_handle: observed_value = file_handle.readlines() assert [expected_value] == observed_value diff --git a/testing/test_terminal.py b/testing/test_terminal.py index c0acb6006..596c3c67e 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -244,7 +244,8 @@ class TestTerminal: def test_method(self): pass """ - ) + ), + encoding="utf-8", ) result = pytester.runpytest("-vv") assert result.ret == 0 @@ -724,12 +725,12 @@ class TestTerminalFunctional: ) assert result.ret == 0 - def test_deselected_with_hookwrapper(self, pytester: Pytester) -> None: + def test_deselected_with_hook_wrapper(self, pytester: Pytester) -> None: pytester.makeconftest( """ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_collection_modifyitems(config, items): yield deselected = items.pop() @@ -1567,7 +1568,8 @@ class TestGenericReporting: """ def pytest_report_header(config, start_path): return ["line1", str(start_path)] -""" +""", + encoding="utf-8", ) result = pytester.runpytest("a") result.stdout.fnmatch_lines(["*hello: 42*", "line1", str(pytester.path)]) @@ -1671,7 +1673,7 @@ def test_fdopen_kept_alive_issue124(pytester: Pytester) -> None: import os, sys k = [] def test_open_file_and_keep_alive(capfd): - stdout = os.fdopen(1, 'w', 1) + stdout = os.fdopen(1, 'w', buffering=1, encoding='utf-8') k.append(stdout) def test_close_kept_alive_file(): diff --git a/testing/test_threadexception.py b/testing/test_threadexception.py index 5b7519f27..fd9a091cc 100644 --- a/testing/test_threadexception.py +++ b/testing/test_threadexception.py @@ -1,13 +1,7 @@ -import sys - import pytest from _pytest.pytester import Pytester -if sys.version_info < (3, 8): - pytest.skip("threadexception plugin needs Python>=3.8", allow_module_level=True) - - @pytest.mark.filterwarnings("default::pytest.PytestUnhandledThreadExceptionWarning") def test_unhandled_thread_exception(pytester: Pytester) -> None: pytester.makepyfile( diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 110a68b27..1e1446af1 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -561,7 +561,7 @@ def test_basetemp_with_read_only_files(pytester: Pytester) -> None: def test(tmp_path): fn = tmp_path / 'foo.txt' - fn.write_text('hello') + fn.write_text('hello', encoding='utf-8') mode = os.stat(str(fn)).st_mode os.chmod(str(fn), mode & ~stat.S_IREAD) """ diff --git a/testing/test_unittest.py b/testing/test_unittest.py index d917d331a..24f954051 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -952,7 +952,7 @@ def test_issue333_result_clearing(pytester: Pytester) -> None: pytester.makeconftest( """ import pytest - @pytest.hookimpl(hookwrapper=True) + @pytest.hookimpl(wrapper=True) def pytest_runtest_call(item): yield assert 0 @@ -1354,9 +1354,6 @@ def test_plain_unittest_does_not_support_async(pytester: Pytester) -> None: result.stdout.fnmatch_lines(expected_lines) -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_success(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ @@ -1382,9 +1379,6 @@ def test_do_class_cleanups_on_success(pytester: Pytester) -> None: assert passed == 3 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_setupclass_failure(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ @@ -1409,9 +1403,6 @@ def test_do_class_cleanups_on_setupclass_failure(pytester: Pytester) -> None: assert passed == 1 -@pytest.mark.skipif( - sys.version_info < (3, 8), reason="Feature introduced in Python 3.8" -) def test_do_class_cleanups_on_teardownclass_failure(pytester: Pytester) -> None: testpath = pytester.makepyfile( """ diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index f625833dc..d255adb2b 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -3,11 +3,10 @@ import sys import pytest from _pytest.pytester import Pytester - -if sys.version_info < (3, 8): - pytest.skip("unraisableexception plugin needs Python>=3.8", allow_module_level=True) +PYPY = hasattr(sys, "pypy_version_info") +@pytest.mark.skipif(PYPY, reason="garbage-collection differences make this flaky") @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable(pytester: Pytester) -> None: pytester.makepyfile( @@ -40,6 +39,7 @@ def test_unraisable(pytester: Pytester) -> None: ) +@pytest.mark.skipif(PYPY, reason="garbage-collection differences make this flaky") @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable_in_setup(pytester: Pytester) -> None: pytester.makepyfile( @@ -76,6 +76,7 @@ def test_unraisable_in_setup(pytester: Pytester) -> None: ) +@pytest.mark.skipif(PYPY, reason="garbage-collection differences make this flaky") @pytest.mark.filterwarnings("default::pytest.PytestUnraisableExceptionWarning") def test_unraisable_in_teardown(pytester: Pytester) -> None: pytester.makepyfile( @@ -116,7 +117,7 @@ def test_unraisable_in_teardown(pytester: Pytester) -> None: @pytest.mark.filterwarnings("error::pytest.PytestUnraisableExceptionWarning") def test_unraisable_warning_error(pytester: Pytester) -> None: pytester.makepyfile( - test_it=""" + test_it=f""" class BrokenDel: def __del__(self) -> None: raise ValueError("del is broken") @@ -124,6 +125,7 @@ def test_unraisable_warning_error(pytester: Pytester) -> None: def test_it() -> None: obj = BrokenDel() del obj + {"import gc; gc.collect()" * PYPY} def test_2(): pass """ diff --git a/testing/test_warnings.py b/testing/test_warnings.py index a1ecba247..96ecad6f6 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -518,7 +518,8 @@ class TestDeprecationWarningsByDefault: assert WARNINGS_SUMMARY_HEADER not in result.stdout.str() -@pytest.mark.skip("not relevant until pytest 8.0") +# In 8.1, uncomment below and change RemovedIn8 -> RemovedIn9. +# @pytest.mark.skip("not relevant until pytest 9.0") @pytest.mark.parametrize("change_default", [None, "ini", "cmdline"]) def test_removed_in_x_warning_as_error(pytester: Pytester, change_default) -> None: """This ensures that PytestRemovedInXWarnings raised by pytest are turned into errors. @@ -810,12 +811,12 @@ def test_resource_warning(pytester: Pytester, monkeypatch: pytest.MonkeyPatch) - pytester.makepyfile( """ def open_file(p): - f = p.open("r") + f = p.open("r", encoding="utf-8") assert p.read_text() == "hello" def test_resource_warning(tmp_path): p = tmp_path.joinpath("foo.txt") - p.write_text("hello") + p.write_text("hello", encoding="utf-8") open_file(p) """ ) diff --git a/tox.ini b/tox.ini index 88ae16dea..1b62b98e4 100644 --- a/tox.ini +++ b/tox.ini @@ -4,17 +4,16 @@ minversion = 3.20.0 distshare = {homedir}/.tox/distshare envlist = linting - py37 py38 py39 py310 py311 py312 pypy3 - py37-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib} + py38-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib} doctesting plugins - py37-freeze + py38-freeze docs docs-checklinks @@ -38,8 +37,12 @@ passenv = setenv = _PYTEST_TOX_DEFAULT_POSARGS={env:_PYTEST_TOX_POSARGS_DOCTESTING:} {env:_PYTEST_TOX_POSARGS_LSOF:} {env:_PYTEST_TOX_POSARGS_XDIST:} + # See https://docs.python.org/3/library/io.html#io-encoding-warning + # If we don't enable this, neither can any of our downstream users! + PYTHONWARNDEFAULTENCODING=1 + # Configuration to run with coverage similar to CI, e.g. - # "tox -e py37-coverage". + # "tox -e py38-coverage". coverage: _PYTEST_TOX_COVERAGE_RUN=coverage run -m coverage: _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess coverage: COVERAGE_FILE={toxinidir}/.coverage @@ -71,6 +74,9 @@ skip_install = True basepython = python3 deps = pre-commit>=2.9.3 commands = pre-commit run --all-files --show-diff-on-failure {posargs:} +setenv = + # pre-commit and tools it launches are not clean of this warning. + PYTHONWARNDEFAULTENCODING= [testenv:docs] basepython = python3 @@ -85,6 +91,9 @@ commands = # changelog in the docs; this does not happen on ReadTheDocs because it uses # the standard sphinx command so the 'changelog_towncrier_draft' is never set there sphinx-build -W --keep-going -b html doc/en doc/en/_build/html -t changelog_towncrier_draft {posargs:} +setenv = + # Sphinx is not clean of this warning. + PYTHONWARNDEFAULTENCODING= [testenv:docs-checklinks] basepython = python3 @@ -93,6 +102,9 @@ changedir = doc/en deps = -r{toxinidir}/doc/en/requirements.txt commands = sphinx-build -W -q --keep-going -b linkcheck . _build +setenv = + # Sphinx is not clean of this warning. + PYTHONWARNDEFAULTENCODING= [testenv:regen] changedir = doc/en @@ -107,6 +119,9 @@ allowlist_externals = make commands = make regen +setenv = + # We don't want this warning to reach regen output. + PYTHONWARNDEFAULTENCODING= [testenv:plugins] # use latest versions of all plugins, including pre-releases @@ -132,7 +147,7 @@ commands = pytest pytest_twisted_integration.py pytest simple_integration.py --force-sugar --flakes -[testenv:py37-freeze] +[testenv:py38-freeze] changedir = testing/freeze deps = pyinstaller