diff --git a/.coveragerc b/.coveragerc index a335557d4f1..b810471417f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -29,3 +29,5 @@ exclude_lines = ^\s*if TYPE_CHECKING: ^\s*@overload( |$) + + ^\s*@pytest\.mark\.xfail diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index bce64a374be..d6aac5c425d 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -31,3 +31,5 @@ c9df77cbd6a365dcb73c39618e4842711817e871 4546d5445aaefe6a03957db028c263521dfb5c4b # Migration to ruff / ruff format 4588653b2497ed25976b7aaff225b889fb476756 +# Use format specifiers instead of percent format +4788165e69d08e10fc6b9c0124083fb358e2e9b0 diff --git a/.github/chronographer.yml b/.github/chronographer.yml new file mode 100644 index 00000000000..803db1e3417 --- /dev/null +++ b/.github/chronographer.yml @@ -0,0 +1,20 @@ +--- + +branch-protection-check-name: Changelog entry +action-hints: + check-title-prefix: "Chronographer: " + external-docs-url: >- + https://docs.pytest.org/en/latest/contributing.html#preparing-pull-requests + inline-markdown: >- + See + https://docs.pytest.org/en/latest/contributing.html#preparing-pull-requests + for details. +enforce-name: + suffix: .rst +exclude: + humans: + - pyup-bot +labels: + skip-changelog: skip news + +... diff --git a/.github/patchback.yml b/.github/patchback.yml new file mode 100644 index 00000000000..5d62fca12fe --- /dev/null +++ b/.github/patchback.yml @@ -0,0 +1,7 @@ +--- + +backport_branch_prefix: patchback/backports/ +backport_label_prefix: 'backport ' # IMPORTANT: the labels are space-delimited +# target_branch_prefix: '' # The project's backport branches are non-prefixed + +... diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml deleted file mode 100644 index 38ce7260278..00000000000 --- a/.github/workflows/backport.yml +++ /dev/null @@ -1,51 +0,0 @@ -name: backport - -on: - # Note that `pull_request_target` has security implications: - # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ - # In particular: - # - Only allow triggers that can be used only be trusted users - # - Don't execute any code from the target branch - # - Don't use cache - pull_request_target: - types: [labeled] - -# Set permissions at the job level. -permissions: {} - -jobs: - backport: - if: startsWith(github.event.label.name, 'backport ') && github.event.pull_request.merged - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: true - - - name: Create backport PR - run: | - set -eux - - git config --global user.name "pytest bot" - git config --global user.email "pytestbot@gmail.com" - - label='${{ github.event.label.name }}' - target_branch="${label#backport }" - backport_branch=backport-${{ github.event.number }}-to-"${target_branch}" - subject="[$target_branch] $(gh pr view --json title -q .title ${{ github.event.number }})" - - git checkout origin/"${target_branch}" -b "${backport_branch}" - git cherry-pick -x --mainline 1 ${{ github.event.pull_request.merge_commit_sha }} - git commit --amend --message "$subject" - git push --set-upstream origin --force-with-lease "${backport_branch}" - gh pr create \ - --base "${target_branch}" \ - --title "${subject}" \ - --body "Backport of PR #${{ github.event.number }} to $target_branch branch. PR created by backport workflow." - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cc0e6331d45..f5ea4d39764 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -19,6 +19,11 @@ jobs: SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }} timeout-minutes: 10 + # Required by attest-build-provenance-github. + permissions: + id-token: write + attestations: write + steps: - uses: actions/checkout@v4 with: @@ -26,7 +31,9 @@ jobs: persist-credentials: false - name: Build and Check Package - uses: hynek/build-and-inspect-python-package@v2.4.0 + uses: hynek/build-and-inspect-python-package@v2.6.0 + with: + attest-build-provenance-github: 'true' deploy: if: github.repository == 'pytest-dev/pytest' @@ -47,7 +54,7 @@ jobs: path: dist - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.14 + uses: pypa/gh-action-pypi-publish@v1.9.0 - name: Push tag run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4434740675e..9158d6bcc72 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,6 +14,11 @@ on: branches: - main - "[0-9]+.[0-9]+.x" + types: + - opened # default + - synchronize # default + - reopened # default + - ready_for_review # used in PRs created from the release workflow env: PYTEST_ADDOPTS: "--color=yes" @@ -35,7 +40,7 @@ jobs: fetch-depth: 0 persist-credentials: false - name: Build and Check Package - uses: hynek/build-and-inspect-python-package@v2.4.0 + uses: hynek/build-and-inspect-python-package@v2.6.0 build: needs: [package] @@ -55,6 +60,7 @@ jobs: "windows-py310", "windows-py311", "windows-py312", + "windows-py313", "ubuntu-py38", "ubuntu-py38-pluggy", @@ -63,12 +69,14 @@ jobs: "ubuntu-py310", "ubuntu-py311", "ubuntu-py312", + "ubuntu-py313", "ubuntu-pypy3", "macos-py38", "macos-py39", "macos-py310", "macos-py312", + "macos-py313", "doctesting", "plugins", @@ -97,9 +105,13 @@ jobs: os: windows-latest tox_env: "py311" - name: "windows-py312" - python: "3.12-dev" + python: "3.12" os: windows-latest tox_env: "py312" + - name: "windows-py313" + python: "3.13-dev" + os: windows-latest + tox_env: "py313" - name: "ubuntu-py38" python: "3.8" @@ -128,12 +140,17 @@ jobs: tox_env: "py311" use_coverage: true - name: "ubuntu-py312" - python: "3.12-dev" + python: "3.12" os: ubuntu-latest tox_env: "py312" use_coverage: true + - name: "ubuntu-py313" + python: "3.13-dev" + os: ubuntu-latest + tox_env: "py313" + use_coverage: true - name: "ubuntu-pypy3" - python: "pypy-3.8" + python: "pypy-3.9" os: ubuntu-latest tox_env: "pypy3-xdist" @@ -151,9 +168,13 @@ jobs: os: macos-latest tox_env: "py310-xdist" - name: "macos-py312" - python: "3.12-dev" + python: "3.12" os: macos-latest tox_env: "py312-xdist" + - name: "macos-py313" + python: "3.13-dev" + os: macos-latest + tox_env: "py313-xdist" - name: "plugins" python: "3.12" @@ -166,6 +187,26 @@ jobs: tox_env: "doctesting" use_coverage: true + continue-on-error: >- + ${{ + contains( + fromJSON( + '[ + "windows-py38-pluggy", + "windows-py313", + "ubuntu-py38-pluggy", + "ubuntu-py38-freeze", + "ubuntu-py313", + "macos-py38", + "macos-py313" + ]' + ), + matrix.name + ) + && true + || false + }} + steps: - uses: actions/checkout@v4 with: @@ -206,8 +247,21 @@ jobs: - name: Upload coverage to Codecov if: "matrix.use_coverage" uses: codecov/codecov-action@v4 - continue-on-error: true with: - fail_ci_if_error: true + fail_ci_if_error: false files: ./coverage.xml verbose: true + + check: # This job does nothing and is only used for the branch protection + if: always() + + needs: + - build + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@223e4bb7a751b91f43eda76992bcfbf23b8b0302 + with: + jobs: ${{ toJSON(needs) }} diff --git a/.github/workflows/update-plugin-list.yml b/.github/workflows/update-plugin-list.yml index 6943e207608..ade8452afd5 100644 --- a/.github/workflows/update-plugin-list.yml +++ b/.github/workflows/update-plugin-list.yml @@ -46,7 +46,8 @@ jobs: run: python scripts/update-plugin-list.py - name: Create Pull Request - uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 + id: pr + uses: peter-evans/create-pull-request@c5a7806660adbe173f04e3e038b0ccdcd758773c with: commit-message: '[automated] Update plugin list' author: 'pytest bot ' @@ -55,3 +56,13 @@ jobs: branch-suffix: short-commit-hash title: '[automated] Update plugin list' body: '[automated] Update plugin list' + draft: true + + - name: Instruct the maintainers to trigger CI by undrafting the PR + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + gh pr comment + --body 'Please mark the PR as ready for review to trigger PR checks.' + --repo '${{ github.repository }}' + '${{ steps.pr.outputs.pull-request-number }}' diff --git a/.gitignore b/.gitignore index 9fccf93f7c3..c4557b33a1c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,6 @@ src/_pytest/_version.py doc/*/_build doc/*/.doctrees -doc/*/_changelog_towncrier_draft.rst build/ dist/ *.egg-info diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a80edd28cdc..419addd95be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: "v0.4.1" + rev: "v0.5.2" hooks: - id: ruff args: ["--fix"] @@ -10,14 +10,9 @@ repos: hooks: - id: trailing-whitespace - id: end-of-file-fixer - - id: fix-encoding-pragma - args: [--remove] - id: check-yaml - - id: debug-statements - exclude: _pytest/(debugging|hookspec).py - language_version: python3 - repo: https://github.com/adamchainz/blacken-docs - rev: 1.16.0 + rev: 1.18.0 hooks: - id: blacken-docs additional_dependencies: [black==24.1.1] @@ -26,7 +21,7 @@ repos: hooks: - id: python-use-type-annotations - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.9.0 + rev: v1.10.1 hooks: - id: mypy files: ^(src/|testing/|scripts/) @@ -43,20 +38,25 @@ repos: # on <3.11 - exceptiongroup>=1.0.0rc8 - repo: https://github.com/tox-dev/pyproject-fmt - rev: "1.8.0" + rev: "2.1.4" hooks: - id: pyproject-fmt # https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version additional_dependencies: ["tox>=4.9"] +- repo: https://github.com/asottile/pyupgrade + rev: v3.16.0 + hooks: + - id: pyupgrade + stages: [manual] - repo: local hooks: - - id: pylint - name: pylint - entry: pylint - language: system - types: [python] - args: ["-rn", "-sn", "--fail-on=I"] - stages: [manual] + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + args: ["-rn", "-sn", "--fail-on=I"] + stages: [manual] - id: rst name: rst entry: rst-lint --encoding utf-8 @@ -66,9 +66,50 @@ repos: - id: changelogs-rst name: changelog filenames language: fail - entry: 'changelog files must be named ####.(breaking|bugfix|deprecation|doc|feature|improvement|trivial|vendor).rst' - exclude: changelog/(\d+\.(breaking|bugfix|deprecation|doc|feature|improvement|trivial|vendor).rst|README.rst|_template.rst) + entry: >- + changelog files must be named + ####.( + breaking + | deprecation + | feature + | improvement + | bugfix + | vendor + | doc + | packaging + | contrib + | misc + )(.#)?(.rst)? + exclude: >- + (?x) + ^ + changelog/( + \.gitignore + |\d+\.( + breaking + |deprecation + |feature + |improvement + |bugfix + |vendor + |doc + |packaging + |contrib + |misc + )(\.\d+)?(\.rst)? + |README\.rst + |_template\.rst + ) + $ files: ^changelog/ + - id: changelogs-user-role + name: Changelog files should use a non-broken :user:`name` role + language: pygrep + entry: :user:([^`]+`?|`[^`]+[\s,]) + pass_filenames: true + types: + - file + - rst - id: py-deprecated name: py library is deprecated language: pygrep diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 76% rename from .readthedocs.yml rename to .readthedocs.yaml index 266d4e07aea..f7370f1bb98 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yaml @@ -14,11 +14,16 @@ sphinx: fail_on_warning: true build: - os: ubuntu-20.04 + os: ubuntu-24.04 tools: - python: "3.9" + python: >- + 3.12 apt_packages: - inkscape + jobs: + post_checkout: + - git fetch --unshallow || true + - git fetch --tags || true formats: - epub diff --git a/AUTHORS b/AUTHORS index 4f61c05914b..9b6cb6a9d23 100644 --- a/AUTHORS +++ b/AUTHORS @@ -36,6 +36,7 @@ Andrey Paramonov Andrzej Klajnert Andrzej Ostrowski Andy Freeland +Anita Hammer Anthon van der Neut Anthony Shaw Anthony Sottile @@ -95,6 +96,7 @@ Christopher Gilling Claire Cecil Claudio Madotto Clément M.T. Robert +Cornelius Riemenschneider CrazyMerlyn Cristian Vera Cyrus Maden @@ -148,6 +150,7 @@ Evgeny Seliverstov Fabian Sturm Fabien Zarifian Fabio Zadrozny +Farbod Ahmadian faph Felix Hofstätter Felix Nieuwenhuizen @@ -191,6 +194,7 @@ Jake VanderPlas Jakob van Santen Jakub Mitoraj James Bourbeau +James Frost Jan Balster Janne Vanhala Jason R. Coombs @@ -210,6 +214,7 @@ Jordan Guymon Jordan Moldow Jordan Speicher Joseph Hunkeler +Joseph Sawaya Josh Karpel Joshua Bronson Jurko Gospodnetić @@ -242,6 +247,7 @@ Levon Saldamli Lewis Cowles Llandy Riveron Del Risco Loic Esteve +lovetheguitar Lukas Bednar Luke Murphy Maciek Fijalkowski @@ -257,6 +263,7 @@ Marc Bresson Marco Gorelli Mark Abramowitz Mark Dickinson +Mark Vong Marko Pacak Markus Unterwaditzer Martijn Faassen @@ -277,6 +284,7 @@ Michael Droettboom Michael Goerz Michael Krebs Michael Seifert +Michael Vogt Michal Wajszczuk Michał Górny Michał Zięba @@ -288,6 +296,7 @@ Mike Lundy Milan Lesnek Miro Hrončok mrbean-bremen +Nathan Goldbaum Nathaniel Compton Nathaniel Waisbrot Ned Batchelder @@ -297,6 +306,8 @@ Nicholas Devenish Nicholas Murphy Niclas Olofsson Nicolas Delaby +Nicolas Simonds +Nico Vidal Nikolay Kondratyev Nipunn Koorapati Oleg Pidsadnyi @@ -357,6 +368,7 @@ Sadra Barikbin Saiprasad Kale Samuel Colvin Samuel Dion-Girardeau +Samuel Jirovec Samuel Searles-Bryant Samuel Therrien (Avasam) Samuele Pedroni @@ -385,6 +397,7 @@ Stefano Taschini Steffen Allner Stephan Obermann Sven-Hendrik Haase +Sviatoslav Sydorenko Sylvain Marié Tadek Teleżyński Takafumi Arakaki @@ -419,6 +432,7 @@ Victor Rodriguez Victor Uriarte Vidar T. Fauske Vijay Arora +Virendra Patil Virgil Dupras Vitaly Lashmanov Vivaan Verma @@ -440,8 +454,10 @@ Yao Xiao Yoav Caspi Yuliang Shao Yusuke Kadowaki +Yutian Li Yuval Shimon Zac Hatfield-Dodds +Zach Snicker Zachary Kneupper Zachary OBrien Zhouxin Qiu diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d7da59c812d..12e2b18bb52 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,14 +1,10 @@ ============================ -Contribution getting started +Contributing ============================ Contributions are highly welcomed and appreciated. Every little bit of help counts, so do not hesitate! -.. contents:: - :depth: 2 - :backlinks: none - .. _submitfeedback: @@ -128,7 +124,7 @@ For example: Submitting Plugins to pytest-dev -------------------------------- -Pytest development of the core, some plugins and support code happens +Development of the pytest core, support code, and some plugins happens in repositories living under the ``pytest-dev`` organisations: - `pytest-dev on GitHub `_ diff --git a/bench/bench.py b/bench/bench.py index 437d3259d83..139c292ecd8 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys @@ -8,7 +10,7 @@ import pytest # noqa: F401 script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"] - cProfile.run("pytest.cmdline.main(%r)" % script, "prof") + cProfile.run(f"pytest.cmdline.main({script!r})", "prof") p = pstats.Stats("prof") p.strip_dirs() p.sort_stats("cumulative") diff --git a/bench/bench_argcomplete.py b/bench/bench_argcomplete.py index 459a12f9314..468c59217df 100644 --- a/bench/bench_argcomplete.py +++ b/bench/bench_argcomplete.py @@ -2,6 +2,8 @@ # 2.7.5 3.3.2 # FilesCompleter 75.1109 69.2116 # FastFilesCompleter 0.7383 1.0760 +from __future__ import annotations + import timeit diff --git a/bench/empty.py b/bench/empty.py index 4e7371b6f80..35abeef4140 100644 --- a/bench/empty.py +++ b/bench/empty.py @@ -1,2 +1,5 @@ +from __future__ import annotations + + for i in range(1000): exec("def test_func_%d(): pass" % i) diff --git a/bench/manyparam.py b/bench/manyparam.py index 1226c73bd9c..579f7b2488d 100644 --- a/bench/manyparam.py +++ b/bench/manyparam.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/bench/skip.py b/bench/skip.py index fd5c292d92c..9145cc0ceed 100644 --- a/bench/skip.py +++ b/bench/skip.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/bench/unit_test.py b/bench/unit_test.py index d3db111e1ae..0f106e16b6c 100644 --- a/bench/unit_test.py +++ b/bench/unit_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from unittest import TestCase # noqa: F401 diff --git a/bench/xunit.py b/bench/xunit.py index 3a77dcdce42..31ab432441c 100644 --- a/bench/xunit.py +++ b/bench/xunit.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + for i in range(5000): exec( f""" diff --git a/changelog/.gitignore b/changelog/.gitignore new file mode 100644 index 00000000000..3b34da34bc6 --- /dev/null +++ b/changelog/.gitignore @@ -0,0 +1,34 @@ +* +!.gitignore +!_template.rst +!README.rst +!*.bugfix +!*.bugfix.rst +!*.bugfix.*.rst +!*.breaking +!*.breaking.rst +!*.breaking.*.rst +!*.contrib +!*.contrib.rst +!*.contrib.*.rst +!*.deprecation +!*.deprecation.rst +!*.deprecation.*.rst +!*.doc +!*.doc.rst +!*.doc.*.rst +!*.feature +!*.feature.rst +!*.feature.*.rst +!*.improvement +!*.improvement.rst +!*.improvement.*.rst +!*.misc +!*.misc.rst +!*.misc.*.rst +!*.packaging +!*.packaging.rst +!*.packaging.*.rst +!*.vendor +!*.vendor.rst +!*.vendor.*.rst diff --git a/changelog/11523.improvement.rst b/changelog/11523.improvement.rst deleted file mode 100644 index f7d8ff89df6..00000000000 --- a/changelog/11523.improvement.rst +++ /dev/null @@ -1,5 +0,0 @@ -:func:`pytest.importorskip` will now issue a warning if the module could be found, but raised :class:`ImportError` instead of :class:`ModuleNotFoundError`. - -The warning can be suppressed by passing ``exc_type=ImportError`` to :func:`pytest.importorskip`. - -See :ref:`import-or-skip-import-error` for details. diff --git a/changelog/11728.improvement.rst b/changelog/11728.improvement.rst deleted file mode 100644 index 1e87fc5ed88..00000000000 --- a/changelog/11728.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -For ``unittest``-based tests, exceptions during class cleanup (as raised by functions registered with :meth:`TestCase.addClassCleanup `) are now reported instead of silently failing. diff --git a/changelog/11777.improvement.rst b/changelog/11777.improvement.rst deleted file mode 100644 index fb53c63c10a..00000000000 --- a/changelog/11777.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Text is no longer truncated in the ``short test summary info`` section when ``-vv`` is given. diff --git a/changelog/11871.feature.rst b/changelog/11871.feature.rst deleted file mode 100644 index 530db8c3c6f..00000000000 --- a/changelog/11871.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added support for reading command line arguments from a file using the prefix character ``@``, like e.g.: ``pytest @tests.txt``. The file must have one argument per line. diff --git a/changelog/12065.bugfix.rst b/changelog/12065.bugfix.rst deleted file mode 100644 index ca55b327e13..00000000000 --- a/changelog/12065.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fixed a regression in pytest 8.0.0 where test classes containing ``setup_method`` and tests using ``@staticmethod`` or ``@classmethod`` would crash with ``AttributeError: 'NoneType' object has no attribute 'setup_method'``. - -Now the :attr:`request.instance ` attribute of tests using ``@staticmethod`` and ``@classmethod`` is no longer ``None``, but a fresh instance of the class, like in non-static methods. -Previously it was ``None``, and all fixtures of such tests would share a single ``self``. diff --git a/changelog/12069.deprecation.rst b/changelog/12069.deprecation.rst deleted file mode 100644 index c8798b5ff25..00000000000 --- a/changelog/12069.deprecation.rst +++ /dev/null @@ -1,12 +0,0 @@ -A deprecation warning is now raised when implementations of one of the following hooks request a deprecated ``py.path.local`` parameter instead of the ``pathlib.Path`` parameter which replaced it: - -- :hook:`pytest_ignore_collect` - the ``path`` parameter - use ``collection_path`` instead. -- :hook:`pytest_collect_file` - the ``path`` parameter - use ``file_path`` instead. -- :hook:`pytest_pycollect_makemodule` - the ``path`` parameter - use ``module_path`` instead. -- :hook:`pytest_report_header` - the ``startdir`` parameter - use ``start_path`` instead. -- :hook:`pytest_report_collectionfinish` - the ``startdir`` parameter - use ``start_path`` instead. - -The replacement parameters are available since pytest 7.0.0. -The old parameters will be removed in pytest 9.0.0. - -See :ref:`legacy-path-hooks-deprecated` for more details. diff --git a/changelog/12069.trivial.rst b/changelog/12069.trivial.rst deleted file mode 100644 index 8eb9b0c464a..00000000000 --- a/changelog/12069.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -``pluggy>=1.5.0`` is now required. diff --git a/changelog/12112.improvement.rst b/changelog/12112.improvement.rst deleted file mode 100644 index 3f997b2af65..00000000000 --- a/changelog/12112.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Improve namespace packages detection when :confval:`consider_namespace_packages` is enabled, covering more situations (like editable installs). diff --git a/changelog/12135.bugfix.rst b/changelog/12135.bugfix.rst deleted file mode 100644 index 734733b100d..00000000000 --- a/changelog/12135.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix fixtures adding their finalizer multiple times to fixtures they request, causing unreliable and non-intuitive teardown ordering in some instances. diff --git a/changelog/12167.trivial.rst b/changelog/12167.trivial.rst deleted file mode 100644 index da9363420e6..00000000000 --- a/changelog/12167.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -cache: create cache directory supporting files (``CACHEDIR.TAG``, ``.gitignore``, etc.) in a temporary directory to provide atomic semantics. diff --git a/changelog/12194.bugfix.rst b/changelog/12194.bugfix.rst deleted file mode 100644 index 6983ba35a90..00000000000 --- a/changelog/12194.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed a bug with ``--importmode=importlib`` and ``--doctest-modules`` where child modules did not appear as attributes in parent modules. diff --git a/changelog/1489.bugfix.rst b/changelog/1489.bugfix.rst deleted file mode 100644 index 70c5dd1252e..00000000000 --- a/changelog/1489.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix some instances where teardown of higher-scoped fixtures was not happening in the reverse order they were initialized in. diff --git a/changelog/9502.improvement.rst b/changelog/9502.improvement.rst deleted file mode 100644 index 2eaf6a72747..00000000000 --- a/changelog/9502.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Added :envvar:`PYTEST_VERSION` environment variable which is defined at the start of the pytest session and undefined afterwards. It contains the value of ``pytest.__version__``, and among other things can be used to easily check if code is running from within a pytest run. diff --git a/changelog/README.rst b/changelog/README.rst index 88956ef28d8..fdaa573d427 100644 --- a/changelog/README.rst +++ b/changelog/README.rst @@ -20,10 +20,22 @@ Each file should be named like ``..rst``, where * ``deprecation``: feature deprecation. * ``breaking``: a change which may break existing suites, such as feature removal or behavior change. * ``vendor``: changes in packages vendored in pytest. -* ``trivial``: fixing a small typo or internal change that might be noteworthy. +* ``packaging``: notes for downstreams about unobvious side effects + and tooling. changes in the test invocation considerations and + runtime assumptions. +* ``contrib``: stuff that affects the contributor experience. e.g. + Running tests, building the docs, setting up the development + environment. +* ``misc``: changes that are hard to assign to any of the above + categories. So for example: ``123.feature.rst``, ``456.bugfix.rst``. +.. tip:: + + See :file:`pyproject.toml` for all available categories + (``tool.towncrier.type``). + If your PR fixes an issue, use that number here. If there is no issue, then after you submit the PR and get the PR number you can add a changelog using that instead. diff --git a/codecov.yml b/codecov.yml index f1cc8697338..0841ab049ff 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,4 +1,9 @@ # reference: https://docs.codecov.io/docs/codecovyml-reference +--- + +codecov: + token: 1eca3b1f-31a2-4fb8-a8c3-138b441b50a7 #repo token + coverage: status: patch: true diff --git a/doc/en/_static/pytest-custom.css b/doc/en/_static/pytest-custom.css new file mode 100644 index 00000000000..bc9eef457f1 --- /dev/null +++ b/doc/en/_static/pytest-custom.css @@ -0,0 +1,21 @@ +/* Tweak how the sidebar logo is presented */ +.sidebar-logo { + width: 70%; +} +.sidebar-brand { + padding: 0; +} + +/* The landing pages' sidebar-in-content highlights */ +#features ul { + padding-left: 1rem; + list-style: none; +} +#features ul li { + margin-bottom: 0; +} +@media (min-width: 46em) { + #features { + width: 50%; + } +} diff --git a/doc/en/img/pytest1.png b/doc/en/_static/pytest1.png similarity index 100% rename from doc/en/img/pytest1.png rename to doc/en/_static/pytest1.png diff --git a/doc/en/_templates/globaltoc.html b/doc/en/_templates/globaltoc.html deleted file mode 100644 index 09d970b64ed..00000000000 --- a/doc/en/_templates/globaltoc.html +++ /dev/null @@ -1,31 +0,0 @@ -

Contents

- - - -

About the project

- - - -{%- if display_toc %} -
- {{ toc }} -{%- endif %} - -
diff --git a/doc/en/_templates/layout.html b/doc/en/_templates/layout.html deleted file mode 100644 index f7096eaaa5e..00000000000 --- a/doc/en/_templates/layout.html +++ /dev/null @@ -1,52 +0,0 @@ -{# - - Copied from: - - https://raw.githubusercontent.com/pallets/pallets-sphinx-themes/b0c6c41849b4e15cbf62cc1d95c05ef2b3e155c8/src/pallets_sphinx_themes/themes/pocoo/layout.html - - And removed the warning version (see #7331). - -#} - -{% extends "basic/layout.html" %} - -{% set metatags %} - {{- metatags }} - -{%- endset %} - -{% block extrahead %} - {%- if page_canonical_url %} - - {%- endif %} - - {{ super() }} -{%- endblock %} - -{% block sidebarlogo %} - {% if pagename != "index" or theme_index_sidebar_logo %} - {{ super() }} - {% endif %} -{% endblock %} - -{% block relbar2 %}{% endblock %} - -{% block sidebar2 %} - - {{- super() }} -{%- endblock %} - -{% block footer %} - {{ super() }} - {%- if READTHEDOCS and not readthedocs_docsearch %} - - {%- endif %} - {{ js_tag("_static/version_warning_offset.js") }} -{% endblock %} diff --git a/doc/en/_templates/relations.html b/doc/en/_templates/relations.html deleted file mode 100644 index 3bbcde85bb4..00000000000 --- a/doc/en/_templates/relations.html +++ /dev/null @@ -1,19 +0,0 @@ -

Related Topics

- diff --git a/doc/en/_templates/sidebarintro.html b/doc/en/_templates/sidebarintro.html deleted file mode 100644 index ae860c172f0..00000000000 --- a/doc/en/_templates/sidebarintro.html +++ /dev/null @@ -1,5 +0,0 @@ -

About pytest

-

- pytest is a mature full-featured Python testing tool that helps - you write better programs. -

diff --git a/doc/en/_templates/slim_searchbox.html b/doc/en/_templates/slim_searchbox.html deleted file mode 100644 index f088ff8d312..00000000000 --- a/doc/en/_templates/slim_searchbox.html +++ /dev/null @@ -1,14 +0,0 @@ -{# - basic/searchbox.html with heading removed. -#} -{%- if pagename != "search" and builder != "singlehtml" %} - - -{%- endif %} diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index bb39eb7e6a1..09311a1a1ab 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,12 @@ Release announcements :maxdepth: 2 + release-8.3.2 + release-8.3.1 + release-8.3.0 + release-8.2.2 + release-8.2.1 + release-8.2.0 release-8.1.2 release-8.1.1 release-8.1.0 diff --git a/doc/en/announce/release-2.0.0.rst b/doc/en/announce/release-2.0.0.rst index ecb1a1db988..c2a9f6da4d5 100644 --- a/doc/en/announce/release-2.0.0.rst +++ b/doc/en/announce/release-2.0.0.rst @@ -62,7 +62,7 @@ New Features - new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. -- many many more detailed improvements details +- many, many, more detailed improvements details Fixes ----------------------- @@ -109,7 +109,7 @@ Important Notes in conftest.py files. They will cause nothing special. - removed support for calling the pre-1.0 collection API of "run()" and "join" - removed reading option values from conftest.py files or env variables. - This can now be done much much better and easier through the ini-file + This can now be done much, much, better and easier through the ini-file mechanism and the "addopts" entry in particular. - removed the "disabled" attribute in test classes. Use the skipping and pytestmark mechanism to skip or xfail a test class. diff --git a/doc/en/announce/release-2.2.2.rst b/doc/en/announce/release-2.2.2.rst index 22ef0bc7a16..510b35ee1d0 100644 --- a/doc/en/announce/release-2.2.2.rst +++ b/doc/en/announce/release-2.2.2.rst @@ -4,7 +4,7 @@ pytest-2.2.2: bug fixes pytest-2.2.2 (updated to 2.2.3 to fix packaging issues) is a minor backward-compatible release of the versatile py.test testing tool. It contains bug fixes and a few refinements particularly to reporting with -"--collectonly", see below for betails. +"--collectonly", see below for details. For general information see here: diff --git a/doc/en/announce/release-2.4.0.rst b/doc/en/announce/release-2.4.0.rst index 138cc89576c..9b864329674 100644 --- a/doc/en/announce/release-2.4.0.rst +++ b/doc/en/announce/release-2.4.0.rst @@ -181,7 +181,7 @@ Bug fixes: partially failed (finalizers would not always be called before) - fix issue320 - fix class scope for fixtures when mixed with - module-level functions. Thanks Anatloy Bubenkoff. + module-level functions. Thanks Anatoly Bubenkoff. - you can specify "-q" or "-qq" to get different levels of "quieter" reporting (thanks Katarzyna Jachim) diff --git a/doc/en/announce/release-2.5.0.rst b/doc/en/announce/release-2.5.0.rst index c6cdcdd8a83..fe64f1b8668 100644 --- a/doc/en/announce/release-2.5.0.rst +++ b/doc/en/announce/release-2.5.0.rst @@ -83,7 +83,7 @@ holger krekel Thanks Ralph Schmitt for the precise failure example. - fix issue244 by implementing special index for parameters to only use - indices for paramentrized test ids + indices for parametrized test ids - fix issue287 by running all finalizers but saving the exception from the first failing finalizer and re-raising it so teardown will diff --git a/doc/en/announce/release-2.6.0.rst b/doc/en/announce/release-2.6.0.rst index 56fbd6cc1e4..c00df585738 100644 --- a/doc/en/announce/release-2.6.0.rst +++ b/doc/en/announce/release-2.6.0.rst @@ -73,7 +73,7 @@ holger krekel - cleanup setup.py a bit and specify supported versions. Thanks Jurko Gospodnetic for the PR. -- change XPASS colour to yellow rather then red when tests are run +- change XPASS colour to yellow rather than red when tests are run with -v. - fix issue473: work around mock putting an unbound method into a class diff --git a/doc/en/announce/release-2.7.0.rst b/doc/en/announce/release-2.7.0.rst index 2840178a07f..83cddb34157 100644 --- a/doc/en/announce/release-2.7.0.rst +++ b/doc/en/announce/release-2.7.0.rst @@ -55,7 +55,7 @@ holger krekel github. See https://pytest.org/en/stable/contributing.html . Thanks to Anatoly for pushing and initial work on this. -- fix issue650: new option ``--docttest-ignore-import-errors`` which +- fix issue650: new option ``--doctest-ignore-import-errors`` which will turn import errors in doctests into skips. Thanks Charles Cloud for the complete PR. diff --git a/doc/en/announce/release-2.9.0.rst b/doc/en/announce/release-2.9.0.rst index 3aea08cb225..753bb7bf6f0 100644 --- a/doc/en/announce/release-2.9.0.rst +++ b/doc/en/announce/release-2.9.0.rst @@ -45,7 +45,7 @@ The py.test Development Team **New Features** * New ``pytest.mark.skip`` mark, which unconditionally skips marked tests. - Thanks :user:`MichaelAquilina` for the complete PR (:pull:`1040`). + Thanks :user:`MichaelAquilina` for the complete PR (:pr:`1040`). * ``--doctest-glob`` may now be passed multiple times in the command-line. Thanks :user:`jab` and :user:`nicoddemus` for the PR. diff --git a/doc/en/announce/release-2.9.1.rst b/doc/en/announce/release-2.9.1.rst index 6a627ad3cd6..7a46d2ae690 100644 --- a/doc/en/announce/release-2.9.1.rst +++ b/doc/en/announce/release-2.9.1.rst @@ -44,7 +44,7 @@ The py.test Development Team Thanks :user:`nicoddemus` for the PR. * Fix (:issue:`469`): junit parses report.nodeid incorrectly, when params IDs - contain ``::``. Thanks :user:`tomviner` for the PR (:pull:`1431`). + contain ``::``. Thanks :user:`tomviner` for the PR (:pr:`1431`). * Fix (:issue:`578`): SyntaxErrors containing non-ascii lines at the point of failure generated an internal diff --git a/doc/en/announce/release-2.9.2.rst b/doc/en/announce/release-2.9.2.rst index 2dc82a1117b..3e75af7fe69 100644 --- a/doc/en/announce/release-2.9.2.rst +++ b/doc/en/announce/release-2.9.2.rst @@ -44,14 +44,14 @@ The py.test Development Team * Fix Xfail does not work with condition keyword argument. Thanks :user:`astraw38` for reporting the issue (:issue:`1496`) and :user:`tomviner` - for PR the (:pull:`1524`). + for PR the (:pr:`1524`). * Fix win32 path issue when putting custom config file with absolute path in ``pytest.main("-c your_absolute_path")``. * Fix maximum recursion depth detection when raised error class is not aware of unicode/encoded bytes. - Thanks :user:`prusse-martin` for the PR (:pull:`1506`). + Thanks :user:`prusse-martin` for the PR (:pr:`1506`). * Fix ``pytest.mark.skip`` mark when used in strict mode. Thanks :user:`pquentin` for the PR and :user:`RonnyPfannschmidt` for diff --git a/doc/en/announce/release-8.2.0.rst b/doc/en/announce/release-8.2.0.rst new file mode 100644 index 00000000000..2a63c8d8722 --- /dev/null +++ b/doc/en/announce/release-8.2.0.rst @@ -0,0 +1,43 @@ +pytest-8.2.0 +======================================= + +The pytest team is proud to announce the 8.2.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: + +* Bruno Oliveira +* Daniel Miller +* Florian Bruhin +* HolyMagician03-UMich +* John Litborn +* Levon Saldamli +* Linghao Zhang +* Manuel López-Ibáñez +* Pierre Sassoulas +* Ran Benita +* Ronny Pfannschmidt +* Sebastian Meyer +* Shekhar verma +* Tamir Duberstein +* Tobias Stoeckmann +* dj +* jakkdl +* poulami-sau +* tserg + + +Happy testing, +The pytest Development Team diff --git a/doc/en/announce/release-8.2.1.rst b/doc/en/announce/release-8.2.1.rst new file mode 100644 index 00000000000..4452edec110 --- /dev/null +++ b/doc/en/announce/release-8.2.1.rst @@ -0,0 +1,19 @@ +pytest-8.2.1 +======================================= + +pytest 8.2.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/announce/release-8.2.2.rst b/doc/en/announce/release-8.2.2.rst new file mode 100644 index 00000000000..3b1d93bd08b --- /dev/null +++ b/doc/en/announce/release-8.2.2.rst @@ -0,0 +1,19 @@ +pytest-8.2.2 +======================================= + +pytest 8.2.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: + +* Bruno Oliveira +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/announce/release-8.3.0.rst b/doc/en/announce/release-8.3.0.rst new file mode 100644 index 00000000000..ec5cd3d0db9 --- /dev/null +++ b/doc/en/announce/release-8.3.0.rst @@ -0,0 +1,60 @@ +pytest-8.3.0 +======================================= + +The pytest team is proud to announce the 8.3.0 release! + +This release contains new features, improvements, and bug fixes, +the full list of changes is available in the changelog: + + https://docs.pytest.org/en/stable/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/stable/ + +As usual, you can upgrade from PyPI via: + + pip install -U pytest + +Thanks to all of the contributors to this release: + +* Anita Hammer +* Ben Brown +* Brian Okken +* Bruno Oliveira +* Cornelius Riemenschneider +* Farbod Ahmadian +* Florian Bruhin +* Hynek Schlawack +* James Frost +* Jason R. Coombs +* Jelle Zijlstra +* Josh Soref +* Marc Bresson +* Michael Vogt +* Nathan Goldbaum +* Nicolas Simonds +* Oliver Bestwalter +* Pavel Březina +* Pierre Sassoulas +* Pradyun Gedam +* Ran Benita +* Ronny Pfannschmidt +* SOUBHIK KUMAR MITRA +* Sam Jirovec +* Stavros Ntentos +* Sviatoslav Sydorenko +* Sviatoslav Sydorenko (Святослав Сидоренко) +* Tomasz Kłoczko +* Virendra Patil +* Yutian Li +* Zach Snicker +* dj +* holger krekel +* joseph-sentry +* lovetheguitar +* neutraljump + + +Happy testing, +The pytest Development Team diff --git a/doc/en/announce/release-8.3.1.rst b/doc/en/announce/release-8.3.1.rst new file mode 100644 index 00000000000..0fb9b40d9c7 --- /dev/null +++ b/doc/en/announce/release-8.3.1.rst @@ -0,0 +1,19 @@ +pytest-8.3.1 +======================================= + +pytest 8.3.1 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Bruno Oliveira +* Ran Benita + + +Happy testing, +The pytest Development Team diff --git a/doc/en/announce/release-8.3.2.rst b/doc/en/announce/release-8.3.2.rst new file mode 100644 index 00000000000..1e4a071692c --- /dev/null +++ b/doc/en/announce/release-8.3.2.rst @@ -0,0 +1,19 @@ +pytest-8.3.2 +======================================= + +pytest 8.3.2 has just been released to PyPI. + +This is a bug-fix release, being a drop-in replacement. To upgrade:: + + pip install --upgrade pytest + +The full changelog is available at https://docs.pytest.org/en/stable/changelog.html. + +Thanks to all of the contributors to this release: + +* Ran Benita +* Ronny Pfannschmidt + + +Happy testing, +The pytest Development Team diff --git a/doc/en/announce/sprint2016.rst b/doc/en/announce/sprint2016.rst index 8e706589876..8d47a205c71 100644 --- a/doc/en/announce/sprint2016.rst +++ b/doc/en/announce/sprint2016.rst @@ -49,7 +49,7 @@ place on 20th, 21st, 22nd, 24th and 25th. On the 23rd we took a break day for some hot hiking in the Black Forest. Sprint activity was organised heavily around pairing, with plenty of group -discusssions to take advantage of the high bandwidth, and lightning talks +discussions to take advantage of the high bandwidth, and lightning talks as well. diff --git a/doc/en/backwards-compatibility.rst b/doc/en/backwards-compatibility.rst index e04e64a76f9..c0feb833ce1 100644 --- a/doc/en/backwards-compatibility.rst +++ b/doc/en/backwards-compatibility.rst @@ -5,30 +5,26 @@ Backwards Compatibility Policy .. versionadded: 6.0 -pytest is actively evolving and is a project that has been decades in the making, -we keep learning about new and better structures to express different details about testing. +Pytest is an actively evolving project that has been decades in the making. +We keep learning about new and better structures to express different details about testing. -While we implement those modifications we try to ensure an easy transition and don't want to impose unnecessary churn on our users and community/plugin authors. +While we implement those modifications, we try to ensure an easy transition and don't want to impose unnecessary churn on our users and community/plugin authors. As of now, pytest considers multiple types of backward compatibility transitions: -a) trivial: APIs which trivially translate to the new mechanism, - and do not cause problematic changes. +a) trivial: APIs that trivially translate to the new mechanism and do not cause problematic changes. - We try to support those indefinitely while encouraging users to switch to newer/better mechanisms through documentation. + We try to support those indefinitely while encouraging users to switch to newer or better mechanisms through documentation. -b) transitional: the old and new API don't conflict - and we can help users transition by using warnings, while supporting both for a prolonged time. +b) transitional: the old and new APIs don't conflict, and we can help users transition by using warnings while supporting both for a prolonged period of time. - We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0). + We will only start the removal of deprecated functionality in major releases (e.g., if we deprecate something in 3.0, we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g., if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0). A deprecated feature scheduled to be removed in major version X will use the warning class `PytestRemovedInXWarning` (a subclass of :class:`~pytest.PytestDeprecationWarning`). - When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g. `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed. + When the deprecation expires (e.g., 4.0 is released), we won't remove the deprecated functionality immediately but will use the standard warning filters to turn `PytestRemovedInXWarning` (e.g., `PytestRemovedIn4Warning`) into **errors** by default. This approach makes it explicit that removal is imminent and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g., 4.1), the feature will be effectively removed. - -c) true breakage: should only be considered when normal transition is unreasonably unsustainable and would offset important development/features by years. - In addition, they should be limited to APIs where the number of actual users is very small (for example only impacting some plugins), and can be coordinated with the community in advance. +c) True breakage should only be considered when a normal transition is unreasonably unsustainable and would offset important developments or features by years. In addition, they should be limited to APIs where the number of actual users is very small (for example, only impacting some plugins) and can be coordinated with the community in advance. Examples for such upcoming changes: @@ -62,11 +58,11 @@ Focus primary on smooth transition - stance (pre 6.0) Keeping backwards compatibility has a very high priority in the pytest project. Although we have deprecated functionality over the years, most of it is still supported. All deprecations in pytest were done because simpler or more efficient ways of accomplishing the same tasks have emerged, making the old way of doing things unnecessary. -With the pytest 3.0 release we introduced a clear communication scheme for when we will actually remove the old busted joint and politely ask you to use the new hotness instead, while giving you enough time to adjust your tests or raise concerns if there are valid reasons to keep deprecated functionality around. +With the pytest 3.0 release, we introduced a clear communication scheme for when we will actually remove the old busted joint and politely ask you to use the new hotness instead, while giving you enough time to adjust your tests or raise concerns if there are valid reasons to keep deprecated functionality around. -To communicate changes we issue deprecation warnings using a custom warning hierarchy (see :ref:`internal-warnings`). These warnings may be suppressed using the standard means: ``-W`` command-line flag or ``filterwarnings`` ini options (see :ref:`warnings`), but we suggest to use these sparingly and temporarily, and heed the warnings when possible. +To communicate changes, we issue deprecation warnings using a custom warning hierarchy (see :ref:`internal-warnings`). These warnings may be suppressed using the standard means: ``-W`` command-line flag or ``filterwarnings`` ini options (see :ref:`warnings`), but we suggest to use these sparingly and temporarily, and heed the warnings when possible. -We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0). +We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0, we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0). When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn them into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed. diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 9d49389f190..b4c0f91f8e5 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a cachedir: .pytest_cache rootdir: /home/sweet/project collected 0 items - cache -- .../_pytest/cacheprovider.py:527 + cache -- .../_pytest/cacheprovider.py:558 Return a cache object that can persist state between testing sessions. cache.get(key, default) @@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Values can be any object handled by the json stdlib module. - capsysbinary -- .../_pytest/capture.py:1008 + capsysbinary -- .../_pytest/capture.py:1005 Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsysbinary.readouterr()`` @@ -43,6 +43,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Returns an instance of :class:`CaptureFixture[bytes] `. Example: + .. code-block:: python def test_output(capsysbinary): @@ -50,7 +51,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capsysbinary.readouterr() assert captured.out == b"hello\n" - capfd -- .../_pytest/capture.py:1035 + capfd -- .../_pytest/capture.py:1033 Enable text capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -60,6 +61,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Returns an instance of :class:`CaptureFixture[str] `. Example: + .. code-block:: python def test_system_echo(capfd): @@ -67,7 +69,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capfd.readouterr() assert captured.out == "hello\n" - capfdbinary -- .../_pytest/capture.py:1062 + capfdbinary -- .../_pytest/capture.py:1061 Enable bytes capturing of writes to file descriptors ``1`` and ``2``. The captured output is made available via ``capfd.readouterr()`` method @@ -77,6 +79,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Returns an instance of :class:`CaptureFixture[bytes] `. Example: + .. code-block:: python def test_system_echo(capfdbinary): @@ -84,7 +87,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capfdbinary.readouterr() assert captured.out == b"hello\n" - capsys -- .../_pytest/capture.py:981 + capsys -- .../_pytest/capture.py:977 Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``. The captured output is made available via ``capsys.readouterr()`` method @@ -94,6 +97,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a Returns an instance of :class:`CaptureFixture[str] `. Example: + .. code-block:: python def test_output(capsys): @@ -101,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a captured = capsys.readouterr() assert captured.out == "hello\n" - doctest_namespace [session scope] -- .../_pytest/doctest.py:737 + doctest_namespace [session scope] -- .../_pytest/doctest.py:740 Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. @@ -115,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a For more details: :ref:`doctest_namespace`. - pytestconfig [session scope] -- .../_pytest/fixtures.py:1346 + pytestconfig [session scope] -- .../_pytest/fixtures.py:1344 Session-scoped fixture that returns the session's :class:`pytest.Config` object. @@ -125,7 +129,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a if pytestconfig.getoption("verbose") > 0: ... - record_property -- .../_pytest/junitxml.py:283 + record_property -- .../_pytest/junitxml.py:280 Add extra properties to the calling test. User properties become part of the test report and are available to the @@ -139,13 +143,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a def test_function(record_property): record_property("example_key", 1) - record_xml_attribute -- .../_pytest/junitxml.py:306 + record_xml_attribute -- .../_pytest/junitxml.py:303 Add extra xml attributes to the tag for the calling test. The fixture is callable with ``name, value``. The value is automatically XML-encoded. - record_testsuite_property [session scope] -- .../_pytest/junitxml.py:344 + record_testsuite_property [session scope] -- .../_pytest/junitxml.py:341 Record a new ```` tag as child of the root ````. This is suitable to writing global information regarding the entire test @@ -170,10 +174,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a `pytest-xdist `__ plugin. See :issue:`7767` for details. - tmpdir_factory [session scope] -- .../_pytest/legacypath.py:303 + tmpdir_factory [session scope] -- .../_pytest/legacypath.py:298 Return a :class:`pytest.TempdirFactory` instance for the test session. - tmpdir -- .../_pytest/legacypath.py:310 + tmpdir -- .../_pytest/legacypath.py:305 Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. @@ -192,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a .. _legacy_path: https://py.readthedocs.io/en/latest/path.html - caplog -- .../_pytest/logging.py:601 + caplog -- .../_pytest/logging.py:600 Access and control log capturing. Captured logs are available through the following properties/methods:: @@ -203,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a * caplog.record_tuples -> list of (logger_name, level, message) tuples * caplog.clear() -> clear captured records and formatted log output string - monkeypatch -- .../_pytest/monkeypatch.py:32 + monkeypatch -- .../_pytest/monkeypatch.py:31 A convenient fixture for monkey-patching. The fixture provides these methods to modify objects, dictionaries, or @@ -227,16 +231,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a To undo modifications done by the fixture in a contained scope, use :meth:`context() `. - recwarn -- .../_pytest/recwarn.py:31 + recwarn -- .../_pytest/recwarn.py:35 Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information on warning categories. - tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:241 + tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:242 Return a :class:`pytest.TempPathFactory` instance for the test session. - tmp_path -- .../_pytest/tmpdir.py:256 + tmp_path -- .../_pytest/tmpdir.py:257 Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. diff --git a/doc/en/changelog.rst b/doc/en/changelog.rst index fb316e706ba..3178f82044d 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -19,15 +19,454 @@ with advance notice in the **Deprecations** section of releases. we named the news folder changelog -.. only:: changelog_towncrier_draft +.. only:: not is_release - .. The 'changelog_towncrier_draft' tag is included by our 'tox -e docs', - but not on readthedocs. + To be included in v\ |release| (if present) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - .. include:: _changelog_towncrier_draft.rst + .. towncrier-draft-entries:: |release| [UNRELEASED DRAFT] + + Released versions + ^^^^^^^^^^^^^^^^^ .. towncrier release notes start +pytest 8.3.2 (2024-07-24) +========================= + +Bug fixes +--------- + +- `#12652 `_: Resolve regression `conda` environments where no longer being automatically detected. + + -- by :user:`RonnyPfannschmidt` + + +pytest 8.3.1 (2024-07-20) +========================= + +The 8.3.0 release failed to include the change notes and docs for the release. This patch release remedies this. There are no other changes. + + +pytest 8.3.0 (2024-07-20) +========================= + +New features +------------ + +- `#12231 `_: Added `--xfail-tb` flag, which turns on traceback output for XFAIL results. + + * If the `--xfail-tb` flag is not given, tracebacks for XFAIL results are NOT shown. + * The style of traceback for XFAIL is set with `--tb`, and can be `auto|long|short|line|native|no`. + * Note: Even if you have `--xfail-tb` set, you won't see them if `--tb=no`. + + Some history: + + With pytest 8.0, `-rx` or `-ra` would not only turn on summary reports for xfail, but also report the tracebacks for xfail results. This caused issues with some projects that utilize xfail, but don't want to see all of the xfail tracebacks. + + This change detaches xfail tracebacks from `-rx`, and now we turn on xfail tracebacks with `--xfail-tb`. With this, the default `-rx`/ `-ra` behavior is identical to pre-8.0 with respect to xfail tracebacks. While this is a behavior change, it brings default behavior back to pre-8.0.0 behavior, which ultimately was considered the better course of action. + + -- by :user:`okken` + + +- `#12281 `_: Added support for keyword matching in marker expressions. + + Now tests can be selected by marker keyword arguments. + Supported values are :class:`int`, (unescaped) :class:`str`, :class:`bool` & :data:`None`. + + See :ref:`marker examples ` for more information. + + -- by :user:`lovetheguitar` + + +- `#12567 `_: Added ``--no-fold-skipped`` command line option. + + If this option is set, then skipped tests in short summary are no longer grouped + by reason but all tests are printed individually with their nodeid in the same + way as other statuses. + + -- by :user:`pbrezina` + + + +Improvements in existing functionality +-------------------------------------- + +- `#12469 `_: The console output now uses the "third-party plugins" terminology, + replacing the previously established but confusing and outdated + reference to :std:doc:`setuptools ` + -- by :user:`webknjaz`. + + +- `#12544 `_, `#12545 `_: Python virtual environment detection was improved by + checking for a :file:`pyvenv.cfg` file, ensuring reliable detection on + various platforms -- by :user:`zachsnickers`. + + +- `#2871 `_: Do not truncate arguments to functions in output when running with `-vvv`. + + +- `#389 `_: The readability of assertion introspection of bound methods has been enhanced + -- by :user:`farbodahm`, :user:`webknjaz`, :user:`obestwalter`, :user:`flub` + and :user:`glyphack`. + + Earlier, it was like: + + .. code-block:: console + + =================================== FAILURES =================================== + _____________________________________ test _____________________________________ + + def test(): + > assert Help().fun() == 2 + E assert 1 == 2 + E + where 1 = >() + E + where > = .fun + E + where = Help() + + example.py:7: AssertionError + =========================== 1 failed in 0.03 seconds =========================== + + + And now it's like: + + .. code-block:: console + + =================================== FAILURES =================================== + _____________________________________ test _____________________________________ + + def test(): + > assert Help().fun() == 2 + E assert 1 == 2 + E + where 1 = fun() + E + where fun = .fun + E + where = Help() + + test_local.py:13: AssertionError + =========================== 1 failed in 0.03 seconds =========================== + + +- `#7662 `_: Added timezone information to the testsuite timestamp in the JUnit XML report. + + + +Bug fixes +--------- + +- `#11706 `_: Fixed reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`. + + Originally added in pytest 8.0.0, but reverted in 8.0.2 due to a regression in pytest-xdist. + This regression was fixed in pytest-xdist 3.6.1. + + +- `#11797 `_: :func:`pytest.approx` now correctly handles :class:`Sequence `-like objects. + + +- `#12204 `_, `#12264 `_: Fixed a regression in pytest 8.0 where tracebacks get longer and longer when multiple + tests fail due to a shared higher-scope fixture which raised -- by :user:`bluetech`. + + Also fixed a similar regression in pytest 5.4 for collectors which raise during setup. + + The fix necessitated internal changes which may affect some plugins: + + * ``FixtureDef.cached_result[2]`` is now a tuple ``(exc, tb)`` + instead of ``exc``. + * ``SetupState.stack`` failures are now a tuple ``(exc, tb)`` + instead of ``exc``. + + +- `#12275 `_: Fixed collection error upon encountering an :mod:`abstract ` class, including abstract `unittest.TestCase` subclasses. + + +- `#12328 `_: Fixed a regression in pytest 8.0.0 where package-scoped parameterized items were not correctly reordered to minimize setups/teardowns in some cases. + + +- `#12424 `_: Fixed crash with `assert testcase is not None` assertion failure when re-running unittest tests using plugins like pytest-rerunfailures. Regressed in 8.2.2. + + +- `#12472 `_: Fixed a crash when returning category ``"error"`` or ``"failed"`` with a custom test status from :hook:`pytest_report_teststatus` hook -- :user:`pbrezina`. + + +- `#12505 `_: Improved handling of invalid regex patterns in :func:`pytest.raises(match=r'...') ` by providing a clear error message. + + +- `#12580 `_: Fixed a crash when using the cache class on Windows and the cache directory was created concurrently. + + +- `#6962 `_: Parametrization parameters are now compared using `==` instead of `is` (`is` is still used as a fallback if the parameter does not support `==`). + This fixes use of parameters such as lists, which have a different `id` but compare equal, causing fixtures to be re-computed instead of being cached. + + +- `#7166 `_: Fixed progress percentages (the ``[ 87%]`` at the edge of the screen) sometimes not aligning correctly when running with pytest-xdist ``-n``. + + + +Improved documentation +---------------------- + +- `#12153 `_: Documented using :envvar:`PYTEST_VERSION` to detect if code is running from within a pytest run. + + +- `#12469 `_: The external plugin mentions in the documentation now avoid mentioning + :std:doc:`setuptools entry-points ` as the concept is + much more generic nowadays. Instead, the terminology of "external", + "installed", or "third-party" plugins (or packages) replaces that. + + -- by :user:`webknjaz` + + +- `#12577 `_: `CI` and `BUILD_NUMBER` environment variables role is discribed in + the reference doc. They now also appear when doing `pytest -h` + -- by :user:`MarcBresson`. + + + +Contributor-facing changes +-------------------------- + +- `#12467 `_: Migrated all internal type-annotations to the python3.10+ style by using the `annotations` future import. + + -- by :user:`RonnyPfannschmidt` + + +- `#11771 `_, `#12557 `_: The PyPy runtime version has been updated to 3.9 from 3.8 that introduced + a flaky bug at the garbage collector which was not expected to fix there + as the 3.8 is EoL. + + -- by :user:`x612skm` + + +- `#12493 `_: The change log draft preview integration has been refactored to use a + third party extension ``sphinxcontib-towncrier``. The previous in-repo + script was putting the change log preview file at + :file:`doc/en/_changelog_towncrier_draft.rst`. Said file is no longer + ignored in Git and might show up among untracked files in the + development environments of the contributors. To address that, the + contributors can run the following command that will clean it up: + + .. code-block:: console + + $ git clean -x -i -- doc/en/_changelog_towncrier_draft.rst + + -- by :user:`webknjaz` + + +- `#12498 `_: All the undocumented ``tox`` environments now have descriptions. + They can be listed in one's development environment by invoking + ``tox -av`` in a terminal. + + -- by :user:`webknjaz` + + +- `#12501 `_: The changelog configuration has been updated to introduce more accurate + audience-tailored categories. Previously, there was a ``trivial`` + change log fragment type with an unclear and broad meaning. It was + removed and we now have ``contrib``, ``misc`` and ``packaging`` in + place of it. + + The new change note types target the readers who are downstream + packagers and project contributors. Additionally, the miscellaneous + section is kept for unspecified updates that do not fit anywhere else. + + -- by :user:`webknjaz` + + +- `#12502 `_: The UX of the GitHub automation making pull requests to update the + plugin list has been updated. Previously, the maintainers had to close + the automatically created pull requests and re-open them to trigger the + CI runs. From now on, they only need to click the `Ready for review` + button instead. + + -- by :user:`webknjaz` + + +- `#12522 `_: The ``:pull:`` RST role has been replaced with a shorter + ``:pr:`` due to starting to use the implementation from + the third-party :pypi:`sphinx-issues` Sphinx extension + -- by :user:`webknjaz`. + + +- `#12531 `_: The coverage reporting configuration has been updated to exclude + pytest's own tests marked as expected to fail from the coverage + report. This has an effect of reducing the influence of flaky + tests on the resulting number. + + -- by :user:`webknjaz` + + +- `#12533 `_: The ``extlinks`` Sphinx extension is no longer enabled. The ``:bpo:`` + role it used to declare has been removed with that. BPO itself has + migrated to GitHub some years ago and it is possible to link the + respective issues by using their GitHub issue numbers and the + ``:issue:`` role that the ``sphinx-issues`` extension implements. + + -- by :user:`webknjaz` + + +- `#12562 `_: Possible typos in using the ``:user:`` RST role is now being linted + through the pre-commit tool integration -- by :user:`webknjaz`. + + +pytest 8.2.2 (2024-06-04) +========================= + +Bug Fixes +--------- + +- `#12355 `_: Fix possible catastrophic performance slowdown on a certain parametrization pattern involving many higher-scoped parameters. + + +- `#12367 `_: Fix a regression in pytest 8.2.0 where unittest class instances (a fresh one is created for each test) were not released promptly on test teardown but only on session teardown. + + +- `#12381 `_: Fix possible "Directory not empty" crashes arising from concurent cache dir (``.pytest_cache``) creation. Regressed in pytest 8.2.0. + + + +Improved Documentation +---------------------- + +- `#12290 `_: Updated Sphinx theme to use Furo instead of Flask, enabling Dark mode theme. + + +- `#12356 `_: Added a subsection to the documentation for debugging flaky tests to mention + lack of thread safety in pytest as a possible source of flakyness. + + +- `#12363 `_: The documentation webpages now links to a canonical version to reduce outdated documentation in search engine results. + + +pytest 8.2.1 (2024-05-19) +========================= + +Improvements +------------ + +- `#12334 `_: Support for Python 3.13 (beta1 at the time of writing). + + + +Bug Fixes +--------- + +- `#12120 `_: Fix `PermissionError` crashes arising from directories which are not selected on the command-line. + + +- `#12191 `_: Keyboard interrupts and system exits are now properly handled during the test collection. + + +- `#12300 `_: Fixed handling of 'Function not implemented' error under squashfuse_ll, which is a different way to say that the mountpoint is read-only. + + +- `#12308 `_: Fix a regression in pytest 8.2.0 where the permissions of automatically-created ``.pytest_cache`` directories became ``rwx------`` instead of the expected ``rwxr-xr-x``. + + + +Trivial/Internal Changes +------------------------ + +- `#12333 `_: pytest releases are now attested using the recent `Artifact Attestation `_ support from GitHub, allowing users to verify the provenance of pytest's sdist and wheel artifacts. + + +pytest 8.2.0 (2024-04-27) +========================= + +Breaking Changes +---------------- + +- `#12089 `_: pytest now requires that :class:`unittest.TestCase` subclasses can be instantiated freely using ``MyTestCase('runTest')``. + + If the class doesn't allow this, you may see an error during collection such as ``AttributeError: 'MyTestCase' object has no attribute 'runTest'``. + + Classes which do not override ``__init__``, or do not access the test method in ``__init__`` using ``getattr`` or similar, are unaffected. + + Classes which do should take care to not crash when ``"runTest"`` is given, as is shown in `unittest.TestCases's implementation `_. + Alternatively, consider using :meth:`setUp ` instead of ``__init__``. + + If you run into this issue using ``tornado.AsyncTestCase``, please see `issue 12263 `_. + + If you run into this issue using an abstract ``TestCase`` subclass, please see `issue 12275 `_. + + Historical note: the effect of this change on custom TestCase implementations was not properly considered initially, this is why it was done in a minor release. We apologize for the inconvenience. + +Deprecations +------------ + +- `#12069 `_: A deprecation warning is now raised when implementations of one of the following hooks request a deprecated ``py.path.local`` parameter instead of the ``pathlib.Path`` parameter which replaced it: + + - :hook:`pytest_ignore_collect` - the ``path`` parameter - use ``collection_path`` instead. + - :hook:`pytest_collect_file` - the ``path`` parameter - use ``file_path`` instead. + - :hook:`pytest_pycollect_makemodule` - the ``path`` parameter - use ``module_path`` instead. + - :hook:`pytest_report_header` - the ``startdir`` parameter - use ``start_path`` instead. + - :hook:`pytest_report_collectionfinish` - the ``startdir`` parameter - use ``start_path`` instead. + + The replacement parameters are available since pytest 7.0.0. + The old parameters will be removed in pytest 9.0.0. + + See :ref:`legacy-path-hooks-deprecated` for more details. + + + +Features +-------- + +- `#11871 `_: Added support for reading command line arguments from a file using the prefix character ``@``, like e.g.: ``pytest @tests.txt``. The file must have one argument per line. + + See :ref:`Read arguments from file ` for details. + + + +Improvements +------------ + +- `#11523 `_: :func:`pytest.importorskip` will now issue a warning if the module could be found, but raised :class:`ImportError` instead of :class:`ModuleNotFoundError`. + + The warning can be suppressed by passing ``exc_type=ImportError`` to :func:`pytest.importorskip`. + + See :ref:`import-or-skip-import-error` for details. + + +- `#11728 `_: For ``unittest``-based tests, exceptions during class cleanup (as raised by functions registered with :meth:`TestCase.addClassCleanup `) are now reported instead of silently failing. + + +- `#11777 `_: Text is no longer truncated in the ``short test summary info`` section when ``-vv`` is given. + + +- `#12112 `_: Improved namespace packages detection when :confval:`consider_namespace_packages` is enabled, covering more situations (like editable installs). + + +- `#9502 `_: Added :envvar:`PYTEST_VERSION` environment variable which is defined at the start of the pytest session and undefined afterwards. It contains the value of ``pytest.__version__``, and among other things can be used to easily check if code is running from within a pytest run. + + + +Bug Fixes +--------- + +- `#12065 `_: Fixed a regression in pytest 8.0.0 where test classes containing ``setup_method`` and tests using ``@staticmethod`` or ``@classmethod`` would crash with ``AttributeError: 'NoneType' object has no attribute 'setup_method'``. + + Now the :attr:`request.instance ` attribute of tests using ``@staticmethod`` and ``@classmethod`` is no longer ``None``, but a fresh instance of the class, like in non-static methods. + Previously it was ``None``, and all fixtures of such tests would share a single ``self``. + + +- `#12135 `_: Fixed issue where fixtures adding their finalizer multiple times to fixtures they request would cause unreliable and non-intuitive teardown ordering in some instances. + + +- `#12194 `_: Fixed a bug with ``--importmode=importlib`` and ``--doctest-modules`` where child modules did not appear as attributes in parent modules. + + +- `#1489 `_: Fixed some instances where teardown of higher-scoped fixtures was not happening in the reverse order they were initialized in. + + + +Trivial/Internal Changes +------------------------ + +- `#12069 `_: ``pluggy>=1.5.0`` is now required. + + +- `#12167 `_: :ref:`cache `: create supporting files (``CACHEDIR.TAG``, ``.gitignore``, etc.) in a temporary directory to provide atomic semantics. + + pytest 8.1.2 (2024-04-26) ========================= @@ -69,7 +508,7 @@ Improvements - `#11311 `_: When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used as the relative directory. - Previoulsy this would raise an :class:`AssertionError`. + Previously this would raise an :class:`AssertionError`. - `#11475 `_: :ref:`--import-mode=importlib ` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails. @@ -105,7 +544,7 @@ Bug Fixes - `#11904 `_: Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using ``--pyargs``. - This change improves the collection tree for tests specified using ``--pyargs``, see :pull:`12043` for a comparison with pytest 8.0 and <8. + This change improves the collection tree for tests specified using ``--pyargs``, see :pr:`12043` for a comparison with pytest 8.0 and <8. - `#12011 `_: Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed. @@ -1259,7 +1698,7 @@ Bug Fixes tests/link -> tests/real running ``pytest tests`` now imports the conftest twice, once as ``tests/real/conftest.py`` and once as ``tests/link/conftest.py``. - This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pull:`6523` for details). + This is a fix to match a similar change made to test collection itself in pytest 6.0 (see :pr:`6523` for details). - `#9626 `_: Fixed count of selected tests on terminal collection summary when there were errors or skipped modules. @@ -1312,7 +1751,7 @@ Deprecations ``__init__`` method, they should take ``**kwargs``. See :ref:`uncooperative-constructors-deprecated` for details. - Note that a deprection warning is only emitted when there is a conflict in the + Note that a deprecation warning is only emitted when there is a conflict in the arguments pytest expected to pass. This deprecation was already part of pytest 7.0.0rc1 but wasn't documented. @@ -1354,7 +1793,7 @@ Breaking Changes - `#7259 `_: The :ref:`Node.reportinfo() ` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`. Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation. - Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted. + Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffected. Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`. Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead. @@ -1862,7 +2301,7 @@ Bug Fixes the ``tmp_path``/``tmpdir`` fixture). Now the directories are created with private permissions. - pytest used to silently use a pre-existing ``/tmp/pytest-of-`` directory, + pytest used to silently use a preexisting ``/tmp/pytest-of-`` directory, even if owned by another user. This means another user could pre-create such a directory and gain control of another user's temporary directory. Now such a condition results in an error. @@ -2428,7 +2867,7 @@ Breaking Changes Resolving symlinks for the current directory and during collection was introduced as a bugfix in 3.9.0, but it actually is a new feature which had unfortunate consequences in Windows and surprising results in other platforms. The team decided to step back on resolving symlinks at all, planning to review this in the future with a more solid solution (see discussion in - :pull:`6523` for details). + :pr:`6523` for details). This might break test suites which made use of this feature; the fix is to create a symlink for the entire test tree, and not only to partial files/tress as it was possible previously. @@ -2589,7 +3028,7 @@ Features also changes ``sys.modules`` as a side-effect), which works but has a number of drawbacks, like requiring test modules that don't live in packages to have unique names (as they need to reside under a unique name in ``sys.modules``). - ``--import-mode=importlib`` uses more fine grained import mechanisms from ``importlib`` which don't + ``--import-mode=importlib`` uses more fine-grained import mechanisms from ``importlib`` which don't require pytest to change ``sys.path`` or ``sys.modules`` at all, eliminating much of the drawbacks of the previous mode. @@ -2606,7 +3045,7 @@ Improvements ------------ - :issue:`4375`: The ``pytest`` command now suppresses the ``BrokenPipeError`` error message that - is printed to stderr when the output of ``pytest`` is piped and and the pipe is + is printed to stderr when the output of ``pytest`` is piped and the pipe is closed by the piped-to program (common examples are ``less`` and ``head``). @@ -2711,7 +3150,7 @@ Bug Fixes - :issue:`6871`: Fix crash with captured output when using :fixture:`capsysbinary`. -- :issue:`6909`: Revert the change introduced by :pull:`6330`, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature. +- :issue:`6909`: Revert the change introduced by :pr:`6330`, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature. The intention of the original change was to remove what was expected to be an unintended/surprising behavior, but it turns out many people relied on it, so the restriction has been reverted. @@ -2881,7 +3320,7 @@ pytest 5.4.1 (2020-03-13) Bug Fixes --------- -- :issue:`6909`: Revert the change introduced by :pull:`6330`, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature. +- :issue:`6909`: Revert the change introduced by :pr:`6330`, which required all arguments to ``@pytest.mark.parametrize`` to be explicitly defined in the function signature. The intention of the original change was to remove what was expected to be an unintended/surprising behavior, but it turns out many people relied on it, so the restriction has been reverted. @@ -2908,7 +3347,7 @@ Breaking Changes This hook has been marked as deprecated and not been even called by pytest for over 10 years now. -- :issue:`6673`: Reversed / fix meaning of "+/-" in error diffs. "-" means that sth. expected is missing in the result and "+" means that there are unexpected extras in the result. +- :issue:`6673`: Reversed / fix meaning of "+/-" in error diffs. "-" means that something expected is missing in the result and "+" means that there are unexpected extras in the result. - :issue:`6737`: The ``cached_result`` attribute of ``FixtureDef`` is now set to ``None`` when @@ -3197,7 +3636,9 @@ Bug Fixes - :issue:`5914`: pytester: fix :py:func:`~pytest.LineMatcher.no_fnmatch_line` when used after positive matching. -- :issue:`6082`: Fix line detection for doctest samples inside :py:class:`python:property` docstrings, as a workaround to :bpo:`17446`. +- :issue:`6082`: Fix line detection for doctest samples inside + :py:class:`python:property` docstrings, as a workaround to + :issue:`python/cpython#61648`. - :issue:`6254`: Fix compatibility with pytest-parallel (regression in pytest 5.3.0). @@ -3904,7 +4345,7 @@ Bug Fixes (``--collect-only``) when ``--log-cli-level`` is used. -- :issue:`5389`: Fix regressions of :pull:`5063` for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. +- :issue:`5389`: Fix regressions of :pr:`5063` for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. - :issue:`5390`: Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods. @@ -4105,7 +4546,7 @@ Bug Fixes (``--collect-only``) when ``--log-cli-level`` is used. -- :issue:`5389`: Fix regressions of :pull:`5063` for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. +- :issue:`5389`: Fix regressions of :pr:`5063` for ``importlib_metadata.PathDistribution`` which have their ``files`` attribute being ``None``. - :issue:`5390`: Fix regression where the ``obj`` attribute of ``TestCase`` items was no longer bound to methods. @@ -4513,7 +4954,7 @@ Bug Fixes Improved Documentation ---------------------- -- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability liminations +- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability limitations @@ -6371,7 +6812,7 @@ Features Bug Fixes --------- -- Fix hanging pexpect test on MacOS by using flush() instead of wait(). +- Fix hanging pexpect test on macOS by using flush() instead of wait(). (:issue:`2022`) - Fix restoring Python state after in-process pytest runs with the @@ -6419,7 +6860,7 @@ Trivial/Internal Changes ------------------------ - Show a simple and easy error when keyword expressions trigger a syntax error - (for example, ``"-k foo and import"`` will show an error that you can not use + (for example, ``"-k foo and import"`` will show an error that you cannot use the ``import`` keyword in expressions). (:issue:`2953`) - Change parametrized automatic test id generation to use the ``__name__`` @@ -7066,10 +7507,10 @@ New Features * Added ``junit_suite_name`` ini option to specify root ```` name for JUnit XML reports (:issue:`533`). * Added an ini option ``doctest_encoding`` to specify which encoding to use for doctest files. - Thanks :user:`wheerd` for the PR (:pull:`2101`). + Thanks :user:`wheerd` for the PR (:pr:`2101`). * ``pytest.warns`` now checks for subclass relationship rather than - class equality. Thanks :user:`lesteve` for the PR (:pull:`2166`) + class equality. Thanks :user:`lesteve` for the PR (:pr:`2166`) * ``pytest.raises`` now asserts that the error message matches a text or regex with the ``match`` keyword argument. Thanks :user:`Kriechi` for the PR. @@ -7097,7 +7538,7 @@ Changes the failure. (:issue:`2228`) Thanks to :user:`kkoukiou` for the PR. * Testcase reports with a ``url`` attribute will now properly write this to junitxml. - Thanks :user:`fushi` for the PR (:pull:`1874`). + Thanks :user:`fushi` for the PR (:pr:`1874`). * Remove common items from dict comparison output when verbosity=1. Also update the truncation message to make it clearer that pytest truncates all @@ -7106,7 +7547,7 @@ Changes * ``--pdbcls`` no longer implies ``--pdb``. This makes it possible to use ``addopts=--pdbcls=module.SomeClass`` on ``pytest.ini``. Thanks :user:`davidszotten` for - the PR (:pull:`1952`). + the PR (:pr:`1952`). * fix :issue:`2013`: turn RecordedWarning into ``namedtuple``, to give it a comprehensible repr while preventing unwarranted modification. @@ -7360,7 +7801,7 @@ Bug Fixes a sequence of strings) when modules are considered for assertion rewriting. Due to this bug, much more modules were being rewritten than necessary if a test suite uses ``pytest_plugins`` to load internal plugins (:issue:`1888`). - Thanks :user:`jaraco` for the report and :user:`nicoddemus` for the PR (:pull:`1891`). + Thanks :user:`jaraco` for the report and :user:`nicoddemus` for the PR (:pr:`1891`). * Do not call tearDown and cleanups when running tests from ``unittest.TestCase`` subclasses with ``--pdb`` @@ -7415,12 +7856,12 @@ time or change existing behaviors in order to make them less surprising/more use * ``--nomagic``: use ``--assert=plain`` instead; * ``--report``: use ``-r`` instead; - Thanks to :user:`RedBeardCode` for the PR (:pull:`1664`). + Thanks to :user:`RedBeardCode` for the PR (:pr:`1664`). * ImportErrors in plugins now are a fatal error instead of issuing a pytest warning (:issue:`1479`). Thanks to :user:`The-Compiler` for the PR. -* Removed support code for Python 3 versions < 3.3 (:pull:`1627`). +* Removed support code for Python 3 versions < 3.3 (:pr:`1627`). * Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points were never documented and a leftover from a pre-virtualenv era. These entry @@ -7431,19 +7872,19 @@ time or change existing behaviors in order to make them less surprising/more use * ``pytest.skip()`` now raises an error when used to decorate a test function, as opposed to its original intent (to imperatively skip a test inside a test function). Previously this usage would cause the entire module to be skipped (:issue:`607`). - Thanks :user:`omarkohl` for the complete PR (:pull:`1519`). + Thanks :user:`omarkohl` for the complete PR (:pr:`1519`). * Exit tests if a collection error occurs. A poll indicated most users will hit CTRL-C anyway as soon as they see collection errors, so pytest might as well make that the default behavior (:issue:`1421`). A ``--continue-on-collection-errors`` option has been added to restore the previous behaviour. - Thanks :user:`olegpidsadnyi` and :user:`omarkohl` for the complete PR (:pull:`1628`). + Thanks :user:`olegpidsadnyi` and :user:`omarkohl` for the complete PR (:pr:`1628`). * Renamed the pytest ``pdb`` module (plugin) into ``debugging`` to avoid clashes with the builtin ``pdb`` module. * Raise a helpful failure message when requesting a parametrized fixture at runtime, e.g. with ``request.getfixturevalue``. Previously these parameters were simply never defined, so a fixture decorated like ``@pytest.fixture(params=[0, 1, 2])`` - only ran once (:pull:`460`). + only ran once (:pr:`460`). Thanks to :user:`nikratio` for the bug report, :user:`RedBeardCode` and :user:`tomviner` for the PR. * ``_pytest.monkeypatch.monkeypatch`` class has been renamed to ``_pytest.monkeypatch.MonkeyPatch`` @@ -7461,7 +7902,7 @@ time or change existing behaviors in order to make them less surprising/more use * New ``doctest_namespace`` fixture for injecting names into the namespace in which doctests run. - Thanks :user:`milliams` for the complete PR (:pull:`1428`). + Thanks :user:`milliams` for the complete PR (:pr:`1428`). * New ``--doctest-report`` option available to change the output format of diffs when running (failing) doctests (implements :issue:`1749`). @@ -7469,23 +7910,23 @@ time or change existing behaviors in order to make them less surprising/more use * New ``name`` argument to ``pytest.fixture`` decorator which allows a custom name for a fixture (to solve the funcarg-shadowing-fixture problem). - Thanks :user:`novas0x2a` for the complete PR (:pull:`1444`). + Thanks :user:`novas0x2a` for the complete PR (:pr:`1444`). * New ``approx()`` function for easily comparing floating-point numbers in tests. - Thanks :user:`kalekundert` for the complete PR (:pull:`1441`). + Thanks :user:`kalekundert` for the complete PR (:pr:`1441`). * Ability to add global properties in the final xunit output file by accessing the internal ``junitxml`` plugin (experimental). - Thanks :user:`tareqalayan` for the complete PR :pull:`1454`). + Thanks :user:`tareqalayan` for the complete PR :pr:`1454`). * New ``ExceptionInfo.match()`` method to match a regular expression on the string representation of an exception (:issue:`372`). - Thanks :user:`omarkohl` for the complete PR (:pull:`1502`). + Thanks :user:`omarkohl` for the complete PR (:pr:`1502`). * ``__tracebackhide__`` can now also be set to a callable which then can decide whether to filter the traceback based on the ``ExceptionInfo`` object passed - to it. Thanks :user:`The-Compiler` for the complete PR (:pull:`1526`). + to it. Thanks :user:`The-Compiler` for the complete PR (:pr:`1526`). * New ``pytest_make_parametrize_id(config, val)`` hook which can be used by plugins to provide friendly strings for custom types. @@ -7503,7 +7944,7 @@ time or change existing behaviors in order to make them less surprising/more use * Introduce ``pytest`` command as recommended entry point. Note that ``py.test`` still works and is not scheduled for removal. Closes proposal :issue:`1629`. Thanks :user:`obestwalter` and :user:`davehunt` for the complete PR - (:pull:`1633`). + (:pr:`1633`). * New cli flags: @@ -7547,19 +7988,19 @@ time or change existing behaviors in order to make them less surprising/more use * Change ``report.outcome`` for ``xpassed`` tests to ``"passed"`` in non-strict mode and ``"failed"`` in strict mode. Thanks to :user:`hackebrot` for the PR - (:pull:`1795`) and :user:`gprasad84` for report (:issue:`1546`). + (:pr:`1795`) and :user:`gprasad84` for report (:issue:`1546`). * Tests marked with ``xfail(strict=False)`` (the default) now appear in JUnitXML reports as passing tests instead of skipped. - Thanks to :user:`hackebrot` for the PR (:pull:`1795`). + Thanks to :user:`hackebrot` for the PR (:pr:`1795`). * Highlight path of the file location in the error report to make it easier to copy/paste. - Thanks :user:`suzaku` for the PR (:pull:`1778`). + Thanks :user:`suzaku` for the PR (:pr:`1778`). * Fixtures marked with ``@pytest.fixture`` can now use ``yield`` statements exactly like those marked with the ``@pytest.yield_fixture`` decorator. This change renders ``@pytest.yield_fixture`` deprecated and makes ``@pytest.fixture`` with ``yield`` statements - the preferred way to write teardown code (:pull:`1461`). + the preferred way to write teardown code (:pr:`1461`). Thanks :user:`csaftoiu` for bringing this to attention and :user:`nicoddemus` for the PR. * Explicitly passed parametrize ids do not get escaped to ascii (:issue:`1351`). @@ -7570,11 +8011,11 @@ time or change existing behaviors in order to make them less surprising/more use Thanks :user:`nicoddemus` for the PR. * ``pytest_terminal_summary`` hook now receives the ``exitstatus`` - of the test session as argument. Thanks :user:`blueyed` for the PR (:pull:`1809`). + of the test session as argument. Thanks :user:`blueyed` for the PR (:pr:`1809`). * Parametrize ids can accept ``None`` as specific test id, in which case the automatically generated id for that argument will be used. - Thanks :user:`palaviv` for the complete PR (:pull:`1468`). + Thanks :user:`palaviv` for the complete PR (:pr:`1468`). * The parameter to xunit-style setup/teardown methods (``setup_method``, ``setup_module``, etc.) is now optional and may be omitted. @@ -7582,32 +8023,32 @@ time or change existing behaviors in order to make them less surprising/more use * Improved automatic id generation selection in case of duplicate ids in parametrize. - Thanks :user:`palaviv` for the complete PR (:pull:`1474`). + Thanks :user:`palaviv` for the complete PR (:pr:`1474`). * Now pytest warnings summary is shown up by default. Added a new flag ``--disable-pytest-warnings`` to explicitly disable the warnings summary (:issue:`1668`). * Make ImportError during collection more explicit by reminding the user to check the name of the test module/package(s) (:issue:`1426`). - Thanks :user:`omarkohl` for the complete PR (:pull:`1520`). + Thanks :user:`omarkohl` for the complete PR (:pr:`1520`). * Add ``build/`` and ``dist/`` to the default ``--norecursedirs`` list. Thanks :user:`mikofski` for the report and :user:`tomviner` for the PR (:issue:`1544`). * ``pytest.raises`` in the context manager form accepts a custom ``message`` to raise when no exception occurred. - Thanks :user:`palaviv` for the complete PR (:pull:`1616`). + Thanks :user:`palaviv` for the complete PR (:pr:`1616`). * ``conftest.py`` files now benefit from assertion rewriting; previously it was only available for test modules. Thanks :user:`flub`, :user:`sober7` and :user:`nicoddemus` for the PR (:issue:`1619`). * Text documents without any doctests no longer appear as "skipped". - Thanks :user:`graingert` for reporting and providing a full PR (:pull:`1580`). + Thanks :user:`graingert` for reporting and providing a full PR (:pr:`1580`). * Ensure that a module within a namespace package can be found when it is specified on the command line together with the ``--pyargs`` - option. Thanks to :user:`taschini` for the PR (:pull:`1597`). + option. Thanks to :user:`taschini` for the PR (:pr:`1597`). * Always include full assertion explanation during assertion rewriting. The previous behaviour was hiding sub-expressions that happened to be ``False``, assuming this was redundant information. @@ -7623,20 +8064,20 @@ time or change existing behaviors in order to make them less surprising/more use Thanks :user:`nicoddemus` for the PR. * ``[pytest]`` sections in ``setup.cfg`` files should now be named ``[tool:pytest]`` - to avoid conflicts with other distutils commands (see :pull:`567`). ``[pytest]`` sections in + to avoid conflicts with other distutils commands (see :pr:`567`). ``[pytest]`` sections in ``pytest.ini`` or ``tox.ini`` files are supported and unchanged. Thanks :user:`nicoddemus` for the PR. * Using ``pytest_funcarg__`` prefix to declare fixtures is considered deprecated and will be - removed in pytest-4.0 (:pull:`1684`). + removed in pytest-4.0 (:pr:`1684`). Thanks :user:`nicoddemus` for the PR. * Passing a command-line string to ``pytest.main()`` is considered deprecated and scheduled - for removal in pytest-4.0. It is recommended to pass a list of arguments instead (:pull:`1723`). + for removal in pytest-4.0. It is recommended to pass a list of arguments instead (:pr:`1723`). * Rename ``getfuncargvalue`` to ``getfixturevalue``. ``getfuncargvalue`` is still present but is now considered deprecated. Thanks to :user:`RedBeardCode` and :user:`tomviner` - for the PR (:pull:`1626`). + for the PR (:pr:`1626`). * ``optparse`` type usage now triggers DeprecationWarnings (:issue:`1740`). @@ -7694,11 +8135,11 @@ time or change existing behaviors in order to make them less surprising/more use :user:`tomviner` for the PR. * ``ConftestImportFailure`` now shows the traceback making it easier to - identify bugs in ``conftest.py`` files (:pull:`1516`). Thanks :user:`txomon` for + identify bugs in ``conftest.py`` files (:pr:`1516`). Thanks :user:`txomon` for the PR. * Text documents without any doctests no longer appear as "skipped". - Thanks :user:`graingert` for reporting and providing a full PR (:pull:`1580`). + Thanks :user:`graingert` for reporting and providing a full PR (:pr:`1580`). * Fixed collection of classes with custom ``__new__`` method. Fixes :issue:`1579`. Thanks to :user:`Stranger6667` for the PR. @@ -7706,7 +8147,7 @@ time or change existing behaviors in order to make them less surprising/more use * Fixed scope overriding inside metafunc.parametrize (:issue:`634`). Thanks to :user:`Stranger6667` for the PR. -* Fixed the total tests tally in junit xml output (:pull:`1798`). +* Fixed the total tests tally in junit xml output (:pr:`1798`). Thanks to :user:`cboelsen` for the PR. * Fixed off-by-one error with lines from ``request.node.warn``. @@ -7723,14 +8164,14 @@ time or change existing behaviors in order to make them less surprising/more use * Fix Xfail does not work with condition keyword argument. Thanks :user:`astraw38` for reporting the issue (:issue:`1496`) and :user:`tomviner` - for PR the (:pull:`1524`). + for PR the (:pr:`1524`). * Fix win32 path issue when putting custom config file with absolute path in ``pytest.main("-c your_absolute_path")``. * Fix maximum recursion depth detection when raised error class is not aware of unicode/encoded bytes. - Thanks :user:`prusse-martin` for the PR (:pull:`1506`). + Thanks :user:`prusse-martin` for the PR (:pr:`1506`). * Fix ``pytest.mark.skip`` mark when used in strict mode. Thanks :user:`pquentin` for the PR and :user:`RonnyPfannschmidt` for @@ -7757,7 +8198,7 @@ time or change existing behaviors in order to make them less surprising/more use Thanks :user:`nicoddemus` for the PR. * Fix (:issue:`469`): junit parses report.nodeid incorrectly, when params IDs - contain ``::``. Thanks :user:`tomviner` for the PR (:pull:`1431`). + contain ``::``. Thanks :user:`tomviner` for the PR (:pr:`1431`). * Fix (:issue:`578`): SyntaxErrors containing non-ascii lines at the point of failure generated an internal @@ -7778,7 +8219,7 @@ time or change existing behaviors in order to make them less surprising/more use **New Features** * New ``pytest.mark.skip`` mark, which unconditionally skips marked tests. - Thanks :user:`MichaelAquilina` for the complete PR (:pull:`1040`). + Thanks :user:`MichaelAquilina` for the complete PR (:pr:`1040`). * ``--doctest-glob`` may now be passed multiple times in the command-line. Thanks :user:`jab` and :user:`nicoddemus` for the PR. @@ -7789,14 +8230,14 @@ time or change existing behaviors in order to make them less surprising/more use * ``pytest.mark.xfail`` now has a ``strict`` option, which makes ``XPASS`` tests to fail the test suite (defaulting to ``False``). There's also a ``xfail_strict`` ini option that can be used to configure it project-wise. - Thanks :user:`rabbbit` for the request and :user:`nicoddemus` for the PR (:pull:`1355`). + Thanks :user:`rabbbit` for the request and :user:`nicoddemus` for the PR (:pr:`1355`). * ``Parser.addini`` now supports options of type ``bool``. Thanks :user:`nicoddemus` for the PR. * New ``ALLOW_BYTES`` doctest option. This strips ``b`` prefixes from byte strings in doctest output (similar to ``ALLOW_UNICODE``). - Thanks :user:`jaraco` for the request and :user:`nicoddemus` for the PR (:pull:`1287`). + Thanks :user:`jaraco` for the request and :user:`nicoddemus` for the PR (:pr:`1287`). * Give a hint on ``KeyboardInterrupt`` to use the ``--fulltrace`` option to show the errors. Fixes :issue:`1366`. @@ -7828,7 +8269,7 @@ time or change existing behaviors in order to make them less surprising/more use * Removed code and documentation for Python 2.5 or lower versions, including removal of the obsolete ``_pytest.assertion.oldinterpret`` module. - Thanks :user:`nicoddemus` for the PR (:pull:`1226`). + Thanks :user:`nicoddemus` for the PR (:pr:`1226`). * Comparisons now always show up in full when ``CI`` or ``BUILD_NUMBER`` is found in the environment, even when ``-vv`` isn't used. @@ -8195,7 +8636,7 @@ time or change existing behaviors in order to make them less surprising/more use one will also have a "reprec" attribute with the recorded events/reports. - fix monkeypatch.setattr("x.y", raising=False) to actually not raise - if "y" is not a pre-existing attribute. Thanks Florian Bruhin. + if "y" is not a preexisting attribute. Thanks Florian Bruhin. - fix issue741: make running output from testdir.run copy/pasteable Thanks Bruno Oliveira. @@ -8251,7 +8692,7 @@ time or change existing behaviors in order to make them less surprising/more use - fix issue854: autouse yield_fixtures defined as class members of unittest.TestCase subclasses now work as expected. - Thannks xmo-odoo for the report and Bruno Oliveira for the PR. + Thanks xmo-odoo for the report and Bruno Oliveira for the PR. - fix issue833: --fixtures now shows all fixtures of collected test files, instead of just the fixtures declared on the first one. @@ -8355,7 +8796,7 @@ time or change existing behaviors in order to make them less surprising/more use github. See https://pytest.org/en/stable/contributing.html . Thanks to Anatoly for pushing and initial work on this. -- fix issue650: new option ``--docttest-ignore-import-errors`` which +- fix issue650: new option ``--doctest-ignore-import-errors`` which will turn import errors in doctests into skips. Thanks Charles Cloud for the complete PR. @@ -8543,7 +8984,7 @@ time or change existing behaviors in order to make them less surprising/more use - cleanup setup.py a bit and specify supported versions. Thanks Jurko Gospodnetic for the PR. -- change XPASS colour to yellow rather then red when tests are run +- change XPASS colour to yellow rather than red when tests are run with -v. - fix issue473: work around mock putting an unbound method into a class @@ -8716,7 +9157,7 @@ time or change existing behaviors in order to make them less surprising/more use Thanks Ralph Schmitt for the precise failure example. - fix issue244 by implementing special index for parameters to only use - indices for paramentrized test ids + indices for parametrized test ids - fix issue287 by running all finalizers but saving the exception from the first failing finalizer and re-raising it so teardown will @@ -8724,7 +9165,7 @@ time or change existing behaviors in order to make them less surprising/more use it might be the cause for other finalizers to fail. - fix ordering when mock.patch or other standard decorator-wrappings - are used with test methods. This fixues issue346 and should + are used with test methods. This fixes issue346 and should help with random "xdist" collection failures. Thanks to Ronny Pfannschmidt and Donald Stufft for helping to isolate it. @@ -8981,7 +9422,7 @@ Bug fixes: partially failed (finalizers would not always be called before) - fix issue320 - fix class scope for fixtures when mixed with - module-level functions. Thanks Anatloy Bubenkoff. + module-level functions. Thanks Anatoly Bubenkoff. - you can specify "-q" or "-qq" to get different levels of "quieter" reporting (thanks Katarzyna Jachim) @@ -9403,7 +9844,7 @@ Bug fixes: unexpected exceptions - fix issue47: timing output in junitxml for test cases is now correct - fix issue48: typo in MarkInfo repr leading to exception -- fix issue49: avoid confusing error when initizaliation partially fails +- fix issue49: avoid confusing error when initialization partially fails - fix issue44: env/username expansion for junitxml file path - show releaselevel information in test runs for pypy - reworked doc pages for better navigation and PDF generation @@ -9528,7 +9969,7 @@ Bug fixes: collection-before-running semantics were not setup as with pytest 1.3.4. Note, however, that the recommended and much cleaner way to do test - parametraization remains the "pytest_generate_tests" + parameterization remains the "pytest_generate_tests" mechanism, see the docs. 2.0.0 (2010-11-25) diff --git a/doc/en/conf.py b/doc/en/conf.py index 32ecaa17435..9558a75f927 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -1,80 +1,42 @@ -# -# pytest documentation build configuration file, created by -# sphinx-quickstart on Fri Oct 8 17:54:28 2010. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The full version, including alpha/beta/rc tags. -# The short X.Y version. +from __future__ import annotations + import os +from pathlib import Path import shutil -import sys from textwrap import dedent from typing import TYPE_CHECKING -from _pytest import __version__ as version +from pytest import __version__ as full_version if TYPE_CHECKING: import sphinx.application +PROJECT_ROOT_DIR = Path(__file__).parents[2].resolve() -release = ".".join(version.split(".")[:2]) - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# sys.path.insert(0, os.path.abspath('.')) - -autodoc_member_order = "bysource" -autodoc_typehints = "description" -autodoc_typehints_description_target = "documented" -todo_include_todos = 1 - -latex_engine = "lualatex" - -latex_elements = { - "preamble": dedent( - r""" - \directlua{ - luaotfload.add_fallback("fallbacks", { - "Noto Serif CJK SC:style=Regular;", - "Symbola:Style=Regular;" - }) - } +# -- Project information --------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information - \setmainfont{FreeSerif}[RawFeature={fallback=fallbacks}] - """ - ) -} - -# -- General configuration ----------------------------------------------------- +project = "pytest" +copyright = "2015, holger krekel and pytest-dev team" +version = full_version.split("+")[0] +release = ".".join(version.split(".")[:2]) -# If your documentation needs a minimal Sphinx version, state it here. -# needs_sphinx = '1.0' +# -- General configuration ------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +root_doc = "index" extensions = [ - "pallets_sphinx_themes", "pygments_pytest", "sphinx.ext.autodoc", "sphinx.ext.autosummary", - "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.viewcode", "sphinx_removed_in", "sphinxcontrib_trio", + "sphinxcontrib.towncrier.ext", # provides `towncrier-draft-entries` directive + "sphinx_issues", # implements `:issue:`, `:pr:` and other GH-related roles ] # Building PDF docs on readthedocs requires inkscape for svg to pdf @@ -84,35 +46,6 @@ if shutil.which("inkscape"): extensions.append("sphinxcontrib.inkscapeconverter") -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# The suffix of source filenames. -source_suffix = ".rst" - -# The encoding of source files. -# source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = "contents" - -# General information about the project. -project = "pytest" -copyright = "2015, holger krekel and pytest-dev team" - - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. exclude_patterns = [ "_build", "naming20.rst", @@ -124,52 +57,9 @@ "setup.rst", "example/remoteinterp.rst", ] - - -# The reST default role (used for this markup: `text`) to use for all documents. +templates_path = ["_templates"] default_role = "literal" -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = False - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = "sphinx" - - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] - -# A list of regular expressions that match URIs that should not be checked when -# doing a linkcheck. -linkcheck_ignore = [ - "https://blogs.msdn.microsoft.com/bharry/2017/06/28/testing-in-a-cloud-delivery-cadence/", - "http://pythontesting.net/framework/pytest-introduction/", - r"https://github.com/pytest-dev/pytest/issues/\d+", - r"https://github.com/pytest-dev/pytest/pull/\d+", -] - -# The number of worker threads to use when checking links (default=5). -linkcheck_workers = 5 - - -_repo = "https://github.com/pytest-dev/pytest" -extlinks = { - "bpo": ("https://bugs.python.org/issue%s", "bpo-%s"), - "pypi": ("https://pypi.org/project/%s/", "%s"), - "issue": (f"{_repo}/issues/%s", "issue #%s"), - "pull": (f"{_repo}/pull/%s", "pull request #%s"), - "user": ("https://github.com/%s", "@%s"), -} - - nitpicky = True nitpick_ignore = [ # TODO (fix in pluggy?) @@ -183,6 +73,7 @@ ("py:class", "SubRequest"), ("py:class", "TerminalReporter"), ("py:class", "_pytest._code.code.TerminalRepr"), + ("py:class", "TerminalRepr"), ("py:class", "_pytest.fixtures.FixtureFunctionMarker"), ("py:class", "_pytest.logging.LogCaptureHandler"), ("py:class", "_pytest.mark.structures.ParameterSet"), @@ -204,129 +95,103 @@ ("py:class", "_PluggyPlugin"), # TypeVars ("py:class", "_pytest._code.code.E"), + ("py:class", "E"), # due to delayed annotation ("py:class", "_pytest.fixtures.FixtureFunction"), ("py:class", "_pytest.nodes._NodeType"), + ("py:class", "_NodeType"), # due to delayed annotation ("py:class", "_pytest.python_api.E"), ("py:class", "_pytest.recwarn.T"), ("py:class", "_pytest.runner.TResult"), ("py:obj", "_pytest.fixtures.FixtureValue"), ("py:obj", "_pytest.stash.T"), + ("py:class", "_ScopeName"), ] +add_module_names = False -# -- Options for HTML output --------------------------------------------------- +# -- Options for Autodoc -------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#configuration -sys.path.append(os.path.abspath("_themes")) -html_theme_path = ["_themes"] +autodoc_member_order = "bysource" +autodoc_typehints = "description" +autodoc_typehints_description_target = "documented" -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = "flask" +# -- Options for intersphinx ---------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html#configuration -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# html_theme_options = {"index_logo": None} +intersphinx_mapping = { + "pluggy": ("https://pluggy.readthedocs.io/en/stable", None), + "python": ("https://docs.python.org/3", None), + "numpy": ("https://numpy.org/doc/stable", None), + "pip": ("https://pip.pypa.io/en/stable", None), + "tox": ("https://tox.wiki/en/stable", None), + "virtualenv": ("https://virtualenv.pypa.io/en/stable", None), + "setuptools": ("https://setuptools.pypa.io/en/stable", None), + "packaging": ("https://packaging.python.org/en/latest", None), +} -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] +# -- Options for todo ----------------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/extensions/todo.html#configuration -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -html_title = "pytest documentation" +todo_include_todos = True -# A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = "pytest-%s" % release +# -- Options for linkcheck builder ---------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-the-linkcheck-builder -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -html_logo = "img/pytest_logo_curves.svg" +linkcheck_ignore = [ + "https://blogs.msdn.microsoft.com/bharry/2017/06/28/testing-in-a-cloud-delivery-cadence/", + "http://pythontesting.net/framework/pytest-introduction/", + r"https://github.com/pytest-dev/pytest/issues/\d+", + r"https://github.com/pytest-dev/pytest/pull/\d+", +] +linkcheck_workers = 5 -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -html_favicon = "img/favicon.png" +# -- Options for HTML output ---------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -# html_static_path = ['_static'] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -# html_sidebars = {} -# html_sidebars = {'index': 'indexsidebar.html'} - -html_sidebars = { - "index": [ - "slim_searchbox.html", - "sidebarintro.html", - "globaltoc.html", - "links.html", - "sourcelink.html", - ], - "**": [ - "slim_searchbox.html", - "globaltoc.html", - "relations.html", - "links.html", - "sourcelink.html", - ], -} +html_theme = "furo" +html_theme_options = {"sidebar_hide_name": True} -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} -# html_additional_pages = {'index': 'index.html'} +html_static_path = ["_static"] +html_css_files = [ + "pytest-custom.css", +] +html_title = "pytest documentation" +html_short_title = f"pytest-{release}" -# If false, no module index is generated. -html_domain_indices = True +html_logo = "_static/pytest1.png" +html_favicon = "img/favicon.png" -# If false, no index is generated. html_use_index = False - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. html_show_sourcelink = False -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True +html_baseurl = "https://docs.pytest.org/en/stable/" -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True +# -- Options for HTML Help output ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-help-output -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' +htmlhelp_basename = "pytestdoc" -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None -# Output file base name for HTML help builder. -htmlhelp_basename = "pytestdoc" +# -- Options for manual page output --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-manual-page-output +man_pages = [ + ("how-to/usage", "pytest", "pytest usage", ["holger krekel at merlinux eu"], 1) +] -# -- Options for LaTeX output -------------------------------------------------- +# -- Options for epub output ---------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-epub-output -# The paper size ('letter' or 'a4'). -# latex_paper_size = 'letter' +epub_title = "pytest" +epub_author = "holger krekel at merlinux eu" +epub_publisher = "holger krekel at merlinux eu" +epub_copyright = "2013, holger krekel et alii" -# The font size ('10pt', '11pt' or '12pt'). -# latex_font_size = '10pt' +# -- Options for LaTeX output -------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-latex-output -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ( "contents", @@ -336,84 +201,29 @@ "manual", ) ] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -latex_logo = "img/pytest1.png" - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Additional stuff for the LaTeX preamble. -# latex_preamble = '' - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. latex_domain_indices = False +latex_engine = "lualatex" +latex_elements = { + "preamble": dedent( + r""" + \directlua{ + luaotfload.add_fallback("fallbacks", { + "Noto Serif CJK SC:style=Regular;", + "Symbola:Style=Regular;" + }) + } -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ("how-to/usage", "pytest", "pytest usage", ["holger krekel at merlinux eu"], 1) -] - - -# -- Options for Epub output --------------------------------------------------- - -# Bibliographic Dublin Core info. -epub_title = "pytest" -epub_author = "holger krekel at merlinux eu" -epub_publisher = "holger krekel at merlinux eu" -epub_copyright = "2013, holger krekel et alii" - -# The language of the text. It defaults to the language option -# or en if the language is not set. -# epub_language = '' - -# The scheme of the identifier. Typical schemes are ISBN or URL. -# epub_scheme = '' - -# The unique identifier of the text. This can be an ISBN number -# or the project homepage. -# epub_identifier = '' - -# A unique identification for the text. -# epub_uid = '' - -# HTML files that should be inserted before the pages created by sphinx. -# The format is a list of tuples containing the path and title. -# epub_pre_files = [] - -# HTML files that should be inserted after the pages created by sphinx. -# The format is a list of tuples containing the path and title. -# epub_post_files = [] - -# A list of files that should not be packed into the epub file. -# epub_exclude_files = [] - -# The depth of the table of contents in toc.ncx. -# epub_tocdepth = 3 - -# Allow duplicate toc entries. -# epub_tocdup = True - + \setmainfont{FreeSerif}[RawFeature={fallback=fallbacks}] + """ + ) +} -# -- Options for texinfo output ------------------------------------------------ +# -- Options for texinfo output ------------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-texinfo-output texinfo_documents = [ ( - master_doc, + root_doc, "pytest", "pytest Documentation", ( @@ -427,44 +237,37 @@ ) ] +# -- Options for towncrier_draft extension -------------------------------------------- +# https://sphinxcontrib-towncrier.readthedocs.io/en/latest/#how-to-use-this -intersphinx_mapping = { - "pluggy": ("https://pluggy.readthedocs.io/en/stable", None), - "python": ("https://docs.python.org/3", None), - "numpy": ("https://numpy.org/doc/stable", None), - "pip": ("https://pip.pypa.io/en/stable", None), - "tox": ("https://tox.wiki/en/stable", None), - "virtualenv": ("https://virtualenv.pypa.io/en/stable", None), - "setuptools": ("https://setuptools.pypa.io/en/stable", None), - "packaging": ("https://packaging.python.org/en/latest", None), -} +towncrier_draft_autoversion_mode = "draft" # or: 'sphinx-version', 'sphinx-release' +towncrier_draft_include_empty = True +towncrier_draft_working_directory = PROJECT_ROOT_DIR +towncrier_draft_config_path = "pyproject.toml" # relative to cwd +# -- Options for sphinx_issues extension ----------------------------------- +# https://github.com/sloria/sphinx-issues#installation-and-configuration -def configure_logging(app: "sphinx.application.Sphinx") -> None: - """Configure Sphinx's WarningHandler to handle (expected) missing include.""" - import logging +issues_github_path = "pytest-dev/pytest" - import sphinx.util.logging +# -- Custom Read the Docs build configuration ----------------------------------------- +# https://docs.readthedocs.io/en/stable/reference/environment-variables.html#environment-variable-reference +# https://www.sphinx-doc.org/en/master/usage/restructuredtext/directives.html#including-content-based-on-tags - class WarnLogFilter(logging.Filter): - def filter(self, record: logging.LogRecord) -> bool: - """Ignore warnings about missing include with "only" directive. +IS_RELEASE_ON_RTD = ( + os.getenv("READTHEDOCS", "False") == "True" + and os.environ["READTHEDOCS_VERSION_TYPE"] == "tag" +) +if IS_RELEASE_ON_RTD: + tags: set[str] + # pylint: disable-next=used-before-assignment + tags.add("is_release") # noqa: F821 - Ref: https://github.com/sphinx-doc/sphinx/issues/2150.""" - if ( - record.msg.startswith('Problems with "include" directive path:') - and "_changelog_towncrier_draft.rst" in record.msg - ): - return False - return True +# -- Custom documentation plugin ------------------------------------------------------ +# https://www.sphinx-doc.org/en/master/development/tutorials/extending_syntax.html#the-setup-function - logger = logging.getLogger(sphinx.util.logging.NAMESPACE) - warn_handler = [x for x in logger.handlers if x.level == logging.WARNING] - assert len(warn_handler) == 1, warn_handler - warn_handler[0].filters.insert(0, WarnLogFilter()) - -def setup(app: "sphinx.application.Sphinx") -> None: +def setup(app: sphinx.application.Sphinx) -> None: app.add_crossref_type( "fixture", "fixture", @@ -493,8 +296,6 @@ def setup(app: "sphinx.application.Sphinx") -> None: indextemplate="pair: %s; hook", ) - configure_logging(app) - # 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/conftest.py b/doc/en/conftest.py index 1a62e1b5df5..50e43a0b544 100644 --- a/doc/en/conftest.py +++ b/doc/en/conftest.py @@ -1 +1,4 @@ +from __future__ import annotations + + collect_ignore = ["conf.py"] diff --git a/doc/en/contact.rst b/doc/en/contact.rst index beed10d7f27..ef9d1e8edca 100644 --- a/doc/en/contact.rst +++ b/doc/en/contact.rst @@ -3,52 +3,51 @@ .. _`contact`: Contact channels -=================================== +================ -- `pytest issue tracker`_ to report bugs or suggest features (for version - 2.0 and above). -- `pytest discussions`_ at github for general questions. -- `pytest discord server `_ - for pytest development visibility and general assistance. +Web +--- + +- `pytest issue tracker`_ to report bugs or suggest features. +- `pytest discussions`_ at GitHub for general questions. - `pytest on stackoverflow.com `_ - to post precise questions with the tag ``pytest``. New Questions will usually + to post precise questions with the tag ``pytest``. New questions will usually be seen by pytest users or developers and answered quickly. -- `Testing In Python`_: a mailing list for Python testing tools and discussion. +Chat +---- -- `pytest-dev at python.org (mailing list)`_ pytest specific announcements and discussions. +- `pytest discord server `_ + for pytest development visibility and general assistance. +- ``#pytest`` `on irc.libera.chat `_ IRC + channel for random questions (using an IRC client, or `via webchat + `_) +- ``#pytest`` `on Matrix `_. -- :doc:`contribution guide ` for help on submitting pull - requests to GitHub. +Mail +---- -- ``#pytest`` `on irc.libera.chat `_ IRC - channel for random questions (using an IRC client, `via webchat - `_, or `via Matrix - `_). +- `Testing In Python`_: a mailing list for Python testing tools and discussion. +- `pytest-dev at python.org`_ a mailing list for pytest specific announcements and discussions. +- Mail to `core@pytest.org `_ for topics that cannot be + discussed in public. Mails sent there will be distributed among the members + in the pytest core team, who can also be contacted individually: -- private mail to Holger.Krekel at gmail com if you want to communicate sensitive issues + * Ronny Pfannschmidt (:user:`RonnyPfannschmidt`, `ronny@pytest.org `_) + * Florian Bruhin (:user:`The-Compiler`, `florian@pytest.org `_) + * Bruno Oliveira (:user:`nicoddemus`, `bruno@pytest.org `_) + * Ran Benita (:user:`bluetech`, `ran@pytest.org `_) + * Zac Hatfield-Dodds (:user:`Zac-HD`, `zac@pytest.org `_) +Other +----- -- `merlinux.eu`_ offers pytest and tox-related professional teaching and - consulting. +- The :doc:`contribution guide ` for help on submitting pull + requests to GitHub. +- Florian Bruhin (:user:`The-Compiler`) offers pytest professional teaching and + consulting via `Bruhin Software `_. .. _`pytest issue tracker`: https://github.com/pytest-dev/pytest/issues -.. _`old issue tracker`: https://bitbucket.org/hpk42/py-trunk/issues/ - .. _`pytest discussions`: https://github.com/pytest-dev/pytest/discussions - -.. _`merlinux.eu`: https://merlinux.eu/ - -.. _`get an account`: - -.. _tetamap: https://tetamap.wordpress.com/ - -.. _`@pylibcommit`: https://twitter.com/pylibcommit - - .. _`Testing in Python`: http://lists.idyll.org/listinfo/testing-in-python -.. _FOAF: https://en.wikipedia.org/wiki/FOAF -.. _`py-dev`: -.. _`development mailing list`: -.. _`pytest-dev at python.org (mailing list)`: http://mail.python.org/mailman/listinfo/pytest-dev -.. _`pytest-commit at python.org (mailing list)`: http://mail.python.org/mailman/listinfo/pytest-commit +.. _`pytest-dev at python.org`: http://mail.python.org/mailman/listinfo/pytest-dev diff --git a/doc/en/contents.rst b/doc/en/contents.rst index 181207203b2..07c0b3ff6b9 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -1,3 +1,5 @@ +:orphan: + .. _toc: Full pytest documentation diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 5ac93f15144..bf6268a4980 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -7,10 +7,6 @@ This page lists all pytest features that are currently deprecated or have been r The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives should be used instead. -.. contents:: - :depth: 3 - :local: - Deprecated Features ------------------- @@ -462,7 +458,7 @@ Now :class:`~pytest.Class` collects the test methods directly. Most plugins which reference ``Instance`` do so in order to ignore or skip it, using a check such as ``if isinstance(node, Instance): return``. Such plugins should simply remove consideration of ``Instance`` on pytest>=7. -However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``, +However, to keep such uses working, a dummy type has been instanced in ``pytest.Instance`` and ``_pytest.python.Instance``, and importing it emits a deprecation warning. This was removed in pytest 8. diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index f7a9c279426..dd1485b0b21 100644 --- a/doc/en/example/assertion/failure_demo.py +++ b/doc/en/example/assertion/failure_demo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest from pytest import raises diff --git a/doc/en/example/assertion/global_testmodule_config/conftest.py b/doc/en/example/assertion/global_testmodule_config/conftest.py index 4aa7ec23bd1..835726473ba 100644 --- a/doc/en/example/assertion/global_testmodule_config/conftest.py +++ b/doc/en/example/assertion/global_testmodule_config/conftest.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import pytest diff --git a/doc/en/example/assertion/global_testmodule_config/test_hello_world.py b/doc/en/example/assertion/global_testmodule_config/test_hello_world.py index a31a601a1ce..e3c927316f9 100644 --- a/doc/en/example/assertion/global_testmodule_config/test_hello_world.py +++ b/doc/en/example/assertion/global_testmodule_config/test_hello_world.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + hello = "world" diff --git a/doc/en/example/assertion/test_failures.py b/doc/en/example/assertion/test_failures.py index 19d862f60b7..17373f62213 100644 --- a/doc/en/example/assertion/test_failures.py +++ b/doc/en/example/assertion/test_failures.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os.path import shutil diff --git a/doc/en/example/assertion/test_setup_flow_example.py b/doc/en/example/assertion/test_setup_flow_example.py index 0e7eded06b6..fe11c2bf3f2 100644 --- a/doc/en/example/assertion/test_setup_flow_example.py +++ b/doc/en/example/assertion/test_setup_flow_example.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + def setup_module(module): module.TestStateFullThing.classcount = 0 diff --git a/doc/en/example/conftest.py b/doc/en/example/conftest.py index 66e70f14dd7..21c9a489961 100644 --- a/doc/en/example/conftest.py +++ b/doc/en/example/conftest.py @@ -1 +1,4 @@ +from __future__ import annotations + + collect_ignore = ["nonpython", "customdirectory"] diff --git a/doc/en/example/customdirectory/conftest.py b/doc/en/example/customdirectory/conftest.py index 350893cab43..ea922e04723 100644 --- a/doc/en/example/customdirectory/conftest.py +++ b/doc/en/example/customdirectory/conftest.py @@ -1,4 +1,6 @@ # content of conftest.py +from __future__ import annotations + import json import pytest @@ -21,7 +23,7 @@ def collect(self): @pytest.hookimpl def pytest_collect_directory(path, parent): - # Use our custom collector for directories containing a `mainfest.json` file. + # Use our custom collector for directories containing a `manifest.json` file. if path.joinpath("manifest.json").is_file(): return ManifestDirectory.from_parent(parent=parent, path=path) # Otherwise fallback to the standard behavior. diff --git a/doc/en/example/customdirectory/tests/test_first.py b/doc/en/example/customdirectory/tests/test_first.py index 0a78de59945..9953dd37785 100644 --- a/doc/en/example/customdirectory/tests/test_first.py +++ b/doc/en/example/customdirectory/tests/test_first.py @@ -1,3 +1,6 @@ # content of test_first.py +from __future__ import annotations + + def test_1(): pass diff --git a/doc/en/example/customdirectory/tests/test_second.py b/doc/en/example/customdirectory/tests/test_second.py index eed724a7d96..df264f48b3b 100644 --- a/doc/en/example/customdirectory/tests/test_second.py +++ b/doc/en/example/customdirectory/tests/test_second.py @@ -1,3 +1,6 @@ # content of test_second.py +from __future__ import annotations + + def test_2(): pass diff --git a/doc/en/example/customdirectory/tests/test_third.py b/doc/en/example/customdirectory/tests/test_third.py index 61cf59dc16c..b8b072dd770 100644 --- a/doc/en/example/customdirectory/tests/test_third.py +++ b/doc/en/example/customdirectory/tests/test_third.py @@ -1,3 +1,6 @@ # content of test_third.py +from __future__ import annotations + + def test_3(): pass diff --git a/doc/en/example/fixtures/test_fixtures_order_autouse.py b/doc/en/example/fixtures/test_fixtures_order_autouse.py index ec282ab4b2b..04cbc268b7f 100644 --- a/doc/en/example/fixtures/test_fixtures_order_autouse.py +++ b/doc/en/example/fixtures/test_fixtures_order_autouse.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py b/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py index de0c2642793..828fa4cf6d6 100644 --- a/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py +++ b/doc/en/example/fixtures/test_fixtures_order_autouse_multiple_scopes.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py b/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py index ba01ad32f57..ebd5d10f5bb 100644 --- a/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py +++ b/doc/en/example/fixtures/test_fixtures_order_autouse_temp_effects.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/doc/en/example/fixtures/test_fixtures_order_dependencies.py b/doc/en/example/fixtures/test_fixtures_order_dependencies.py index e76e3f93c62..1c59f010341 100644 --- a/doc/en/example/fixtures/test_fixtures_order_dependencies.py +++ b/doc/en/example/fixtures/test_fixtures_order_dependencies.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/doc/en/example/fixtures/test_fixtures_order_scope.py b/doc/en/example/fixtures/test_fixtures_order_scope.py index 5d9487cab34..4b4260fbdcd 100644 --- a/doc/en/example/fixtures/test_fixtures_order_scope.py +++ b/doc/en/example/fixtures/test_fixtures_order_scope.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/doc/en/example/fixtures/test_fixtures_request_different_scope.py b/doc/en/example/fixtures/test_fixtures_request_different_scope.py index 00e2e46d845..dee61f8c4d7 100644 --- a/doc/en/example/fixtures/test_fixtures_request_different_scope.py +++ b/doc/en/example/fixtures/test_fixtures_request_different_scope.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index c04d2a078dd..babcd9e2f3a 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -25,10 +25,12 @@ You can "mark" a test function with custom metadata like this: pass # perform some webtest test for your app + @pytest.mark.device(serial="123") def test_something_quick(): pass + @pytest.mark.device(serial="abc") def test_another(): pass @@ -71,6 +73,28 @@ Or the inverse, running all tests except the webtest ones: ===================== 3 passed, 1 deselected in 0.12s ====================== +.. _`marker_keyword_expression_example`: + +Additionally, you can restrict a test run to only run tests matching one or multiple marker +keyword arguments, e.g. to run only tests marked with ``device`` and the specific ``serial="123"``: + +.. code-block:: pytest + + $ pytest -v -m "device(serial='123')" + =========================== test session starts ============================ + platform linux -- Python 3.x.y, pytest-8.x.y, pluggy-1.x.y -- $PYTHON_PREFIX/bin/python + cachedir: .pytest_cache + rootdir: /home/sweet/project + collecting ... collected 4 items / 3 deselected / 1 selected + + test_server.py::test_something_quick PASSED [100%] + + ===================== 1 passed, 3 deselected in 0.12s ====================== + +.. note:: Only keyword argument matching is supported in marker expressions. + +.. note:: Only :class:`int`, (unescaped) :class:`str`, :class:`bool` & :data:`None` values are supported in marker expressions. + Selecting tests based on their node ID -------------------------------------- diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index 861ae9e528d..f54524213bc 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -1,6 +1,8 @@ """Module containing a parametrized tests testing cross-python serialization via the pickle module.""" +from __future__ import annotations + import shutil import subprocess import textwrap diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py index e969e3e2518..b7bdc77a004 100644 --- a/doc/en/example/nonpython/conftest.py +++ b/doc/en/example/nonpython/conftest.py @@ -1,4 +1,6 @@ # content of conftest.py +from __future__ import annotations + import pytest diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 672c7c4457d..3e449b2eaa2 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -162,7 +162,7 @@ objects, they are still using the default pytest representation: rootdir: /home/sweet/project collected 8 items - + @@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia rootdir: /home/sweet/project collected 4 items - + @@ -318,7 +318,7 @@ Let's first see how it looks like at collection time: rootdir: /home/sweet/project collected 2 items - + @@ -505,8 +505,8 @@ Running it results in some skips if we don't have all the python interpreters in . $ pytest -rs -q multipython.py ssssssssssss...ssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [12] multipython.py:65: 'python3.9' not found - SKIPPED [12] multipython.py:65: 'python3.11' not found + SKIPPED [12] multipython.py:67: 'python3.9' not found + SKIPPED [12] multipython.py:67: 'python3.11' not found 3 passed, 24 skipped in 0.12s Parametrization of optional implementations/imports diff --git a/doc/en/example/pythoncollection.py b/doc/en/example/pythoncollection.py index 8742526a191..7595ee02ca4 100644 --- a/doc/en/example/pythoncollection.py +++ b/doc/en/example/pythoncollection.py @@ -1,5 +1,6 @@ # run this with $ pytest --collect-only test_collectonly.py # +from __future__ import annotations def test_function(): diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index c01e685f3f4..5bd03035c14 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -152,7 +152,7 @@ The test collection would look like this: configfile: pytest.ini collected 2 items - + @@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this: configfile: pytest.ini collected 3 items - + diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 2c34cc2b00d..0da58d0490e 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -25,7 +25,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert param1 * 2 < param2 E assert (3 * 2) < 6 - failure_demo.py:19: AssertionError + failure_demo.py:21: AssertionError _________________________ TestFailing.test_simple __________________________ self = @@ -42,7 +42,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 42 = .f at 0xdeadbeef0002>() E + and 43 = .g at 0xdeadbeef0003>() - failure_demo.py:30: AssertionError + failure_demo.py:32: AssertionError ____________________ TestFailing.test_simple_multiline _____________________ self = @@ -50,7 +50,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_simple_multiline(self): > otherfunc_multi(42, 6 * 9) - failure_demo.py:33: + failure_demo.py:35: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ a = 42, b = 54 @@ -59,7 +59,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert a == b E assert 42 == 54 - failure_demo.py:14: AssertionError + failure_demo.py:16: AssertionError ___________________________ TestFailing.test_not ___________________________ self = @@ -72,7 +72,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert not 42 E + where 42 = .f at 0xdeadbeef0006>() - failure_demo.py:39: AssertionError + failure_demo.py:41: AssertionError _________________ TestSpecialisedExplanations.test_eq_text _________________ self = @@ -84,7 +84,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E - eggs E + spam - failure_demo.py:44: AssertionError + failure_demo.py:46: AssertionError _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ self = @@ -98,7 +98,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + foo 1 bar E ? ^ - failure_demo.py:47: AssertionError + failure_demo.py:49: AssertionError ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ self = @@ -112,7 +112,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + spam E bar - failure_demo.py:50: AssertionError + failure_demo.py:52: AssertionError ______________ TestSpecialisedExplanations.test_eq_long_text _______________ self = @@ -130,7 +130,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + 1111111111a222222222 E ? ^ - failure_demo.py:55: AssertionError + failure_demo.py:57: AssertionError _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ self = @@ -150,7 +150,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (7 lines hidden), use '-vv' to show - failure_demo.py:60: AssertionError + failure_demo.py:62: AssertionError _________________ TestSpecialisedExplanations.test_eq_list _________________ self = @@ -162,7 +162,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E At index 2 diff: 2 != 3 E Use -v to get more diff - failure_demo.py:63: AssertionError + failure_demo.py:65: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ self = @@ -176,7 +176,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E At index 100 diff: 1 != 2 E Use -v to get more diff - failure_demo.py:68: AssertionError + failure_demo.py:70: AssertionError _________________ TestSpecialisedExplanations.test_eq_dict _________________ self = @@ -194,7 +194,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E {'d': 0} E Use -v to get more diff - failure_demo.py:71: AssertionError + failure_demo.py:73: AssertionError _________________ TestSpecialisedExplanations.test_eq_set __________________ self = @@ -212,7 +212,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E 21 E Use -v to get more diff - failure_demo.py:74: AssertionError + failure_demo.py:76: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ self = @@ -224,7 +224,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E Right contains one more item: 3 E Use -v to get more diff - failure_demo.py:77: AssertionError + failure_demo.py:79: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ self = @@ -233,7 +233,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert 1 in [0, 2, 3, 4, 5] E assert 1 in [0, 2, 3, 4, 5] - failure_demo.py:80: AssertionError + failure_demo.py:82: AssertionError __________ TestSpecialisedExplanations.test_not_in_text_multiline __________ self = @@ -252,7 +252,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E and a E tail - failure_demo.py:84: AssertionError + failure_demo.py:86: AssertionError ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ self = @@ -266,7 +266,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E single foo line E ? +++ - failure_demo.py:88: AssertionError + failure_demo.py:90: AssertionError _________ TestSpecialisedExplanations.test_not_in_text_single_long _________ self = @@ -280,7 +280,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? +++ - failure_demo.py:92: AssertionError + failure_demo.py:94: AssertionError ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______ self = @@ -294,7 +294,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - failure_demo.py:96: AssertionError + failure_demo.py:98: AssertionError ______________ TestSpecialisedExplanations.test_eq_dataclass _______________ self = @@ -321,7 +321,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E - c E + b - failure_demo.py:108: AssertionError + failure_demo.py:110: AssertionError ________________ TestSpecialisedExplanations.test_eq_attrs _________________ self = @@ -348,7 +348,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E - c E + b - failure_demo.py:120: AssertionError + failure_demo.py:122: AssertionError ______________________________ test_attribute ______________________________ def test_attribute(): @@ -360,7 +360,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .Foo object at 0xdeadbeef0018>.b - failure_demo.py:128: AssertionError + failure_demo.py:130: AssertionError _________________________ test_attribute_instance __________________________ def test_attribute_instance(): @@ -372,7 +372,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 1 = .Foo object at 0xdeadbeef0019>.b E + where .Foo object at 0xdeadbeef0019> = .Foo'>() - failure_demo.py:135: AssertionError + failure_demo.py:137: AssertionError __________________________ test_attribute_failure __________________________ def test_attribute_failure(): @@ -385,7 +385,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: i = Foo() > assert i.b == 2 - failure_demo.py:146: + failure_demo.py:148: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = .Foo object at 0xdeadbeef001a> @@ -394,7 +394,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raise Exception("Failed to get attrib") E Exception: Failed to get attrib - failure_demo.py:141: Exception + failure_demo.py:143: Exception _________________________ test_attribute_multiple __________________________ def test_attribute_multiple(): @@ -411,7 +411,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + and 2 = .Bar object at 0xdeadbeef001c>.b E + where .Bar object at 0xdeadbeef001c> = .Bar'>() - failure_demo.py:156: AssertionError + failure_demo.py:158: AssertionError __________________________ TestRaises.test_raises __________________________ self = @@ -421,7 +421,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raises(TypeError, int, s) E ValueError: invalid literal for int() with base 10: 'qwe' - failure_demo.py:166: ValueError + failure_demo.py:168: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ self = @@ -430,7 +430,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raises(OSError, int, "3") E Failed: DID NOT RAISE - failure_demo.py:169: Failed + failure_demo.py:171: Failed __________________________ TestRaises.test_raise ___________________________ self = @@ -439,7 +439,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raise ValueError("demo error") E ValueError: demo error - failure_demo.py:172: ValueError + failure_demo.py:174: ValueError ________________________ TestRaises.test_tupleerror ________________________ self = @@ -448,7 +448,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = [1] # noqa: F841 E ValueError: not enough values to unpack (expected 2, got 1) - failure_demo.py:175: ValueError + failure_demo.py:177: ValueError ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ self = @@ -459,7 +459,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items.pop() E TypeError: cannot unpack non-iterable int object - failure_demo.py:180: TypeError + failure_demo.py:182: TypeError --------------------------- Captured stdout call --------------------------- items is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ @@ -470,7 +470,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > if namenotexi: # noqa: F821 E NameError: name 'namenotexi' is not defined - failure_demo.py:183: NameError + failure_demo.py:185: NameError ____________________ test_dynamic_compile_shows_nicely _____________________ def test_dynamic_compile_shows_nicely(): @@ -486,7 +486,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: sys.modules[name] = module > module.foo() - failure_demo.py:202: + failure_demo.py:204: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > ??? @@ -506,9 +506,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: > somefunc(f(), g()) - failure_demo.py:213: + failure_demo.py:215: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - failure_demo.py:10: in somefunc + failure_demo.py:12: in somefunc otherfunc(x, y) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @@ -518,7 +518,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert a == b E assert 44 == 43 - failure_demo.py:6: AssertionError + failure_demo.py:8: AssertionError ___________________ TestMoreErrors.test_z1_unpack_error ____________________ self = @@ -528,7 +528,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E ValueError: not enough values to unpack (expected 2, got 0) - failure_demo.py:217: ValueError + failure_demo.py:219: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ self = @@ -538,7 +538,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E TypeError: cannot unpack non-iterable int object - failure_demo.py:221: TypeError + failure_demo.py:223: TypeError ______________________ TestMoreErrors.test_startswith ______________________ self = @@ -551,7 +551,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = ('456') E + where = '123'.startswith - failure_demo.py:226: AssertionError + failure_demo.py:228: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ self = @@ -570,7 +570,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where '123' = .f at 0xdeadbeef0029>() E + and '456' = .g at 0xdeadbeef002a>() - failure_demo.py:235: AssertionError + failure_demo.py:237: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ self = @@ -581,7 +581,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = isinstance(43, float) E + where 43 = globf(42) - failure_demo.py:238: AssertionError + failure_demo.py:240: AssertionError _______________________ TestMoreErrors.test_instance _______________________ self = @@ -592,7 +592,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 42 != 42 E + where 42 = .x - failure_demo.py:242: AssertionError + failure_demo.py:244: AssertionError _______________________ TestMoreErrors.test_compare ________________________ self = @@ -602,7 +602,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 11 < 5 E + where 11 = globf(10) - failure_demo.py:245: AssertionError + failure_demo.py:247: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ self = @@ -613,7 +613,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert x == 0 E assert 1 == 0 - failure_demo.py:250: AssertionError + failure_demo.py:252: AssertionError ___________________ TestCustomAssertMsg.test_single_line ___________________ self = @@ -628,7 +628,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:261: AssertionError + failure_demo.py:263: AssertionError ____________________ TestCustomAssertMsg.test_multiline ____________________ self = @@ -647,7 +647,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:268: AssertionError + failure_demo.py:270: AssertionError ___________________ TestCustomAssertMsg.test_custom_repr ___________________ self = @@ -669,7 +669,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a - failure_demo.py:281: AssertionError + failure_demo.py:283: AssertionError ========================= short test summary info ========================== FAILED failure_demo.py::test_generative[3-6] - assert (3 * 2) < 6 FAILED failure_demo.py::TestFailing::test_simple - assert 42 == 43 diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 7064f61f0e2..a5e2e78c397 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -212,7 +212,7 @@ the command line arguments before they get processed: .. code-block:: python - # setuptools plugin + # installable external plugin import sys @@ -405,35 +405,20 @@ Detect if running from within a pytest run Usually it is a bad idea to make application code behave differently if called from a test. But if you absolutely must find out if your application code is -running from a test you can do something like this: +running from a test you can do this: .. code-block:: python - # content of your_module.py + import os - _called_from_test = False - -.. code-block:: python - - # content of conftest.py - - - def pytest_configure(config): - your_module._called_from_test = True - -and then check for the ``your_module._called_from_test`` flag: - -.. code-block:: python - - if your_module._called_from_test: - # called from within a test run + if os.environ.get("PYTEST_VERSION") is not None: + # Things you want to to do if your code is called by pytest. ... else: - # called "normally" + # Things you want to to do if your code is not called by pytest. ... -accordingly in your application. Adding info to test report header -------------------------------------------------------------- @@ -660,31 +645,6 @@ If we run this: E assert 0 test_step.py:11: AssertionError - ================================ XFAILURES ================================= - ______________________ TestUserHandling.test_deletion ______________________ - - item = - - def pytest_runtest_setup(item): - if "incremental" in item.keywords: - # retrieve the class name of the test - cls_name = str(item.cls) - # check if a previous test has failed for this class - if cls_name in _test_failed_incremental: - # retrieve the index of the test (if parametrize is used in combination with incremental) - parametrize_index = ( - tuple(item.callspec.indices.values()) - if hasattr(item, "callspec") - else () - ) - # retrieve the name of the first test function to fail for this class name and index - test_name = _test_failed_incremental[cls_name].get(parametrize_index, None) - # if name found, test has failed for the combination of class name & test name - if test_name is not None: - > pytest.xfail(f"previous test failed ({test_name})") - E _pytest.outcomes.XFailed: previous test failed (test_modification) - - conftest.py:47: XFailed ========================= short test summary info ========================== XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification) ================== 1 failed, 2 passed, 1 xfailed in 0.12s ================== @@ -1088,8 +1048,8 @@ Instead of freezing the pytest runner as a separate executable, you can make your frozen program work as the pytest runner by some clever argument handling during program startup. This allows you to have a single executable, which is usually more convenient. -Please note that the mechanism for plugin discovery used by pytest -(setuptools entry points) doesn't work with frozen executables so pytest +Please note that the mechanism for plugin discovery used by pytest (:ref:`entry +points `) doesn't work with frozen executables so pytest can't find any third party plugins automatically. To include third party plugins like ``pytest-timeout`` they must be imported explicitly and passed on to pytest.main. diff --git a/doc/en/example/xfail_demo.py b/doc/en/example/xfail_demo.py index 1040c89298d..4999e15f238 100644 --- a/doc/en/example/xfail_demo.py +++ b/doc/en/example/xfail_demo.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import pytest diff --git a/doc/en/explanation/ci.rst b/doc/en/explanation/ci.rst new file mode 100644 index 00000000000..45fe658d14f --- /dev/null +++ b/doc/en/explanation/ci.rst @@ -0,0 +1,70 @@ +.. _`ci-pipelines`: + +CI Pipelines +============ + +Rationale +--------- + +The goal of testing in a CI pipeline is different from testing locally. Indeed, +you can quickly edit some code and run your tests again on your computer, but +it is not possible with CI pipeline. They run on a separate server and are +triggered by specific actions. + +From that observation, pytest can detect when it is in a CI environment and +adapt some of its behaviours. + +How CI is detected +------------------ + +Pytest knows it is in a CI environment when either one of these environment variables are set, +regardless of their value: + +* `CI`: used by many CI systems. +* `BUILD_NUMBER`: used by Jenkins. + +Effects on CI +------------- + +For now, the effects on pytest of being in a CI environment are limited. + +When a CI environment is detected, the output of the short test summary info is no longer truncated to the terminal size i.e. the entire message will be shown. + + .. code-block:: python + + # content of test_ci.py + import pytest + + + def test_db_initialized(): + pytest.fail( + "deliberately failing for demo purpose, Lorem ipsum dolor sit amet, " + "consectetur adipiscing elit. Cras facilisis, massa in suscipit " + "dignissim, mauris lacus molestie nisi, quis varius metus nulla ut ipsum." + ) + + +Running this locally, without any extra options, will output: + + .. code-block:: pytest + + $ pytest test_ci.py + ... + ========================= short test summary info ========================== + FAILED test_backends.py::test_db_initialized[d2] - Failed: deliberately f... + +*(Note the truncated text)* + + +While running this on CI will output: + + .. code-block:: pytest + + $ export CI=true + $ pytest test_ci.py + ... + ========================= short test summary info ========================== + FAILED test_backends.py::test_db_initialized[d2] - Failed: deliberately failing + for demo purpose, Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras + facilisis, massa in suscipit dignissim, mauris lacus molestie nisi, quis varius + metus nulla ut ipsum. diff --git a/doc/en/explanation/flaky.rst b/doc/en/explanation/flaky.rst index 41cbe847989..cb6c3983424 100644 --- a/doc/en/explanation/flaky.rst +++ b/doc/en/explanation/flaky.rst @@ -18,7 +18,7 @@ System state Broadly speaking, a flaky test indicates that the test relies on some system state that is not being appropriately controlled - the test environment is not sufficiently isolated. Higher level tests are more likely to be flaky as they rely on more state. -Flaky tests sometimes appear when a test suite is run in parallel (such as use of pytest-xdist). This can indicate a test is reliant on test ordering. +Flaky tests sometimes appear when a test suite is run in parallel (such as use of `pytest-xdist`_). This can indicate a test is reliant on test ordering. - Perhaps a different test is failing to clean up after itself and leaving behind data which causes the flaky test to fail. - The flaky test is reliant on data from a previous test that doesn't clean up after itself, and in parallel runs that previous test is not always present @@ -30,9 +30,22 @@ Overly strict assertion Overly strict assertions can cause problems with floating point comparison as well as timing issues. :func:`pytest.approx` is useful here. +Thread safety +~~~~~~~~~~~~~ -Pytest features -^^^^^^^^^^^^^^^ +pytest is single-threaded, executing its tests always in the same thread, sequentially, never spawning any threads itself. + +Even in case of plugins which run tests in parallel, for example `pytest-xdist`_, usually work by spawning multiple *processes* and running tests in batches, without using multiple threads. + +It is of course possible (and common) for tests and fixtures to spawn threads themselves as part of their testing workflow (for example, a fixture that starts a server thread in the background, or a test which executes production code that spawns threads), but some care must be taken: + +* Make sure to eventually wait on any spawned threads -- for example at the end of a test, or during the teardown of a fixture. +* Avoid using primitives provided by pytest (:func:`pytest.warns`, :func:`pytest.raises`, etc) from multiple threads, as they are not thread-safe. + +If your test suite uses threads and your are seeing flaky test results, do not discount the possibility that the test is implicitly using global state in pytest itself. + +Related features +^^^^^^^^^^^^^^^^ Xfail strict ~~~~~~~~~~~~ @@ -123,3 +136,6 @@ Resources * `Flaky Tests at Google and How We Mitigate Them `_ by John Micco, 2016 * `Where do Google's flaky tests come from? `_ by Jeff Listfield, 2017 + + +.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist diff --git a/doc/en/explanation/index.rst b/doc/en/explanation/index.rst index 53910f1eb7b..2edf60a5d8b 100644 --- a/doc/en/explanation/index.rst +++ b/doc/en/explanation/index.rst @@ -11,5 +11,6 @@ Explanation anatomy fixtures goodpractices - flaky pythonpath + ci + flaky diff --git a/doc/en/explanation/pythonpath.rst b/doc/en/explanation/pythonpath.rst index 33eba86b57a..d0314a6dbcd 100644 --- a/doc/en/explanation/pythonpath.rst +++ b/doc/en/explanation/pythonpath.rst @@ -8,15 +8,15 @@ pytest import mechanisms and ``sys.path``/``PYTHONPATH`` Import modes ------------ -pytest as a testing framework needs to import test modules and ``conftest.py`` files for execution. +pytest as a testing framework that needs to import test modules and ``conftest.py`` files for execution. -Importing files in Python is a non-trivial processes, so aspects of the +Importing files in Python is a non-trivial process, so aspects of the import process can be controlled through the ``--import-mode`` command-line flag, which can assume these values: .. _`import-mode-prepend`: -* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning* +* ``prepend`` (default): The directory path containing each module will be inserted into the *beginning* of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module ` function. @@ -34,7 +34,7 @@ these values: * ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already there, and imported with :func:`importlib.import_module `. - This better allows to run test modules against installed versions of a package even if the + This better allows users to run test modules against installed versions of a package even if the package under test has the same import root. For example: :: @@ -45,7 +45,7 @@ these values: the tests will run against the installed version of ``pkg_under_test`` when ``--import-mode=append`` is used whereas - with ``prepend`` they would pick up the local version. This kind of confusion is why + with ``prepend``, they would pick up the local version. This kind of confusion is why we advocate for using :ref:`src-layouts `. Same as ``prepend``, requires test module names to be unique when the test directory tree is @@ -67,7 +67,7 @@ these values: are not importable. The recommendation in this case it to place testing utility modules together with the application/library code, for example ``app.testing.helpers``. - Important: by "test utility modules" we mean functions/classes which are imported by + Important: by "test utility modules", we mean functions/classes which are imported by other tests directly; this does not include fixtures, which should be placed in ``conftest.py`` files, along with the test modules, and are discovered automatically by pytest. @@ -76,8 +76,8 @@ these values: 1. Given a certain module path, for example ``tests/core/test_models.py``, derives a canonical name like ``tests.core.test_models`` and tries to import it. - For non-test modules this will work if they are accessible via :py:data:`sys.path`, so - for example ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``. + For non-test modules, this will work if they are accessible via :py:data:`sys.path`. So + for example, ``.env/lib/site-packages/app/core.py`` will be importable as ``app.core``. This is happens when plugins import non-test modules (for example doctesting). If this step succeeds, the module is returned. diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index e96eabbc5e7..050fd2d80ec 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -22,7 +22,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - pytest 8.1.2 + pytest 8.3.2 .. _`simpletest`: @@ -274,7 +274,7 @@ Continue reading Check out additional pytest resources to help you customize tests for your unique workflow: * ":ref:`usage`" for command line invocation examples -* ":ref:`existingtestsuite`" for working with pre-existing tests +* ":ref:`existingtestsuite`" for working with preexisting tests * ":ref:`mark`" for information on the ``pytest.mark`` mechanism * ":ref:`fixtures`" for providing a functional baseline to your tests * ":ref:`plugins`" for managing and writing plugins diff --git a/doc/en/historical-notes.rst b/doc/en/historical-notes.rst index 5eb527c582b..be67036d6ca 100644 --- a/doc/en/historical-notes.rst +++ b/doc/en/historical-notes.rst @@ -107,7 +107,7 @@ Here is a non-exhaustive list of issues fixed by the new implementation: * Marker transfer incompatible with inheritance (:issue:`535`). -More details can be found in the :pull:`original PR <3317>`. +More details can be found in the :pr:`original PR <3317>`. .. note:: diff --git a/doc/en/how-to/fixtures.rst b/doc/en/how-to/fixtures.rst index ba416ccbc23..f4d59ff93c0 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used: rootdir: /home/sweet/project collected 12 items - + @@ -1931,7 +1931,7 @@ The same applies for the test folder level obviously. Using fixtures from other projects ---------------------------------- -Usually projects that provide pytest support will use :ref:`entry points `, +Usually projects that provide pytest support will use :ref:`entry points `, so just installing those projects into an environment will make those fixtures available for use. In case you want to use fixtures from a project that does not use entry points, you can diff --git a/doc/en/how-to/output.rst b/doc/en/how-to/output.rst index 5b47a5c7776..8b15f95f0fd 100644 --- a/doc/en/how-to/output.rst +++ b/doc/en/how-to/output.rst @@ -294,9 +294,47 @@ Now if we increase verbosity even more: test_verbosity_example.py:19: AssertionError ========================= short test summary info ========================== - FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser... - FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass... - FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a... + FAILED test_verbosity_example.py::test_words_fail - AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi'] + + At index 2 diff: 'grapes' != 'orange' + + Full diff: + [ + 'banana', + 'apple', + - 'orange', + ? ^ ^^ + + 'grapes', + ? ^ ^ + + 'melon', + 'kiwi', + ] + FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40} + + Common items: + {'0': 0} + Left contains 4 more items: + {'1': 1, '2': 2, '3': 3, '4': 4} + Right contains 4 more items: + {'10': 10, '20': 20, '30': 30, '40': 40} + + Full diff: + { + '0': 0, + - '10': 10, + ? - - + + '1': 1, + - '20': 20, + ? - - + + '2': 2, + - '30': 30, + ? - - + + '3': 3, + - '40': 40, + ? - - + + '4': 4, + } + FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet ' ======================= 3 failed, 1 passed in 0.12s ======================== Notice now that: @@ -406,14 +444,6 @@ Example: E assert 0 test_example.py:14: AssertionError - ================================ XFAILURES ================================= - ________________________________ test_xfail ________________________________ - - def test_xfail(): - > pytest.xfail("xfailing this test") - E _pytest.outcomes.XFailed: xfailing this test - - test_example.py:26: XFailed ================================= XPASSES ================================== ========================= short test summary info ========================== SKIPPED [1] test_example.py:22: skipping this test @@ -514,6 +544,11 @@ captured output: PASSED test_example.py::test_ok == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === +.. note:: + + By default, parametrized variants of skipped tests are grouped together if + they share the same skip reason. You can use ``--no-fold-skipped`` to print each skipped test separately. + Creating resultlog format files -------------------------------------------------- diff --git a/doc/en/how-to/usage.rst b/doc/en/how-to/usage.rst index fe46fad2db5..0e0a0310fd8 100644 --- a/doc/en/how-to/usage.rst +++ b/doc/en/how-to/usage.rst @@ -76,11 +76,19 @@ Specifying a specific parametrization of a test: **Run tests by marker expressions** +To run all tests which are decorated with the ``@pytest.mark.slow`` decorator: + .. code-block:: bash pytest -m slow -Will run all tests which are decorated with the ``@pytest.mark.slow`` decorator. + +To run all tests which are decorated with the annotated ``@pytest.mark.slow(phase=1)`` decorator, +with the ``phase`` keyword argument set to ``1``: + +.. code-block:: bash + + pytest -m "slow(phase=1)" For more information see :ref:`marks `. @@ -154,7 +162,7 @@ You can early-load plugins (internal and external) explicitly in the command-lin The option receives a ``name`` parameter, which can be: * A full module dotted name, for example ``myproject.plugins``. This dotted name must be importable. -* The entry-point name of a plugin. This is the name passed to ``setuptools`` when the plugin is +* The entry-point name of a plugin. This is the name passed to ``importlib`` when the plugin is registered. For example to early-load the :pypi:`pytest-cov` plugin you can use:: pytest -p pytest_cov diff --git a/doc/en/how-to/writing_plugins.rst b/doc/en/how-to/writing_plugins.rst index 4bb6d183333..1bba9644649 100644 --- a/doc/en/how-to/writing_plugins.rst +++ b/doc/en/how-to/writing_plugins.rst @@ -16,8 +16,8 @@ reporting by calling :ref:`well specified hooks ` of the followi * builtin plugins: loaded from pytest's internal ``_pytest`` directory. -* :ref:`external plugins `: modules discovered through - `setuptools entry points`_ +* :ref:`external plugins `: installed third-party modules discovered + through :ref:`entry points ` in their packaging metadata * `conftest.py plugins`_: modules auto-discovered in test directories @@ -42,7 +42,9 @@ Plugin discovery order at tool startup 3. by scanning the command line for the ``-p name`` option and loading the specified plugin. This happens before normal command-line parsing. -4. by loading all plugins registered through `setuptools entry points`_. +4. by loading all plugins registered through installed third-party package + :ref:`entry points `, unless the + :envvar:`PYTEST_DISABLE_PLUGIN_AUTOLOAD` environment variable is set. 5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable. @@ -142,7 +144,8 @@ Making your plugin installable by others If you want to make your plugin externally available, you may define a so-called entry point for your distribution so that ``pytest`` finds your plugin module. Entry points are -a feature that is provided by :std:doc:`setuptools `. +a feature that is provided by :std:doc:`packaging tools +`. pytest looks up the ``pytest11`` entrypoint to discover its plugins, thus you can make your plugin available by defining @@ -265,8 +268,9 @@ of the variable will also be loaded as plugins, and so on. tests root directory is deprecated, and will raise a warning. This mechanism makes it easy to share fixtures within applications or even -external applications without the need to create external plugins using -the ``setuptools``'s entry point technique. +external applications without the need to create external plugins using the +:std:doc:`entry point packaging metadata +` technique. Plugins imported by :globalvar:`pytest_plugins` will also automatically be marked for assertion rewriting (see :func:`pytest.register_assert_rewrite`). diff --git a/doc/en/index.rst b/doc/en/index.rst index 67501c0530c..95044e8a544 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -1,20 +1,45 @@ -:orphan: - -.. sidebar:: Next Open Trainings and Events +.. _features: - - `Professional Testing with Python `_, via `Python Academy `_ (3 day in-depth training): - * **June 11th to 13th 2024**, Remote - * **March 4th to 6th 2025**, Leipzig, Germany / Remote - - `pytest development sprint `_, **June 17th -- 22nd 2024** - - pytest tips and tricks for a better testsuite, `Europython 2024 `_, **July 8th -- 14th 2024** (3h), Prague +.. sidebar:: **Next Open Trainings and Events** - Also see :doc:`previous talks and blogposts `. + - `pytest: Professionelles Testen (nicht nur) für Python `_, at `CH Open Workshoptage `_, **September 2nd 2024**, HSLU Rotkreuz (CH) + - `Professional Testing with Python `_, via `Python Academy `_ (3 day in-depth training), **March 4th -- 6th 2025**, Leipzig (DE) / Remote -.. _features: + Also see :doc:`previous talks and blogposts ` pytest: helps you write better programs ======================================= +.. toctree:: + :hidden: + + getting-started + how-to/index + reference/index + explanation/index + example/index + +.. toctree:: + :caption: About the project + :hidden: + + changelog + contributing + backwards-compatibility + sponsor + tidelift + license + contact + +.. toctree:: + :caption: Useful links + :hidden: + + pytest @ PyPI + pytest @ GitHub + Issue Tracker + PDF Documentation + .. module:: pytest The ``pytest`` framework makes it easy to write small, readable tests, and can @@ -25,7 +50,6 @@ scale to support complex functional testing for applications and libraries. **PyPI package name**: :pypi:`pytest` - A quick example --------------- diff --git a/doc/en/naming20.rst b/doc/en/naming20.rst index 5a81df2698d..11213066384 100644 --- a/doc/en/naming20.rst +++ b/doc/en/naming20.rst @@ -8,7 +8,7 @@ If you used older version of the ``py`` distribution (which included the py.test command line tool and Python name space) you accessed helpers and possibly collection classes through the ``py.test`` Python namespaces. The new ``pytest`` -Python module flaty provides the same objects, following +Python module flatly provides the same objects, following these renaming rules:: py.test.XYZ -> pytest.XYZ diff --git a/doc/en/reference/customize.rst b/doc/en/reference/customize.rst index cab1117266f..373223ec913 100644 --- a/doc/en/reference/customize.rst +++ b/doc/en/reference/customize.rst @@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se setup.cfg ~~~~~~~~~ -``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools `__, and can also be used to hold pytest configuration +``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and :std:doc:`setuptools `, and can also be used to hold pytest configuration if they have a ``[tool:pytest]`` section. .. code-block:: ini diff --git a/doc/en/reference/fixtures.rst b/doc/en/reference/fixtures.rst index 8ba59395e3e..dff93a035ef 100644 --- a/doc/en/reference/fixtures.rst +++ b/doc/en/reference/fixtures.rst @@ -39,7 +39,7 @@ Built-in fixtures Store and retrieve values across pytest runs. :fixture:`doctest_namespace` - Provide a dict injected into the docstests namespace. + Provide a dict injected into the doctests namespace. :fixture:`monkeypatch` Temporarily modify classes, functions, dictionaries, diff --git a/doc/en/reference/plugin_list.rst b/doc/en/reference/plugin_list.rst index e1d1e3ec24a..7526b055943 100644 --- a/doc/en/reference/plugin_list.rst +++ b/doc/en/reference/plugin_list.rst @@ -27,7 +27,7 @@ please refer to `the update script =7; extra == "pytest" - :pypi:`nuts` Network Unit Testing System Aug 11, 2023 N/A pytest (>=7.3.0,<8.0.0) + :pypi:`nuts` Network Unit Testing System May 28, 2024 N/A pytest<8,>=7 :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 Feb 10, 2024 N/A pytest (>=6) @@ -66,6 +66,7 @@ This list contains 1448 plugins. :pypi:`pytest-allure-adaptor2` Plugin for py.test to generate allure xml reports Oct 14, 2020 N/A pytest (>=2.7.3) :pypi:`pytest-allure-collection` pytest plugin to collect allure markers without running any tests Apr 13, 2023 N/A pytest :pypi:`pytest-allure-dsl` pytest plugin to test case doc string dls instructions Oct 25, 2020 4 - Beta pytest + :pypi:`pytest-allure-id2history` Overwrite allure history id with testcase full name and testcase id if testcase has id, exclude parameters. May 14, 2024 4 - Beta pytest>=6.2.0 :pypi:`pytest-allure-intersection` Oct 27, 2022 N/A pytest (<5) :pypi:`pytest-allure-spec-coverage` The pytest plugin aimed to display test coverage of the specs(requirements) in Allure Oct 26, 2021 N/A pytest :pypi:`pytest-alphamoon` Static code checks used at Alphamoon Dec 30, 2021 5 - Production/Stable pytest (>=3.5.0) @@ -73,7 +74,7 @@ This list contains 1448 plugins. :pypi:`pytest-android` This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. Feb 21, 2019 3 - Alpha pytest :pypi:`pytest-anki` A pytest plugin for testing Anki add-ons Jul 31, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-annotate` pytest-annotate: Generate PyAnnotate annotations from your pytest tests. Jun 07, 2022 3 - Alpha pytest (<8.0.0,>=3.2.0) - :pypi:`pytest-ansible` Plugin for pytest to simplify calling ansible modules from tests or fixtures Jan 18, 2024 5 - Production/Stable pytest >=6 + :pypi:`pytest-ansible` Plugin for pytest to simplify calling ansible modules from tests or fixtures Jul 10, 2024 5 - Production/Stable pytest>=6 :pypi:`pytest-ansible-playbook` Pytest fixture which runs given ansible playbook file. Mar 08, 2019 4 - Beta N/A :pypi:`pytest-ansible-playbook-runner` Pytest fixture which runs given ansible playbook file. Dec 02, 2020 4 - Beta pytest (>=3.1.0) :pypi:`pytest-ansible-units` A pytest plugin for running unit tests within an ansible collection Apr 14, 2022 N/A N/A @@ -85,6 +86,7 @@ This list contains 1448 plugins. :pypi:`pytest-api` An ASGI middleware to populate OpenAPI Specification examples from pytest functions May 12, 2022 N/A pytest (>=7.1.1,<8.0.0) :pypi:`pytest-api-soup` Validate multiple endpoints with unit testing using a single source of truth. Aug 27, 2022 N/A N/A :pypi:`pytest-apistellar` apistellar plugin for pytest. Jun 18, 2019 N/A N/A + :pypi:`pytest-apiver` Jun 21, 2024 N/A pytest :pypi:`pytest-appengine` AppEngine integration that works well with pytest-django Feb 27, 2017 N/A N/A :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) @@ -99,6 +101,7 @@ This list contains 1448 plugins. :pypi:`pytest-assertions` Pytest Assertions Apr 27, 2022 N/A N/A :pypi:`pytest-assertutil` pytest-assertutil May 10, 2019 N/A N/A :pypi:`pytest-assert-utils` Useful assertion utilities for use with pytest Apr 14, 2022 3 - Alpha N/A + :pypi:`pytest-assist` load testing library Jun 24, 2024 N/A pytest :pypi:`pytest-assume` A pytest plugin that allows multiple failures per test Jun 24, 2021 N/A pytest (>=2.7) :pypi:`pytest-assurka` A pytest plugin for Assurka Studio Aug 04, 2022 N/A N/A :pypi:`pytest-ast-back-to-python` A plugin for pytest devs to view how assertion rewriting recodes the AST Sep 29, 2019 4 - Beta N/A @@ -108,24 +111,27 @@ This list contains 1448 plugins. :pypi:`pytest-ast-transformer` May 04, 2019 3 - Alpha pytest :pypi:`pytest_async` pytest-async - Run your coroutine in event loop without decorator Feb 26, 2020 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 Mar 19, 2024 4 - Beta pytest <9,>=7.0.0 - :pypi:`pytest-asyncio-cooperative` Run all your asynchronous tests cooperatively. Feb 25, 2024 N/A N/A + :pypi:`pytest-asyncio` Pytest support for asyncio May 19, 2024 4 - Beta pytest<9,>=7.0.0 + :pypi:`pytest-asyncio-cooperative` Run all your asynchronous tests cooperatively. Jul 04, 2024 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) :pypi:`pytest-atf-allure` 基于allure-pytest进行自定义 Nov 29, 2023 N/A pytest (>=7.4.2,<8.0.0) :pypi:`pytest-atomic` Skip rest of tests if previous test failed. Nov 24, 2018 4 - Beta N/A :pypi:`pytest-attrib` pytest plugin to select tests based on attributes similar to the nose-attrib plugin May 24, 2016 4 - Beta N/A + :pypi:`pytest-attributes` A plugin that allows users to add attributes to their tests. These attributes can then be referenced by fixtures or the test itself. Jun 24, 2024 4 - Beta pytest>=6.2.0 :pypi:`pytest-austin` Austin plugin for pytest Oct 11, 2020 4 - Beta N/A :pypi:`pytest-autocap` automatically capture test & fixture stdout/stderr to files May 15, 2022 N/A pytest (<7.2,>=7.1.2) :pypi:`pytest-autochecklog` automatically check condition and log all the checks Apr 25, 2015 4 - Beta N/A - :pypi:`pytest-automation` pytest plugin for building a test suite, using YAML files to extend pytest parameterize functionality. May 20, 2022 N/A pytest (>=7.0.0) + :pypi:`pytest-automation` pytest plugin for building a test suite, using YAML files to extend pytest parameterize functionality. Apr 24, 2024 N/A pytest>=7.0.0 :pypi:`pytest-automock` Pytest plugin for automatical mocks creation May 16, 2023 N/A pytest ; extra == 'dev' :pypi:`pytest-auto-parametrize` pytest plugin: avoid repeating arguments in parametrize Oct 02, 2016 3 - Alpha N/A :pypi:`pytest-autotest` This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. Aug 25, 2021 N/A pytest + :pypi:`pytest-aux` templates/examples and aux for pytest Jul 05, 2024 N/A N/A :pypi:`pytest-aviator` Aviator's Flakybot pytest plugin that automatically reruns flaky tests. Nov 04, 2022 4 - Beta pytest :pypi:`pytest-avoidance` Makes pytest skip tests that don not need rerunning May 23, 2019 4 - Beta pytest (>=3.5.0) :pypi:`pytest-aws` pytest plugin for testing AWS resource configurations Oct 04, 2017 4 - Beta N/A + :pypi:`pytest-aws-apigateway` pytest plugin for AWS ApiGateway May 24, 2024 4 - Beta pytest :pypi:`pytest-aws-config` Protect your AWS credentials in unit tests May 28, 2021 N/A N/A :pypi:`pytest-aws-fixtures` A series of fixtures to use in integration tests involving actual AWS services. Feb 02, 2024 N/A pytest (>=8.0.0,<9.0.0) :pypi:`pytest-axe` pytest plugin for axe-selenium-python Nov 12, 2018 N/A pytest (>=3.0.0) @@ -136,16 +142,18 @@ This list contains 1448 plugins. :pypi:`pytest-bandit` A bandit plugin for pytest Feb 23, 2021 4 - Beta pytest (>=3.5.0) :pypi:`pytest-bandit-xayon` A bandit plugin for pytest Oct 17, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-base-url` pytest plugin for URL based testing Jan 31, 2024 5 - Production/Stable pytest>=7.0.0 - :pypi:`pytest-bdd` BDD for pytest Mar 17, 2024 6 - Mature pytest (>=6.2.0) + :pypi:`pytest-batch-regression` A pytest plugin to repeat the entire test suite in batches. May 08, 2024 N/A pytest>=6.0.0 + :pypi:`pytest-bazel` A pytest runner with bazel support Jul 12, 2024 4 - Beta pytest + :pypi:`pytest-bdd` BDD for pytest Jun 04, 2024 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 Dec 31, 2023 4 - Beta pytest >=5.0 - :pypi:`pytest-bdd-report` A pytest-bdd plugin for generating useful and informative BDD test reports Feb 19, 2024 N/A pytest >=7.1.3 + :pypi:`pytest-bdd-report` A pytest-bdd plugin for generating useful and informative BDD test reports May 20, 2024 N/A pytest >=7.1.3 :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-beartype` Pytest plugin to run your tests with beartype checking enabled. Jan 25, 2024 N/A pytest - :pypi:`pytest-bec-e2e` BEC pytest plugin for end-to-end tests Apr 19, 2024 3 - Alpha pytest + :pypi:`pytest-bec-e2e` BEC pytest plugin for end-to-end tests Jul 08, 2024 3 - Alpha 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 @@ -155,7 +163,7 @@ This list contains 1448 plugins. :pypi:`pytest-bg-process` Pytest plugin to initialize background process Jan 24, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-bigchaindb` A BigchainDB plugin for pytest. Jan 24, 2022 4 - Beta N/A :pypi:`pytest-bigquery-mock` Provides a mock fixture for python bigquery client Dec 28, 2022 N/A pytest (>=5.0) - :pypi:`pytest-bisect-tests` Find tests leaking state and affecting other Mar 25, 2024 N/A N/A + :pypi:`pytest-bisect-tests` Find tests leaking state and affecting other Jun 09, 2024 N/A N/A :pypi:`pytest-black` A pytest plugin to enable format checking with black Oct 05, 2020 4 - Beta N/A :pypi:`pytest-black-multipy` Allow '--black' on older Pythons Jan 14, 2021 5 - Production/Stable pytest (!=3.7.3,>=3.5) ; extra == 'testing' :pypi:`pytest-black-ng` A pytest plugin to enable format checking with black Oct 20, 2022 4 - Beta pytest (>=7.0.0) @@ -168,7 +176,9 @@ This list contains 1448 plugins. :pypi:`pytest-board` Local continuous test runner with pytest and watchdog. Jan 20, 2019 N/A N/A :pypi:`pytest-boost-xml` Plugin for pytest to generate boost xml reports Nov 30, 2022 4 - Beta N/A :pypi:`pytest-bootstrap` Mar 04, 2022 N/A N/A + :pypi:`pytest-boto-mock` Thin-wrapper around the mock package for easier use with pytest Jun 05, 2024 5 - Production/Stable pytest>=8.2.0 :pypi:`pytest-bpdb` A py.test plug-in to enable drop to bpdb debugger on test failure. Jan 19, 2015 2 - Pre-Alpha N/A + :pypi:`pytest-bq` BigQuery fixtures and fixture factories for Pytest. May 08, 2024 5 - Production/Stable pytest>=6.2 :pypi:`pytest-bravado` Pytest-bravado automatically generates from OpenAPI specification client fixtures. Feb 15, 2022 N/A N/A :pypi:`pytest-breakword` Use breakword with pytest Aug 04, 2021 N/A pytest (>=6.2.4,<7.0.0) :pypi:`pytest-breed-adapter` A simple plugin to connect with breed-server Nov 07, 2018 4 - Beta pytest (>=3.5.0) @@ -179,7 +189,7 @@ This list contains 1448 plugins. :pypi:`pytest_browserstack` Py.test plugin for BrowserStack Jan 27, 2016 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 Sep 23, 2023 5 - Production/Stable pytest >=7.1.0 + :pypi:`pytest-bug` Pytest plugin for marking tests as a bug Jun 05, 2024 5 - Production/Stable pytest>=8.0.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) @@ -210,12 +220,12 @@ This list contains 1448 plugins. :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. Jan 18, 2024 N/A pytest>=7.0.0 - :pypi:`pytest-checkdocs` check the README when running tests Mar 31, 2024 5 - Production/Stable pytest>=6; extra == "testing" + :pypi:`pytest-checkdocs` check the README when running tests Apr 30, 2024 5 - Production/Stable pytest!=8.1.*,>=6; extra == "testing" :pypi:`pytest-checkipdb` plugin to check if there are ipdb debugs left Dec 04, 2023 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 :pypi:`pytest-check-links` Check links in files Jul 29, 2020 N/A pytest<9,>=7.0 - :pypi:`pytest-checklist` Pytest plugin to track and report unit/function coverage. Mar 12, 2024 N/A N/A + :pypi:`pytest-checklist` Pytest plugin to track and report unit/function coverage. Jun 10, 2024 N/A N/A :pypi:`pytest-check-mk` pytest plugin to test Check_MK checks Nov 19, 2015 4 - Beta pytest :pypi:`pytest-check-requirements` A package to prevent Dependency Confusion attacks against Yandex. Feb 20, 2024 N/A N/A :pypi:`pytest-ch-framework` My pytest framework Apr 17, 2024 N/A pytest==8.0.1 @@ -229,16 +239,17 @@ This list contains 1448 plugins. :pypi:`pytest-ckan` Backport of CKAN 2.9 pytest plugin and fixtures to CAKN 2.8 Apr 28, 2020 4 - Beta pytest :pypi:`pytest-clarity` A plugin providing an alternative, colourful diff output for failing assertions. Jun 11, 2021 N/A N/A :pypi:`pytest-cldf` Easy quality control for CLDF datasets using pytest Nov 07, 2022 N/A pytest (>=3.6) + :pypi:`pytest-cleanslate` Collects and executes pytest tests separately Jun 17, 2024 N/A pytest :pypi:`pytest_cleanup` Automated, comprehensive and well-organised pytest test cases. Jan 28, 2020 N/A N/A :pypi:`pytest-cleanuptotal` A cleanup plugin for pytest Mar 19, 2024 5 - Production/Stable N/A - :pypi:`pytest-clerk` A set of pytest fixtures to help with integration testing with Clerk. Apr 19, 2024 N/A pytest<9.0.0,>=8.0.0 + :pypi:`pytest-clerk` A set of pytest fixtures to help with integration testing with Clerk. Jun 27, 2024 N/A pytest<9.0.0,>=8.0.0 :pypi:`pytest-click` Pytest plugin for Click Feb 11, 2022 5 - Production/Stable pytest (>=5.0) :pypi:`pytest-cli-fixtures` Automatically register fixtures for custom CLI arguments Jul 28, 2022 N/A pytest (~=7.0) :pypi:`pytest-clld` Jul 06, 2022 N/A pytest (>=3.6) :pypi:`pytest-cloud` Distributed tests planner plugin for pytest testing framework. Oct 05, 2020 6 - Mature N/A :pypi:`pytest-cloudflare-worker` pytest plugin for testing cloudflare workers Mar 30, 2021 4 - Beta pytest (>=6.0.0) :pypi:`pytest-cloudist` Distribute tests to cloud machines without fuss Sep 02, 2022 4 - Beta pytest (>=7.1.2,<8.0.0) - :pypi:`pytest-cmake` Provide CMake module for Pytest Mar 18, 2024 N/A pytest<9,>=4 + :pypi:`pytest-cmake` Provide CMake module for Pytest May 31, 2024 N/A pytest<9,>=4 :pypi:`pytest-cmake-presets` Execute CMake Presets via pytest Dec 26, 2022 N/A pytest (>=7.2.0,<8.0.0) :pypi:`pytest-cobra` PyTest plugin for testing Smart Contracts for Ethereum blockchain. Jun 29, 2019 3 - Alpha pytest (<4.0.0,>=3.7.1) :pypi:`pytest_codeblocks` Test code blocks in your READMEs Sep 17, 2023 5 - Production/Stable pytest >= 7.0.0 @@ -257,7 +268,7 @@ This list contains 1448 plugins. :pypi:`pytest-collect-pytest-interinfo` A simple plugin to use with pytest Sep 26, 2023 4 - Beta N/A :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-common-subject` pytest framework for testing different aspects of a common method Jun 12, 2024 N/A pytest<9,>=3.6 :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 @@ -267,8 +278,9 @@ This list contains 1448 plugins. :pypi:`pytest-container` Pytest fixtures for writing container based tests Apr 10, 2024 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-continuous` A pytest plugin to run tests continuously until failure or interruption. Apr 23, 2024 N/A N/A :pypi:`pytest-cookies` The pytest plugin for your Cookiecutter templates. 🍪 Mar 22, 2023 5 - Production/Stable pytest (>=3.9.0) - :pypi:`pytest-copie` The pytest plugin for your copier templates 📒 Jan 27, 2024 3 - Alpha pytest + :pypi:`pytest-copie` The pytest plugin for your copier templates 📒 Jun 26, 2024 3 - Alpha pytest :pypi:`pytest-copier` A pytest plugin to help testing Copier templates Dec 11, 2023 4 - Beta pytest>=7.3.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 @@ -276,7 +288,7 @@ This list contains 1448 plugins. :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 Jun 28, 2023 4 - Beta N/A - :pypi:`pytest-coveragemarkers` Using pytest markers to track functional coverage and filtering of tests Apr 15, 2024 N/A pytest<8.0.0,>=7.1.2 + :pypi:`pytest-coveragemarkers` Using pytest markers to track functional coverage and filtering of tests Jun 04, 2024 N/A pytest<8.0.0,>=7.1.2 :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_covid` Too many faillure, less tests. Jun 24, 2020 N/A N/A :pypi:`pytest-cpp` Use pytest's runner to discover and execute C++ tests Nov 01, 2023 5 - Production/Stable pytest >=7.0 @@ -295,15 +307,16 @@ This list contains 1448 plugins. :pypi:`pytest-custom-concurrency` Custom grouping concurrence for pytest Feb 08, 2021 N/A N/A :pypi:`pytest-custom-exit-code` Exit pytest test session with custom exit code in different scenarios Aug 07, 2019 4 - Beta pytest (>=4.0.2) :pypi:`pytest-custom-nodeid` Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report Mar 07, 2021 N/A N/A + :pypi:`pytest-custom-outputs` A plugin that allows users to create and use custom outputs instead of the standard Pass and Fail. Also allows users to retrieve test results in fixtures. Jul 10, 2024 4 - Beta pytest>=6.2.0 :pypi:`pytest-custom-report` Configure the symbols displayed for test outcomes Jan 30, 2019 N/A pytest :pypi:`pytest-custom-scheduling` Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report Mar 01, 2021 N/A N/A :pypi:`pytest-cython` A plugin for testing Cython extension modules Apr 05, 2024 5 - Production/Stable pytest>=8 :pypi:`pytest-cython-collect` Jun 17, 2022 N/A pytest :pypi:`pytest-darker` A pytest plugin for checking of modified code using Darker Feb 25, 2024 N/A pytest <7,>=6.0.1 :pypi:`pytest-dash` pytest fixtures to run dash applications. Mar 18, 2019 N/A N/A - :pypi:`pytest-dashboard` Apr 18, 2024 N/A pytest<8.0.0,>=7.4.3 + :pypi:`pytest-dashboard` May 30, 2024 N/A pytest<8.0.0,>=7.4.3 :pypi:`pytest-data` Useful functions for managing data for pytest fixtures Nov 01, 2016 5 - Production/Stable N/A - :pypi:`pytest-databases` Reusable database fixtures for any and all databases. Apr 19, 2024 4 - Beta pytest + :pypi:`pytest-databases` Reusable database fixtures for any and all databases. Jul 02, 2024 4 - Beta pytest :pypi:`pytest-databricks` Pytest plugin for remote Databricks notebooks testing Jul 29, 2020 N/A pytest :pypi:`pytest-datadir` pytest plugin for test data directories and files Oct 03, 2023 5 - Production/Stable pytest >=5.0 :pypi:`pytest-datadir-mgr` Manager for test data: downloads, artifact caching, and a tmpdir context. Apr 06, 2023 5 - Production/Stable pytest (>=7.1) @@ -325,7 +338,7 @@ This list contains 1448 plugins. :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. Aug 25, 2023 N/A pytest >=6.2.5 ; extra == 'test' + :pypi:`pytest-dbt-core` Pytest extension for dbt. Jun 04, 2024 N/A pytest>=6.2.5; extra == "test" :pypi:`pytest-dbt-postgres` Pytest tooling to unittest DBT & Postgres models Jan 02, 2024 N/A pytest (>=7.4.3,<8.0.0) :pypi:`pytest-dbus-notification` D-BUS notifications for pytest results. Mar 05, 2014 5 - Production/Stable N/A :pypi:`pytest-dbx` Pytest plugin to run unit tests for dbx (Databricks CLI extensions) related code Nov 29, 2022 N/A pytest (>=7.1.3,<8.0.0) @@ -351,17 +364,21 @@ This list contains 1448 plugins. :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 Dec 11, 2023 4 - Beta pytest>=7.3.2 + :pypi:`pytest-dirty` Static import analysis for thrifty testing. Jul 11, 2024 3 - Alpha pytest>=8.2; extra == "dev" :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. Oct 18, 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. May 11, 2024 4 - Beta pytest!=6.0.0,<9,>=3.3.2 :pypi:`pytest-discover` Pytest plugin to record discovered tests in a file Mar 26, 2024 N/A pytest + :pypi:`pytest-ditto` Snapshot testing pytest plugin with minimal ceremony and flexible persistence formats. Jun 09, 2024 4 - Beta pytest>=3.5.0 + :pypi:`pytest-ditto-pandas` pytest-ditto plugin for pandas snapshots. May 29, 2024 4 - Beta pytest>=3.5.0 + :pypi:`pytest-ditto-pyarrow` pytest-ditto plugin for pyarrow tables. Jun 09, 2024 4 - Beta pytest>=3.5.0 :pypi:`pytest-django` A Django plugin for pytest. Jan 30, 2024 5 - Production/Stable pytest >=7.0.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 :pypi:`pytest-django-cache-xdist` A djangocachexdist plugin for pytest May 12, 2020 4 - Beta N/A :pypi:`pytest-django-casperjs` Integrate CasperJS with your django tests as a pytest fixture. Mar 15, 2015 2 - Pre-Alpha N/A :pypi:`pytest-django-class` A pytest plugin for running django in class-scoped fixtures Aug 08, 2023 4 - Beta N/A - :pypi:`pytest-django-docker-pg` Jan 30, 2024 5 - Production/Stable pytest <8.0.0,>=7.0.0 + :pypi:`pytest-django-docker-pg` Jun 13, 2024 5 - Production/Stable pytest<9.0.0,>=7.0.0 :pypi:`pytest-django-dotenv` Pytest plugin used to setup environment variables with django-dotenv Nov 26, 2019 4 - Beta pytest (>=2.6.0) :pypi:`pytest-django-factories` Factories for your Django models that can be used as Pytest fixtures. Nov 12, 2020 4 - Beta N/A :pypi:`pytest-django-filefield` Replaces FileField.storage with something you can patch globally. May 09, 2022 5 - Production/Stable pytest >= 5.2 @@ -404,6 +421,7 @@ This list contains 1448 plugins. :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-doctest-mkdocstrings` Run pytest --doctest-modules with markdown docstrings in code blocks (\`\`\`) Mar 02, 2024 N/A pytest :pypi:`pytest-doctestplus` Pytest plugin with advanced doctest features. Mar 10, 2024 5 - Production/Stable pytest >=4.6 + :pypi:`pytest-documentary` A simple pytest plugin to generate test documentation Jul 11, 2024 N/A pytest :pypi:`pytest-dogu-report` pytest plugin for dogu report Jul 07, 2023 N/A N/A :pypi:`pytest-dogu-sdk` pytest plugin for the Dogu Dec 14, 2023 N/A N/A :pypi:`pytest-dolphin` Some extra stuff that we use ininternally Nov 30, 2016 4 - Beta pytest (==3.0.4) @@ -430,20 +448,21 @@ This list contains 1448 plugins. :pypi:`pytest-ebics-sandbox` A pytest plugin for testing against an EBICS sandbox server. Requires docker. Aug 15, 2022 N/A N/A :pypi:`pytest-ec2` Pytest execution on EC2 instance Oct 22, 2019 3 - Alpha N/A :pypi:`pytest-echo` pytest plugin with mechanisms for echoing environment variables, package version and generic attributes Dec 05, 2023 5 - Production/Stable pytest >=2.2 + :pypi:`pytest-edit` Edit the source code of a failed test with \`pytest --edit\`. Jun 09, 2024 N/A pytest :pypi:`pytest-ekstazi` Pytest plugin to select test using Ekstazi algorithm Sep 10, 2022 N/A pytest :pypi:`pytest-elasticsearch` Elasticsearch fixtures and fixture factories for Pytest. Mar 15, 2024 5 - Production/Stable pytest >=7.0 :pypi:`pytest-elements` Tool to help automate user interfaces Jan 13, 2021 N/A pytest (>=5.4,<6.0) :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 Apr 04, 2024 4 - Beta pytest>=3.5.0 :pypi:`pytest-email` Send execution result email Jul 08, 2020 N/A pytest - :pypi:`pytest-embedded` A pytest plugin that designed for embedded testing. Apr 09, 2024 5 - Production/Stable pytest>=7.0 - :pypi:`pytest-embedded-arduino` Make pytest-embedded plugin work with Arduino. Apr 09, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-idf` Make pytest-embedded plugin work with ESP-IDF. Apr 09, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-jtag` Make pytest-embedded plugin work with JTAG. Apr 09, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-qemu` Make pytest-embedded plugin work with QEMU. Apr 09, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-serial` Make pytest-embedded plugin work with Serial. Apr 09, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-serial-esp` Make pytest-embedded plugin work with Espressif target boards. Apr 09, 2024 5 - Production/Stable N/A - :pypi:`pytest-embedded-wokwi` Make pytest-embedded plugin work with the Wokwi CLI. Apr 09, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded` A pytest plugin that designed for embedded testing. May 31, 2024 5 - Production/Stable pytest>=7.0 + :pypi:`pytest-embedded-arduino` Make pytest-embedded plugin work with Arduino. May 23, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-idf` Make pytest-embedded plugin work with ESP-IDF. May 23, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-jtag` Make pytest-embedded plugin work with JTAG. May 23, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-qemu` Make pytest-embedded plugin work with QEMU. May 23, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-serial` Make pytest-embedded plugin work with Serial. May 31, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-serial-esp` Make pytest-embedded plugin work with Espressif target boards. May 31, 2024 5 - Production/Stable N/A + :pypi:`pytest-embedded-wokwi` Make pytest-embedded plugin work with the Wokwi CLI. May 23, 2024 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) @@ -468,16 +487,17 @@ This list contains 1448 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-evm` The testing package containing tools to test Web3-based projects Apr 20, 2024 4 - Beta pytest<9.0.0,>=8.1.1 + :pypi:`pytest-evm` The testing package containing tools to test Web3-based projects Apr 22, 2024 4 - Beta pytest<9.0.0,>=8.1.1 :pypi:`pytest_exact_fixtures` Parse queries in Lucene and Elasticsearch syntaxes Feb 04, 2019 N/A N/A - :pypi:`pytest-examples` Pytest plugin for testing examples in docstrings and markdown files. Jul 11, 2023 4 - Beta pytest>=7 - :pypi:`pytest-exasol-itde` Feb 15, 2024 N/A pytest (>=7,<9) - :pypi:`pytest-excel` pytest plugin for generating excel reports Sep 14, 2023 5 - Production/Stable N/A + :pypi:`pytest-examples` Pytest plugin for testing examples in docstrings and markdown files. Jul 02, 2024 4 - Beta pytest>=7 + :pypi:`pytest-exasol-itde` Jul 01, 2024 N/A pytest<9,>=7 + :pypi:`pytest-exasol-saas` Jun 07, 2024 N/A pytest<9,>=7 + :pypi:`pytest-excel` pytest plugin for generating excel reports Jun 18, 2024 5 - Production/Stable pytest>3.6 :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 :pypi:`pytest-executable` pytest plugin for testing executables Oct 07, 2023 N/A pytest <8,>=5 :pypi:`pytest-execution-timer` A timer for the phases of Pytest's execution. Dec 24, 2021 4 - Beta N/A - :pypi:`pytest-exit-code` A pytest plugin that overrides the built-in exit codes to retain more information about the test results. Feb 23, 2024 4 - Beta pytest >=6.2.0 + :pypi:`pytest-exit-code` A pytest plugin that overrides the built-in exit codes to retain more information about the test results. May 06, 2024 4 - Beta pytest>=6.2.0 :pypi:`pytest-expect` py.test plugin to store test expectations and mark tests based on them Apr 21, 2016 4 - Beta N/A :pypi:`pytest-expectdir` A pytest plugin to provide initial/expected directories, and check a test transforms the initial directory to the expected one Mar 19, 2023 5 - Production/Stable pytest (>=5.0) :pypi:`pytest-expecter` Better testing with expecter and pytest. Sep 18, 2022 5 - Production/Stable N/A @@ -502,7 +522,7 @@ This list contains 1448 plugins. :pypi:`pytest-failed-screen-record` Create a video of the screen when pytest fails Jan 05, 2023 4 - Beta pytest (>=7.1.2d,<8.0.0) :pypi:`pytest-failed-screenshot` Test case fails,take a screenshot,save it,attach it to the allure Apr 21, 2021 N/A N/A :pypi:`pytest-failed-to-verify` A pytest plugin that helps better distinguishing real test failures from setup flakiness. Aug 08, 2019 5 - Production/Stable pytest (>=4.1.0) - :pypi:`pytest-fail-slow` Fail tests that take too long to run Feb 11, 2024 N/A pytest>=7.0 + :pypi:`pytest-fail-slow` Fail tests that take too long to run Jun 01, 2024 N/A pytest>=7.0 :pypi:`pytest-faker` Faker integration with the pytest framework. Dec 19, 2016 6 - Mature N/A :pypi:`pytest-falcon` Pytest helpers for Falcon. Sep 07, 2016 4 - Beta N/A :pypi:`pytest-falcon-client` A package to prevent Dependency Confusion attacks against Yandex. Feb 21, 2024 N/A N/A @@ -512,11 +532,12 @@ This list contains 1448 plugins. :pypi:`pytest-fastest` Use SCM and coverage to run only needed tests Oct 04, 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-fauna` A collection of helpful test fixtures for Fauna DB. May 30, 2024 N/A N/A :pypi:`pytest-fauxfactory` Integration of fauxfactory into pytest. Dec 06, 2017 5 - Production/Stable pytest (>=3.2) :pypi:`pytest-figleaf` py.test figleaf coverage plugin Jan 18, 2010 5 - Production/Stable N/A :pypi:`pytest-file` Pytest File Mar 18, 2024 1 - Planning N/A :pypi:`pytest-filecov` A pytest plugin to detect unused files Jun 27, 2021 4 - Beta pytest - :pypi:`pytest-filedata` easily load data from files Jan 17, 2019 4 - Beta N/A + :pypi:`pytest-filedata` easily load test data from files Apr 29, 2024 5 - Production/Stable N/A :pypi:`pytest-filemarker` A pytest plugin that runs marked tests when files change. Dec 01, 2020 N/A pytest :pypi:`pytest-file-watcher` Pytest-File-Watcher is a CLI tool that watches for changes in your code and runs pytest on the changed files. Mar 23, 2023 N/A pytest :pypi:`pytest-filter-case` run test cases filter by mark Nov 05, 2020 N/A N/A @@ -547,7 +568,7 @@ This list contains 1448 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 Jun 26, 2023 4 - Beta pytest (>=7.0.0) + :pypi:`pytest-fluent` A pytest plugin in order to provide logs via fluentd Jun 05, 2024 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-fly` pytest observer Apr 14, 2024 3 - Alpha pytest :pypi:`pytest-flyte` Pytest fixtures for simplifying Flyte integration testing May 03, 2021 N/A pytest @@ -558,6 +579,7 @@ This list contains 1448 plugins. :pypi:`pytest-forward-compatability` A name to avoid typosquating pytest-foward-compatibility Sep 06, 2020 N/A N/A :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 Oct 29, 2023 4 - Beta pytest>=7.0.0 + :pypi:`pytest-freezeblaster` Wrap tests with fixtures in freeze_time Jul 10, 2024 N/A pytest>=6.2.5 :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 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 @@ -566,18 +588,18 @@ This list contains 1448 plugins. :pypi:`pytest-funparam` An alternative way to parametrize test cases. Dec 02, 2021 4 - Beta pytest >=4.6.0 :pypi:`pytest-fxa` pytest plugin for Firefox Accounts Aug 28, 2018 5 - Production/Stable N/A :pypi:`pytest-fxtest` Oct 27, 2020 N/A N/A - :pypi:`pytest-fzf` fzf-based test selector for pytest Feb 07, 2024 4 - Beta pytest >=6.0.0 + :pypi:`pytest-fzf` fzf-based test selector for pytest Jul 03, 2024 4 - Beta pytest>=6.0.0 :pypi:`pytest_gae` pytest plugin for apps written with Google's AppEngine Aug 03, 2016 3 - Alpha N/A :pypi:`pytest-gather-fixtures` set up asynchronous pytest fixtures concurrently Apr 12, 2022 N/A pytest (>=6.0.0) :pypi:`pytest-gc` The garbage collector plugin for py.test Feb 01, 2018 N/A N/A :pypi:`pytest-gcov` Uses gcov to measure test coverage of a C library Feb 01, 2018 3 - Alpha N/A :pypi:`pytest-gcs` GCS fixtures and fixture factories for Pytest. Mar 01, 2024 5 - Production/Stable pytest >=6.2 - :pypi:`pytest-gee` The Python plugin for your GEE based packages. Feb 15, 2024 3 - Alpha pytest + :pypi:`pytest-gee` The Python plugin for your GEE based packages. Jun 30, 2024 3 - Alpha pytest :pypi:`pytest-gevent` Ensure that gevent is properly patched when invoking pytest Feb 25, 2020 N/A pytest :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. Apr 12, 2024 N/A pytest>=3.6 + :pypi:`pytest-girder` A set of pytest fixtures for testing Girder applications. Jul 08, 2024 N/A pytest>=3.6 :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 Oct 15, 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 @@ -591,7 +613,7 @@ This list contains 1448 plugins. :pypi:`pytest-gitlab-code-quality` Collects warnings while testing and generates a GitLab Code Quality Report. Apr 03, 2024 N/A pytest>=8.1.1 :pypi:`pytest-gitlab-fold` Folds output sections in GitLab CI build log Dec 31, 2023 4 - Beta pytest >=2.6.0 :pypi:`pytest-git-selector` Utility to select tests that have had its dependencies modified (as identified by git diff) Nov 17, 2022 N/A N/A - :pypi:`pytest-glamor-allure` Extends allure-pytest functionality Jul 22, 2022 4 - Beta pytest + :pypi:`pytest-glamor-allure` Extends allure-pytest functionality Apr 30, 2024 4 - Beta pytest<=8.2.0 :pypi:`pytest-gnupg-fixtures` Pytest fixtures for testing with gnupg. Mar 04, 2021 4 - Beta pytest :pypi:`pytest-golden` Plugin for pytest that offloads expected outputs to data files Jul 18, 2022 N/A pytest (>=6.1.2) :pypi:`pytest-goldie` A plugin to support golden tests with pytest. May 23, 2023 4 - Beta pytest (>=3.5.0) @@ -607,37 +629,36 @@ This list contains 1448 plugins. :pypi:`pytest-hardware-test-report` A simple plugin to use with pytest Apr 01, 2024 4 - Beta pytest<9.0.0,>=8.0.0 :pypi:`pytest-harmony` Chain tests and data with pytest Jan 17, 2023 N/A pytest (>=7.2.1,<8.0.0) :pypi:`pytest-harvest` Store data created during your pytest tests execution, and retrieve it at the end of the session, e.g. for applicative benchmarking purposes. Mar 16, 2024 5 - Production/Stable N/A - :pypi:`pytest-helm-chart` A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. Jun 15, 2020 4 - Beta pytest (>=5.4.2,<6.0.0) :pypi:`pytest-helm-charts` A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. Feb 07, 2024 4 - Beta pytest (>=8.0.0,<9.0.0) - :pypi:`pytest-helm-templates` Pytest fixtures for unit testing the output of helm templates Apr 05, 2024 N/A pytest~=7.4.0; extra == "dev" + :pypi:`pytest-helm-templates` Pytest fixtures for unit testing the output of helm templates May 08, 2024 N/A pytest~=7.4.0; extra == "dev" :pypi:`pytest-helper` Functions to help in using the pytest testing framework May 31, 2019 5 - Production/Stable N/A :pypi:`pytest-helpers` pytest helpers May 17, 2020 N/A pytest :pypi:`pytest-helpers-namespace` Pytest Helpers Namespace Plugin Dec 29, 2021 5 - Production/Stable pytest (>=6.0.0) :pypi:`pytest-henry` Aug 29, 2023 N/A N/A :pypi:`pytest-hidecaptured` Hide captured output May 04, 2018 4 - Beta pytest (>=2.8.5) - :pypi:`pytest-himark` A plugin that will filter pytest's test collection using a json file. It will read a json file provided with a --json argument in pytest command line (or in pytest.ini), search the markers key and automatically add -m option to the command line for filtering out the tests marked with disabled markers. Apr 14, 2024 4 - Beta pytest>=6.2.0 + :pypi:`pytest-himark` This plugin aims to create markers automatically based on a json configuration. Jun 05, 2024 4 - Beta pytest>=6.2.0 :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-history` Pytest plugin to keep a history of your pytest runs Jan 14, 2024 N/A pytest (>=7.4.3,<8.0.0) :pypi:`pytest-home` Home directory fixtures Oct 09, 2023 5 - Production/Stable 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 Apr 13, 2024 3 - Alpha pytest==8.1.1 + :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Jul 11, 2024 3 - Alpha pytest==8.2.0 :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` Apr 18, 2024 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-houdini` pytest plugin for testing code in Houdini. Feb 09, 2024 N/A pytest + :pypi:`pytest-houdini` pytest plugin for testing code in Houdini. Jul 05, 2024 N/A pytest :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) :pypi:`pytest-hpfeeds` Helpers for testing hpfeeds in your python project Feb 28, 2023 4 - Beta pytest (>=6.2.4,<7.0.0) :pypi:`pytest-html` pytest plugin for generating HTML reports Nov 07, 2023 5 - Production/Stable pytest>=7.0.0 :pypi:`pytest-html-cn` pytest plugin for generating HTML reports Aug 01, 2023 5 - Production/Stable N/A :pypi:`pytest-html-lee` optimized pytest plugin for generating HTML reports Jun 30, 2020 5 - Production/Stable pytest (>=5.0) - :pypi:`pytest-html-merger` Pytest HTML reports merging utility Nov 11, 2023 N/A N/A + :pypi:`pytest-html-merger` Pytest HTML reports merging utility Jul 12, 2024 N/A N/A :pypi:`pytest-html-object-storage` Pytest report plugin for send HTML report on object-storage Jan 17, 2024 5 - Production/Stable N/A :pypi:`pytest-html-profiling` Pytest plugin for generating HTML reports with per-test profiling and optionally call graph visualizations. Based on pytest-html by Dave Hunt. Feb 11, 2020 5 - Production/Stable pytest (>=3.0) :pypi:`pytest-html-reporter` Generates a static html report based on pytest framework Feb 13, 2022 N/A N/A - :pypi:`pytest-html-report-merger` Oct 23, 2023 N/A N/A + :pypi:`pytest-html-report-merger` May 22, 2024 N/A N/A :pypi:`pytest-html-thread` pytest plugin for generating HTML reports Dec 29, 2020 5 - Production/Stable N/A :pypi:`pytest-http` Fixture "http" for http requests Dec 05, 2019 N/A N/A :pypi:`pytest-httpbin` Easily test your HTTP library against a local copy of httpbin May 08, 2023 5 - Production/Stable pytest ; extra == 'test' @@ -645,35 +666,36 @@ This list contains 1448 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 Feb 24, 2024 3 - Alpha N/A - :pypi:`pytest-httptesting` http_testing framework on top of pytest Jul 24, 2023 N/A pytest (>=7.2.0,<8.0.0) + :pypi:`pytest-httptesting` http_testing framework on top of pytest May 08, 2024 N/A pytest<9.0.0,>=8.2.0 :pypi:`pytest-httpx` Send responses to httpx. Feb 21, 2024 5 - Production/Stable pytest <9,>=7 :pypi:`pytest-httpx-blockage` Disable httpx requests during a test run Feb 16, 2023 N/A pytest (>=7.2.1) :pypi:`pytest-httpx-recorder` Recorder feature based on pytest_httpx, like recorder feature in responses. Jan 04, 2024 5 - Production/Stable pytest :pypi:`pytest-hue` Visualise PyTest status via your Phillips Hue lights May 09, 2019 N/A N/A :pypi:`pytest-hylang` Pytest plugin to allow running tests written in hylang Mar 28, 2021 N/A pytest :pypi:`pytest-hypo-25` help hypo module for pytest Jan 12, 2020 3 - Alpha N/A - :pypi:`pytest-iam` A fully functional OAUTH2 / OpenID Connect (OIDC) server to be used in your testsuite Apr 12, 2024 3 - Alpha pytest>=7.0.0 + :pypi:`pytest-iam` A fully functional OAUTH2 / OpenID Connect (OIDC) server to be used in your testsuite Apr 22, 2024 3 - Alpha pytest>=7.0.0 :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 Dec 05, 2023 4 - Beta pytest :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 Dec 13, 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 08, 2024 5 - Production/Stable pytest>=6.0 + :pypi:`pytest-ignore-flaky` ignore failures from flaky tests (pytest plugin) Apr 20, 2024 5 - Production/Stable pytest>=6.0 :pypi:`pytest-ignore-test-results` A pytest plugin to ignore test results. Aug 17, 2023 2 - Pre-Alpha pytest>=7.0 :pypi:`pytest-image-diff` Mar 09, 2023 3 - Alpha pytest - :pypi:`pytest-image-snapshot` A pytest plugin for image snapshot management and comparison. Dec 01, 2023 4 - Beta pytest >=3.5.0 + :pypi:`pytest-image-snapshot` A pytest plugin for image snapshot management and comparison. Jul 01, 2024 4 - Beta pytest>=3.5.0 :pypi:`pytest-incremental` an incremental test runner (pytest plugin) Apr 24, 2021 5 - Production/Stable N/A + :pypi:`pytest-infinity` Jun 09, 2024 N/A pytest<9.0.0,>=8.0.0 :pypi:`pytest-influxdb` Plugin for influxdb and pytest integration. Apr 20, 2021 N/A N/A :pypi:`pytest-info-collector` pytest plugin to collect information from tests May 26, 2019 3 - Alpha N/A :pypi:`pytest-info-plugin` Get executed interface information in pytest interface automation framework Sep 14, 2023 N/A N/A :pypi:`pytest-informative-node` display more node ininformation. Apr 25, 2019 4 - Beta N/A :pypi:`pytest-infrastructure` pytest stack validation prior to testing executing Apr 12, 2020 4 - Beta N/A :pypi:`pytest-ini` Reuse pytest.ini to store env variables Apr 26, 2022 N/A N/A - :pypi:`pytest-initry` Plugin for sending automation test data from Pytest to the initry Apr 14, 2024 N/A pytest<9.0.0,>=8.1.1 + :pypi:`pytest-initry` Plugin for sending automation test data from Pytest to the initry Apr 30, 2024 N/A pytest<9.0.0,>=8.1.1 :pypi:`pytest-inline` A pytest plugin for writing inline tests. Oct 19, 2023 4 - Beta pytest >=7.0.0 - :pypi:`pytest-inmanta` A py.test plugin providing fixtures to simplify inmanta modules testing. Dec 13, 2023 5 - Production/Stable pytest - :pypi:`pytest-inmanta-extensions` Inmanta tests package Apr 02, 2024 5 - Production/Stable N/A - :pypi:`pytest-inmanta-lsm` Common fixtures for inmanta LSM related modules Apr 15, 2024 5 - Production/Stable N/A + :pypi:`pytest-inmanta` A py.test plugin providing fixtures to simplify inmanta modules testing. Jul 05, 2024 5 - Production/Stable pytest + :pypi:`pytest-inmanta-extensions` Inmanta tests package Jul 05, 2024 5 - Production/Stable N/A + :pypi:`pytest-inmanta-lsm` Common fixtures for inmanta LSM related modules Jul 06, 2024 5 - Production/Stable N/A :pypi:`pytest-inmanta-yang` Common fixtures used in inmanta yang related modules Feb 22, 2024 4 - Beta pytest :pypi:`pytest-Inomaly` A simple image diff plugin for pytest Feb 13, 2018 4 - Beta N/A :pypi:`pytest-in-robotframework` The extension enables easy execution of pytest tests within the Robot Framework environment. Mar 02, 2024 N/A pytest @@ -686,11 +708,11 @@ This list contains 1448 plugins. :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. Feb 09, 2024 4 - Beta pytest - :pypi:`pytest-invenio` Pytest fixtures for Invenio. Feb 28, 2024 5 - Production/Stable pytest <7.2.0,>=6 + :pypi:`pytest-invenio` Pytest fixtures for Invenio. Jun 27, 2024 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-ipywidgets` Apr 08, 2024 N/A pytest + :pypi:`pytest-ipywidgets` Jul 11, 2024 N/A pytest :pypi:`pytest-isolate` Feb 20, 2023 4 - Beta pytest :pypi:`pytest-isort` py.test plugin to check import ordering using isort Mar 05, 2024 5 - Production/Stable pytest (>=5.0) :pypi:`pytest-it` Pytest plugin to display test reports as a plaintext spec, inspired by Rspec: https://github.com/mattduck/pytest-it. Jan 29, 2024 4 - Beta N/A @@ -701,19 +723,20 @@ This list contains 1448 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 12, 2024 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` py.test JIRA integration plugin, using markers Apr 30, 2024 3 - Alpha N/A + :pypi:`pytest-jira-xfail` Plugin skips (xfail) tests if unresolved Jira issue(s) linked Jul 09, 2024 N/A pytest>=7.2.0 :pypi:`pytest-jira-xray` pytest plugin to integrate tests with JIRA XRAY Mar 27, 2024 4 - Beta pytest>=6.2.4 :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) :pypi:`pytest-json` Generate JSON test reports Jan 18, 2016 4 - Beta N/A + :pypi:`pytest-json-ctrf` Pytest plugin to generate json report in CTRF (Common Test Report Format) Jun 15, 2024 N/A pytest>6.0.0 :pypi:`pytest-json-fixtures` JSON output for the --fixtures flag Mar 14, 2023 4 - Beta N/A :pypi:`pytest-jsonlint` UNKNOWN Aug 04, 2016 N/A N/A :pypi:`pytest-json-report` A pytest plugin to report test results as JSON files Mar 15, 2022 4 - Beta pytest (>=3.8.0) :pypi:`pytest-json-report-wip` A pytest plugin to report test results as JSON files Oct 28, 2023 4 - Beta pytest >=3.8.0 :pypi:`pytest-jsonschema` A pytest plugin to perform JSONSchema validations Mar 27, 2024 4 - Beta pytest>=6.2.0 - :pypi:`pytest-jtr` pytest plugin supporting json test report output Apr 15, 2024 N/A pytest<8.0.0,>=7.1.2 + :pypi:`pytest-jtr` pytest plugin supporting json test report output Jun 04, 2024 N/A pytest<8.0.0,>=7.1.2 :pypi:`pytest-jupyter` A pytest plugin for testing Jupyter libraries and extensions. Apr 04, 2024 4 - Beta pytest>=7.0 :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 Jun 14, 2023 N/A pytest @@ -726,12 +749,13 @@ This list contains 1448 plugins. :pypi:`pytest-kivy` Kivy GUI tests fixtures using pytest Jul 06, 2021 4 - Beta pytest (>=3.6) :pypi:`pytest-knows` A pytest plugin that can automaticly skip test case based on dependence info calculated by trace Aug 22, 2014 N/A N/A :pypi:`pytest-konira` Run Konira DSL tests with py.test Oct 09, 2011 N/A N/A + :pypi:`pytest-kookit` Your simple but kooky integration testing with pytest May 16, 2024 N/A N/A :pypi:`pytest-koopmans` A plugin for testing the koopmans package Nov 21, 2022 4 - Beta pytest (>=3.5.0) :pypi:`pytest-krtech-common` pytest krtech common library Nov 28, 2016 4 - Beta N/A :pypi:`pytest-kubernetes` Sep 14, 2023 N/A pytest (>=7.2.1,<8.0.0) :pypi:`pytest-kuunda` pytest plugin to help with test data setup for PySpark tests Feb 25, 2024 4 - Beta pytest >=6.2.0 :pypi:`pytest-kwparametrize` Alternate syntax for @pytest.mark.parametrize with test cases as dictionaries and default value fallbacks Jan 22, 2021 N/A pytest (>=6) - :pypi:`pytest-lambda` Define pytest fixtures with lambda functions. Aug 20, 2022 3 - Alpha pytest (>=3.6,<8) + :pypi:`pytest-lambda` Define pytest fixtures with lambda functions. May 27, 2024 5 - Production/Stable pytest<9,>=3.6 :pypi:`pytest-lamp` Jan 06, 2017 3 - Alpha N/A :pypi:`pytest-langchain` Pytest-style test runner for langchain agents Feb 26, 2023 N/A pytest :pypi:`pytest-lark` Create fancy and clear HTML test reports. Nov 05, 2023 N/A N/A @@ -757,7 +781,7 @@ This list contains 1448 plugins. :pypi:`pytest-litter` Pytest plugin which verifies that tests do not modify file trees. Nov 23, 2023 4 - Beta pytest >=6.1 :pypi:`pytest-live` Live results for pytest Mar 08, 2020 N/A pytest :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 14, 2023 5 - Production/Stable pytest + :pypi:`pytest-localftpserver` A PyTest plugin which provides an FTP fixture for your tests May 19, 2024 5 - Production/Stable pytest :pypi:`pytest-localserver` pytest plugin to test server connections locally. Oct 12, 2023 4 - Beta N/A :pypi:`pytest-localstack` Pytest plugin for AWS integration tests Jun 07, 2023 4 - Beta pytest (>=6.0.0,<7.0.0) :pypi:`pytest-lock` pytest-lock is a pytest plugin that allows you to "lock" the results of unit tests, storing them in a local cache. This is particularly useful for tests that are resource-intensive or don't need to be run every time. When the tests are run subsequently, pytest-lock will compare the current results with the locked results and issue a warning if there are any discrepancies. Feb 03, 2024 N/A pytest (>=7.4.3,<8.0.0) @@ -770,11 +794,11 @@ This list contains 1448 plugins. :pypi:`pytest-logger` Plugin configuring handlers for loggers from Python logging module. Mar 10, 2024 5 - Production/Stable 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 Mar 30, 2024 5 - Production/Stable pytest==8.1.1 + :pypi:`pytest-logikal` Common testing environment Jun 27, 2024 5 - Production/Stable pytest==8.2.2 :pypi:`pytest-log-report` Package for creating a pytest test run reprot Dec 26, 2019 N/A N/A :pypi:`pytest-loguru` Pytest Loguru Mar 20, 2024 5 - Production/Stable pytest; extra == "test" :pypi:`pytest-loop` pytest plugin for looping tests Mar 30, 2024 5 - Production/Stable pytest - :pypi:`pytest-lsp` A pytest plugin for end-to-end testing of language servers Feb 07, 2024 3 - Alpha pytest + :pypi:`pytest-lsp` A pytest plugin for end-to-end testing of language servers May 22, 2024 3 - Alpha pytest :pypi:`pytest-manual-marker` pytest marker for marking manual tests Aug 04, 2022 3 - Alpha pytest>=7 :pypi:`pytest-markdoctest` A pytest plugin to doctest your markdown files Jul 22, 2022 4 - Beta pytest (>=6) :pypi:`pytest-markdown` Test your markdown docs with pytest Jan 15, 2021 4 - Beta pytest (>=6.0.1,<7.0.0) @@ -782,6 +806,7 @@ This list contains 1448 plugins. :pypi:`pytest-marker-bugzilla` py.test bugzilla integration plugin, using markers Jan 09, 2020 N/A N/A :pypi:`pytest-markers-presence` A simple plugin to detect missed pytest tags and markers" Feb 04, 2021 4 - Beta pytest (>=6.0) :pypi:`pytest-markfiltration` UNKNOWN Nov 08, 2011 3 - Alpha N/A + :pypi:`pytest-mark-manage` 用例标签化管理 Jul 08, 2024 N/A pytest :pypi:`pytest-mark-no-py3` pytest plugin and bowler codemod to help migrate tests to Python 3 May 17, 2019 N/A pytest :pypi:`pytest-marks` UNKNOWN Nov 23, 2012 3 - Alpha N/A :pypi:`pytest-matcher` Easy way to match captured \`pytest\` output against expectations stored in files Mar 15, 2024 5 - Production/Stable pytest @@ -793,7 +818,7 @@ This list contains 1448 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. Feb 04, 2024 4 - Beta pytest !=6.0.0,<9,>=3.3.2 + :pypi:`pytest-md-report` A pytest plugin to generate test outcomes reports with markdown table format. May 18, 2024 4 - Beta pytest!=6.0.0,<9,>=3.3.2 :pypi:`pytest-meilisearch` Pytest helpers for testing projects using Meilisearch Feb 15, 2024 N/A pytest (>=7.4.3) :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 @@ -805,13 +830,13 @@ This list contains 1448 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 Feb 12, 2024 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 Mar 14, 2024 N/A pytest + :pypi:`pytest-mh` Pytest multihost plugin Jul 02, 2024 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-minio-mock` A pytest plugin for mocking Minio S3 interactions Apr 15, 2024 N/A pytest>=5.0.0 + :pypi:`pytest-minio-mock` A pytest plugin for mocking Minio S3 interactions May 26, 2024 N/A pytest>=5.0.0 :pypi:`pytest-missing-fixtures` Pytest plugin that creates missing fixtures Oct 14, 2020 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-mitmproxy` pytest plugin for mitmproxy tests Mar 07, 2024 N/A pytest >=7.0 + :pypi:`pytest-mitmproxy` pytest plugin for mitmproxy tests May 28, 2024 N/A pytest>=7.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 Mar 21, 2024 5 - Production/Stable pytest>=6.2.5 @@ -820,14 +845,13 @@ This list contains 1448 plugins. :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. Apr 11, 2024 N/A pytest>=1.0 + :pypi:`pytest-mock-resources` A pytest plugin for easily instantiating reproducible mock resources. Jun 20, 2024 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 :pypi:`pytest-modalt` Massively distributed pytest runs using modal.com Feb 27, 2024 4 - Beta pytest >=6.2.0 :pypi:`pytest-modified-env` Pytest plugin to fail a test if it leaves modified \`os.environ\` afterwards. Jan 29, 2022 4 - Beta N/A :pypi:`pytest-modifyjunit` Utility for adding additional properties to junit xml for IDM QE Jan 10, 2019 N/A N/A - :pypi:`pytest-modifyscope` pytest plugin to modify fixture scope Apr 12, 2020 N/A pytest :pypi:`pytest-molecule` PyTest Molecule Plugin :: discover and run molecule tests Mar 29, 2022 5 - Production/Stable pytest (>=7.0.0) :pypi:`pytest-molecule-JC` PyTest Molecule Plugin :: discover and run molecule tests Jul 18, 2023 5 - Production/Stable pytest (>=7.0.0) :pypi:`pytest-mongo` MongoDB process and client fixtures plugin for Pytest. Mar 13, 2024 5 - Production/Stable pytest >=6.2 @@ -842,7 +866,7 @@ This list contains 1448 plugins. :pypi:`pytest-mpiexec` pytest plugin for running individual tests with mpiexec Apr 13, 2023 3 - Alpha pytest :pypi:`pytest-mpl` pytest plugin to help with testing figures output from Matplotlib Feb 14, 2024 4 - Beta pytest :pypi:`pytest-mproc` low-startup-overhead, scalable, distributed-testing pytest plugin Nov 15, 2022 4 - Beta pytest (>=6) - :pypi:`pytest-mqtt` pytest-mqtt supports testing systems based on MQTT Mar 31, 2024 4 - Beta pytest<8; extra == "test" + :pypi:`pytest-mqtt` pytest-mqtt supports testing systems based on MQTT May 08, 2024 4 - Beta pytest<9; extra == "test" :pypi:`pytest-multihost` Utility for writing multi-host tests for pytest Apr 07, 2020 4 - Beta N/A :pypi:`pytest-multilog` Multi-process logs handling and other helpers for pytest Jan 17, 2023 N/A pytest :pypi:`pytest-multithreading` a pytest plugin for th and concurrent testing Dec 07, 2022 N/A N/A @@ -853,13 +877,14 @@ This list contains 1448 plugins. :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 Mar 31, 2024 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-runner` Run the mypy static type checker as a pytest test case Apr 23, 2024 N/A pytest>=8.0 :pypi:`pytest-mypy-testing` Pytest plugin to check mypy output. Mar 04, 2024 N/A pytest>=7,<9 - :pypi:`pytest-mysql` MySQL process and client fixtures for pytest Oct 30, 2023 5 - Production/Stable pytest >=6.2 - :pypi:`pytest-ndb` pytest notebook debugger Oct 15, 2023 N/A pytest + :pypi:`pytest-mysql` MySQL process and client fixtures for pytest May 23, 2024 5 - Production/Stable pytest>=6.2 + :pypi:`pytest-ndb` pytest notebook debugger Apr 28, 2024 N/A pytest :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-neos` Pytest plugin for neos Apr 15, 2024 1 - Planning N/A - :pypi:`pytest-netdut` "Automated software testing for switches using pytest" Mar 07, 2024 N/A pytest <7.3,>=3.5.0 + :pypi:`pytest-neos` Pytest plugin for neos Jun 11, 2024 1 - Planning N/A + :pypi:`pytest-netdut` "Automated software testing for switches using pytest" Jul 05, 2024 N/A pytest<7.3,>=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) @@ -867,7 +892,7 @@ This list contains 1448 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 Feb 16, 2024 N/A pytest (>=6.2.5,<7.0.0) + :pypi:`pytest-nhsd-apim` Pytest plugin accessing NHSDigital's APIM proxies Jul 01, 2024 N/A pytest<9.0.0,>=8.2.0 :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 Apr 11, 2024 N/A N/A @@ -894,10 +919,10 @@ This list contains 1448 plugins. :pypi:`pytest-offline` Mar 09, 2023 1 - Planning pytest (>=7.0.0,<8.0.0) :pypi:`pytest-ogsm-plugin` 针对特定项目定制化插件,优化了pytest报告展示方式,并添加了项目所需特定参数 May 16, 2023 N/A N/A :pypi:`pytest-ok` The ultimate pytest output plugin Apr 01, 2019 4 - Beta N/A - :pypi:`pytest-only` Use @pytest.mark.only to run a single test Mar 09, 2024 5 - Production/Stable pytest (<7.1) ; python_full_version <= "3.6.0" + :pypi:`pytest-only` Use @pytest.mark.only to run a single test May 27, 2024 5 - Production/Stable pytest<9,>=3.6.0 :pypi:`pytest-oof` A Pytest plugin providing structured, programmatic access to a test run's results Dec 11, 2023 4 - Beta N/A :pypi:`pytest-oot` Run object-oriented tests in a simple format Sep 18, 2016 4 - Beta N/A - :pypi:`pytest-openfiles` Pytest plugin for detecting inadvertent open file handles Apr 16, 2020 3 - Alpha pytest (>=4.6) + :pypi:`pytest-openfiles` Pytest plugin for detecting inadvertent open file handles Jun 05, 2024 3 - Alpha pytest>=4.6 :pypi:`pytest-opentelemetry` A pytest plugin for instrumenting test runs via OpenTelemetry Oct 01, 2023 N/A pytest :pypi:`pytest-opentmi` pytest plugin for publish results to opentmi Jun 02, 2022 5 - Production/Stable pytest (>=5.0) :pypi:`pytest-operator` Fixtures for Operators Sep 28, 2022 N/A pytest @@ -919,7 +944,6 @@ This list contains 1448 plugins. :pypi:`pytest-parallelize-tests` pytest plugin that parallelizes test execution across multiple hosts Jan 27, 2023 4 - Beta N/A :pypi:`pytest-param` pytest plugin to test all, first, last or random params Sep 11, 2016 4 - Beta pytest (>=2.6.0) :pypi:`pytest-paramark` Configure pytest fixtures using a combination of"parametrize" and markers Jan 10, 2020 4 - Beta pytest (>=4.5.0) - :pypi:`pytest-parameterize-from-files` A pytest plugin that parameterizes tests from data files. Feb 15, 2024 4 - Beta pytest>=7.2.0 :pypi:`pytest-parametrization` Simpler PyTest parametrization May 22, 2022 5 - Production/Stable N/A :pypi:`pytest-parametrize-cases` A more user-friendly way to write parametrized tests. Mar 13, 2022 N/A pytest (>=6.1.2) :pypi:`pytest-parametrized` Pytest decorator for parametrizing tests with default iterables. Nov 03, 2023 5 - Production/Stable pytest @@ -932,19 +956,19 @@ This list contains 1448 plugins. :pypi:`pytest-paste-config` Allow setting the path to a paste config file Sep 18, 2013 3 - Alpha N/A :pypi:`pytest-patch` An automagic \`patch\` fixture that can patch objects directly or by name. Apr 29, 2023 3 - Alpha pytest (>=7.0.0) :pypi:`pytest-patches` A contextmanager pytest fixture for handling multiple mock patches Aug 30, 2021 4 - Beta pytest (>=3.5.0) - :pypi:`pytest-patterns` pytest plugin to make testing complicated long string output easy to write and easy to debug Nov 17, 2023 4 - Beta N/A + :pypi:`pytest-patterns` pytest plugin to make testing complicated long string output easy to write and easy to debug Jun 14, 2024 4 - Beta N/A :pypi:`pytest-pdb` pytest plugin which adds pdb helper commands related to pytest. Jul 31, 2018 N/A N/A :pypi:`pytest-peach` pytest plugin for fuzzing with Peach API Security Apr 12, 2019 4 - Beta pytest (>=2.8.7) :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-percents` Mar 16, 2024 N/A N/A - :pypi:`pytest-perf` Run performance tests against the mainline code. Jan 28, 2024 5 - Production/Stable pytest >=6 ; extra == 'testing' + :pypi:`pytest-perf` Run performance tests against the mainline code. May 20, 2024 5 - Production/Stable pytest!=8.1.*,>=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-performancetotal` A performance plugin for pytest Mar 19, 2024 4 - Beta N/A - :pypi:`pytest-persistence` Pytest tool for persistent objects Jul 04, 2023 N/A N/A + :pypi:`pytest-persistence` Pytest tool for persistent objects May 23, 2024 N/A N/A :pypi:`pytest-pexpect` Pytest pexpect plugin. Mar 27, 2024 4 - Beta pytest>=6.2.0 - :pypi:`pytest-pg` A tiny plugin for pytest which runs PostgreSQL in Docker Apr 03, 2024 5 - Production/Stable pytest>=6.0.0 + :pypi:`pytest-pg` A tiny plugin for pytest which runs PostgreSQL in Docker May 21, 2024 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) :pypi:`pytest-picked` Run the tests related to the changed files Jul 27, 2023 N/A pytest (>=3.7.0) @@ -960,19 +984,19 @@ This list contains 1448 plugins. :pypi:`pytest-platform-markers` Markers for pytest to skip tests on specific platforms Sep 09, 2019 4 - Beta pytest (>=3.6.0) :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 Feb 02, 2024 N/A pytest (<9.0.0,>=6.2.4) - :pypi:`pytest_playwright_async` ASYNC Pytest plugin for Playwright Feb 25, 2024 N/A N/A + :pypi:`pytest-playwright` A pytest wrapper with fixtures for Playwright to automate web browsers Jul 03, 2024 N/A N/A + :pypi:`pytest_playwright_async` ASYNC Pytest plugin for Playwright May 24, 2024 N/A N/A :pypi:`pytest-playwright-asyncio` Aug 29, 2023 N/A N/A :pypi:`pytest-playwright-enhanced` A pytest plugin for playwright python Mar 24, 2024 N/A pytest<9.0.0,>=8.0.0 :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 - :pypi:`pytest-plone` Pytest plugin to test Plone addons Jan 05, 2023 3 - Alpha pytest + :pypi:`pytest-plone` Pytest plugin to test Plone addons May 15, 2024 3 - Alpha pytest<8.0.0 :pypi:`pytest-plt` Fixtures for quickly making Matplotlib plots in tests Jan 17, 2024 5 - Production/Stable pytest :pypi:`pytest-plugin-helpers` A plugin to help developing and testing other plugins Nov 23, 2019 4 - Beta pytest (>=3.5.0) :pypi:`pytest-plus` PyTest Plus Plugin :: extends pytest functionality Mar 26, 2024 5 - Production/Stable pytest>=7.4.2 :pypi:`pytest-pmisc` Mar 21, 2019 5 - Production/Stable N/A - :pypi:`pytest-pogo` Pytest plugin for pogo-migrate Mar 11, 2024 1 - Planning pytest (>=7,<9) + :pypi:`pytest-pogo` Pytest plugin for pogo-migrate May 22, 2024 1 - Planning pytest<9,>=7 :pypi:`pytest-pointers` Pytest plugin to define functions you test with special marks for better navigation and reports Dec 26, 2022 N/A N/A :pypi:`pytest-pokie` Pokie plugin for pytest Oct 19, 2023 5 - Production/Stable N/A :pypi:`pytest-polarion-cfme` pytest plugin for collecting test cases and recording test results Nov 13, 2017 3 - Alpha N/A @@ -998,14 +1022,16 @@ This list contains 1448 plugins. :pypi:`pytest-proceed` Apr 10, 2024 N/A pytest :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 + :pypi:`pytest-progress` pytest plugin for instant test progress status Jun 18, 2024 5 - Production/Stable pytest>=2.7 :pypi:`pytest-prometheus` Report test pass / failures to a Prometheus PushGateway Oct 03, 2017 N/A N/A :pypi:`pytest-prometheus-pushgateway` Pytest report plugin for Zulip Sep 27, 2022 5 - Production/Stable pytest :pypi:`pytest-prosper` Test helpers for Prosper projects Sep 24, 2018 N/A N/A :pypi:`pytest-prysk` Pytest plugin for prysk Mar 12, 2024 4 - Beta pytest (>=7.3.2) :pypi:`pytest-pspec` A rspec format reporter for Python ptest Jun 02, 2020 4 - Beta pytest (>=3.0.0) :pypi:`pytest-psqlgraph` pytest plugin for testing applications that use psqlgraph Oct 19, 2021 4 - Beta pytest (>=6.0) + :pypi:`pytest-pt` pytest plugin to use \*.pt files as tests May 15, 2024 4 - Beta pytest :pypi:`pytest-ptera` Use ptera probes in tests Mar 01, 2022 N/A pytest (>=6.2.4,<7.0.0) + :pypi:`pytest-publish` Jun 04, 2024 N/A pytest<9.0.0,>=8.0.0 :pypi:`pytest-pudb` Pytest PuDB debugger integration Oct 25, 2018 3 - Alpha pytest (>=2.0) :pypi:`pytest-pumpkin-spice` A pytest plugin that makes your test reporting pumpkin-spiced Sep 18, 2022 4 - Beta N/A :pypi:`pytest-purkinje` py.test plugin for purkinje test runner Oct 28, 2017 2 - Pre-Alpha N/A @@ -1017,27 +1043,29 @@ This list contains 1448 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 Oct 06, 2023 5 - Production/Stable pytest >=7.0 :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 Dec 09, 2023 N/A pytest + :pypi:`pytest-pyodide` Pytest plugin for testing applications that use Pyodide Jun 12, 2024 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 Oct 11, 2023 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 Feb 03, 2024 N/A pytest + :pypi:`pytest-pyreport` PyReport is a lightweight reporting plugin for Pytest that provides concise HTML report May 05, 2024 N/A pytest :pypi:`pytest-pyright` Pytest plugin for type checking code with Pyright Jan 26, 2024 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". Jan 02, 2024 N/A pytest (>=7.2.1,<8.0.0) :pypi:`pytest-pystack` Plugin to run pystack after a timeout for a test suite. Jan 04, 2024 N/A pytest >=3.5.0 :pypi:`pytest-pytestrail` Pytest plugin for interaction with TestRail Aug 27, 2020 4 - Beta pytest (>=3.8.0) :pypi:`pytest-pythonhashseed` Pytest plugin to set PYTHONHASHSEED env var. Feb 25, 2024 4 - Beta pytest>=3.0.0 :pypi:`pytest-pythonpath` pytest plugin for adding to the PYTHONPATH from command line or configs. Feb 10, 2022 5 - Production/Stable pytest (<7,>=2.5.2) + :pypi:`pytest-python-test-engineer-sort` Sort plugin for Pytest May 13, 2024 N/A pytest>=6.2.0 :pypi:`pytest-pytorch` pytest plugin for a better developer experience when working with the PyTorch test suite May 25, 2021 4 - Beta pytest :pypi:`pytest-pyvenv` A package for create venv in tests Feb 27, 2024 N/A pytest ; extra == 'test' :pypi:`pytest-pyvista` Pytest-pyvista package Sep 29, 2023 4 - Beta pytest>=3.5.0 - :pypi:`pytest-qaseio` Pytest plugin for Qase.io integration Sep 12, 2023 4 - Beta pytest (>=7.2.2,<8.0.0) + :pypi:`pytest-qanova` A pytest plugin to collect test information May 26, 2024 3 - Alpha pytest + :pypi:`pytest-qaseio` Pytest plugin for Qase.io integration May 30, 2024 4 - Beta pytest<9.0.0,>=7.2.2 :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 Nov 29, 2023 5 - Production/Stable pytest >=6.0 + :pypi:`pytest-qgis` A pytest plugin for testing QGIS python plugins Jun 14, 2024 5 - Production/Stable pytest>=6.0 :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 Feb 07, 2024 5 - Production/Stable pytest @@ -1045,7 +1073,7 @@ This list contains 1448 plugins. :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_quickify` Run test suites with pytest-quickify. Jun 14, 2019 N/A pytest - :pypi:`pytest-rabbitmq` RabbitMQ process and client fixtures for pytest Jul 05, 2023 5 - Production/Stable pytest (>=6.2) + :pypi:`pytest-rabbitmq` RabbitMQ process and client fixtures for pytest May 08, 2024 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) @@ -1058,13 +1086,13 @@ This list contains 1448 plugins. :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 Jan 20, 2024 5 - Production/Stable pytest >=3.0.0 - :pypi:`pytest-ranking` A Pytest plugin for automatically prioritizing/ranking tests to speed up failure detection Mar 18, 2024 4 - Beta pytest >=7.4.3 + :pypi:`pytest-ranking` A Pytest plugin for automatically prioritizing/ranking tests to speed up failure detection Jun 07, 2024 4 - Beta pytest>=7.4.3 :pypi:`pytest-readme` Test your README.md file Sep 02, 2022 5 - Production/Stable N/A :pypi:`pytest-reana` Pytest fixtures for REANA. Mar 14, 2024 3 - Alpha N/A - :pypi:`pytest-recorder` Pytest plugin, meant to facilitate unit tests writing for tools consumming Web APIs. Nov 21, 2023 N/A N/A - :pypi:`pytest-recording` A pytest plugin that allows you recording of network interactions via VCR.py Dec 06, 2023 4 - Beta pytest>=3.5.0 + :pypi:`pytest-recorder` Pytest plugin, meant to facilitate unit tests writing for tools consumming Web APIs. Jun 27, 2024 N/A N/A + :pypi:`pytest-recording` A pytest plugin that allows you recording of network interactions via VCR.py Jul 09, 2024 4 - Beta pytest>=3.5.0 :pypi:`pytest-recordings` Provides pytest plugins for reporting request/response traffic, screenshots, and more to ReportPortal Aug 13, 2020 N/A N/A - :pypi:`pytest-redis` Redis fixtures and fixture factories for Pytest. Apr 19, 2023 5 - Production/Stable pytest (>=6.2) + :pypi:`pytest-redis` Redis fixtures and fixture factories for Pytest. Jun 19, 2024 5 - Production/Stable pytest>=6.2 :pypi:`pytest-redislite` Pytest plugin for testing code using Redis Apr 05, 2022 4 - Beta pytest :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) @@ -1086,7 +1114,7 @@ This list contains 1448 plugins. :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 Feb 28, 2024 4 - Beta pytest - :pypi:`pytest-reporter-html1` A basic HTML report template for Pytest Feb 28, 2024 4 - Beta N/A + :pypi:`pytest-reporter-html1` A basic HTML report template for Pytest Jun 28, 2024 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) @@ -1104,11 +1132,11 @@ This list contains 1448 plugins. :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-rerun-all` Rerun testsuite for a certain time or iterations Nov 16, 2023 3 - Alpha pytest (>=7.0.0) - :pypi:`pytest-rerunclassfailures` pytest rerun class failures plugin Mar 29, 2024 5 - Production/Stable pytest>=7.2 + :pypi:`pytest-rerunclassfailures` pytest rerun class failures plugin Apr 24, 2024 5 - Production/Stable pytest>=7.2 :pypi:`pytest-rerunfailures` pytest plugin to re-run tests to eliminate flaky failures Mar 13, 2024 5 - Production/Stable pytest >=7.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. Feb 08, 2024 4 - Beta pytest - :pypi:`pytest-resilient-circuits` Resilient Circuits fixtures for PyTest Apr 03, 2024 N/A pytest~=4.6; python_version == "2.7" + :pypi:`pytest-reserial` Pytest fixture for recording and replaying serial port traffic. May 23, 2024 4 - Beta pytest + :pypi:`pytest-resilient-circuits` Resilient Circuits fixtures for PyTest May 17, 2024 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 @@ -1120,7 +1148,7 @@ This list contains 1448 plugins. :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) :pypi:`pytest-rethinkdb` A RethinkDB plugin for pytest. Jul 24, 2016 4 - Beta N/A - :pypi:`pytest-retry` Adds the ability to retry flaky tests in CI environments Feb 04, 2024 N/A pytest >=7.0.0 + :pypi:`pytest-retry` Adds the ability to retry flaky tests in CI environments May 14, 2024 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. Jul 10, 2023 5 - Production/Stable pytest @@ -1132,14 +1160,14 @@ This list contains 1448 plugins. :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 :pypi:`pytest-roast` pytest plugin for ROAST configuration override and fixtures Nov 09, 2022 5 - Production/Stable pytest - :pypi:`pytest_robotframework` a pytest plugin that can run both python and robotframework tests while generating robot reports for them Mar 29, 2024 N/A pytest<9,>=7 + :pypi:`pytest_robotframework` a pytest plugin that can run both python and robotframework tests while generating robot reports for them Jul 01, 2024 N/A pytest<9,>=7 :pypi:`pytest-rocketchat` Pytest to Rocket.Chat reporting plugin Apr 18, 2021 5 - Production/Stable N/A :pypi:`pytest-rotest` Pytest integration with rotest Sep 08, 2019 N/A pytest (>=3.5.0) :pypi:`pytest-rpc` Extend py.test for RPC OpenStack testing. Feb 22, 2019 4 - Beta pytest (~=3.6) :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. Mar 10, 2024 4 - Beta pytest (>=5) + :pypi:`pytest-ruff` pytest plugin to check ruff requirements. Jul 09, 2024 4 - Beta pytest>=5 :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-run-subprocess` Pytest Plugin for running and testing subprocesses. Nov 12, 2022 5 - Production/Stable pytest @@ -1152,12 +1180,14 @@ This list contains 1448 plugins. :pypi:`pytest-salt-factories` Pytest Salt Plugin Mar 22, 2024 5 - Production/Stable pytest>=7.0.0 :pypi:`pytest-salt-from-filenames` Simple PyTest Plugin For Salt's Test Suite Specifically Jan 29, 2019 4 - Beta pytest (>=4.1) :pypi:`pytest-salt-runtests-bridge` Simple PyTest Plugin For Salt's Test Suite Specifically Dec 05, 2019 4 - Beta pytest (>=4.1) + :pypi:`pytest-sample-argvalues` A utility function to help choose a random sample from your argvalues in pytest. May 07, 2024 N/A pytest :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_sauce` pytest_sauce provides sane and helpful methods worked out in clearcode to run py.test tests with selenium/saucelabs Jul 14, 2014 3 - Alpha N/A - :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Apr 14, 2024 5 - Production/Stable N/A + :pypi:`pytest-sbase` A complete web automation framework for end-to-end testing. Jul 08, 2024 5 - Production/Stable N/A :pypi:`pytest-scenario` pytest plugin for test scenarios Feb 06, 2017 3 - Alpha N/A + :pypi:`pytest-scenario-files` A pytest plugin that generates unit test scenarios from data files. May 19, 2024 5 - Production/Stable pytest>=7.2.0 :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 Feb 16, 2024 5 - Production/Stable pytest >=3.5.0 :pypi:`pytest-screenshot-on-failure` Saves a screenshot when a test case from a pytest execution fails Jul 21, 2023 4 - Beta N/A @@ -1165,16 +1195,18 @@ This list contains 1448 plugins. :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 Feb 01, 2024 5 - Production/Stable pytest>=6.0.0 :pypi:`pytest-selenium-auto` pytest plugin to automatically capture screenshots upon selenium webdriver events Nov 07, 2023 N/A pytest >= 7.0.0 - :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Apr 14, 2024 5 - Production/Stable N/A + :pypi:`pytest-seleniumbase` A complete web automation framework for end-to-end testing. Jul 08, 2024 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-selfie` A pytest plugin for selfie snapshot testing. Apr 05, 2024 N/A pytest<9.0.0,>=8.0.0 :pypi:`pytest-send-email` Send pytest execution result email Dec 04, 2019 N/A N/A - :pypi:`pytest-sentry` A pytest plugin to send testrun information to Sentry.io Apr 05, 2024 N/A pytest + :pypi:`pytest-sentry` A pytest plugin to send testrun information to Sentry.io Apr 25, 2024 N/A pytest :pypi:`pytest-sequence-markers` Pytest plugin for sequencing markers for execution of tests May 23, 2023 5 - Production/Stable N/A + :pypi:`pytest-server` test server exec cmd Jun 24, 2024 N/A N/A :pypi:`pytest-server-fixtures` Extensible server fixures for py.test Dec 19, 2023 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 Mar 19, 2024 3 - Alpha pytest>=6.2 + :pypi:`pytest-servers` pytest servers Jun 17, 2024 3 - Alpha pytest>=6.2 + :pypi:`pytest-service` May 11, 2024 5 - Production/Stable pytest>=6.0.0 :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 @@ -1236,17 +1268,17 @@ This list contains 1448 plugins. :pypi:`pytest-spiratest` Exports unit tests as test runs in Spira (SpiraTest/Team/Plan) Jan 01, 2024 N/A N/A :pypi:`pytest-splinter` Splinter plugin for pytest testing framework Sep 09, 2022 6 - Mature pytest (>=3.0.0) :pypi:`pytest-splinter4` Pytest plugin for the splinter automation library Feb 01, 2024 6 - Mature pytest >=8.0.0 - :pypi:`pytest-split` Pytest plugin which splits the test suite to equally sized sub suites based on test execution time. Jan 29, 2024 4 - Beta pytest (>=5,<9) + :pypi:`pytest-split` Pytest plugin which splits the test suite to equally sized sub suites based on test execution time. Jun 19, 2024 4 - Beta pytest<9,>=5 :pypi:`pytest-split-ext` Pytest plugin which splits the test suite to equally sized sub suites based on test execution time. Sep 23, 2023 4 - Beta pytest (>=5,<8) :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 Apr 19, 2024 N/A pytest (>5.4.0,<8) - :pypi:`pytest-splunk-addon-ui-smartx` Library to support testing Splunk Add-on UX Mar 26, 2024 N/A N/A + :pypi:`pytest-splunk-addon` A Dynamic test tool for Splunk Apps and Add-ons Jul 11, 2024 N/A pytest<8,>5.4.0 + :pypi:`pytest-splunk-addon-ui-smartx` Library to support testing Splunk Add-on UX Jul 10, 2024 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 :pypi:`pytest-sqlalchemy` pytest plugin with sqlalchemy related fixtures Mar 13, 2018 3 - Alpha N/A - :pypi:`pytest-sqlalchemy-mock` pytest sqlalchemy plugin for mock Mar 15, 2023 3 - Alpha pytest (>=2.0) + :pypi:`pytest-sqlalchemy-mock` pytest sqlalchemy plugin for mock May 21, 2024 3 - Alpha pytest>=7.0.0 :pypi:`pytest-sqlalchemy-session` A pytest plugin for preserving test isolation that use SQLAlchemy. May 19, 2023 4 - Beta pytest (>=7.0) :pypi:`pytest-sql-bigquery` Yet another SQL-testing framework for BigQuery provided by pytest plugin Dec 19, 2019 N/A pytest :pypi:`pytest-sqlfluff` A pytest plugin to use sqlfluff to enable format checking of sql files. Dec 21, 2022 4 - Beta pytest (>=3.5.0) @@ -1255,7 +1287,8 @@ This list contains 1448 plugins. :pypi:`pytest-ssh` pytest plugin for ssh command run May 27, 2019 N/A pytest :pypi:`pytest-start-from` Start pytest run from a given point Apr 11, 2016 N/A N/A :pypi:`pytest-star-track-issue` A package to prevent Dependency Confusion attacks against Yandex. Feb 20, 2024 N/A N/A - :pypi:`pytest-static` pytest-static Jan 15, 2024 1 - Planning pytest (>=7.4.3,<8.0.0) + :pypi:`pytest-static` pytest-static Jun 20, 2024 1 - Planning pytest<8.0.0,>=7.4.3 + :pypi:`pytest-stats` Collects tests metadata for future analysis, easy to extend for any data store Jul 03, 2024 N/A pytest>=8.0.0 :pypi:`pytest-statsd` pytest plugin for reporting to graphite Nov 30, 2018 5 - Production/Stable pytest (>=3.0.0) :pypi:`pytest-stepfunctions` A small description May 08, 2021 4 - Beta pytest :pypi:`pytest-steps` Create step-wise / incremental tests in pytest. Sep 23, 2021 5 - Production/Stable N/A @@ -1264,7 +1297,7 @@ This list contains 1448 plugins. :pypi:`pytest-stoq` A plugin to pytest stoq Feb 09, 2021 4 - Beta N/A :pypi:`pytest-store` Pytest plugin to store values from test runs Nov 16, 2023 3 - Alpha pytest (>=7.0.0) :pypi:`pytest-stress` A Pytest plugin that allows you to loop tests for a user defined amount of time. Dec 07, 2019 4 - Beta pytest (>=3.6.0) - :pypi:`pytest-structlog` Structured logging assertions Mar 13, 2024 N/A pytest + :pypi:`pytest-structlog` Structured logging assertions Jun 09, 2024 N/A pytest :pypi:`pytest-structmpd` provide structured temporary directory Oct 17, 2018 N/A N/A :pypi:`pytest-stub` Stub packages, modules and attributes. Apr 28, 2020 5 - Production/Stable N/A :pypi:`pytest-stubprocess` Provide stub implementations for subprocesses in Python tests Sep 17, 2018 3 - Alpha pytest (>=3.5.0) @@ -1272,7 +1305,7 @@ This list contains 1448 plugins. :pypi:`pytest-subinterpreter` Run pytest in a subinterpreter Nov 25, 2023 N/A pytest>=7.0.0 :pypi:`pytest-subprocess` A plugin to fake subprocess for pytest Jan 28, 2023 5 - Production/Stable pytest (>=4.0.0) :pypi:`pytest-subtesthack` A hack to explicitly set up and tear down fixtures. Jul 16, 2022 N/A N/A - :pypi:`pytest-subtests` unittest subTest() support and subtests fixture Mar 07, 2024 4 - Beta pytest >=7.0 + :pypi:`pytest-subtests` unittest subTest() support and subtests fixture Jul 07, 2024 4 - Beta pytest>=7.0 :pypi:`pytest-subunit` pytest-subunit is a plugin for py.test which outputs testsresult in subunit format. Sep 17, 2023 N/A pytest (>=2.3) :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). Feb 01, 2024 4 - Beta pytest >=6.2.0 :pypi:`pytest-suitemanager` A simple plugin to use with pytest Apr 28, 2023 4 - Beta N/A @@ -1294,11 +1327,11 @@ This list contains 1448 plugins. :pypi:`pytest-tcpclient` A pytest plugin for testing TCP clients Nov 16, 2022 N/A pytest (<8,>=7.1.3) :pypi:`pytest-tdd` run pytest on a python module Aug 18, 2023 4 - Beta N/A :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` Pytest to Telegram reporting plugin Apr 25, 2024 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 Jun 20, 2023 N/A pytest (>=6.0) + :pypi:`pytest-terraform` A pytest plugin for using terraform fixtures May 21, 2024 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) @@ -1306,7 +1339,7 @@ This list contains 1448 plugins. :pypi:`pytest-testdox` A testdox format reporter for pytest Jul 22, 2023 5 - Production/Stable pytest (>=4.6.0) :pypi:`pytest-test-grouping` A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. Feb 01, 2023 5 - Production/Stable pytest (>=2.5) :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 Feb 15, 2024 5 - Production/Stable pytest >=6 + :pypi:`pytest-testinfra` Test infrastructures May 26, 2024 5 - Production/Stable pytest>=6 :pypi:`pytest-testinfra-jpic` Test infrastructures Sep 21, 2023 5 - Production/Stable N/A :pypi:`pytest-testinfra-winrm-transport` Test infrastructures Sep 21, 2023 5 - Production/Stable N/A :pypi:`pytest-testlink-adaptor` pytest reporting plugin for testlink Dec 20, 2018 4 - Beta pytest (>=2.6) @@ -1331,10 +1364,13 @@ This list contains 1448 plugins. :pypi:`pytest-testreport-new` Oct 07, 2023 4 - Beta pytest >=3.5.0 :pypi:`pytest-testslide` TestSlide fixture for pytest Jan 07, 2021 5 - Production/Stable pytest (~=6.2) :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-tracer-for-pytest` A plugin that allows coll test data for use on Test Tracer Jun 28, 2024 4 - Beta pytest>=6.2.0 + :pypi:`pytest-test-tracer-for-pytest-bdd` A plugin that allows coll test data for use on Test Tracer Jul 01, 2024 4 - Beta pytest>=6.2.0 :pypi:`pytest-test-utils` Feb 08, 2024 N/A pytest >=3.9 :pypi:`pytest-tesults` Tesults plugin for pytest Feb 15, 2024 5 - Production/Stable pytest >=3.5.0 :pypi:`pytest-textual-snapshot` Snapshot testing for Textual apps Aug 23, 2023 4 - Beta pytest (>=7.0.0) :pypi:`pytest-tezos` pytest-ligo Jan 16, 2020 4 - Beta N/A + :pypi:`pytest-tf` Test your OpenTofu and Terraform config using a PyTest plugin May 29, 2024 N/A pytest<9.0.0,>=8.2.1 :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 @@ -1363,8 +1399,9 @@ This list contains 1448 plugins. :pypi:`pytest-tomato` Mar 01, 2019 5 - Production/Stable N/A :pypi:`pytest-toolbelt` This is just a collection of utilities for pytest, but don't really belong in pytest proper. Aug 12, 2019 3 - Alpha N/A :pypi:`pytest-toolbox` Numerous useful plugins for pytest. Apr 07, 2018 N/A pytest (>=3.5.0) - :pypi:`pytest-toolkit` Useful utils for testing Apr 13, 2024 N/A N/A + :pypi:`pytest-toolkit` Useful utils for testing Jun 07, 2024 N/A N/A :pypi:`pytest-tools` Pytest tools Oct 21, 2022 4 - Beta N/A + :pypi:`pytest-topo` Topological sorting for pytest Jun 05, 2024 N/A pytest>=7.0.0 :pypi:`pytest-tornado` A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Jun 17, 2020 5 - Production/Stable pytest (>=3.6) :pypi:`pytest-tornado5` A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Nov 16, 2018 5 - Production/Stable pytest (>=3.6) :pypi:`pytest-tornado-yen3` A py.test plugin providing fixtures and markers to simplify testing of asynchronous tornado applications. Oct 15, 2018 5 - Production/Stable N/A @@ -1384,7 +1421,7 @@ This list contains 1448 plugins. :pypi:`pytest-tui` Text User Interface (TUI) and HTML report for Pytest test runs Dec 08, 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. Mar 19, 2024 5 - Production/Stable pytest >=2.3 + :pypi:`pytest-twisted` A twisted plugin for pytest. Jul 10, 2024 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-polarion` Typhoontest plugin for Siemens Polarion Feb 01, 2024 4 - Beta N/A @@ -1395,12 +1432,12 @@ This list contains 1448 plugins. :pypi:`pytest-ui-failed-screenshot` UI自动测试失败时自动截图,并将截图加入到测试报告中 Dec 06, 2022 N/A N/A :pypi:`pytest-ui-failed-screenshot-allure` UI自动测试失败时自动截图,并将截图加入到Allure测试报告中 Dec 06, 2022 N/A N/A :pypi:`pytest-uncollect-if` A plugin to uncollect pytests tests rather than using skipif Mar 24, 2024 4 - Beta pytest>=6.2.0 - :pypi:`pytest-unflakable` Unflakable plugin for PyTest Nov 12, 2023 4 - Beta pytest >=6.2.0 + :pypi:`pytest-unflakable` Unflakable plugin for PyTest Apr 30, 2024 4 - Beta pytest>=6.2.0 :pypi:`pytest-unhandled-exception-exit-code` Plugin for py.test set a different exit code on uncaught exceptions Jun 22, 2020 5 - Production/Stable pytest (>=2.3) :pypi:`pytest-unique` Pytest fixture to generate unique values. Sep 15, 2023 N/A pytest (>=7.4.2,<8.0.0) :pypi:`pytest-unittest-filter` A pytest plugin for filtering unittest-based test classes Jan 12, 2019 4 - Beta pytest (>=3.1.0) :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 Mar 13, 2024 4 - Beta pytest >=7.0.0 + :pypi:`pytest-unordered` Test equality of unordered collections in pytest Jul 05, 2024 4 - Beta pytest>=7.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. Apr 08, 2024 4 - Beta pytest>7.3.2 :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 @@ -1414,7 +1451,6 @@ This list contains 1448 plugins. :pypi:`pytest-vcrpandas` Test from HTTP interactions to dataframe processed. Jan 12, 2019 4 - Beta pytest :pypi:`pytest-vcs` Sep 22, 2022 4 - Beta N/A :pypi:`pytest-venv` py.test fixture for creating a virtual environment Nov 23, 2023 4 - Beta pytest - :pypi:`pytest-ver` Pytest module with Verification Protocol, Verification Report and Trace Matrix Feb 07, 2024 4 - Beta pytest :pypi:`pytest-verbose-parametrize` More descriptive output for parametrized py.test tests May 28, 2019 5 - Production/Stable pytest :pypi:`pytest-vimqf` A simple pytest plugin that will shrink pytest output when specified, to fit vim quickfix window. Feb 08, 2021 4 - Beta pytest (>=6.2.2,<7.0.0) :pypi:`pytest-virtualenv` Virtualenv fixture for py.test May 28, 2019 5 - Production/Stable pytest @@ -1435,9 +1471,9 @@ This list contains 1448 plugins. :pypi:`pytest-wdl` Pytest plugin for testing WDL workflows. Nov 17, 2020 5 - Production/Stable N/A :pypi:`pytest-web3-data` A pytest plugin to fetch test data from IPFS HTTP gateways during pytest execution. Oct 04, 2023 4 - Beta pytest :pypi:`pytest-webdriver` Selenium webdriver fixture for py.test May 28, 2019 5 - Production/Stable pytest - :pypi:`pytest-webtest-extras` Pytest plugin to enhance pytest-html and allure reports of webtest projects by adding screenshots, comments and webpage sources. Nov 13, 2023 N/A pytest >= 7.0.0 + :pypi:`pytest-webtest-extras` Pytest plugin to enhance pytest-html and allure reports of webtest projects by adding screenshots, comments and webpage sources. Jun 08, 2024 N/A pytest>=7.0.0 :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 Mar 22, 2024 N/A pytest>=7.3.1 + :pypi:`pytest-when` Utility which makes mocking more readable and controllable May 28, 2024 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 @@ -1445,7 +1481,7 @@ This list contains 1448 plugins. :pypi:`pytest-wiremock` A pytest plugin for programmatically using wiremock in integration tests Mar 27, 2022 N/A pytest (>=7.1.1,<8.0.0) :pypi:`pytest-with-docker` pytest with docker helpers. Nov 09, 2021 N/A pytest :pypi:`pytest-workflow` A pytest plugin for configuring workflow/pipeline tests using YAML files Mar 18, 2024 5 - Production/Stable pytest >=7.0.0 - :pypi:`pytest-xdist` pytest xdist plugin for distributed testing, most importantly across multiple CPUs Apr 19, 2024 5 - Production/Stable pytest >=6.2.0 + :pypi:`pytest-xdist` pytest xdist plugin for distributed testing, most importantly across multiple CPUs Apr 28, 2024 5 - Production/Stable pytest>=7.0.0 :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) @@ -1454,17 +1490,18 @@ This list contains 1448 plugins. :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-xiuyu` This is a pytest plugin Jul 25, 2023 5 - Production/Stable 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 22, 2024 N/A N/A + :pypi:`pytest-xlsx` pytest plugin for generating test cases by xlsx(excel) Apr 23, 2024 N/A pytest~=7.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. Mar 31, 2024 4 - Beta pytest>=2.8 + :pypi:`pytest-xprocess` A pytest plugin for managing processes across test runs. May 19, 2024 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 20, 2024 N/A N/A + :pypi:`pytest-xstress` Jun 01, 2024 N/A pytest<9.0.0,>=8.0.0 :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. Oct 01, 2023 4 - Beta pytest >=7.1.0 + :pypi:`pytest-xvirt` A pytest plugin to virtualize test. For example to transparently running them on a remote box. Jul 03, 2024 4 - Beta pytest>=7.2.2 :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 Apr 19, 2024 N/A pytest>=7.4.0 + :pypi:`pytest-yaml-sanmu` pytest plugin for generating test cases by yaml Jul 12, 2024 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 Jun 19, 2023 N/A pytest (>=7.2.0) @@ -1472,10 +1509,12 @@ This list contains 1448 plugins. :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 30, 2024 N/A pytest<8.0.0,>=7.2.2 + :pypi:`pytest-youqu-playwright` pytest-youqu-playwright Jun 12, 2024 N/A pytest :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 - :pypi:`pytest-zebrunner` Pytest connector for Zebrunner reporting Jan 08, 2024 5 - Production/Stable pytest (>=4.5.0) + :pypi:`pytest-zcc` eee Jun 02, 2024 N/A N/A + :pypi:`pytest-zebrunner` Pytest connector for Zebrunner reporting Jul 04, 2024 5 - Production/Stable pytest>=4.5.0 :pypi:`pytest-zeebe` Pytest fixtures for testing Camunda 8 processes using a Zeebe test engine. Feb 01, 2024 N/A pytest (>=7.4.2,<8.0.0) :pypi:`pytest-zest` Zesty additions to pytest. Nov 17, 2022 N/A N/A :pypi:`pytest-zhongwen-wendang` PyTest 中文文档 Mar 04, 2024 4 - Beta N/A @@ -1502,9 +1541,9 @@ This list contains 1448 plugins. Test whether your code is logging correctly 🪵 :pypi:`nuts` - *last release*: Aug 11, 2023, + *last release*: May 28, 2024, *status*: N/A, - *requires*: pytest (>=7.3.0,<8.0.0) + *requires*: pytest<8,>=7 Network Unit Testing System @@ -1711,6 +1750,13 @@ This list contains 1448 plugins. pytest plugin to test case doc string dls instructions + :pypi:`pytest-allure-id2history` + *last release*: May 14, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + Overwrite allure history id with testcase full name and testcase id if testcase has id, exclude parameters. + :pypi:`pytest-allure-intersection` *last release*: Oct 27, 2022, *status*: N/A, @@ -1761,9 +1807,9 @@ This list contains 1448 plugins. pytest-annotate: Generate PyAnnotate annotations from your pytest tests. :pypi:`pytest-ansible` - *last release*: Jan 18, 2024, + *last release*: Jul 10, 2024, *status*: 5 - Production/Stable, - *requires*: pytest >=6 + *requires*: pytest>=6 Plugin for pytest to simplify calling ansible modules from tests or fixtures @@ -1844,6 +1890,13 @@ This list contains 1448 plugins. apistellar plugin for pytest. + :pypi:`pytest-apiver` + *last release*: Jun 21, 2024, + *status*: N/A, + *requires*: pytest + + + :pypi:`pytest-appengine` *last release*: Feb 27, 2017, *status*: N/A, @@ -1942,6 +1995,13 @@ This list contains 1448 plugins. Useful assertion utilities for use with pytest + :pypi:`pytest-assist` + *last release*: Jun 24, 2024, + *status*: N/A, + *requires*: pytest + + load testing library + :pypi:`pytest-assume` *last release*: Jun 24, 2021, *status*: N/A, @@ -2006,14 +2066,14 @@ This list contains 1448 plugins. Pytest fixtures for async generators :pypi:`pytest-asyncio` - *last release*: Mar 19, 2024, + *last release*: May 19, 2024, *status*: 4 - Beta, - *requires*: pytest <9,>=7.0.0 + *requires*: pytest<9,>=7.0.0 Pytest support for asyncio :pypi:`pytest-asyncio-cooperative` - *last release*: Feb 25, 2024, + *last release*: Jul 04, 2024, *status*: N/A, *requires*: N/A @@ -2061,6 +2121,13 @@ This list contains 1448 plugins. pytest plugin to select tests based on attributes similar to the nose-attrib plugin + :pypi:`pytest-attributes` + *last release*: Jun 24, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + A plugin that allows users to add attributes to their tests. These attributes can then be referenced by fixtures or the test itself. + :pypi:`pytest-austin` *last release*: Oct 11, 2020, *status*: 4 - Beta, @@ -2083,9 +2150,9 @@ This list contains 1448 plugins. automatically check condition and log all the checks :pypi:`pytest-automation` - *last release*: May 20, 2022, + *last release*: Apr 24, 2024, *status*: N/A, - *requires*: pytest (>=7.0.0) + *requires*: pytest>=7.0.0 pytest plugin for building a test suite, using YAML files to extend pytest parameterize functionality. @@ -2110,6 +2177,13 @@ This list contains 1448 plugins. This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. + :pypi:`pytest-aux` + *last release*: Jul 05, 2024, + *status*: N/A, + *requires*: N/A + + templates/examples and aux for pytest + :pypi:`pytest-aviator` *last release*: Nov 04, 2022, *status*: 4 - Beta, @@ -2131,6 +2205,13 @@ This list contains 1448 plugins. pytest plugin for testing AWS resource configurations + :pypi:`pytest-aws-apigateway` + *last release*: May 24, 2024, + *status*: 4 - Beta, + *requires*: pytest + + pytest plugin for AWS ApiGateway + :pypi:`pytest-aws-config` *last release*: May 28, 2021, *status*: N/A, @@ -2201,10 +2282,24 @@ This list contains 1448 plugins. pytest plugin for URL based testing + :pypi:`pytest-batch-regression` + *last release*: May 08, 2024, + *status*: N/A, + *requires*: pytest>=6.0.0 + + A pytest plugin to repeat the entire test suite in batches. + + :pypi:`pytest-bazel` + *last release*: Jul 12, 2024, + *status*: 4 - Beta, + *requires*: pytest + + A pytest runner with bazel support + :pypi:`pytest-bdd` - *last release*: Mar 17, 2024, + *last release*: Jun 04, 2024, *status*: 6 - Mature, - *requires*: pytest (>=6.2.0) + *requires*: pytest>=6.2.0 BDD for pytest @@ -2223,7 +2318,7 @@ This list contains 1448 plugins. BDD for pytest :pypi:`pytest-bdd-report` - *last release*: Feb 19, 2024, + *last release*: May 20, 2024, *status*: N/A, *requires*: pytest >=7.1.3 @@ -2265,7 +2360,7 @@ This list contains 1448 plugins. Pytest plugin to run your tests with beartype checking enabled. :pypi:`pytest-bec-e2e` - *last release*: Apr 19, 2024, + *last release*: Jul 08, 2024, *status*: 3 - Alpha, *requires*: pytest @@ -2335,7 +2430,7 @@ This list contains 1448 plugins. Provides a mock fixture for python bigquery client :pypi:`pytest-bisect-tests` - *last release*: Mar 25, 2024, + *last release*: Jun 09, 2024, *status*: N/A, *requires*: N/A @@ -2425,6 +2520,13 @@ This list contains 1448 plugins. + :pypi:`pytest-boto-mock` + *last release*: Jun 05, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=8.2.0 + + Thin-wrapper around the mock package for easier use with pytest + :pypi:`pytest-bpdb` *last release*: Jan 19, 2015, *status*: 2 - Pre-Alpha, @@ -2432,6 +2534,13 @@ This list contains 1448 plugins. A py.test plug-in to enable drop to bpdb debugger on test failure. + :pypi:`pytest-bq` + *last release*: May 08, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=6.2 + + BigQuery fixtures and fixture factories for Pytest. + :pypi:`pytest-bravado` *last release*: Feb 15, 2022, *status*: N/A, @@ -2503,9 +2612,9 @@ This list contains 1448 plugins. Budo Systems is a martial arts school management system. This module is the Budo Systems Pytest Plugin. :pypi:`pytest-bug` - *last release*: Sep 23, 2023, + *last release*: Jun 05, 2024, *status*: 5 - Production/Stable, - *requires*: pytest >=7.1.0 + *requires*: pytest>=8.0.0 Pytest plugin for marking tests as a bug @@ -2720,9 +2829,9 @@ This list contains 1448 plugins. A pytest plugin that allows multiple failures per test. :pypi:`pytest-checkdocs` - *last release*: Mar 31, 2024, + *last release*: Apr 30, 2024, *status*: 5 - Production/Stable, - *requires*: pytest>=6; extra == "testing" + *requires*: pytest!=8.1.*,>=6; extra == "testing" check the README when running tests @@ -2755,7 +2864,7 @@ This list contains 1448 plugins. Check links in files :pypi:`pytest-checklist` - *last release*: Mar 12, 2024, + *last release*: Jun 10, 2024, *status*: N/A, *requires*: N/A @@ -2852,6 +2961,13 @@ This list contains 1448 plugins. Easy quality control for CLDF datasets using pytest + :pypi:`pytest-cleanslate` + *last release*: Jun 17, 2024, + *status*: N/A, + *requires*: pytest + + Collects and executes pytest tests separately + :pypi:`pytest_cleanup` *last release*: Jan 28, 2020, *status*: N/A, @@ -2867,7 +2983,7 @@ This list contains 1448 plugins. A cleanup plugin for pytest :pypi:`pytest-clerk` - *last release*: Apr 19, 2024, + *last release*: Jun 27, 2024, *status*: N/A, *requires*: pytest<9.0.0,>=8.0.0 @@ -2916,7 +3032,7 @@ This list contains 1448 plugins. Distribute tests to cloud machines without fuss :pypi:`pytest-cmake` - *last release*: Mar 18, 2024, + *last release*: May 31, 2024, *status*: N/A, *requires*: pytest<9,>=4 @@ -3049,9 +3165,9 @@ This list contains 1448 plugins. An interactive GUI test runner for PyTest :pypi:`pytest-common-subject` - *last release*: May 15, 2022, + *last release*: Jun 12, 2024, *status*: N/A, - *requires*: pytest (>=3.6,<8) + *requires*: pytest<9,>=3.6 pytest framework for testing different aspects of a common method @@ -3118,6 +3234,13 @@ This list contains 1448 plugins. A plugin to run tests written with the Contexts framework using pytest + :pypi:`pytest-continuous` + *last release*: Apr 23, 2024, + *status*: N/A, + *requires*: N/A + + A pytest plugin to run tests continuously until failure or interruption. + :pypi:`pytest-cookies` *last release*: Mar 22, 2023, *status*: 5 - Production/Stable, @@ -3126,7 +3249,7 @@ This list contains 1448 plugins. The pytest plugin for your Cookiecutter templates. 🍪 :pypi:`pytest-copie` - *last release*: Jan 27, 2024, + *last release*: Jun 26, 2024, *status*: 3 - Alpha, *requires*: pytest @@ -3182,7 +3305,7 @@ This list contains 1448 plugins. Coverage dynamic context support for PyTest, including sub-processes :pypi:`pytest-coveragemarkers` - *last release*: Apr 15, 2024, + *last release*: Jun 04, 2024, *status*: N/A, *requires*: pytest<8.0.0,>=7.1.2 @@ -3314,6 +3437,13 @@ This list contains 1448 plugins. Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report + :pypi:`pytest-custom-outputs` + *last release*: Jul 10, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + A plugin that allows users to create and use custom outputs instead of the standard Pass and Fail. Also allows users to retrieve test results in fixtures. + :pypi:`pytest-custom-report` *last release*: Jan 30, 2019, *status*: N/A, @@ -3357,7 +3487,7 @@ This list contains 1448 plugins. pytest fixtures to run dash applications. :pypi:`pytest-dashboard` - *last release*: Apr 18, 2024, + *last release*: May 30, 2024, *status*: N/A, *requires*: pytest<8.0.0,>=7.4.3 @@ -3371,7 +3501,7 @@ This list contains 1448 plugins. Useful functions for managing data for pytest fixtures :pypi:`pytest-databases` - *last release*: Apr 19, 2024, + *last release*: Jul 02, 2024, *status*: 4 - Beta, *requires*: pytest @@ -3525,9 +3655,9 @@ This list contains 1448 plugins. A pytest plugin for linting a dbt project's conventions :pypi:`pytest-dbt-core` - *last release*: Aug 25, 2023, + *last release*: Jun 04, 2024, *status*: N/A, - *requires*: pytest >=6.2.5 ; extra == 'test' + *requires*: pytest>=6.2.5; extra == "test" Pytest extension for dbt. @@ -3706,6 +3836,13 @@ This list contains 1448 plugins. pytest-dir-equals is a pytest plugin providing helpers to assert directories equality allowing golden testing + :pypi:`pytest-dirty` + *last release*: Jul 11, 2024, + *status*: 3 - Alpha, + *requires*: pytest>=8.2; extra == "dev" + + Static import analysis for thrifty testing. + :pypi:`pytest-disable` *last release*: Sep 10, 2015, *status*: 4 - Beta, @@ -3721,9 +3858,9 @@ This list contains 1448 plugins. Disable plugins per test :pypi:`pytest-discord` - *last release*: Oct 18, 2023, + *last release*: May 11, 2024, *status*: 4 - Beta, - *requires*: pytest !=6.0.0,<8,>=3.3.2 + *requires*: pytest!=6.0.0,<9,>=3.3.2 A pytest plugin to notify test results to a Discord channel. @@ -3734,6 +3871,27 @@ This list contains 1448 plugins. Pytest plugin to record discovered tests in a file + :pypi:`pytest-ditto` + *last release*: Jun 09, 2024, + *status*: 4 - Beta, + *requires*: pytest>=3.5.0 + + Snapshot testing pytest plugin with minimal ceremony and flexible persistence formats. + + :pypi:`pytest-ditto-pandas` + *last release*: May 29, 2024, + *status*: 4 - Beta, + *requires*: pytest>=3.5.0 + + pytest-ditto plugin for pandas snapshots. + + :pypi:`pytest-ditto-pyarrow` + *last release*: Jun 09, 2024, + *status*: 4 - Beta, + *requires*: pytest>=3.5.0 + + pytest-ditto plugin for pyarrow tables. + :pypi:`pytest-django` *last release*: Jan 30, 2024, *status*: 5 - Production/Stable, @@ -3777,9 +3935,9 @@ This list contains 1448 plugins. A pytest plugin for running django in class-scoped fixtures :pypi:`pytest-django-docker-pg` - *last release*: Jan 30, 2024, + *last release*: Jun 13, 2024, *status*: 5 - Production/Stable, - *requires*: pytest <8.0.0,>=7.0.0 + *requires*: pytest<9.0.0,>=7.0.0 @@ -4077,6 +4235,13 @@ This list contains 1448 plugins. Pytest plugin with advanced doctest features. + :pypi:`pytest-documentary` + *last release*: Jul 11, 2024, + *status*: N/A, + *requires*: pytest + + A simple pytest plugin to generate test documentation + :pypi:`pytest-dogu-report` *last release*: Jul 07, 2023, *status*: N/A, @@ -4259,6 +4424,13 @@ This list contains 1448 plugins. pytest plugin with mechanisms for echoing environment variables, package version and generic attributes + :pypi:`pytest-edit` + *last release*: Jun 09, 2024, + *status*: N/A, + *requires*: pytest + + Edit the source code of a failed test with \`pytest --edit\`. + :pypi:`pytest-ekstazi` *last release*: Sep 10, 2022, *status*: N/A, @@ -4302,56 +4474,56 @@ This list contains 1448 plugins. Send execution result email :pypi:`pytest-embedded` - *last release*: Apr 09, 2024, + *last release*: May 31, 2024, *status*: 5 - Production/Stable, *requires*: pytest>=7.0 A pytest plugin that designed for embedded testing. :pypi:`pytest-embedded-arduino` - *last release*: Apr 09, 2024, + *last release*: May 23, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with Arduino. :pypi:`pytest-embedded-idf` - *last release*: Apr 09, 2024, + *last release*: May 23, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with ESP-IDF. :pypi:`pytest-embedded-jtag` - *last release*: Apr 09, 2024, + *last release*: May 23, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with JTAG. :pypi:`pytest-embedded-qemu` - *last release*: Apr 09, 2024, + *last release*: May 23, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with QEMU. :pypi:`pytest-embedded-serial` - *last release*: Apr 09, 2024, + *last release*: May 31, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with Serial. :pypi:`pytest-embedded-serial-esp` - *last release*: Apr 09, 2024, + *last release*: May 31, 2024, *status*: 5 - Production/Stable, *requires*: N/A Make pytest-embedded plugin work with Espressif target boards. :pypi:`pytest-embedded-wokwi` - *last release*: Apr 09, 2024, + *last release*: May 23, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -4526,7 +4698,7 @@ This list contains 1448 plugins. Applies eventlet monkey-patch as a pytest plugin. :pypi:`pytest-evm` - *last release*: Apr 20, 2024, + *last release*: Apr 22, 2024, *status*: 4 - Beta, *requires*: pytest<9.0.0,>=8.1.1 @@ -4540,23 +4712,30 @@ This list contains 1448 plugins. Parse queries in Lucene and Elasticsearch syntaxes :pypi:`pytest-examples` - *last release*: Jul 11, 2023, + *last release*: Jul 02, 2024, *status*: 4 - Beta, *requires*: pytest>=7 Pytest plugin for testing examples in docstrings and markdown files. :pypi:`pytest-exasol-itde` - *last release*: Feb 15, 2024, + *last release*: Jul 01, 2024, *status*: N/A, - *requires*: pytest (>=7,<9) + *requires*: pytest<9,>=7 + + + + :pypi:`pytest-exasol-saas` + *last release*: Jun 07, 2024, + *status*: N/A, + *requires*: pytest<9,>=7 :pypi:`pytest-excel` - *last release*: Sep 14, 2023, + *last release*: Jun 18, 2024, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest>3.6 pytest plugin for generating excel reports @@ -4589,9 +4768,9 @@ This list contains 1448 plugins. A timer for the phases of Pytest's execution. :pypi:`pytest-exit-code` - *last release*: Feb 23, 2024, + *last release*: May 06, 2024, *status*: 4 - Beta, - *requires*: pytest >=6.2.0 + *requires*: pytest>=6.2.0 A pytest plugin that overrides the built-in exit codes to retain more information about the test results. @@ -4764,7 +4943,7 @@ This list contains 1448 plugins. A pytest plugin that helps better distinguishing real test failures from setup flakiness. :pypi:`pytest-fail-slow` - *last release*: Feb 11, 2024, + *last release*: Jun 01, 2024, *status*: N/A, *requires*: pytest>=7.0 @@ -4833,6 +5012,13 @@ This list contains 1448 plugins. py.test plugin that activates the fault handler module for tests (dummy package) + :pypi:`pytest-fauna` + *last release*: May 30, 2024, + *status*: N/A, + *requires*: N/A + + A collection of helpful test fixtures for Fauna DB. + :pypi:`pytest-fauxfactory` *last release*: Dec 06, 2017, *status*: 5 - Production/Stable, @@ -4862,11 +5048,11 @@ This list contains 1448 plugins. A pytest plugin to detect unused files :pypi:`pytest-filedata` - *last release*: Jan 17, 2019, - *status*: 4 - Beta, + *last release*: Apr 29, 2024, + *status*: 5 - Production/Stable, *requires*: N/A - easily load data from files + easily load test data from files :pypi:`pytest-filemarker` *last release*: Dec 01, 2020, @@ -5079,9 +5265,9 @@ This list contains 1448 plugins. :pypi:`pytest-fluent` - *last release*: Jun 26, 2023, + *last release*: Jun 05, 2024, *status*: 4 - Beta, - *requires*: pytest (>=7.0.0) + *requires*: pytest>=7.0.0 A pytest plugin in order to provide logs via fluentd @@ -5155,6 +5341,13 @@ This list contains 1448 plugins. Pytest Frappe Plugin - A set of pytest fixtures to test Frappe applications + :pypi:`pytest-freezeblaster` + *last release*: Jul 10, 2024, + *status*: N/A, + *requires*: pytest>=6.2.5 + + Wrap tests with fixtures in freeze_time + :pypi:`pytest-freezegun` *last release*: Jul 19, 2020, *status*: 4 - Beta, @@ -5212,9 +5405,9 @@ This list contains 1448 plugins. :pypi:`pytest-fzf` - *last release*: Feb 07, 2024, + *last release*: Jul 03, 2024, *status*: 4 - Beta, - *requires*: pytest >=6.0.0 + *requires*: pytest>=6.0.0 fzf-based test selector for pytest @@ -5254,7 +5447,7 @@ This list contains 1448 plugins. GCS fixtures and fixture factories for Pytest. :pypi:`pytest-gee` - *last release*: Feb 15, 2024, + *last release*: Jun 30, 2024, *status*: 3 - Alpha, *requires*: pytest @@ -5289,7 +5482,7 @@ This list contains 1448 plugins. For finding/executing Ghost Inspector tests :pypi:`pytest-girder` - *last release*: Apr 12, 2024, + *last release*: Jul 08, 2024, *status*: N/A, *requires*: pytest>=3.6 @@ -5387,9 +5580,9 @@ This list contains 1448 plugins. Utility to select tests that have had its dependencies modified (as identified by git diff) :pypi:`pytest-glamor-allure` - *last release*: Jul 22, 2022, + *last release*: Apr 30, 2024, *status*: 4 - Beta, - *requires*: pytest + *requires*: pytest<=8.2.0 Extends allure-pytest functionality @@ -5498,13 +5691,6 @@ This list contains 1448 plugins. Store data created during your pytest tests execution, and retrieve it at the end of the session, e.g. for applicative benchmarking purposes. - :pypi:`pytest-helm-chart` - *last release*: Jun 15, 2020, - *status*: 4 - Beta, - *requires*: pytest (>=5.4.2,<6.0.0) - - A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. - :pypi:`pytest-helm-charts` *last release*: Feb 07, 2024, *status*: 4 - Beta, @@ -5513,7 +5699,7 @@ This list contains 1448 plugins. A plugin to provide different types and configs of Kubernetes clusters that can be used for testing. :pypi:`pytest-helm-templates` - *last release*: Apr 05, 2024, + *last release*: May 08, 2024, *status*: N/A, *requires*: pytest~=7.4.0; extra == "dev" @@ -5555,11 +5741,11 @@ This list contains 1448 plugins. Hide captured output :pypi:`pytest-himark` - *last release*: Apr 14, 2024, + *last release*: Jun 05, 2024, *status*: 4 - Beta, *requires*: pytest>=6.2.0 - A plugin that will filter pytest's test collection using a json file. It will read a json file provided with a --json argument in pytest command line (or in pytest.ini), search the markers key and automatically add -m option to the command line for filtering out the tests marked with disabled markers. + This plugin aims to create markers automatically based on a json configuration. :pypi:`pytest-historic` *last release*: Apr 08, 2020, @@ -5597,9 +5783,9 @@ This list contains 1448 plugins. A pytest plugin for use with homeassistant custom components. :pypi:`pytest-homeassistant-custom-component` - *last release*: Apr 13, 2024, + *last release*: Jul 11, 2024, *status*: 3 - Alpha, - *requires*: pytest==8.1.1 + *requires*: pytest==8.2.0 Experimental package to automatically extract test plugins for Home Assistant custom components @@ -5632,7 +5818,7 @@ This list contains 1448 plugins. A plugin that tracks test changes :pypi:`pytest-houdini` - *last release*: Feb 09, 2024, + *last release*: Jul 05, 2024, *status*: N/A, *requires*: pytest @@ -5681,7 +5867,7 @@ This list contains 1448 plugins. optimized pytest plugin for generating HTML reports :pypi:`pytest-html-merger` - *last release*: Nov 11, 2023, + *last release*: Jul 12, 2024, *status*: N/A, *requires*: N/A @@ -5709,7 +5895,7 @@ This list contains 1448 plugins. Generates a static html report based on pytest framework :pypi:`pytest-html-report-merger` - *last release*: Oct 23, 2023, + *last release*: May 22, 2024, *status*: N/A, *requires*: N/A @@ -5765,9 +5951,9 @@ This list contains 1448 plugins. pytest-httpserver is a httpserver for pytest :pypi:`pytest-httptesting` - *last release*: Jul 24, 2023, + *last release*: May 08, 2024, *status*: N/A, - *requires*: pytest (>=7.2.0,<8.0.0) + *requires*: pytest<9.0.0,>=8.2.0 http_testing framework on top of pytest @@ -5814,7 +6000,7 @@ This list contains 1448 plugins. help hypo module for pytest :pypi:`pytest-iam` - *last release*: Apr 12, 2024, + *last release*: Apr 22, 2024, *status*: 3 - Alpha, *requires*: pytest>=7.0.0 @@ -5856,7 +6042,7 @@ This list contains 1448 plugins. Pytest plugin for testing function idempotence. :pypi:`pytest-ignore-flaky` - *last release*: Apr 08, 2024, + *last release*: Apr 20, 2024, *status*: 5 - Production/Stable, *requires*: pytest>=6.0 @@ -5877,9 +6063,9 @@ This list contains 1448 plugins. :pypi:`pytest-image-snapshot` - *last release*: Dec 01, 2023, + *last release*: Jul 01, 2024, *status*: 4 - Beta, - *requires*: pytest >=3.5.0 + *requires*: pytest>=3.5.0 A pytest plugin for image snapshot management and comparison. @@ -5890,6 +6076,13 @@ This list contains 1448 plugins. an incremental test runner (pytest plugin) + :pypi:`pytest-infinity` + *last release*: Jun 09, 2024, + *status*: N/A, + *requires*: pytest<9.0.0,>=8.0.0 + + + :pypi:`pytest-influxdb` *last release*: Apr 20, 2021, *status*: N/A, @@ -5933,7 +6126,7 @@ This list contains 1448 plugins. Reuse pytest.ini to store env variables :pypi:`pytest-initry` - *last release*: Apr 14, 2024, + *last release*: Apr 30, 2024, *status*: N/A, *requires*: pytest<9.0.0,>=8.1.1 @@ -5947,21 +6140,21 @@ This list contains 1448 plugins. A pytest plugin for writing inline tests. :pypi:`pytest-inmanta` - *last release*: Dec 13, 2023, + *last release*: Jul 05, 2024, *status*: 5 - Production/Stable, *requires*: pytest A py.test plugin providing fixtures to simplify inmanta modules testing. :pypi:`pytest-inmanta-extensions` - *last release*: Apr 02, 2024, + *last release*: Jul 05, 2024, *status*: 5 - Production/Stable, *requires*: N/A Inmanta tests package :pypi:`pytest-inmanta-lsm` - *last release*: Apr 15, 2024, + *last release*: Jul 06, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -6052,9 +6245,9 @@ This list contains 1448 plugins. Pytest plugin for checking charm relation interface protocol compliance. :pypi:`pytest-invenio` - *last release*: Feb 28, 2024, + *last release*: Jun 27, 2024, *status*: 5 - Production/Stable, - *requires*: pytest <7.2.0,>=6 + *requires*: pytest<7.2.0,>=6 Pytest fixtures for Invenio. @@ -6080,7 +6273,7 @@ This list contains 1448 plugins. THIS PROJECT IS ABANDONED :pypi:`pytest-ipywidgets` - *last release*: Apr 08, 2024, + *last release*: Jul 11, 2024, *status*: N/A, *requires*: pytest @@ -6157,16 +6350,16 @@ This list contains 1448 plugins. A plugin to generate customizable jinja-based HTML reports in pytest :pypi:`pytest-jira` - *last release*: Apr 12, 2024, + *last release*: Apr 30, 2024, *status*: 3 - Alpha, *requires*: N/A py.test JIRA integration plugin, using markers :pypi:`pytest-jira-xfail` - *last release*: Jun 19, 2023, + *last release*: Jul 09, 2024, *status*: N/A, - *requires*: pytest (>=7.2.0) + *requires*: pytest>=7.2.0 Plugin skips (xfail) tests if unresolved Jira issue(s) linked @@ -6205,6 +6398,13 @@ This list contains 1448 plugins. Generate JSON test reports + :pypi:`pytest-json-ctrf` + *last release*: Jun 15, 2024, + *status*: N/A, + *requires*: pytest>6.0.0 + + Pytest plugin to generate json report in CTRF (Common Test Report Format) + :pypi:`pytest-json-fixtures` *last release*: Mar 14, 2023, *status*: 4 - Beta, @@ -6241,7 +6441,7 @@ This list contains 1448 plugins. A pytest plugin to perform JSONSchema validations :pypi:`pytest-jtr` - *last release*: Apr 15, 2024, + *last release*: Jun 04, 2024, *status*: N/A, *requires*: pytest<8.0.0,>=7.1.2 @@ -6331,6 +6531,13 @@ This list contains 1448 plugins. Run Konira DSL tests with py.test + :pypi:`pytest-kookit` + *last release*: May 16, 2024, + *status*: N/A, + *requires*: N/A + + Your simple but kooky integration testing with pytest + :pypi:`pytest-koopmans` *last release*: Nov 21, 2022, *status*: 4 - Beta, @@ -6367,9 +6574,9 @@ This list contains 1448 plugins. Alternate syntax for @pytest.mark.parametrize with test cases as dictionaries and default value fallbacks :pypi:`pytest-lambda` - *last release*: Aug 20, 2022, - *status*: 3 - Alpha, - *requires*: pytest (>=3.6,<8) + *last release*: May 27, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest<9,>=3.6 Define pytest fixtures with lambda functions. @@ -6549,7 +6756,7 @@ This list contains 1448 plugins. Generate local badges (shields) reporting your test suite status. :pypi:`pytest-localftpserver` - *last release*: Oct 14, 2023, + *last release*: May 19, 2024, *status*: 5 - Production/Stable, *requires*: pytest @@ -6640,9 +6847,9 @@ This list contains 1448 plugins. :pypi:`pytest-logikal` - *last release*: Mar 30, 2024, + *last release*: Jun 27, 2024, *status*: 5 - Production/Stable, - *requires*: pytest==8.1.1 + *requires*: pytest==8.2.2 Common testing environment @@ -6668,7 +6875,7 @@ This list contains 1448 plugins. pytest plugin for looping tests :pypi:`pytest-lsp` - *last release*: Feb 07, 2024, + *last release*: May 22, 2024, *status*: 3 - Alpha, *requires*: pytest @@ -6723,6 +6930,13 @@ This list contains 1448 plugins. UNKNOWN + :pypi:`pytest-mark-manage` + *last release*: Jul 08, 2024, + *status*: N/A, + *requires*: pytest + + 用例标签化管理 + :pypi:`pytest-mark-no-py3` *last release*: May 17, 2019, *status*: N/A, @@ -6801,11 +7015,11 @@ This list contains 1448 plugins. Plugin for generating Markdown reports for pytest results :pypi:`pytest-md-report` - *last release*: Feb 04, 2024, + *last release*: May 18, 2024, *status*: 4 - Beta, - *requires*: pytest !=6.0.0,<9,>=3.3.2 + *requires*: pytest!=6.0.0,<9,>=3.3.2 - A pytest plugin to make a test results report with Markdown table format. + A pytest plugin to generate test outcomes reports with markdown table format. :pypi:`pytest-meilisearch` *last release*: Feb 15, 2024, @@ -6885,7 +7099,7 @@ This list contains 1448 plugins. Custom metrics report for pytest :pypi:`pytest-mh` - *last release*: Mar 14, 2024, + *last release*: Jul 02, 2024, *status*: N/A, *requires*: pytest @@ -6913,7 +7127,7 @@ This list contains 1448 plugins. A plugin to test mp :pypi:`pytest-minio-mock` - *last release*: Apr 15, 2024, + *last release*: May 26, 2024, *status*: N/A, *requires*: pytest>=5.0.0 @@ -6927,9 +7141,9 @@ This list contains 1448 plugins. Pytest plugin that creates missing fixtures :pypi:`pytest-mitmproxy` - *last release*: Mar 07, 2024, + *last release*: May 28, 2024, *status*: N/A, - *requires*: pytest >=7.0 + *requires*: pytest>=7.0 pytest plugin for mitmproxy tests @@ -6990,7 +7204,7 @@ This list contains 1448 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*: Apr 11, 2024, + *last release*: Jun 20, 2024, *status*: N/A, *requires*: pytest>=1.0 @@ -7038,13 +7252,6 @@ This list contains 1448 plugins. Utility for adding additional properties to junit xml for IDM QE - :pypi:`pytest-modifyscope` - *last release*: Apr 12, 2020, - *status*: N/A, - *requires*: pytest - - pytest plugin to modify fixture scope - :pypi:`pytest-molecule` *last release*: Mar 29, 2022, *status*: 5 - Production/Stable, @@ -7144,9 +7351,9 @@ This list contains 1448 plugins. low-startup-overhead, scalable, distributed-testing pytest plugin :pypi:`pytest-mqtt` - *last release*: Mar 31, 2024, + *last release*: May 08, 2024, *status*: 4 - Beta, - *requires*: pytest<8; extra == "test" + *requires*: pytest<9; extra == "test" pytest-mqtt supports testing systems based on MQTT @@ -7220,6 +7427,13 @@ This list contains 1448 plugins. Substitute for "pytest-mypy-plugins" for Python implementations which aren't supported by mypy. + :pypi:`pytest-mypy-runner` + *last release*: Apr 23, 2024, + *status*: N/A, + *requires*: pytest>=8.0 + + Run the mypy static type checker as a pytest test case + :pypi:`pytest-mypy-testing` *last release*: Mar 04, 2024, *status*: N/A, @@ -7228,14 +7442,14 @@ This list contains 1448 plugins. Pytest plugin to check mypy output. :pypi:`pytest-mysql` - *last release*: Oct 30, 2023, + *last release*: May 23, 2024, *status*: 5 - Production/Stable, - *requires*: pytest >=6.2 + *requires*: pytest>=6.2 MySQL process and client fixtures for pytest :pypi:`pytest-ndb` - *last release*: Oct 15, 2023, + *last release*: Apr 28, 2024, *status*: N/A, *requires*: pytest @@ -7256,16 +7470,16 @@ This list contains 1448 plugins. pytest-neo is a plugin for pytest that shows tests like screen of Matrix. :pypi:`pytest-neos` - *last release*: Apr 15, 2024, + *last release*: Jun 11, 2024, *status*: 1 - Planning, *requires*: N/A Pytest plugin for neos :pypi:`pytest-netdut` - *last release*: Mar 07, 2024, + *last release*: Jul 05, 2024, *status*: N/A, - *requires*: pytest <7.3,>=3.5.0 + *requires*: pytest<7.3,>=3.5.0 "Automated software testing for switches using pytest" @@ -7319,9 +7533,9 @@ This list contains 1448 plugins. pytest ngs fixtures :pypi:`pytest-nhsd-apim` - *last release*: Feb 16, 2024, + *last release*: Jul 01, 2024, *status*: N/A, - *requires*: pytest (>=6.2.5,<7.0.0) + *requires*: pytest<9.0.0,>=8.2.0 Pytest plugin accessing NHSDigital's APIM proxies @@ -7508,9 +7722,9 @@ This list contains 1448 plugins. The ultimate pytest output plugin :pypi:`pytest-only` - *last release*: Mar 09, 2024, + *last release*: May 27, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (<7.1) ; python_full_version <= "3.6.0" + *requires*: pytest<9,>=3.6.0 Use @pytest.mark.only to run a single test @@ -7529,9 +7743,9 @@ This list contains 1448 plugins. Run object-oriented tests in a simple format :pypi:`pytest-openfiles` - *last release*: Apr 16, 2020, + *last release*: Jun 05, 2024, *status*: 3 - Alpha, - *requires*: pytest (>=4.6) + *requires*: pytest>=4.6 Pytest plugin for detecting inadvertent open file handles @@ -7682,13 +7896,6 @@ This list contains 1448 plugins. Configure pytest fixtures using a combination of"parametrize" and markers - :pypi:`pytest-parameterize-from-files` - *last release*: Feb 15, 2024, - *status*: 4 - Beta, - *requires*: pytest>=7.2.0 - - A pytest plugin that parameterizes tests from data files. - :pypi:`pytest-parametrization` *last release*: May 22, 2022, *status*: 5 - Production/Stable, @@ -7774,7 +7981,7 @@ This list contains 1448 plugins. A contextmanager pytest fixture for handling multiple mock patches :pypi:`pytest-patterns` - *last release*: Nov 17, 2023, + *last release*: Jun 14, 2024, *status*: 4 - Beta, *requires*: N/A @@ -7823,9 +8030,9 @@ This list contains 1448 plugins. :pypi:`pytest-perf` - *last release*: Jan 28, 2024, + *last release*: May 20, 2024, *status*: 5 - Production/Stable, - *requires*: pytest >=6 ; extra == 'testing' + *requires*: pytest!=8.1.*,>=6; extra == "testing" Run performance tests against the mainline code. @@ -7844,7 +8051,7 @@ This list contains 1448 plugins. A performance plugin for pytest :pypi:`pytest-persistence` - *last release*: Jul 04, 2023, + *last release*: May 23, 2024, *status*: N/A, *requires*: N/A @@ -7858,7 +8065,7 @@ This list contains 1448 plugins. Pytest pexpect plugin. :pypi:`pytest-pg` - *last release*: Apr 03, 2024, + *last release*: May 21, 2024, *status*: 5 - Production/Stable, *requires*: pytest>=6.0.0 @@ -7970,14 +8177,14 @@ This list contains 1448 plugins. Pytest plugin for reading playbooks. :pypi:`pytest-playwright` - *last release*: Feb 02, 2024, + *last release*: Jul 03, 2024, *status*: N/A, - *requires*: pytest (<9.0.0,>=6.2.4) + *requires*: N/A A pytest wrapper with fixtures for Playwright to automate web browsers :pypi:`pytest_playwright_async` - *last release*: Feb 25, 2024, + *last release*: May 24, 2024, *status*: N/A, *requires*: N/A @@ -8019,9 +8226,9 @@ This list contains 1448 plugins. A pytest fixture for visual testing with Playwright :pypi:`pytest-plone` - *last release*: Jan 05, 2023, + *last release*: May 15, 2024, *status*: 3 - Alpha, - *requires*: pytest + *requires*: pytest<8.0.0 Pytest plugin to test Plone addons @@ -8054,9 +8261,9 @@ This list contains 1448 plugins. :pypi:`pytest-pogo` - *last release*: Mar 11, 2024, + *last release*: May 22, 2024, *status*: 1 - Planning, - *requires*: pytest (>=7,<9) + *requires*: pytest<9,>=7 Pytest plugin for pogo-migrate @@ -8236,9 +8443,9 @@ This list contains 1448 plugins. Profiling plugin for py.test :pypi:`pytest-progress` - *last release*: Jan 31, 2022, + *last release*: Jun 18, 2024, *status*: 5 - Production/Stable, - *requires*: N/A + *requires*: pytest>=2.7 pytest plugin for instant test progress status @@ -8284,6 +8491,13 @@ This list contains 1448 plugins. pytest plugin for testing applications that use psqlgraph + :pypi:`pytest-pt` + *last release*: May 15, 2024, + *status*: 4 - Beta, + *requires*: pytest + + pytest plugin to use \*.pt files as tests + :pypi:`pytest-ptera` *last release*: Mar 01, 2022, *status*: N/A, @@ -8291,6 +8505,13 @@ This list contains 1448 plugins. Use ptera probes in tests + :pypi:`pytest-publish` + *last release*: Jun 04, 2024, + *status*: N/A, + *requires*: pytest<9.0.0,>=8.0.0 + + + :pypi:`pytest-pudb` *last release*: Oct 25, 2018, *status*: 3 - Alpha, @@ -8369,7 +8590,7 @@ This list contains 1448 plugins. Record PyMySQL queries and mock with the stored data. :pypi:`pytest-pyodide` - *last release*: Dec 09, 2023, + *last release*: Jun 12, 2024, *status*: N/A, *requires*: pytest @@ -8418,7 +8639,7 @@ This list contains 1448 plugins. Pyramid server fixture for py.test :pypi:`pytest-pyreport` - *last release*: Feb 03, 2024, + *last release*: May 05, 2024, *status*: N/A, *requires*: pytest @@ -8466,6 +8687,13 @@ This list contains 1448 plugins. pytest plugin for adding to the PYTHONPATH from command line or configs. + :pypi:`pytest-python-test-engineer-sort` + *last release*: May 13, 2024, + *status*: N/A, + *requires*: pytest>=6.2.0 + + Sort plugin for Pytest + :pypi:`pytest-pytorch` *last release*: May 25, 2021, *status*: 4 - Beta, @@ -8487,10 +8715,17 @@ This list contains 1448 plugins. Pytest-pyvista package + :pypi:`pytest-qanova` + *last release*: May 26, 2024, + *status*: 3 - Alpha, + *requires*: pytest + + A pytest plugin to collect test information + :pypi:`pytest-qaseio` - *last release*: Sep 12, 2023, + *last release*: May 30, 2024, *status*: 4 - Beta, - *requires*: pytest (>=7.2.2,<8.0.0) + *requires*: pytest<9.0.0,>=7.2.2 Pytest plugin for Qase.io integration @@ -8509,9 +8744,9 @@ This list contains 1448 plugins. Pytest plugin for uploading test results to your QA Touch Testrun. :pypi:`pytest-qgis` - *last release*: Nov 29, 2023, + *last release*: Jun 14, 2024, *status*: 5 - Production/Stable, - *requires*: pytest >=6.0 + *requires*: pytest>=6.0 A pytest plugin for testing QGIS python plugins @@ -8565,9 +8800,9 @@ This list contains 1448 plugins. Run test suites with pytest-quickify. :pypi:`pytest-rabbitmq` - *last release*: Jul 05, 2023, + *last release*: May 08, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=6.2) + *requires*: pytest>=6.2 RabbitMQ process and client fixtures for pytest @@ -8656,9 +8891,9 @@ This list contains 1448 plugins. Randomise the order in which pytest tests are run with some control over the randomness :pypi:`pytest-ranking` - *last release*: Mar 18, 2024, + *last release*: Jun 07, 2024, *status*: 4 - Beta, - *requires*: pytest >=7.4.3 + *requires*: pytest>=7.4.3 A Pytest plugin for automatically prioritizing/ranking tests to speed up failure detection @@ -8677,14 +8912,14 @@ This list contains 1448 plugins. Pytest fixtures for REANA. :pypi:`pytest-recorder` - *last release*: Nov 21, 2023, + *last release*: Jun 27, 2024, *status*: N/A, *requires*: N/A Pytest plugin, meant to facilitate unit tests writing for tools consumming Web APIs. :pypi:`pytest-recording` - *last release*: Dec 06, 2023, + *last release*: Jul 09, 2024, *status*: 4 - Beta, *requires*: pytest>=3.5.0 @@ -8698,9 +8933,9 @@ This list contains 1448 plugins. Provides pytest plugins for reporting request/response traffic, screenshots, and more to ReportPortal :pypi:`pytest-redis` - *last release*: Apr 19, 2023, + *last release*: Jun 19, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=6.2) + *requires*: pytest>=6.2 Redis fixtures and fixture factories for Pytest. @@ -8852,7 +9087,7 @@ This list contains 1448 plugins. Generate Pytest reports with templates :pypi:`pytest-reporter-html1` - *last release*: Feb 28, 2024, + *last release*: Jun 28, 2024, *status*: 4 - Beta, *requires*: N/A @@ -8978,7 +9213,7 @@ This list contains 1448 plugins. Rerun testsuite for a certain time or iterations :pypi:`pytest-rerunclassfailures` - *last release*: Mar 29, 2024, + *last release*: Apr 24, 2024, *status*: 5 - Production/Stable, *requires*: pytest>=7.2 @@ -8999,14 +9234,14 @@ This list contains 1448 plugins. pytest plugin to re-run tests to eliminate flaky failures :pypi:`pytest-reserial` - *last release*: Feb 08, 2024, + *last release*: May 23, 2024, *status*: 4 - Beta, *requires*: pytest Pytest fixture for recording and replaying serial port traffic. :pypi:`pytest-resilient-circuits` - *last release*: Apr 03, 2024, + *last release*: May 17, 2024, *status*: N/A, *requires*: pytest~=4.6; python_version == "2.7" @@ -9090,9 +9325,9 @@ This list contains 1448 plugins. A RethinkDB plugin for pytest. :pypi:`pytest-retry` - *last release*: Feb 04, 2024, + *last release*: May 14, 2024, *status*: N/A, - *requires*: pytest >=7.0.0 + *requires*: pytest>=7.0.0 Adds the ability to retry flaky tests in CI environments @@ -9174,7 +9409,7 @@ This list contains 1448 plugins. pytest plugin for ROAST configuration override and fixtures :pypi:`pytest_robotframework` - *last release*: Mar 29, 2024, + *last release*: Jul 01, 2024, *status*: N/A, *requires*: pytest<9,>=7 @@ -9223,9 +9458,9 @@ This list contains 1448 plugins. Coverage-based regression test selection (RTS) plugin for pytest :pypi:`pytest-ruff` - *last release*: Mar 10, 2024, + *last release*: Jul 09, 2024, *status*: 4 - Beta, - *requires*: pytest (>=5) + *requires*: pytest>=5 pytest plugin to check ruff requirements. @@ -9313,6 +9548,13 @@ This list contains 1448 plugins. Simple PyTest Plugin For Salt's Test Suite Specifically + :pypi:`pytest-sample-argvalues` + *last release*: May 07, 2024, + *status*: N/A, + *requires*: pytest + + A utility function to help choose a random sample from your argvalues in pytest. + :pypi:`pytest-sanic` *last release*: Oct 25, 2021, *status*: N/A, @@ -9342,7 +9584,7 @@ This list contains 1448 plugins. pytest_sauce provides sane and helpful methods worked out in clearcode to run py.test tests with selenium/saucelabs :pypi:`pytest-sbase` - *last release*: Apr 14, 2024, + *last release*: Jul 08, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -9355,6 +9597,13 @@ This list contains 1448 plugins. pytest plugin for test scenarios + :pypi:`pytest-scenario-files` + *last release*: May 19, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=7.2.0 + + A pytest plugin that generates unit test scenarios from data files. + :pypi:`pytest-schedule` *last release*: Jan 07, 2023, *status*: 5 - Production/Stable, @@ -9405,7 +9654,7 @@ This list contains 1448 plugins. pytest plugin to automatically capture screenshots upon selenium webdriver events :pypi:`pytest-seleniumbase` - *last release*: Apr 14, 2024, + *last release*: Jul 08, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -9440,7 +9689,7 @@ This list contains 1448 plugins. Send pytest execution result email :pypi:`pytest-sentry` - *last release*: Apr 05, 2024, + *last release*: Apr 25, 2024, *status*: N/A, *requires*: pytest @@ -9453,6 +9702,13 @@ This list contains 1448 plugins. Pytest plugin for sequencing markers for execution of tests + :pypi:`pytest-server` + *last release*: Jun 24, 2024, + *status*: N/A, + *requires*: N/A + + test server exec cmd + :pypi:`pytest-server-fixtures` *last release*: Dec 19, 2023, *status*: 5 - Production/Stable, @@ -9468,12 +9724,19 @@ This list contains 1448 plugins. Automatically mocks resources from serverless.yml in pytest using moto. :pypi:`pytest-servers` - *last release*: Mar 19, 2024, + *last release*: Jun 17, 2024, *status*: 3 - Alpha, *requires*: pytest>=6.2 pytest servers + :pypi:`pytest-service` + *last release*: May 11, 2024, + *status*: 5 - Production/Stable, + *requires*: pytest>=6.0.0 + + + :pypi:`pytest-services` *last release*: Oct 30, 2020, *status*: 6 - Mature, @@ -9902,9 +10165,9 @@ This list contains 1448 plugins. Pytest plugin for the splinter automation library :pypi:`pytest-split` - *last release*: Jan 29, 2024, + *last release*: Jun 19, 2024, *status*: 4 - Beta, - *requires*: pytest (>=5,<9) + *requires*: pytest<9,>=5 Pytest plugin which splits the test suite to equally sized sub suites based on test execution time. @@ -9937,14 +10200,14 @@ This list contains 1448 plugins. :pypi:`pytest-splunk-addon` - *last release*: Apr 19, 2024, + *last release*: Jul 11, 2024, *status*: N/A, - *requires*: pytest (>5.4.0,<8) + *requires*: pytest<8,>5.4.0 A Dynamic test tool for Splunk Apps and Add-ons :pypi:`pytest-splunk-addon-ui-smartx` - *last release*: Mar 26, 2024, + *last release*: Jul 10, 2024, *status*: N/A, *requires*: N/A @@ -9972,9 +10235,9 @@ This list contains 1448 plugins. pytest plugin with sqlalchemy related fixtures :pypi:`pytest-sqlalchemy-mock` - *last release*: Mar 15, 2023, + *last release*: May 21, 2024, *status*: 3 - Alpha, - *requires*: pytest (>=2.0) + *requires*: pytest>=7.0.0 pytest sqlalchemy plugin for mock @@ -10035,12 +10298,19 @@ This list contains 1448 plugins. A package to prevent Dependency Confusion attacks against Yandex. :pypi:`pytest-static` - *last release*: Jan 15, 2024, + *last release*: Jun 20, 2024, *status*: 1 - Planning, - *requires*: pytest (>=7.4.3,<8.0.0) + *requires*: pytest<8.0.0,>=7.4.3 pytest-static + :pypi:`pytest-stats` + *last release*: Jul 03, 2024, + *status*: N/A, + *requires*: pytest>=8.0.0 + + Collects tests metadata for future analysis, easy to extend for any data store + :pypi:`pytest-statsd` *last release*: Nov 30, 2018, *status*: 5 - Production/Stable, @@ -10098,7 +10368,7 @@ This list contains 1448 plugins. A Pytest plugin that allows you to loop tests for a user defined amount of time. :pypi:`pytest-structlog` - *last release*: Mar 13, 2024, + *last release*: Jun 09, 2024, *status*: N/A, *requires*: pytest @@ -10154,9 +10424,9 @@ This list contains 1448 plugins. A hack to explicitly set up and tear down fixtures. :pypi:`pytest-subtests` - *last release*: Mar 07, 2024, + *last release*: Jul 07, 2024, *status*: 4 - Beta, - *requires*: pytest >=7.0 + *requires*: pytest>=7.0 unittest subTest() support and subtests fixture @@ -10308,7 +10578,7 @@ This list contains 1448 plugins. py.test plugin to introduce block structure in teamcity build log, if output is not captured :pypi:`pytest-telegram` - *last release*: Dec 10, 2020, + *last release*: Apr 25, 2024, *status*: 5 - Production/Stable, *requires*: N/A @@ -10336,9 +10606,9 @@ This list contains 1448 plugins. Terraform and Terragrunt fixtures for pytest :pypi:`pytest-terraform` - *last release*: Jun 20, 2023, + *last release*: May 21, 2024, *status*: N/A, - *requires*: pytest (>=6.0) + *requires*: pytest>=6.0 A pytest plugin for using terraform fixtures @@ -10392,9 +10662,9 @@ This list contains 1448 plugins. A Pytest plugin for running a subset of your tests by splitting them in to equally sized groups. :pypi:`pytest-testinfra` - *last release*: Feb 15, 2024, + *last release*: May 26, 2024, *status*: 5 - Production/Stable, - *requires*: pytest >=6 + *requires*: pytest>=6 Test infrastructures @@ -10566,6 +10836,20 @@ This list contains 1448 plugins. Plugin for py.test to run relevant tests, based on naively checking if a test contains a reference to the symbol you supply + :pypi:`pytest-test-tracer-for-pytest` + *last release*: Jun 28, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + A plugin that allows coll test data for use on Test Tracer + + :pypi:`pytest-test-tracer-for-pytest-bdd` + *last release*: Jul 01, 2024, + *status*: 4 - Beta, + *requires*: pytest>=6.2.0 + + A plugin that allows coll test data for use on Test Tracer + :pypi:`pytest-test-utils` *last release*: Feb 08, 2024, *status*: N/A, @@ -10594,6 +10878,13 @@ This list contains 1448 plugins. pytest-ligo + :pypi:`pytest-tf` + *last release*: May 29, 2024, + *status*: N/A, + *requires*: pytest<9.0.0,>=8.2.1 + + Test your OpenTofu and Terraform config using a PyTest plugin + :pypi:`pytest-th2-bdd` *last release*: May 13, 2022, *status*: N/A, @@ -10791,7 +11082,7 @@ This list contains 1448 plugins. Numerous useful plugins for pytest. :pypi:`pytest-toolkit` - *last release*: Apr 13, 2024, + *last release*: Jun 07, 2024, *status*: N/A, *requires*: N/A @@ -10804,6 +11095,13 @@ This list contains 1448 plugins. Pytest tools + :pypi:`pytest-topo` + *last release*: Jun 05, 2024, + *status*: N/A, + *requires*: pytest>=7.0.0 + + Topological sorting for pytest + :pypi:`pytest-tornado` *last release*: Jun 17, 2020, *status*: 5 - Production/Stable, @@ -10938,9 +11236,9 @@ This list contains 1448 plugins. :pypi:`pytest-twisted` - *last release*: Mar 19, 2024, + *last release*: Jul 10, 2024, *status*: 5 - Production/Stable, - *requires*: pytest >=2.3 + *requires*: pytest>=2.3 A twisted plugin for pytest. @@ -11015,9 +11313,9 @@ This list contains 1448 plugins. A plugin to uncollect pytests tests rather than using skipif :pypi:`pytest-unflakable` - *last release*: Nov 12, 2023, + *last release*: Apr 30, 2024, *status*: 4 - Beta, - *requires*: pytest >=6.2.0 + *requires*: pytest>=6.2.0 Unflakable plugin for PyTest @@ -11050,9 +11348,9 @@ This list contains 1448 plugins. Run only unmarked tests :pypi:`pytest-unordered` - *last release*: Mar 13, 2024, + *last release*: Jul 05, 2024, *status*: 4 - Beta, - *requires*: pytest >=7.0.0 + *requires*: pytest>=7.0.0 Test equality of unordered collections in pytest @@ -11147,13 +11445,6 @@ This list contains 1448 plugins. py.test fixture for creating a virtual environment - :pypi:`pytest-ver` - *last release*: Feb 07, 2024, - *status*: 4 - Beta, - *requires*: pytest - - Pytest module with Verification Protocol, Verification Report and Trace Matrix - :pypi:`pytest-verbose-parametrize` *last release*: May 28, 2019, *status*: 5 - Production/Stable, @@ -11295,9 +11586,9 @@ This list contains 1448 plugins. Selenium webdriver fixture for py.test :pypi:`pytest-webtest-extras` - *last release*: Nov 13, 2023, + *last release*: Jun 08, 2024, *status*: N/A, - *requires*: pytest >= 7.0.0 + *requires*: pytest>=7.0.0 Pytest plugin to enhance pytest-html and allure reports of webtest projects by adding screenshots, comments and webpage sources. @@ -11309,7 +11600,7 @@ This list contains 1448 plugins. Welian API Automation test framework pytest plugin :pypi:`pytest-when` - *last release*: Mar 22, 2024, + *last release*: May 28, 2024, *status*: N/A, *requires*: pytest>=7.3.1 @@ -11365,9 +11656,9 @@ This list contains 1448 plugins. A pytest plugin for configuring workflow/pipeline tests using YAML files :pypi:`pytest-xdist` - *last release*: Apr 19, 2024, + *last release*: Apr 28, 2024, *status*: 5 - Production/Stable, - *requires*: pytest >=6.2.0 + *requires*: pytest>=7.0.0 pytest xdist plugin for distributed testing, most importantly across multiple CPUs @@ -11428,9 +11719,9 @@ This list contains 1448 plugins. Extended logging for test and decorators :pypi:`pytest-xlsx` - *last release*: Mar 22, 2024, + *last release*: Apr 23, 2024, *status*: N/A, - *requires*: N/A + *requires*: pytest~=7.0 pytest plugin for generating test cases by xlsx(excel) @@ -11442,7 +11733,7 @@ This list contains 1448 plugins. An extended parametrizing plugin of pytest. :pypi:`pytest-xprocess` - *last release*: Mar 31, 2024, + *last release*: May 19, 2024, *status*: 4 - Beta, *requires*: pytest>=2.8 @@ -11476,6 +11767,13 @@ This list contains 1448 plugins. A package to prevent Dependency Confusion attacks against Yandex. + :pypi:`pytest-xstress` + *last release*: Jun 01, 2024, + *status*: N/A, + *requires*: pytest<9.0.0,>=8.0.0 + + + :pypi:`pytest-xvfb` *last release*: May 29, 2023, *status*: 4 - Beta, @@ -11484,9 +11782,9 @@ This list contains 1448 plugins. A pytest plugin to run Xvfb (or Xephyr/Xvnc) for tests. :pypi:`pytest-xvirt` - *last release*: Oct 01, 2023, + *last release*: Jul 03, 2024, *status*: 4 - Beta, - *requires*: pytest >=7.1.0 + *requires*: pytest>=7.2.2 A pytest plugin to virtualize test. For example to transparently running them on a remote box. @@ -11498,7 +11796,7 @@ This list contains 1448 plugins. This plugin is used to load yaml output to your test using pytest framework. :pypi:`pytest-yaml-sanmu` - *last release*: Apr 19, 2024, + *last release*: Jul 12, 2024, *status*: N/A, *requires*: pytest>=7.4.0 @@ -11553,6 +11851,13 @@ This list contains 1448 plugins. Pytest plugin to test the YLS as a whole. + :pypi:`pytest-youqu-playwright` + *last release*: Jun 12, 2024, + *status*: N/A, + *requires*: pytest + + pytest-youqu-playwright + :pypi:`pytest-yuk` *last release*: Mar 26, 2021, *status*: N/A, @@ -11574,10 +11879,17 @@ This list contains 1448 plugins. OWASP ZAP plugin for py.test. + :pypi:`pytest-zcc` + *last release*: Jun 02, 2024, + *status*: N/A, + *requires*: N/A + + eee + :pypi:`pytest-zebrunner` - *last release*: Jan 08, 2024, + *last release*: Jul 04, 2024, *status*: 5 - Production/Stable, - *requires*: pytest (>=4.5.0) + *requires*: pytest>=4.5.0 Pytest connector for Zebrunner reporting diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 39317497ebd..099c8a00260 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -7,9 +7,6 @@ API Reference This page contains the full reference to pytest's API. -.. contents:: - :depth: 3 - :local: Constants --------- @@ -59,11 +56,19 @@ pytest.fail .. autofunction:: pytest.fail(reason, [pytrace=True, msg=None]) +.. class:: pytest.fail.Exception + + The exception raised by :func:`pytest.fail`. + pytest.skip ~~~~~~~~~~~ .. autofunction:: pytest.skip(reason, [allow_module_level=False, msg=None]) +.. class:: pytest.skip.Exception + + The exception raised by :func:`pytest.skip`. + .. _`pytest.importorskip ref`: pytest.importorskip @@ -76,11 +81,19 @@ pytest.xfail .. autofunction:: pytest.xfail +.. class:: pytest.xfail.Exception + + The exception raised by :func:`pytest.xfail`. + pytest.exit ~~~~~~~~~~~ .. autofunction:: pytest.exit(reason, [returncode=None, msg=None]) +.. class:: pytest.exit.Exception + + The exception raised by :func:`pytest.exit`. + pytest.main ~~~~~~~~~~~ @@ -246,9 +259,10 @@ Marks a test function as *expected to fail*. to specify ``reason`` (see :ref:`condition string `). :keyword str reason: Reason why the test function is marked as xfail. - :keyword Type[Exception] raises: + :keyword raises: Exception class (or tuple of classes) expected to be raised by the test function; other exceptions will fail the test. Note that subclasses of the classes passed will also result in a match (similar to how the ``except`` statement works). + :type raises: Type[:py:exc:`Exception`] :keyword bool run: Whether the test function should actually be executed. If ``False``, the function will always xfail and will @@ -636,7 +650,7 @@ Reference to all hooks which can be implemented by :ref:`conftest.py files `. Only explicitly +specified plugins will be loaded. .. envvar:: PYTEST_PLUGINS @@ -1687,13 +1702,13 @@ passed multiple times. The expected format is ``name=value``. For example:: This would tell ``pytest`` to not look into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. - Additionally, ``pytest`` will attempt to intelligently identify and ignore a - virtualenv by the presence of an activation script. Any directory deemed to - be the root of a virtual environment will not be considered during test - collection unless ``--collect-in-virtualenv`` is given. Note also that - ``norecursedirs`` takes precedence over ``--collect-in-virtualenv``; e.g. if - you intend to run tests in a virtualenv with a base directory that matches - ``'.*'`` you *must* override ``norecursedirs`` in addition to using the + Additionally, ``pytest`` will attempt to intelligently identify and ignore + a virtualenv. Any directory deemed to be the root of a virtual environment + will not be considered during test collection unless + ``--collect-in-virtualenv`` is given. Note also that ``norecursedirs`` + takes precedence over ``--collect-in-virtualenv``; e.g. if you intend to + run tests in a virtualenv with a base directory that matches ``'.*'`` you + *must* override ``norecursedirs`` in addition to using the ``--collect-in-virtualenv`` flag. @@ -1923,7 +1938,7 @@ All the command-line flags can be obtained by running ``pytest --help``:: general: -k EXPRESSION Only run tests which match the given substring - expression. An expression is a Python evaluatable + expression. An expression is a Python evaluable expression where all names are substring-matched against test names and their parent classes. Example: -k 'test_method or test_other' matches all @@ -1987,6 +2002,7 @@ All the command-line flags can be obtained by running ``pytest --help``:: -v, --verbose Increase verbosity --no-header Disable header --no-summary Disable summary + --no-fold-skipped Do not fold skipped tests in short summary. -q, --quiet Decrease verbosity --verbosity=VERBOSE Set verbosity. Default: 0. -r chars Show extra test summary info as specified by chars: @@ -2002,6 +2018,7 @@ All the command-line flags can be obtained by running ``pytest --help``:: passed through addopts) --tb=style Traceback print mode (auto/long/short/line/native/no) + --xfail-tb Show tracebacks for xfail (as long as --tb != no) --show-capture={no,stdout,stderr,log,all} Controls how captured stdout/stderr/log is shown on failed tests. Default: all. @@ -2227,6 +2244,8 @@ All the command-line flags can be obtained by running ``pytest --help``:: Plugins that must be present for pytest to run Environment variables: + CI When set (regardless of value), pytest knows it is running in a CI process and does not truncate summary info + BUILD_NUMBER Equivalent to CI PYTEST_ADDOPTS Extra command line options PYTEST_PLUGINS Comma-separated plugins to load during startup PYTEST_DISABLE_PLUGIN_AUTOLOAD Set to disable plugin auto-loading diff --git a/doc/en/requirements.txt b/doc/en/requirements.txt index 974988c8cf4..0637c967b8a 100644 --- a/doc/en/requirements.txt +++ b/doc/en/requirements.txt @@ -1,4 +1,3 @@ -pallets-sphinx-themes pluggy>=1.5.0 pygments-pytest>=2.3.0 sphinx-removed-in>=0.2.0 @@ -8,4 +7,7 @@ sphinxcontrib-svg2pdfconverter # Pin packaging because it no longer handles 'latest' version, which # is the version that is assigned to the docs. # See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045. -packaging <22 +packaging +furo +sphinxcontrib-towncrier +sphinx-issues diff --git a/extra/get_issues.py b/extra/get_issues.py index 716233ccba1..851d2f6d7f3 100644 --- a/extra/get_issues.py +++ b/extra/get_issues.py @@ -1,5 +1,8 @@ +from __future__ import annotations + import json from pathlib import Path +import sys import requests @@ -17,7 +20,7 @@ def get_issues(): if r.status_code == 403: # API request limit exceeded print(data["message"]) - exit(1) + sys.exit(1) issues.extend(data) # Look for next page @@ -60,7 +63,7 @@ def report(issues): kind = _get_kind(issue) status = issue["state"] number = issue["number"] - link = "https://github.com/pytest-dev/pytest/issues/%s/" % number + link = f"https://github.com/pytest-dev/pytest/issues/{number}/" print("----") print(status, kind, link) print(title) @@ -69,7 +72,7 @@ def report(issues): # print("\n".join(lines[:3])) # if len(lines) > 3 or len(body) > 240: # print("...") - print("\n\nFound %s open issues" % len(issues)) + print(f"\n\nFound {len(issues)} open issues") if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index 43efacf09f8..f3eba4a08a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,10 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "setuptools>=61", + "setuptools-scm[toml]>=6.2.3", +] + [project] name = "pytest" description = "pytest: simple powerful testing with Python" @@ -6,15 +13,15 @@ keywords = [ "test", "unittest", ] -license = {text = "MIT"} +license = { text = "MIT" } authors = [ - {name = "Holger Krekel"}, - {name = "Bruno Oliveira"}, - {name = "Ronny Pfannschmidt"}, - {name = "Floris Bruynooghe"}, - {name = "Brianna Laugher"}, - {name = "Florian Bruhin"}, - {name = "Others (See AUTHORS)"}, + { name = "Holger Krekel" }, + { name = "Bruno Oliveira" }, + { name = "Ronny Pfannschmidt" }, + { name = "Floris Bruynooghe" }, + { name = "Brianna Laugher" }, + { name = "Florian Bruhin" }, + { name = "Others (See AUTHORS)" }, ] requires-python = ">=3.8" classifiers = [ @@ -39,15 +46,14 @@ dynamic = [ "version", ] dependencies = [ - 'colorama; sys_platform == "win32"', - 'exceptiongroup>=1.0.0rc8; python_version < "3.11"', + "colorama; sys_platform=='win32'", + "exceptiongroup>=1.0.0rc8; python_version<'3.11'", "iniconfig", "packaging", - "pluggy<2.0,>=1.5", - 'tomli>=1; python_version < "3.11"', + "pluggy<2,>=1.5", + "tomli>=1; python_version<'3.11'", ] -[project.optional-dependencies] -dev = [ +optional-dependencies.dev = [ "argcomplete", "attrs>=19.2", "hypothesis>=3.56", @@ -57,58 +63,57 @@ dev = [ "setuptools", "xmlschema", ] -[project.urls] -Changelog = "https://docs.pytest.org/en/stable/changelog.html" -Homepage = "https://docs.pytest.org/en/latest/" -Source = "https://github.com/pytest-dev/pytest" -Tracker = "https://github.com/pytest-dev/pytest/issues" -Twitter = "https://twitter.com/pytestdotorg" -[project.scripts] -"py.test" = "pytest:console_main" -pytest = "pytest:console_main" - -[build-system] -build-backend = "setuptools.build_meta" -requires = [ - "setuptools>=61", - "setuptools-scm[toml]>=6.2.3", -] +urls.Changelog = "https://docs.pytest.org/en/stable/changelog.html" +urls.Homepage = "https://docs.pytest.org/en/latest/" +urls.Source = "https://github.com/pytest-dev/pytest" +urls.Tracker = "https://github.com/pytest-dev/pytest/issues" +urls.Twitter = "https://twitter.com/pytestdotorg" +scripts."py.test" = "pytest:console_main" +scripts.pytest = "pytest:console_main" [tool.setuptools.package-data] -"_pytest" = ["py.typed"] -"pytest" = ["py.typed"] +"_pytest" = [ + "py.typed", +] +"pytest" = [ + "py.typed", +] [tool.setuptools_scm] write_to = "src/_pytest/_version.py" [tool.black] -target-version = ['py38'] +target-version = [ + 'py38', +] [tool.ruff] -src = ["src"] line-length = 88 - -[tool.ruff.format] -docstring-code-format = true - -[tool.ruff.lint] -select = [ - "B", # bugbear - "D", # pydocstyle - "E", # pycodestyle - "F", # pyflakes - "I", # isort - "PYI", # flake8-pyi - "UP", # pyupgrade - "RUF", # ruff - "W", # pycodestyle - "PIE", # flake8-pie - "PGH004", # pygrep-hooks - Use specific rule codes when using noqa - "PLE", # pylint error - "PLW", # pylint warning +src = [ + "src", +] +format.docstring-code-format = true +lint.select = [ + "B", # bugbear + "D", # pydocstyle + "E", # pycodestyle + "F", # pyflakes + "FA100", # add future annotations + "I", # isort + "PGH004", # pygrep-hooks - Use specific rule codes when using noqa + "PIE", # flake8-pie + "PLC", # pylint convention + "PLE", # pylint error + "PLR", # pylint refactor "PLR1714", # Consider merging multiple comparisons + "PLW", # pylint warning + "PYI", # flake8-pyi + "RUF", # ruff + "T100", # flake8-debugger + "UP", # pyupgrade + "W", # pycodestyle ] -ignore = [ +lint.ignore = [ # bugbear ignore "B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable. "B007", # Loop control variable `i` not used within loop body @@ -116,10 +121,6 @@ ignore = [ "B010", # [*] Do not call `setattr` with a constant attribute value. "B011", # Do not `assert False` (`python -O` removes these calls) "B028", # No explicit `stacklevel` keyword argument found - # pycodestyle ignore - # pytest can do weird low-level things, and we usually know - # what we're doing when we use type(..) is ... - "E721", # Do not compare types, use `isinstance()` # pydocstyle ignore "D100", # Missing docstring in public module "D101", # Missing docstring in public class @@ -129,46 +130,70 @@ ignore = [ "D105", # Missing docstring in magic method "D106", # Missing docstring in public nested class "D107", # Missing docstring in `__init__` - "D209", # [*] Multi-line docstring closing quotes should be on a separate line "D205", # 1 blank line required between summary line and description + "D209", # [*] Multi-line docstring closing quotes should be on a separate line "D400", # First line should end with a period "D401", # First line of docstring should be in imperative mood "D402", # First line should not be the function's signature "D404", # First word of the docstring should not be "This" "D415", # First line should end with a period, question mark, or exclamation point + # pytest can do weird low-level things, and we usually know + # what we're doing when we use type(..) is ... + "E721", # Do not compare types, use `isinstance()` + # pylint ignore + "PLC0105", # `TypeVar` name "E" does not reflect its covariance; + "PLC0414", # Import alias does not rename original package + "PLR0124", # Name compared with itself + "PLR0133", # Two constants compared in a comparison (lots of those in tests) + "PLR0402", # Use `from x.y import z` in lieu of alias + "PLR0911", # Too many return statements + "PLR0912", # Too many branches + "PLR0913", # Too many arguments in function definition + "PLR0915", # Too many statements + "PLR2004", # Magic value used in comparison + "PLR2044", # Line with empty comment + "PLR5501", # Use `elif` instead of `else` then `if` + "PLW0120", # remove the else and dedent its contents + "PLW0603", # Using the global statement + "PLW2901", # for loop variable overwritten by assignment target # ruff ignore "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` - # pylint ignore - "PLW0603", # Using the global statement - "PLW0120", # remove the else and dedent its contents - "PLW2901", # for loop variable overwritten by assignment target - "PLR5501", # Use `elif` instead of `else` then `if` ] - -[tool.ruff.lint.pycodestyle] +lint.per-file-ignores."src/_pytest/_py/**/*.py" = [ + "B", + "PYI", +] +lint.per-file-ignores."src/_pytest/_version.py" = [ + "I001", +] +lint.per-file-ignores."testing/python/approx.py" = [ + "B015", +] +lint.extend-safe-fixes = [ + "UP006", + "UP007", +] +lint.isort.combine-as-imports = true +lint.isort.force-single-line = true +lint.isort.force-sort-within-sections = true +lint.isort.known-local-folder = [ + "pytest", + "_pytest", +] +lint.isort.lines-after-imports = 2 +lint.isort.order-by-type = false +lint.isort.required-imports = [ + "from __future__ import annotations", +] # In order to be able to format for 88 char in ruff format -max-line-length = 120 - -[tool.ruff.lint.pydocstyle] -convention = "pep257" - -[tool.ruff.lint.isort] -force-single-line = true -combine-as-imports = true -force-sort-within-sections = true -order-by-type = false -known-local-folder = ["pytest", "_pytest"] -lines-after-imports = 2 - -[tool.ruff.lint.per-file-ignores] -"src/_pytest/_py/**/*.py" = ["B", "PYI"] -"src/_pytest/_version.py" = ["I001"] -"testing/python/approx.py" = ["B015"] +lint.pycodestyle.max-line-length = 120 +lint.pydocstyle.convention = "pep257" +lint.pyupgrade.keep-runtime-typing = false [tool.pylint.main] # Maximum number of characters on a single line. max-line-length = 120 -disable= [ +disable = [ "abstract-method", "arguments-differ", "arguments-renamed", @@ -178,28 +203,28 @@ disable= [ "bad-mcs-method-argument", "broad-exception-caught", "broad-exception-raised", - "cell-var-from-loop", - "comparison-of-constants", + "cell-var-from-loop", # B023 from ruff / flake8-bugbear + "comparison-of-constants", # disabled in ruff (PLR0133) "comparison-with-callable", - "comparison-with-itself", + "comparison-with-itself", # PLR0124 from ruff "condition-evals-to-constant", "consider-using-dict-items", - "consider-using-enumerate", "consider-using-from-import", "consider-using-f-string", "consider-using-in", - "consider-using-sys-exit", "consider-using-ternary", "consider-using-with", + "consider-using-from-import", # not activated by default, PLR0402 disabled in ruff "cyclic-import", - "disallowed-name", + "disallowed-name", # foo / bar are used often in tests "duplicate-code", + "else-if-used", # not activated by default, PLR5501 disabled in ruff + "empty-comment", # not activated by default, PLR2044 disabled in ruff "eval-used", "exec-used", "expression-not-assigned", "fixme", - "global-statement", - "implicit-str-concat", + "global-statement", # PLW0603 disabled in ruff "import-error", "import-outside-toplevel", "inconsistent-return-statements", @@ -209,11 +234,12 @@ disable= [ "invalid-str-returned", "keyword-arg-before-vararg", "line-too-long", + "magic-value-comparison", # not activated by default, PLR2004 disabled in ruff "method-hidden", - "misplaced-bare-raise", "missing-docstring", "missing-timeout", - "multiple-statements", + "misplaced-bare-raise", # PLE0704 from ruff + "multiple-statements", # multiple-statements-on-one-line-colon (E701) from ruff "no-else-break", "no-else-continue", "no-else-raise", @@ -223,13 +249,15 @@ disable= [ "no-self-argument", "not-an-iterable", "not-callable", - "pointless-exception-statement", - "pointless-statement", - "pointless-string-statement", + "pointless-exception-statement", # https://github.com/pytest-dev/pytest/pull/12379 + "pointless-statement", # https://github.com/pytest-dev/pytest/pull/12379 + "pointless-string-statement", # https://github.com/pytest-dev/pytest/pull/12379 + "possibly-used-before-assignment", "protected-access", "raise-missing-from", "redefined-argument-from-local", "redefined-builtin", + "redefined-loop-name", # PLW2901 disabled in ruff "redefined-outer-name", "reimported", "simplifiable-condition", @@ -239,18 +267,18 @@ disable= [ "super-init-not-called", "too-few-public-methods", "too-many-ancestors", - "too-many-arguments", - "too-many-branches", + "too-many-arguments", # disabled in ruff + "too-many-branches", # disabled in ruff "too-many-function-args", "too-many-instance-attributes", "too-many-lines", "too-many-locals", "too-many-nested-blocks", "too-many-public-methods", - "too-many-return-statements", - "too-many-statements", + "too-many-return-statements", # disabled in ruff + "too-many-statements", # disabled in ruff "try-except-raise", - "typevar-name-incorrect-variance", + "typevar-name-incorrect-variance", # PLC0105 disabled in ruff "unbalanced-tuple-unpacking", "undefined-loop-variable", "undefined-variable", @@ -270,12 +298,12 @@ disable= [ "use-dict-literal", "use-implicit-booleaness-not-comparison", "use-implicit-booleaness-not-len", - "useless-else-on-loop", + "useless-else-on-loop", # PLC0414 disabled in ruff "useless-import-alias", "useless-return", - "use-maxsplit-arg", "using-constant-test", - "wrong-import-order", + "wrong-import-order", # handled by isort / ruff + "wrong-import-position", # handled by isort / ruff ] [tool.check-wheel-contents] @@ -289,16 +317,27 @@ indent = 4 [tool.pytest.ini_options] minversion = "2.0" addopts = "-rfEX -p pytester --strict-markers" -python_files = ["test_*.py", "*_test.py", "testing/python/*.py"] -python_classes = ["Test", "Acceptance"] -python_functions = ["test"] +python_files = [ + "test_*.py", + "*_test.py", + "testing/python/*.py", +] +python_classes = [ + "Test", + "Acceptance", +] +python_functions = [ + "test", +] # NOTE: "doc" is not included here, but gets tested explicitly via "doctesting". -testpaths = ["testing"] +testpaths = [ + "testing", +] norecursedirs = [ - "testing/example_scripts", - ".*", - "build", - "dist", + "testing/example_scripts", + ".*", + "build", + "dist", ] xfail_strict = true filterwarnings = [ @@ -339,6 +378,9 @@ markers = [ "foo", "bar", "baz", + "number_mark", + "builtin_matchers_mark", + "str_mark", # conftest.py reorders tests moving slow ones to the end of the list "slow", # experimental mark for all tests using pexpect @@ -353,49 +395,85 @@ directory = "changelog/" title_format = "pytest {version} ({project_date})" template = "changelog/_template.rst" - [[tool.towncrier.type]] - directory = "breaking" - name = "Breaking Changes" - showcontent = true +# NOTE: The types are declared because: +# NOTE: - there is no mechanism to override just the value of +# NOTE: `tool.towncrier.type.misc.showcontent`; +# NOTE: - and, we want to declare extra non-default types for +# NOTE: clarity and flexibility. + +[[tool.towncrier.type]] +# When something public gets removed in a breaking way. Could be +# deprecated in an earlier release. +directory = "breaking" +name = "Removals and backward incompatible breaking changes" +showcontent = true + +[[tool.towncrier.type]] +# Declarations of future API removals and breaking changes in behavior. +directory = "deprecation" +name = "Deprecations (removal in next major release)" +showcontent = true - [[tool.towncrier.type]] - directory = "deprecation" - name = "Deprecations" - showcontent = true +[[tool.towncrier.type]] +# New behaviors, public APIs. That sort of stuff. +directory = "feature" +name = "New features" +showcontent = true - [[tool.towncrier.type]] - directory = "feature" - name = "Features" - showcontent = true +[[tool.towncrier.type]] +# New behaviors in existing features. +directory = "improvement" +name = "Improvements in existing functionality" +showcontent = true - [[tool.towncrier.type]] - directory = "improvement" - name = "Improvements" - showcontent = true +[[tool.towncrier.type]] +# Something we deemed an improper undesired behavior that got corrected +# in the release to match pre-agreed expectations. +directory = "bugfix" +name = "Bug fixes" +showcontent = true - [[tool.towncrier.type]] - directory = "bugfix" - name = "Bug Fixes" - showcontent = true +[[tool.towncrier.type]] +# Updates regarding bundling dependencies. +directory = "vendor" +name = "Vendored libraries" +showcontent = true - [[tool.towncrier.type]] - directory = "vendor" - name = "Vendored Libraries" - showcontent = true +[[tool.towncrier.type]] +# Notable updates to the documentation structure or build process. +directory = "doc" +name = "Improved documentation" +showcontent = true - [[tool.towncrier.type]] - directory = "doc" - name = "Improved Documentation" - showcontent = true +[[tool.towncrier.type]] +# Notes for downstreams about unobvious side effects and tooling. Changes +# in the test invocation considerations and runtime assumptions. +directory = "packaging" +name = "Packaging updates and notes for downstreams" +showcontent = true - [[tool.towncrier.type]] - directory = "trivial" - name = "Trivial/Internal Changes" - showcontent = true +[[tool.towncrier.type]] +# Stuff that affects the contributor experience. e.g. Running tests, +# building the docs, setting up the development environment. +directory = "contrib" +name = "Contributor-facing changes" +showcontent = true + +[[tool.towncrier.type]] +# Changes that are hard to assign to any of the above categories. +directory = "misc" +name = "Miscellaneous internal changes" +showcontent = true [tool.mypy] -files = ["src", "testing", "scripts"] -mypy_path = ["src"] +files = [ + "src", + "testing", + "scripts", +] +mypy_path = [ + "src", +] check_untyped_defs = true disallow_any_generics = true disallow_untyped_defs = true diff --git a/scripts/generate-gh-release-notes.py b/scripts/generate-gh-release-notes.py index 4222702d5d4..7f195ba1e0a 100644 --- a/scripts/generate-gh-release-notes.py +++ b/scripts/generate-gh-release-notes.py @@ -9,6 +9,8 @@ Requires Python3.6+. """ +from __future__ import annotations + from pathlib import Path import re import sys diff --git a/scripts/prepare-release-pr.py b/scripts/prepare-release-pr.py index 7dabbd3b328..49cb2110639 100644 --- a/scripts/prepare-release-pr.py +++ b/scripts/prepare-release-pr.py @@ -14,6 +14,8 @@ `pytest bot ` commit author. """ +from __future__ import annotations + import argparse from pathlib import Path import re diff --git a/scripts/release.py b/scripts/release.py index bcbc4262d08..545919cd60b 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -1,6 +1,8 @@ # mypy: disallow-untyped-defs """Invoke development tasks.""" +from __future__ import annotations + import argparse import os from pathlib import Path diff --git a/scripts/towncrier-draft-to-file.py b/scripts/towncrier-draft-to-file.py deleted file mode 100644 index f771295a01f..00000000000 --- a/scripts/towncrier-draft-to-file.py +++ /dev/null @@ -1,18 +0,0 @@ -# mypy: disallow-untyped-defs -from subprocess import call -import sys - - -def main() -> int: - """ - 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", encoding="utf-8" - ) as draft_file: - return call(("towncrier", "--draft"), stdout=draft_file) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/update-plugin-list.py b/scripts/update-plugin-list.py index 6831fc984dd..75df0ddba40 100644 --- a/scripts/update-plugin-list.py +++ b/scripts/update-plugin-list.py @@ -1,4 +1,6 @@ # mypy: disallow-untyped-defs +from __future__ import annotations + import datetime import pathlib import re diff --git a/src/_pytest/__init__.py b/src/_pytest/__init__.py index b694a5f244a..8eb8ec9605c 100644 --- a/src/_pytest/__init__.py +++ b/src/_pytest/__init__.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + __all__ = ["__version__", "version_tuple"] try: diff --git a/src/_pytest/_argcomplete.py b/src/_pytest/_argcomplete.py index c24f925202a..59426ef949e 100644 --- a/src/_pytest/_argcomplete.py +++ b/src/_pytest/_argcomplete.py @@ -62,13 +62,13 @@ global argcomplete script). """ +from __future__ import annotations + import argparse from glob import glob import os import sys from typing import Any -from typing import List -from typing import Optional class FastFilesCompleter: @@ -77,7 +77,7 @@ class FastFilesCompleter: def __init__(self, directories: bool = True) -> None: self.directories = directories - def __call__(self, prefix: str, **kwargs: Any) -> List[str]: + def __call__(self, prefix: str, **kwargs: Any) -> list[str]: # Only called on non option completions. if os.sep in prefix[1:]: prefix_dir = len(os.path.dirname(prefix) + os.sep) @@ -104,7 +104,7 @@ def __call__(self, prefix: str, **kwargs: Any) -> List[str]: import argcomplete.completers except ImportError: sys.exit(-1) - filescompleter: Optional[FastFilesCompleter] = FastFilesCompleter() + filescompleter: FastFilesCompleter | None = FastFilesCompleter() def try_argcomplete(parser: argparse.ArgumentParser) -> None: argcomplete.autocomplete(parser, always_complete_options=False) diff --git a/src/_pytest/_code/__init__.py b/src/_pytest/_code/__init__.py index b0a418e9555..0bfde42604d 100644 --- a/src/_pytest/_code/__init__.py +++ b/src/_pytest/_code/__init__.py @@ -1,5 +1,7 @@ """Python inspection/code generation API.""" +from __future__ import annotations + from .code import Code from .code import ExceptionInfo from .code import filter_traceback diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index c65ce79f7e5..e7452825756 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import ast import dataclasses import inspect @@ -17,7 +19,6 @@ 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 @@ -25,11 +26,9 @@ 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 @@ -55,7 +54,9 @@ if sys.version_info < (3, 11): from exceptiongroup import BaseExceptionGroup -_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] +TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] + +EXCEPTION_OR_MORE = Union[Type[Exception], Tuple[Type[Exception], ...]] class Code: @@ -67,7 +68,7 @@ def __init__(self, obj: CodeType) -> None: self.raw = obj @classmethod - def from_function(cls, obj: object) -> "Code": + def from_function(cls, obj: object) -> Code: return cls(getrawcode(obj)) def __eq__(self, other): @@ -85,7 +86,7 @@ def name(self) -> str: return self.raw.co_name @property - def path(self) -> Union[Path, str]: + def path(self) -> Path | str: """Return a path object pointing to source code, or an ``str`` in case of ``OSError`` / non-existing file.""" if not self.raw.co_filename: @@ -102,17 +103,17 @@ def path(self) -> Union[Path, str]: return self.raw.co_filename @property - def fullsource(self) -> Optional["Source"]: + def fullsource(self) -> Source | None: """Return a _pytest._code.Source object for the full source file of the code.""" full, _ = findsource(self.raw) return full - def source(self) -> "Source": + def source(self) -> Source: """Return a _pytest._code.Source object for the code object's source only.""" # return source only for that part of code return Source(self.raw) - def getargs(self, var: bool = False) -> Tuple[str, ...]: + def getargs(self, var: bool = False) -> tuple[str, ...]: """Return a tuple with the argument names for the code object. If 'var' is set True also return the names of the variable and @@ -141,11 +142,11 @@ def lineno(self) -> int: return self.raw.f_lineno - 1 @property - def f_globals(self) -> Dict[str, Any]: + def f_globals(self) -> dict[str, Any]: return self.raw.f_globals @property - def f_locals(self) -> Dict[str, Any]: + def f_locals(self) -> dict[str, Any]: return self.raw.f_locals @property @@ -153,7 +154,7 @@ def code(self) -> Code: return Code(self.raw.f_code) @property - def statement(self) -> "Source": + def statement(self) -> Source: """Statement this frame is at.""" if self.code.fullsource is None: return Source("") @@ -197,14 +198,14 @@ class TracebackEntry: def __init__( self, rawentry: TracebackType, - repr_style: Optional['Literal["short", "long"]'] = None, + repr_style: Literal["short", "long"] | None = None, ) -> None: - self._rawentry: "Final" = rawentry - self._repr_style: "Final" = repr_style + self._rawentry: Final = rawentry + self._repr_style: Final = repr_style def with_repr_style( - self, repr_style: Optional['Literal["short", "long"]'] - ) -> "TracebackEntry": + self, repr_style: Literal["short", "long"] | None + ) -> TracebackEntry: return TracebackEntry(self._rawentry, repr_style) @property @@ -223,19 +224,19 @@ def __repr__(self) -> str: return "" % (self.frame.code.path, self.lineno + 1) @property - def statement(self) -> "Source": + def statement(self) -> Source: """_pytest._code.Source object for the current statement.""" source = self.frame.code.fullsource assert source is not None return source.getstatement(self.lineno) @property - def path(self) -> Union[Path, str]: + def path(self) -> Path | str: """Path to the source code.""" return self.frame.code.path @property - def locals(self) -> Dict[str, Any]: + def locals(self) -> dict[str, Any]: """Locals of underlying frame.""" return self.frame.f_locals @@ -243,8 +244,8 @@ def getfirstlinesource(self) -> int: return self.frame.code.firstlineno def getsource( - self, astcache: Optional[Dict[Union[str, Path], ast.AST]] = None - ) -> Optional["Source"]: + self, astcache: dict[str | Path, ast.AST] | None = None + ) -> Source | None: """Return failing source code.""" # we use the passed in astcache to not reparse asttrees # within exception info printing @@ -270,7 +271,7 @@ def getsource( source = property(getsource) - def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool: + def ishidden(self, excinfo: ExceptionInfo[BaseException] | None) -> bool: """Return True if the current frame has a var __tracebackhide__ resolving to True. @@ -279,9 +280,7 @@ def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool: Mostly for internal use. """ - tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = ( - False - ) + tbh: bool | Callable[[ExceptionInfo[BaseException] | None], bool] = False for maybe_ns_dct in (self.frame.f_locals, self.frame.f_globals): # in normal cases, f_locals and f_globals are dictionaries # however via `exec(...)` / `eval(...)` they can be other types @@ -326,13 +325,13 @@ class Traceback(List[TracebackEntry]): def __init__( self, - tb: Union[TracebackType, Iterable[TracebackEntry]], + tb: TracebackType | Iterable[TracebackEntry], ) -> None: """Initialize from given python traceback object and ExceptionInfo.""" if isinstance(tb, TracebackType): def f(cur: TracebackType) -> Iterable[TracebackEntry]: - cur_: Optional[TracebackType] = cur + cur_: TracebackType | None = cur while cur_ is not None: yield TracebackEntry(cur_) cur_ = cur_.tb_next @@ -343,11 +342,11 @@ def f(cur: TracebackType) -> Iterable[TracebackEntry]: def cut( self, - path: Optional[Union["os.PathLike[str]", str]] = None, - lineno: Optional[int] = None, - firstlineno: Optional[int] = None, - excludepath: Optional["os.PathLike[str]"] = None, - ) -> "Traceback": + path: os.PathLike[str] | str | None = None, + lineno: int | None = None, + firstlineno: int | None = None, + excludepath: os.PathLike[str] | None = None, + ) -> Traceback: """Return a Traceback instance wrapping part of this Traceback. By providing any combination of path, lineno and firstlineno, the @@ -378,14 +377,12 @@ def cut( return self @overload - def __getitem__(self, key: "SupportsIndex") -> TracebackEntry: ... + def __getitem__(self, key: SupportsIndex) -> TracebackEntry: ... @overload - def __getitem__(self, key: slice) -> "Traceback": ... + def __getitem__(self, key: slice) -> Traceback: ... - def __getitem__( - self, key: Union["SupportsIndex", slice] - ) -> Union[TracebackEntry, "Traceback"]: + def __getitem__(self, key: SupportsIndex | slice) -> TracebackEntry | Traceback: if isinstance(key, slice): return self.__class__(super().__getitem__(key)) else: @@ -393,12 +390,9 @@ def __getitem__( def filter( self, - excinfo_or_fn: Union[ - "ExceptionInfo[BaseException]", - Callable[[TracebackEntry], bool], - ], + excinfo_or_fn: ExceptionInfo[BaseException] | Callable[[TracebackEntry], bool], /, - ) -> "Traceback": + ) -> Traceback: """Return a Traceback instance with certain items removed. If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s @@ -414,25 +408,24 @@ def filter( fn = excinfo_or_fn return Traceback(filter(fn, self)) - def recursionindex(self) -> Optional[int]: + def recursionindex(self) -> int | None: """Return the index of the frame/TracebackEntry where recursion originates if appropriate, None if no recursion occurred.""" - cache: Dict[Tuple[Any, int, int], List[Dict[str, Any]]] = {} + cache: dict[tuple[Any, int, int], list[dict[str, Any]]] = {} for i, entry in enumerate(self): # id for the code.raw is needed to work around # the strange metaprogramming in the decorator lib from pypi # which generates code objects that have hash/value equality # XXX needs a test key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno - # print "checking for recursion at", key values = cache.setdefault(key, []) + # Since Python 3.13 f_locals is a proxy, freeze it. + loc = dict(entry.frame.f_locals) if values: - f = entry.frame - loc = f.f_locals for otherloc in values: if otherloc == loc: return i - values.append(entry.frame.f_locals) + values.append(loc) return None @@ -446,15 +439,15 @@ class ExceptionInfo(Generic[E]): _assert_start_repr: ClassVar = "AssertionError('assert " - _excinfo: Optional[Tuple[Type["E"], "E", TracebackType]] + _excinfo: tuple[type[E], E, TracebackType] | None _striptext: str - _traceback: Optional[Traceback] + _traceback: Traceback | None def __init__( self, - excinfo: Optional[Tuple[Type["E"], "E", TracebackType]], + excinfo: tuple[type[E], E, TracebackType] | None, striptext: str = "", - traceback: Optional[Traceback] = None, + traceback: Traceback | None = None, *, _ispytest: bool = False, ) -> None: @@ -470,8 +463,8 @@ def from_exception( # This is OK to ignore because this class is (conceptually) readonly. # See https://github.com/python/mypy/issues/7049. exception: E, # type: ignore[misc] - exprinfo: Optional[str] = None, - ) -> "ExceptionInfo[E]": + exprinfo: str | None = None, + ) -> ExceptionInfo[E]: """Return an ExceptionInfo for an existing exception. The exception must have a non-``None`` ``__traceback__`` attribute, @@ -496,9 +489,9 @@ def from_exception( @classmethod def from_exc_info( cls, - exc_info: Tuple[Type[E], E, TracebackType], - exprinfo: Optional[str] = None, - ) -> "ExceptionInfo[E]": + exc_info: tuple[type[E], E, TracebackType], + exprinfo: str | None = None, + ) -> ExceptionInfo[E]: """Like :func:`from_exception`, but using old-style exc_info tuple.""" _striptext = "" if exprinfo is None and isinstance(exc_info[1], AssertionError): @@ -511,9 +504,7 @@ def from_exc_info( return cls(exc_info, _striptext, _ispytest=True) @classmethod - def from_current( - cls, exprinfo: Optional[str] = None - ) -> "ExceptionInfo[BaseException]": + def from_current(cls, exprinfo: str | None = None) -> ExceptionInfo[BaseException]: """Return an ExceptionInfo matching the current traceback. .. warning:: @@ -533,17 +524,17 @@ def from_current( return ExceptionInfo.from_exc_info(exc_info, exprinfo) @classmethod - def for_later(cls) -> "ExceptionInfo[E]": + def for_later(cls) -> ExceptionInfo[E]: """Return an unfilled ExceptionInfo.""" return cls(None, _ispytest=True) - def fill_unfilled(self, exc_info: Tuple[Type[E], E, TracebackType]) -> None: + def fill_unfilled(self, exc_info: tuple[type[E], E, TracebackType]) -> None: """Fill an unfilled ExceptionInfo created with ``for_later()``.""" assert self._excinfo is None, "ExceptionInfo was already filled" self._excinfo = exc_info @property - def type(self) -> Type[E]: + def type(self) -> type[E]: """The exception class.""" assert ( self._excinfo is not None @@ -606,16 +597,14 @@ def exconly(self, tryshort: bool = False) -> str: text = text[len(self._striptext) :] return text - def errisinstance( - self, exc: Union[Type[BaseException], Tuple[Type[BaseException], ...]] - ) -> bool: + def errisinstance(self, exc: EXCEPTION_OR_MORE) -> bool: """Return True if the exception is an instance of exc. Consider using ``isinstance(excinfo.value, exc)`` instead. """ return isinstance(self.value, exc) - def _getreprcrash(self) -> Optional["ReprFileLocation"]: + def _getreprcrash(self) -> ReprFileLocation | None: # Find last non-hidden traceback entry that led to the exception of the # traceback, or None if all hidden. for i in range(-1, -len(self.traceback) - 1, -1): @@ -629,15 +618,15 @@ def _getreprcrash(self) -> Optional["ReprFileLocation"]: def getrepr( self, showlocals: bool = False, - style: _TracebackStyle = "long", + style: TracebackStyle = "long", abspath: bool = False, - tbfilter: Union[ - bool, Callable[["ExceptionInfo[BaseException]"], Traceback] - ] = True, + tbfilter: bool + | Callable[[ExceptionInfo[BaseException]], _pytest._code.code.Traceback] = True, funcargs: bool = False, truncate_locals: bool = True, + truncate_args: bool = True, chain: bool = True, - ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]: + ) -> ReprExceptionInfo | ExceptionChainRepr: """Return str()able representation of this exception info. :param bool showlocals: @@ -666,6 +655,9 @@ def getrepr( :param bool truncate_locals: With ``showlocals==True``, make sure locals can be safely represented as strings. + :param bool truncate_args: + With ``showargs==True``, make sure args can be safely represented as strings. + :param bool chain: If chained exceptions in Python 3 should be shown. @@ -692,6 +684,7 @@ def getrepr( tbfilter=tbfilter, funcargs=funcargs, truncate_locals=truncate_locals, + truncate_args=truncate_args, chain=chain, ) return fmt.repr_excinfo(self) @@ -715,7 +708,7 @@ def _stringify_exception(self, exc: BaseException) -> str: ] ) - def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]": + def match(self, regexp: str | Pattern[str]) -> Literal[True]: """Check whether the regular expression `regexp` matches the string representation of the exception using :func:`python:re.search`. @@ -733,9 +726,9 @@ def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]": def _group_contains( self, exc_group: BaseExceptionGroup[BaseException], - expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]], - match: Union[str, Pattern[str], None], - target_depth: Optional[int] = None, + expected_exception: EXCEPTION_OR_MORE, + match: str | Pattern[str] | None, + target_depth: int | None = None, current_depth: int = 1, ) -> bool: """Return `True` if a `BaseExceptionGroup` contains a matching exception.""" @@ -762,10 +755,10 @@ def _group_contains( def group_contains( self, - expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]], + expected_exception: EXCEPTION_OR_MORE, *, - match: Union[str, Pattern[str], None] = None, - depth: Optional[int] = None, + match: str | Pattern[str] | None = None, + depth: int | None = None, ) -> bool: """Check whether a captured exception group contains a matching exception. @@ -805,17 +798,18 @@ 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 + tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] = True funcargs: bool = False truncate_locals: bool = True + truncate_args: bool = True chain: bool = True - astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field( + astcache: dict[str | Path, ast.AST] = dataclasses.field( default_factory=dict, init=False, repr=False ) - def _getindent(self, source: "Source") -> int: + def _getindent(self, source: Source) -> int: # Figure out indent for the given source. try: s = str(source.getstatement(len(source) - 1)) @@ -830,27 +824,31 @@ def _getindent(self, source: "Source") -> int: return 0 return 4 + (len(s) - len(s.lstrip())) - def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]: + def _getentrysource(self, entry: TracebackEntry) -> Source | None: source = entry.getsource(self.astcache) if source is not None: source = source.deindent() return source - def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]: + def repr_args(self, entry: TracebackEntry) -> ReprFuncArgs | None: if self.funcargs: args = [] for argname, argvalue in entry.frame.getargs(var=True): - args.append((argname, saferepr(argvalue))) + if self.truncate_args: + str_repr = saferepr(argvalue) + else: + str_repr = saferepr(argvalue, maxsize=None) + args.append((argname, str_repr)) return ReprFuncArgs(args) return None def get_source( self, - source: Optional["Source"], + source: Source | None, line_index: int = -1, - excinfo: Optional[ExceptionInfo[BaseException]] = None, + excinfo: ExceptionInfo[BaseException] | None = None, short: bool = False, - ) -> List[str]: + ) -> list[str]: """Return formatted and marked up source lines.""" lines = [] if source is not None and line_index < 0: @@ -879,7 +877,7 @@ def get_exconly( excinfo: ExceptionInfo[BaseException], indent: int = 4, markall: bool = False, - ) -> List[str]: + ) -> list[str]: lines = [] indentstr = " " * indent # Get the real exception information out. @@ -891,7 +889,7 @@ def get_exconly( failindent = indentstr return lines - def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]: + def repr_locals(self, locals: Mapping[str, object]) -> ReprLocals | None: if self.showlocals: lines = [] keys = [loc for loc in locals if loc[0] != "@"] @@ -919,10 +917,10 @@ def repr_locals(self, locals: Mapping[str, object]) -> Optional["ReprLocals"]: def repr_traceback_entry( self, - entry: Optional[TracebackEntry], - excinfo: Optional[ExceptionInfo[BaseException]] = None, - ) -> "ReprEntry": - lines: List[str] = [] + entry: TracebackEntry | None, + excinfo: ExceptionInfo[BaseException] | None = None, + ) -> ReprEntry: + lines: list[str] = [] style = ( entry._repr_style if entry is not None and entry._repr_style is not None @@ -940,7 +938,7 @@ def repr_traceback_entry( s = self.get_source(source, line_index, excinfo, short=short) lines.extend(s) if short: - message = "in %s" % (entry.name) + message = f"in {entry.name}" else: message = excinfo and excinfo.typename or "" entry_path = entry.path @@ -957,7 +955,7 @@ def repr_traceback_entry( lines.extend(self.get_exconly(excinfo, indent=4)) return ReprEntry(lines, None, None, None, style) - def _makepath(self, path: Union[Path, str]) -> str: + def _makepath(self, path: Path | str) -> str: if not self.abspath and isinstance(path, Path): try: np = bestrelpath(Path.cwd(), path) @@ -967,7 +965,7 @@ def _makepath(self, path: Union[Path, str]) -> str: return np return str(path) - def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": + def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> ReprTraceback: traceback = excinfo.traceback if callable(self.tbfilter): traceback = self.tbfilter(excinfo) @@ -998,7 +996,7 @@ def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTracebac def _truncate_recursive_traceback( self, traceback: Traceback - ) -> Tuple[Traceback, Optional[str]]: + ) -> tuple[Traceback, str | None]: """Truncate the given recursive traceback trying to find the starting point of the recursion. @@ -1015,7 +1013,7 @@ def _truncate_recursive_traceback( recursionindex = traceback.recursionindex() except Exception as e: max_frames = 10 - extraline: Optional[str] = ( + extraline: str | None = ( "!!! Recursion error detected, but an error occurred locating the origin of recursion.\n" " The following exception happened when comparing locals in the stack frame:\n" f" {type(e).__name__}: {e!s}\n" @@ -1033,16 +1031,12 @@ def _truncate_recursive_traceback( return traceback, extraline - def repr_excinfo( - self, excinfo: ExceptionInfo[BaseException] - ) -> "ExceptionChainRepr": - repr_chain: List[ - Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]] - ] = [] - e: Optional[BaseException] = excinfo.value - excinfo_: Optional[ExceptionInfo[BaseException]] = excinfo + def repr_excinfo(self, excinfo: ExceptionInfo[BaseException]) -> ExceptionChainRepr: + repr_chain: list[tuple[ReprTraceback, ReprFileLocation | None, str | None]] = [] + e: BaseException | None = excinfo.value + excinfo_: ExceptionInfo[BaseException] | None = excinfo descr = None - seen: Set[int] = set() + seen: set[int] = set() while e is not None and id(e) not in seen: seen.add(id(e)) @@ -1051,7 +1045,7 @@ def repr_excinfo( # full support for exception groups added to ExceptionInfo. # See https://github.com/pytest-dev/pytest/issues/9159 if isinstance(e, BaseExceptionGroup): - reprtraceback: Union[ReprTracebackNative, ReprTraceback] = ( + reprtraceback: ReprTracebackNative | ReprTraceback = ( ReprTracebackNative( traceback.format_exception( type(excinfo_.value), @@ -1109,9 +1103,9 @@ def toterminal(self, tw: TerminalWriter) -> None: @dataclasses.dataclass(eq=False) class ExceptionRepr(TerminalRepr): # Provided by subclasses. - reprtraceback: "ReprTraceback" - reprcrash: Optional["ReprFileLocation"] - sections: List[Tuple[str, str, str]] = dataclasses.field( + reprtraceback: ReprTraceback + reprcrash: ReprFileLocation | None + sections: list[tuple[str, str, str]] = dataclasses.field( init=False, default_factory=list ) @@ -1126,13 +1120,11 @@ def toterminal(self, tw: TerminalWriter) -> None: @dataclasses.dataclass(eq=False) class ExceptionChainRepr(ExceptionRepr): - chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]] + chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]] def __init__( self, - chain: Sequence[ - Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]] - ], + chain: Sequence[tuple[ReprTraceback, ReprFileLocation | None, str | None]], ) -> None: # reprcrash and reprtraceback of the outermost (the newest) exception # in the chain. @@ -1153,8 +1145,8 @@ def toterminal(self, tw: TerminalWriter) -> None: @dataclasses.dataclass(eq=False) class ReprExceptionInfo(ExceptionRepr): - reprtraceback: "ReprTraceback" - reprcrash: Optional["ReprFileLocation"] + reprtraceback: ReprTraceback + reprcrash: ReprFileLocation | None def toterminal(self, tw: TerminalWriter) -> None: self.reprtraceback.toterminal(tw) @@ -1163,9 +1155,9 @@ def toterminal(self, tw: TerminalWriter) -> None: @dataclasses.dataclass(eq=False) class ReprTraceback(TerminalRepr): - reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] - extraline: Optional[str] - style: _TracebackStyle + reprentries: Sequence[ReprEntry | ReprEntryNative] + extraline: str | None + style: TracebackStyle entrysep: ClassVar = "_ " @@ -1199,7 +1191,7 @@ def __init__(self, tblines: Sequence[str]) -> None: 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)) @@ -1208,10 +1200,10 @@ def toterminal(self, tw: TerminalWriter) -> None: @dataclasses.dataclass(eq=False) class ReprEntry(TerminalRepr): lines: Sequence[str] - reprfuncargs: Optional["ReprFuncArgs"] - reprlocals: Optional["ReprLocals"] - reprfileloc: Optional["ReprFileLocation"] - style: _TracebackStyle + reprfuncargs: ReprFuncArgs | None + reprlocals: ReprLocals | None + reprfileloc: ReprFileLocation | None + style: TracebackStyle def _write_entry_lines(self, tw: TerminalWriter) -> None: """Write the source code portions of a list of traceback entries with syntax highlighting. @@ -1234,9 +1226,9 @@ def _write_entry_lines(self, tw: TerminalWriter) -> None: # such as "> assert 0" fail_marker = f"{FormattedExcinfo.fail_marker} " indent_size = len(fail_marker) - indents: List[str] = [] - source_lines: List[str] = [] - failure_lines: List[str] = [] + indents: list[str] = [] + source_lines: list[str] = [] + failure_lines: list[str] = [] for index, line in enumerate(self.lines): is_failure_line = line.startswith(fail_marker) if is_failure_line: @@ -1315,7 +1307,7 @@ def toterminal(self, tw: TerminalWriter, indent="") -> None: @dataclasses.dataclass(eq=False) class ReprFuncArgs(TerminalRepr): - args: Sequence[Tuple[str, object]] + args: Sequence[tuple[str, object]] def toterminal(self, tw: TerminalWriter) -> None: if self.args: @@ -1336,7 +1328,7 @@ def toterminal(self, tw: TerminalWriter) -> None: tw.line("") -def getfslineno(obj: object) -> Tuple[Union[str, Path], int]: +def getfslineno(obj: object) -> tuple[str | Path, int]: """Return source location (path, lineno) for the given object. If the source cannot be determined return ("", -1). diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 7fa577e03b3..604aff8ba19 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import ast from bisect import bisect_right import inspect @@ -7,11 +9,7 @@ import types from typing import Iterable from typing import Iterator -from typing import List -from typing import Optional from typing import overload -from typing import Tuple -from typing import Union import warnings @@ -23,7 +21,7 @@ class Source: def __init__(self, obj: object = None) -> None: if not obj: - self.lines: List[str] = [] + self.lines: list[str] = [] elif isinstance(obj, Source): self.lines = obj.lines elif isinstance(obj, (tuple, list)): @@ -50,9 +48,9 @@ def __eq__(self, other: object) -> bool: def __getitem__(self, key: int) -> str: ... @overload - def __getitem__(self, key: slice) -> "Source": ... + def __getitem__(self, key: slice) -> Source: ... - def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: + def __getitem__(self, key: int | slice) -> str | Source: if isinstance(key, int): return self.lines[key] else: @@ -68,7 +66,7 @@ def __iter__(self) -> Iterator[str]: def __len__(self) -> int: return len(self.lines) - def strip(self) -> "Source": + def strip(self) -> Source: """Return new Source object with trailing and leading blank lines removed.""" start, end = 0, len(self) while start < end and not self.lines[start].strip(): @@ -79,20 +77,20 @@ def strip(self) -> "Source": source.lines[:] = self.lines[start:end] return source - def indent(self, indent: str = " " * 4) -> "Source": + def indent(self, indent: str = " " * 4) -> Source: """Return a copy of the source object with all lines indented by the given indent-string.""" newsource = Source() newsource.lines = [(indent + line) for line in self.lines] return newsource - def getstatement(self, lineno: int) -> "Source": + def getstatement(self, lineno: int) -> Source: """Return Source statement which contains the given linenumber (counted from 0).""" start, end = self.getstatementrange(lineno) return self[start:end] - def getstatementrange(self, lineno: int) -> Tuple[int, int]: + def getstatementrange(self, lineno: int) -> tuple[int, int]: """Return (start, end) tuple which spans the minimal statement region which containing the given lineno.""" if not (0 <= lineno < len(self)): @@ -100,7 +98,7 @@ def getstatementrange(self, lineno: int) -> Tuple[int, int]: ast, start, end = getstatementrange_ast(lineno, self) return start, end - def deindent(self) -> "Source": + def deindent(self) -> Source: """Return a new Source object deindented.""" newsource = Source() newsource.lines[:] = deindent(self.lines) @@ -115,7 +113,7 @@ def __str__(self) -> str: # -def findsource(obj) -> Tuple[Optional[Source], int]: +def findsource(obj) -> tuple[Source | None, int]: try: sourcelines, lineno = inspect.findsource(obj) except Exception: @@ -138,14 +136,14 @@ def getrawcode(obj: object, trycall: bool = True) -> types.CodeType: raise TypeError(f"could not get code object for {obj!r}") -def deindent(lines: Iterable[str]) -> List[str]: +def deindent(lines: Iterable[str]) -> list[str]: return textwrap.dedent("\n".join(lines)).splitlines() -def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]: +def get_statement_startend2(lineno: int, node: ast.AST) -> tuple[int, int | None]: # Flatten all statements and except handlers into one lineno-list. # AST's line numbers start indexing at 1. - values: List[int] = [] + values: list[int] = [] for x in ast.walk(node): if isinstance(x, (ast.stmt, ast.ExceptHandler)): # The lineno points to the class/def, so need to include the decorators. @@ -154,7 +152,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i values.append(d.lineno - 1) values.append(x.lineno - 1) for name in ("finalbody", "orelse"): - val: Optional[List[ast.stmt]] = getattr(x, name, None) + val: list[ast.stmt] | None = getattr(x, name, None) if val: # Treat the finally/orelse part as its own statement. values.append(val[0].lineno - 1 - 1) @@ -172,8 +170,8 @@ def getstatementrange_ast( lineno: int, source: Source, assertion: bool = False, - astnode: Optional[ast.AST] = None, -) -> Tuple[ast.AST, int, int]: + astnode: ast.AST | None = None, +) -> tuple[ast.AST, int, int]: if astnode is None: content = str(source) # See #4260: diff --git a/src/_pytest/_io/__init__.py b/src/_pytest/_io/__init__.py index db001e918cb..b0155b18b60 100644 --- a/src/_pytest/_io/__init__.py +++ b/src/_pytest/_io/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from .terminalwriter import get_terminal_width from .terminalwriter import TerminalWriter diff --git a/src/_pytest/_io/pprint.py b/src/_pytest/_io/pprint.py index 75e9a7123b5..7213be7ba9b 100644 --- a/src/_pytest/_io/pprint.py +++ b/src/_pytest/_io/pprint.py @@ -13,6 +13,8 @@ # tuples with fairly non-descriptive content. This is modeled very much # after Lisp/Scheme - style pretty-printing of lists. If you find it # useful, thank small children who sleep at night. +from __future__ import annotations + import collections as _collections import dataclasses as _dataclasses from io import StringIO as _StringIO @@ -20,13 +22,8 @@ import types as _types from typing import Any from typing import Callable -from typing import Dict from typing import IO from typing import Iterator -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple class _safe_key: @@ -64,7 +61,7 @@ def __init__( self, indent: int = 4, width: int = 80, - depth: Optional[int] = None, + depth: int | None = None, ) -> None: """Handle pretty printing operations onto a stream using a set of configured parameters. @@ -100,7 +97,7 @@ def _format( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: objid = id(object) @@ -136,7 +133,7 @@ def _pprint_dataclass( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: cls_name = object.__class__.__name__ @@ -149,9 +146,9 @@ def _pprint_dataclass( self._format_namespace_items(items, stream, indent, allowance, context, level) stream.write(")") - _dispatch: Dict[ + _dispatch: dict[ Callable[..., str], - Callable[["PrettyPrinter", Any, IO[str], int, int, Set[int], int], None], + Callable[[PrettyPrinter, Any, IO[str], int, int, set[int], int], None], ] = {} def _pprint_dict( @@ -160,7 +157,7 @@ def _pprint_dict( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: write = stream.write @@ -177,7 +174,7 @@ def _pprint_ordered_dict( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: if not len(object): @@ -196,7 +193,7 @@ def _pprint_list( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: stream.write("[") @@ -211,7 +208,7 @@ def _pprint_tuple( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: stream.write("(") @@ -226,7 +223,7 @@ def _pprint_set( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: if not len(object): @@ -252,7 +249,7 @@ def _pprint_str( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: write = stream.write @@ -311,7 +308,7 @@ def _pprint_bytes( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: write = stream.write @@ -340,7 +337,7 @@ def _pprint_bytearray( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: write = stream.write @@ -358,7 +355,7 @@ def _pprint_mappingproxy( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: stream.write("mappingproxy(") @@ -373,7 +370,7 @@ def _pprint_simplenamespace( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: if type(object) is _types.SimpleNamespace: @@ -391,11 +388,11 @@ def _pprint_simplenamespace( def _format_dict_items( self, - items: List[Tuple[Any, Any]], + items: list[tuple[Any, Any]], stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: if not items: @@ -415,11 +412,11 @@ def _format_dict_items( def _format_namespace_items( self, - items: List[Tuple[Any, Any]], + items: list[tuple[Any, Any]], stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: if not items: @@ -452,11 +449,11 @@ def _format_namespace_items( def _format_items( self, - items: List[Any], + items: list[Any], stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: if not items: @@ -473,7 +470,7 @@ def _format_items( write("\n" + " " * indent) - def _repr(self, object: Any, context: Set[int], level: int) -> str: + def _repr(self, object: Any, context: set[int], level: int) -> str: return self._safe_repr(object, context.copy(), self._depth, level) def _pprint_default_dict( @@ -482,7 +479,7 @@ def _pprint_default_dict( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: rdf = self._repr(object.default_factory, context, level) @@ -498,7 +495,7 @@ def _pprint_counter( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: stream.write(object.__class__.__name__ + "(") @@ -519,7 +516,7 @@ def _pprint_chain_map( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: if not len(object.maps) or (len(object.maps) == 1 and not len(object.maps[0])): @@ -538,7 +535,7 @@ def _pprint_deque( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: stream.write(object.__class__.__name__ + "(") @@ -557,7 +554,7 @@ def _pprint_user_dict( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: self._format(object.data, stream, indent, allowance, context, level - 1) @@ -570,7 +567,7 @@ def _pprint_user_list( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: self._format(object.data, stream, indent, allowance, context, level - 1) @@ -583,7 +580,7 @@ def _pprint_user_string( stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: self._format(object.data, stream, indent, allowance, context, level - 1) @@ -591,7 +588,7 @@ def _pprint_user_string( _dispatch[_collections.UserString.__repr__] = _pprint_user_string def _safe_repr( - self, object: Any, context: Set[int], maxlevels: Optional[int], level: int + self, object: Any, context: set[int], maxlevels: int | None, level: int ) -> str: typ = type(object) if typ in _builtin_scalars: @@ -608,7 +605,7 @@ def _safe_repr( if objid in context: return _recursion(object) context.add(objid) - components: List[str] = [] + components: list[str] = [] append = components.append level += 1 for k, v in sorted(object.items(), key=_safe_tuple): @@ -616,7 +613,7 @@ def _safe_repr( vrepr = self._safe_repr(v, context, maxlevels, level) append(f"{krepr}: {vrepr}") context.remove(objid) - return "{%s}" % ", ".join(components) + return "{{{}}}".format(", ".join(components)) if (issubclass(typ, list) and r is list.__repr__) or ( issubclass(typ, tuple) and r is tuple.__repr__ diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 9f33fced676..cee70e332f9 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -1,6 +1,7 @@ +from __future__ import annotations + import pprint import reprlib -from typing import Optional def _try_repr_or_str(obj: object) -> str: @@ -17,8 +18,8 @@ def _format_repr_exception(exc: BaseException, obj: object) -> str: exc_info = _try_repr_or_str(exc) except (KeyboardInterrupt, SystemExit): raise - except BaseException as exc: - exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})" + except BaseException as inner_exc: + exc_info = f"unpresentable exception ({_try_repr_or_str(inner_exc)})" return ( f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>" ) @@ -38,7 +39,7 @@ class SafeRepr(reprlib.Repr): information on exceptions raised during the call. """ - def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None: + def __init__(self, maxsize: int | None, use_ascii: bool = False) -> None: """ :param maxsize: If not None, will truncate the resulting repr to that specific size, using ellipsis @@ -59,7 +60,6 @@ def repr(self, x: object) -> str: s = ascii(x) else: s = super().repr(x) - except (KeyboardInterrupt, SystemExit): raise except BaseException as exc: @@ -97,7 +97,7 @@ def safeformat(obj: object) -> str: def saferepr( - obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False + obj: object, maxsize: int | None = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False ) -> str: """Return a size-limited safe repr-string for the given object. diff --git a/src/_pytest/_io/terminalwriter.py b/src/_pytest/_io/terminalwriter.py index deb6ecc3c94..70ebd3d061b 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -1,18 +1,25 @@ """Helper functions for writing to terminals and files.""" +from __future__ import annotations + import os import shutil import sys from typing import final from typing import Literal -from typing import Optional from typing import Sequence from typing import TextIO +from typing import TYPE_CHECKING from ..compat import assert_never from .wcwidth import wcswidth +if TYPE_CHECKING: + from pygments.formatter import Formatter + from pygments.lexer import Lexer + + # This code was initially copied from py 1.8.1, file _io/terminalwriter.py. @@ -65,7 +72,7 @@ class TerminalWriter: invert=7, ) - def __init__(self, file: Optional[TextIO] = None) -> None: + def __init__(self, file: TextIO | None = None) -> None: if file is None: file = sys.stdout if hasattr(file, "isatty") and file.isatty() and sys.platform == "win32": @@ -79,7 +86,7 @@ def __init__(self, file: Optional[TextIO] = None) -> None: self._file = file self.hasmarkup = should_do_markup(file) self._current_line = "" - self._terminal_width: Optional[int] = None + self._terminal_width: int | None = None self.code_highlight = True @property @@ -104,14 +111,14 @@ def markup(self, text: str, **markup: bool) -> str: if self.hasmarkup: esc = [self._esctable[name] for name, on in markup.items() if on] if esc: - text = "".join("\x1b[%sm" % cod for cod in esc) + text + "\x1b[0m" + text = "".join(f"\x1b[{cod}m" for cod in esc) + text + "\x1b[0m" return text def sep( self, sepchar: str, - title: Optional[str] = None, - fullwidth: Optional[int] = None, + title: str | None = None, + fullwidth: int | None = None, **markup: bool, ) -> None: if fullwidth is None: @@ -194,58 +201,74 @@ def _write_source(self, lines: Sequence[str], indents: Sequence[str] = ()) -> No for indent, new_line in zip(indents, new_lines): self.line(indent + new_line) + def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer | None: + try: + if lexer == "python": + from pygments.lexers.python import PythonLexer + + return PythonLexer() + elif lexer == "diff": + from pygments.lexers.diff import DiffLexer + + return DiffLexer() + else: + assert_never(lexer) + except ModuleNotFoundError: + return None + + def _get_pygments_formatter(self) -> Formatter | None: + try: + import pygments.util + except ModuleNotFoundError: + return None + + from _pytest.config.exceptions import UsageError + + theme = os.getenv("PYTEST_THEME") + theme_mode = os.getenv("PYTEST_THEME_MODE", "dark") + + try: + from pygments.formatters.terminal import TerminalFormatter + + return TerminalFormatter(bg=theme_mode, style=theme) + + except pygments.util.ClassNotFound as e: + raise UsageError( + f"PYTEST_THEME environment variable has an invalid value: '{theme}'. " + "Hint: See available pygments styles with `pygmentize -L styles`." + ) from e + except pygments.util.OptionError as e: + raise UsageError( + f"PYTEST_THEME_MODE environment variable has an invalid value: '{theme_mode}'. " + "The allowed values are 'dark' (default) and 'light'." + ) from e + def _highlight( self, source: str, lexer: Literal["diff", "python"] = "python" ) -> str: """Highlight the given source if we have markup support.""" - from _pytest.config.exceptions import UsageError - if not source or not self.hasmarkup or not self.code_highlight: return source - try: - from pygments.formatters.terminal import TerminalFormatter + pygments_lexer = self._get_pygments_lexer(lexer) + if pygments_lexer is None: + return source - if lexer == "python": - from pygments.lexers.python import PythonLexer as Lexer - elif lexer == "diff": - from pygments.lexers.diff import DiffLexer as Lexer - else: - assert_never(lexer) - from pygments import highlight - import pygments.util - except ImportError: + pygments_formatter = self._get_pygments_formatter() + if pygments_formatter is None: return source - else: - try: - highlighted: str = highlight( - source, - Lexer(), - TerminalFormatter( - bg=os.getenv("PYTEST_THEME_MODE", "dark"), - style=os.getenv("PYTEST_THEME"), - ), - ) - # pygments terminal formatter may add a newline when there wasn't one. - # We don't want this, remove. - if highlighted[-1] == "\n" and source[-1] != "\n": - highlighted = highlighted[:-1] - - # Some lexers will not set the initial color explicitly - # which may lead to the previous color being propagated to the - # start of the expression, so reset first. - return "\x1b[0m" + highlighted - except pygments.util.ClassNotFound as e: - raise UsageError( - "PYTEST_THEME environment variable had an invalid value: '{}'. " - "Only valid pygment styles are allowed.".format( - os.getenv("PYTEST_THEME") - ) - ) from e - except pygments.util.OptionError as e: - raise UsageError( - "PYTEST_THEME_MODE environment variable had an invalid value: '{}'. " - "The only allowed values are 'dark' and 'light'.".format( - os.getenv("PYTEST_THEME_MODE") - ) - ) from e + + from pygments import highlight + + highlighted: str = highlight(source, pygments_lexer, pygments_formatter) + # pygments terminal formatter may add a newline when there wasn't one. + # We don't want this, remove. + if highlighted[-1] == "\n" and source[-1] != "\n": + highlighted = highlighted[:-1] + + # Some lexers will not set the initial color explicitly + # which may lead to the previous color being propagated to the + # start of the expression, so reset first. + highlighted = "\x1b[0m" + highlighted + + return highlighted diff --git a/src/_pytest/_io/wcwidth.py b/src/_pytest/_io/wcwidth.py index 53803133519..23886ff1581 100644 --- a/src/_pytest/_io/wcwidth.py +++ b/src/_pytest/_io/wcwidth.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from functools import lru_cache import unicodedata diff --git a/src/_pytest/_py/error.py b/src/_pytest/_py/error.py index 68f1eed7ec0..ab3a4ed318e 100644 --- a/src/_pytest/_py/error.py +++ b/src/_pytest/_py/error.py @@ -41,7 +41,7 @@ def __str__(self) -> str: 3: errno.ENOENT, 17: errno.EEXIST, 18: errno.EXDEV, - 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable + 13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable 22: errno.ENOTDIR, 20: errno.ENOTDIR, 267: errno.ENOTDIR, diff --git a/src/_pytest/_py/path.py b/src/_pytest/_py/path.py index 7bb3693f938..c7ab1182f4a 100644 --- a/src/_pytest/_py/path.py +++ b/src/_pytest/_py/path.py @@ -161,15 +161,13 @@ def gen(self, path): ) if not self.breadthfirst: for subdir in dirs: - for p in self.gen(subdir): - yield p + yield from self.gen(subdir) for p in self.optsort(entries): if self.fil is None or self.fil(p): yield p if self.breadthfirst: for subdir in dirs: - for p in self.gen(subdir): - yield p + yield from self.gen(subdir) class FNMatcher: @@ -659,7 +657,7 @@ def new(self, **kw): ) if "basename" in kw: if "purebasename" in kw or "ext" in kw: - raise ValueError("invalid specification %r" % kw) + raise ValueError(f"invalid specification {kw!r}") else: pb = kw.setdefault("purebasename", purebasename) try: @@ -705,7 +703,7 @@ def _getbyspec(self, spec: str) -> list[str]: elif name == "ext": res.append(ext) else: - raise ValueError("invalid part specification %r" % name) + raise ValueError(f"invalid part specification {name!r}") return res def dirpath(self, *args, **kwargs): @@ -836,7 +834,7 @@ def mtime(self) -> float: def copy(self, target, mode=False, stat=False): """Copy path to target. - If mode is True, will copy copy permission from path to target. + If mode is True, will copy permission from path to target. If stat is True, copy permission, last modification time, last access time, and flags from path to target. """ @@ -1026,7 +1024,7 @@ def atime(self): return self.stat().atime def __repr__(self): - return "local(%r)" % self.strpath + return f"local({self.strpath!r})" def __str__(self): """Return string representation of the Path.""" @@ -1047,7 +1045,7 @@ def chmod(self, mode, rec=0): def pypkgpath(self): """Return the Python package path by looking for the last directory upwards which still contains an __init__.py. - Return None if a pkgpath can not be determined. + Return None if a pkgpath cannot be determined. """ pkgpath = None for parent in self.parts(reverse=True): diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 21dd4a4a4bb..f2f1d029b4c 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -1,11 +1,11 @@ # mypy: allow-untyped-defs """Support for presenting detailed information in failing assertions.""" +from __future__ import annotations + import sys from typing import Any from typing import Generator -from typing import List -from typing import Optional from typing import TYPE_CHECKING from _pytest.assertion import rewrite @@ -94,7 +94,7 @@ class AssertionState: def __init__(self, config: Config, mode) -> None: self.mode = mode self.trace = config.trace.root.get("assertion") - self.hook: Optional[rewrite.AssertionRewritingHook] = None + self.hook: rewrite.AssertionRewritingHook | None = None def install_importhook(config: Config) -> rewrite.AssertionRewritingHook: @@ -113,7 +113,7 @@ def undo() -> None: return hook -def pytest_collection(session: "Session") -> None: +def pytest_collection(session: Session) -> None: # This hook is only called when test modules are collected # so for example not in the managing process of pytest-xdist # (which does not collect test modules). @@ -133,7 +133,7 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: """ ihook = item.ihook - def callbinrepr(op, left: object, right: object) -> Optional[str]: + def callbinrepr(op, left: object, right: object) -> str | None: """Call the pytest_assertrepr_compare hook and prepare the result. This uses the first result from the hook and then ensures the @@ -179,7 +179,7 @@ def call_assertion_pass_hook(lineno: int, orig: str, expl: str) -> None: util._config = None -def pytest_sessionfinish(session: "Session") -> None: +def pytest_sessionfinish(session: Session) -> None: assertstate = session.config.stash.get(assertstate_key, None) if assertstate: if assertstate.hook is not None: @@ -188,5 +188,5 @@ def pytest_sessionfinish(session: "Session") -> None: def pytest_assertrepr_compare( config: Config, op: str, left: Any, right: Any -) -> Optional[List[str]]: +) -> list[str] | None: return util.assertrepr_compare(config=config, op=op, left=left, right=right) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 678471ee992..bfcbcbd3f8d 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1,5 +1,7 @@ """Rewrite assertion AST to produce nice error messages.""" +from __future__ import annotations + import ast from collections import defaultdict import errno @@ -18,17 +20,11 @@ import tokenize import types from typing import Callable -from typing import Dict from typing import IO from typing import Iterable from typing import Iterator -from typing import List -from typing import Optional from typing import Sequence -from typing import Set -from typing import Tuple from typing import TYPE_CHECKING -from typing import Union from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest._io.saferepr import saferepr @@ -73,17 +69,17 @@ def __init__(self, config: Config) -> None: self.fnpats = config.getini("python_files") except ValueError: self.fnpats = ["test_*.py", "*_test.py"] - self.session: Optional[Session] = None - self._rewritten_names: Dict[str, Path] = {} - self._must_rewrite: Set[str] = set() + self.session: Session | None = None + self._rewritten_names: dict[str, Path] = {} + self._must_rewrite: set[str] = set() # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, # which might result in infinite recursion (#3506) self._writing_pyc = False self._basenames_to_check_rewrite = {"conftest"} - self._marked_for_rewrite_cache: Dict[str, bool] = {} + self._marked_for_rewrite_cache: dict[str, bool] = {} self._session_paths_checked = False - def set_session(self, session: Optional[Session]) -> None: + def set_session(self, session: Session | None) -> None: self.session = session self._session_paths_checked = False @@ -93,15 +89,15 @@ def set_session(self, session: Optional[Session]) -> None: def find_spec( self, name: str, - path: Optional[Sequence[Union[str, bytes]]] = None, - target: Optional[types.ModuleType] = None, - ) -> Optional[importlib.machinery.ModuleSpec]: + path: Sequence[str | bytes] | None = None, + target: types.ModuleType | None = None, + ) -> importlib.machinery.ModuleSpec | None: if self._writing_pyc: return None state = self.config.stash[assertstate_key] if self._early_rewrite_bailout(name, state): return None - state.trace("find_module called for: %s" % name) + state.trace(f"find_module called for: {name}") # Type ignored because mypy is confused about the `self` binding here. spec = self._find_spec(name, path) # type: ignore @@ -132,7 +128,7 @@ def find_spec( def create_module( self, spec: importlib.machinery.ModuleSpec - ) -> Optional[types.ModuleType]: + ) -> types.ModuleType | None: return None # default behaviour is fine def exec_module(self, module: types.ModuleType) -> None: @@ -177,7 +173,7 @@ def exec_module(self, module: types.ModuleType) -> None: state.trace(f"found cached rewritten pyc for {fn}") exec(co, module.__dict__) - def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool: + def _early_rewrite_bailout(self, name: str, state: AssertionState) -> bool: """A fast way to get out of rewriting modules. Profiling has shown that the call to PathFinder.find_spec (inside of @@ -216,7 +212,7 @@ def _early_rewrite_bailout(self, name: str, state: "AssertionState") -> bool: state.trace(f"early skip of rewriting module: {name}") return True - def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool: + def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool: # always rewrite conftest files if os.path.basename(fn) == "conftest.py": state.trace(f"rewriting conftest file: {fn!r}") @@ -237,7 +233,7 @@ def _should_rewrite(self, name: str, fn: str, state: "AssertionState") -> bool: return self._is_marked_for_rewrite(name, state) - def _is_marked_for_rewrite(self, name: str, state: "AssertionState") -> bool: + def _is_marked_for_rewrite(self, name: str, state: AssertionState) -> bool: try: return self._marked_for_rewrite_cache[name] except KeyError: @@ -273,12 +269,12 @@ def _warn_already_imported(self, name: str) -> None: self.config.issue_config_time_warning( PytestAssertRewriteWarning( - "Module already imported so cannot be rewritten: %s" % name + f"Module already imported so cannot be rewritten: {name}" ), stacklevel=5, ) - def get_data(self, pathname: Union[str, bytes]) -> bytes: + def get_data(self, pathname: str | bytes) -> bytes: """Optional PEP302 get_data API.""" with open(pathname, "rb") as f: return f.read() @@ -317,7 +313,7 @@ def _write_pyc_fp( def _write_pyc( - state: "AssertionState", + state: AssertionState, co: types.CodeType, source_stat: os.stat_result, pyc: Path, @@ -341,7 +337,7 @@ def _write_pyc( return True -def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]: +def _rewrite_test(fn: Path, config: Config) -> tuple[os.stat_result, types.CodeType]: """Read and rewrite *fn* and return the code object.""" stat = os.stat(fn) source = fn.read_bytes() @@ -354,7 +350,7 @@ def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeT def _read_pyc( source: Path, pyc: Path, trace: Callable[[str], None] = lambda x: None -) -> Optional[types.CodeType]: +) -> types.CodeType | None: """Possibly read a pytest pyc containing rewritten code. Return rewritten code if successful or None if not. @@ -374,21 +370,21 @@ def _read_pyc( return None # Check for invalid or out of date pyc file. if len(data) != (16): - trace("_read_pyc(%s): invalid pyc (too short)" % source) + trace(f"_read_pyc({source}): invalid pyc (too short)") return None if data[:4] != importlib.util.MAGIC_NUMBER: - trace("_read_pyc(%s): invalid pyc (bad magic number)" % source) + trace(f"_read_pyc({source}): invalid pyc (bad magic number)") return None if data[4:8] != b"\x00\x00\x00\x00": - trace("_read_pyc(%s): invalid pyc (unsupported flags)" % source) + trace(f"_read_pyc({source}): invalid pyc (unsupported flags)") return None mtime_data = data[8:12] if int.from_bytes(mtime_data, "little") != mtime & 0xFFFFFFFF: - trace("_read_pyc(%s): out of date" % source) + trace(f"_read_pyc({source}): out of date") return None size_data = data[12:16] if int.from_bytes(size_data, "little") != size & 0xFFFFFFFF: - trace("_read_pyc(%s): invalid pyc (incorrect size)" % source) + trace(f"_read_pyc({source}): invalid pyc (incorrect size)") return None try: co = marshal.load(fp) @@ -396,7 +392,7 @@ def _read_pyc( trace(f"_read_pyc({source}): marshal.load error {e}") return None if not isinstance(co, types.CodeType): - trace("_read_pyc(%s): not a code object" % source) + trace(f"_read_pyc({source}): not a code object") return None return co @@ -404,8 +400,8 @@ def _read_pyc( def rewrite_asserts( mod: ast.Module, source: bytes, - module_path: Optional[str] = None, - config: Optional[Config] = None, + module_path: str | None = None, + config: Config | None = None, ) -> None: """Rewrite the assert statements in mod.""" AssertionRewriter(module_path, config, source).run(mod) @@ -421,11 +417,15 @@ def _saferepr(obj: object) -> str: sequences, especially '\n{' and '\n}' are likely to be present in JSON reprs. """ + if isinstance(obj, types.MethodType): + # for bound methods, skip redundant information + return obj.__name__ + maxsize = _get_maxsize_for_saferepr(util._config) return saferepr(obj, maxsize=maxsize).replace("\n", "\\n") -def _get_maxsize_for_saferepr(config: Optional[Config]) -> Optional[int]: +def _get_maxsize_for_saferepr(config: Config | None) -> int | None: """Get `maxsize` configuration for saferepr based on the given config object.""" if config is None: verbosity = 0 @@ -543,14 +543,14 @@ def traverse_node(node: ast.AST) -> Iterator[ast.AST]: @functools.lru_cache(maxsize=1) -def _get_assertion_exprs(src: bytes) -> Dict[int, str]: +def _get_assertion_exprs(src: bytes) -> dict[int, str]: """Return a mapping from {lineno: "assertion test expression"}.""" - ret: Dict[int, str] = {} + ret: dict[int, str] = {} depth = 0 - lines: List[str] = [] - assert_lineno: Optional[int] = None - seen_lines: Set[int] = set() + lines: list[str] = [] + assert_lineno: int | None = None + seen_lines: set[int] = set() def _write_and_reset() -> None: nonlocal depth, lines, assert_lineno, seen_lines @@ -584,7 +584,7 @@ def _write_and_reset() -> None: # multi-line assert with message elif lineno in seen_lines: lines[-1] = lines[-1][:offset] - # multi line assert with escapd newline before message + # multi line assert with escaped newline before message else: lines.append(line[:offset]) _write_and_reset() @@ -657,7 +657,7 @@ class AssertionRewriter(ast.NodeVisitor): """ def __init__( - self, module_path: Optional[str], config: Optional[Config], source: bytes + self, module_path: str | None, config: Config | None, source: bytes ) -> None: super().__init__() self.module_path = module_path @@ -670,7 +670,7 @@ def __init__( self.enable_assertion_pass_hook = False self.source = source self.scope: tuple[ast.AST, ...] = () - self.variables_overwrite: defaultdict[tuple[ast.AST, ...], Dict[str, str]] = ( + self.variables_overwrite: defaultdict[tuple[ast.AST, ...], dict[str, str]] = ( defaultdict(dict) ) @@ -737,7 +737,7 @@ def run(self, mod: ast.Module) -> None: # Collect asserts. self.scope = (mod,) - nodes: List[Union[ast.AST, Sentinel]] = [mod] + nodes: list[ast.AST | Sentinel] = [mod] while nodes: node = nodes.pop() if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): @@ -749,7 +749,7 @@ def run(self, mod: ast.Module) -> None: assert isinstance(node, ast.AST) for name, field in ast.iter_fields(node): if isinstance(field, list): - new: List[ast.AST] = [] + new: list[ast.AST] = [] for i, child in enumerate(field): if isinstance(child, ast.Assert): # Transform assert. @@ -821,7 +821,7 @@ def push_format_context(self) -> None: to format a string of %-formatted values as added by .explanation_param(). """ - self.explanation_specifiers: Dict[str, ast.expr] = {} + self.explanation_specifiers: dict[str, ast.expr] = {} self.stack.append(self.explanation_specifiers) def pop_format_context(self, expl_expr: ast.expr) -> ast.Name: @@ -835,7 +835,7 @@ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name: current = self.stack.pop() if self.stack: self.explanation_specifiers = self.stack[-1] - keys = [ast.Constant(key) for key in current.keys()] + keys: list[ast.expr | None] = [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)) @@ -844,13 +844,13 @@ def pop_format_context(self, expl_expr: ast.expr) -> ast.Name: self.expl_stmts.append(ast.Assign([ast.Name(name, ast.Store())], form)) return ast.Name(name, ast.Load()) - def generic_visit(self, node: ast.AST) -> Tuple[ast.Name, str]: + def generic_visit(self, node: ast.AST) -> tuple[ast.Name, str]: """Handle expressions we don't have custom code for.""" assert isinstance(node, ast.expr) res = self.assign(node) return res, self.explanation_param(self.display(res)) - def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: + def visit_Assert(self, assert_: ast.Assert) -> list[ast.stmt]: """Return the AST statements to replace the ast.Assert instance. This rewrites the test of an assertion to provide @@ -874,15 +874,15 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: lineno=assert_.lineno, ) - self.statements: List[ast.stmt] = [] - self.variables: List[str] = [] + self.statements: list[ast.stmt] = [] + self.variables: list[str] = [] self.variable_counter = itertools.count() if self.enable_assertion_pass_hook: - self.format_variables: List[str] = [] + self.format_variables: list[str] = [] - self.stack: List[Dict[str, ast.expr]] = [] - self.expl_stmts: List[ast.stmt] = [] + self.stack: list[dict[str, ast.expr]] = [] + self.expl_stmts: list[ast.stmt] = [] self.push_format_context() # Rewrite assert into a bunch of statements. top_condition, explanation = self.visit(assert_.test) @@ -926,13 +926,13 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: [*self.expl_stmts, hook_call_pass], [], ) - statements_pass = [hook_impl_test] + statements_pass: list[ast.stmt] = [hook_impl_test] # Test for assertion condition main_test = ast.If(negation, statements_fail, statements_pass) self.statements.append(main_test) if self.format_variables: - variables = [ + variables: list[ast.expr] = [ ast.Name(name, ast.Store()) for name in self.format_variables ] clear_format = ast.Assign(variables, ast.Constant(None)) @@ -968,7 +968,7 @@ def visit_Assert(self, assert_: ast.Assert) -> List[ast.stmt]: ast.copy_location(node, assert_) return self.statements - def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.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. @@ -980,7 +980,7 @@ def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.NamedExpr, str]: 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]: + 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"), [], []) @@ -990,7 +990,7 @@ def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]: 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]: + def visit_BoolOp(self, boolop: ast.BoolOp) -> tuple[ast.Name, str]: res_var = self.variable() expl_list = self.assign(ast.List([], ast.Load())) app = ast.Attribute(expl_list, "append", ast.Load()) @@ -1002,7 +1002,7 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: # Process each operand, short-circuiting if needed. for i, v in enumerate(boolop.values): if i: - fail_inner: List[ast.stmt] = [] + fail_inner: list[ast.stmt] = [] # cond is set in a prior loop iteration below self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821 self.expl_stmts = fail_inner @@ -1030,7 +1030,7 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: cond: ast.expr = res if is_or: cond = ast.UnaryOp(ast.Not(), cond) - inner: List[ast.stmt] = [] + inner: list[ast.stmt] = [] self.statements.append(ast.If(cond, inner, [])) self.statements = body = inner self.statements = save @@ -1039,13 +1039,13 @@ def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: expl = self.pop_format_context(expl_template) return ast.Name(res_var, ast.Load()), self.explanation_param(expl) - def visit_UnaryOp(self, unary: ast.UnaryOp) -> Tuple[ast.Name, str]: + def visit_UnaryOp(self, unary: ast.UnaryOp) -> tuple[ast.Name, str]: pattern = UNARY_MAP[unary.op.__class__] operand_res, operand_expl = self.visit(unary.operand) res = self.assign(ast.UnaryOp(unary.op, operand_res)) return res, pattern % (operand_expl,) - def visit_BinOp(self, binop: ast.BinOp) -> Tuple[ast.Name, str]: + def visit_BinOp(self, binop: ast.BinOp) -> tuple[ast.Name, str]: symbol = BINOP_MAP[binop.op.__class__] left_expr, left_expl = self.visit(binop.left) right_expr, right_expl = self.visit(binop.right) @@ -1053,7 +1053,7 @@ def visit_BinOp(self, binop: ast.BinOp) -> Tuple[ast.Name, str]: res = self.assign(ast.BinOp(left_expr, binop.op, right_expr)) return res, explanation - def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]: + def visit_Call(self, call: ast.Call) -> tuple[ast.Name, str]: new_func, func_expl = self.visit(call.func) arg_expls = [] new_args = [] @@ -1085,13 +1085,13 @@ def visit_Call(self, call: ast.Call) -> Tuple[ast.Name, str]: outer_expl = f"{res_expl}\n{{{res_expl} = {expl}\n}}" return res, outer_expl - def visit_Starred(self, starred: ast.Starred) -> Tuple[ast.Starred, str]: + def visit_Starred(self, starred: ast.Starred) -> tuple[ast.Starred, str]: # A Starred node can appear in a function call. res, expl = self.visit(starred.value) new_starred = ast.Starred(res, starred.ctx) return new_starred, "*" + expl - def visit_Attribute(self, attr: ast.Attribute) -> Tuple[ast.Name, str]: + def visit_Attribute(self, attr: ast.Attribute) -> tuple[ast.Name, str]: if not isinstance(attr.ctx, ast.Load): return self.generic_visit(attr) value, value_expl = self.visit(attr.value) @@ -1101,7 +1101,7 @@ def visit_Attribute(self, attr: ast.Attribute) -> Tuple[ast.Name, str]: expl = pat % (res_expl, res_expl, value_expl, attr.attr) return res, expl - def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: + def visit_Compare(self, comp: ast.Compare) -> tuple[ast.expr, str]: self.push_format_context() # We first check if we have overwritten a variable in the previous assert if isinstance( @@ -1114,11 +1114,11 @@ def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: if isinstance(comp.left, (ast.Compare, ast.BoolOp)): left_expl = f"({left_expl})" res_variables = [self.variable() for i in range(len(comp.ops))] - load_names = [ast.Name(v, ast.Load()) for v in res_variables] + load_names: list[ast.expr] = [ast.Name(v, ast.Load()) for v in res_variables] store_names = [ast.Name(v, ast.Store()) for v in res_variables] it = zip(range(len(comp.ops)), comp.ops, comp.comparators) - expls = [] - syms = [] + expls: list[ast.expr] = [] + syms: list[ast.expr] = [] results = [left_res] for i, op, next_operand in it: if ( @@ -1171,7 +1171,10 @@ def try_makedirs(cache_dir: Path) -> bool: return False except OSError as e: # as of now, EROFS doesn't have an equivalent OSError-subclass - if e.errno == errno.EROFS: + # + # squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not + # implemented" for a read-only error + if e.errno in {errno.EROFS, errno.ENOSYS}: return False raise return True diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index 4fdfd86a519..b67f02ccaf8 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -4,8 +4,7 @@ terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI. """ -from typing import List -from typing import Optional +from __future__ import annotations from _pytest.assertion import util from _pytest.config import Config @@ -18,8 +17,8 @@ def truncate_if_required( - explanation: List[str], item: Item, max_length: Optional[int] = None -) -> List[str]: + explanation: list[str], item: Item, max_length: int | None = None +) -> list[str]: """Truncate this assertion explanation if the given test item is eligible.""" if _should_truncate_item(item): return _truncate_explanation(explanation) @@ -33,10 +32,10 @@ def _should_truncate_item(item: Item) -> bool: def _truncate_explanation( - input_lines: List[str], - max_lines: Optional[int] = None, - max_chars: Optional[int] = None, -) -> List[str]: + input_lines: list[str], + max_lines: int | None = None, + max_chars: int | None = None, +) -> list[str]: """Truncate given list of strings that makes up the assertion explanation. Truncates to either 8 lines, or 640 characters - whichever the input reaches @@ -100,7 +99,7 @@ def _truncate_explanation( ] -def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]: +def _truncate_by_char_count(input_lines: list[str], max_chars: int) -> list[str]: # Find point at which input length exceeds total allowed length iterated_char_count = 0 for iterated_index, input_line in enumerate(input_lines): diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index cb671641041..4dc1af4af03 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Utilities for assertion debugging.""" +from __future__ import annotations + import collections.abc import os import pprint @@ -8,10 +10,8 @@ from typing import Any from typing import Callable from typing import Iterable -from typing import List from typing import Literal from typing import Mapping -from typing import Optional from typing import Protocol from typing import Sequence from unicodedata import normalize @@ -28,14 +28,14 @@ # interpretation code and assertion rewriter to detect this plugin was # loaded and in turn call the hooks defined here as part of the # DebugInterpreter. -_reprcompare: Optional[Callable[[str, object, object], Optional[str]]] = None +_reprcompare: Callable[[str, object, object], str | None] | None = None # Works similarly as _reprcompare attribute. Is populated with the hook call # when pytest_runtest_setup is called. -_assertion_pass: Optional[Callable[[int, str, str], None]] = None +_assertion_pass: Callable[[int, str, str], None] | None = None # Config object which is assigned during pytest_runtest_protocol. -_config: Optional[Config] = None +_config: Config | None = None class _HighlightFunc(Protocol): @@ -58,7 +58,7 @@ def format_explanation(explanation: str) -> str: return "\n".join(result) -def _split_explanation(explanation: str) -> List[str]: +def _split_explanation(explanation: str) -> list[str]: r"""Return a list of individual lines in the explanation. This will return a list of lines split on '\n{', '\n}' and '\n~'. @@ -75,7 +75,7 @@ def _split_explanation(explanation: str) -> List[str]: return lines -def _format_lines(lines: Sequence[str]) -> List[str]: +def _format_lines(lines: Sequence[str]) -> list[str]: """Format the individual lines. This will replace the '{', '}' and '~' characters of our mini formatting @@ -169,7 +169,7 @@ def has_default_eq( def assertrepr_compare( config, op: str, left: Any, right: Any, use_ascii: bool = False -) -> Optional[List[str]]: +) -> list[str] | None: """Return specialised explanations for some operators/operands.""" verbose = config.get_verbosity(Config.VERBOSITY_ASSERTIONS) @@ -239,7 +239,7 @@ def assertrepr_compare( def _compare_eq_any( left: Any, right: Any, highlighter: _HighlightFunc, verbose: int = 0 -) -> List[str]: +) -> list[str]: explanation = [] if istext(left) and istext(right): explanation = _diff_text(left, right, verbose) @@ -274,7 +274,7 @@ def _compare_eq_any( return explanation -def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: +def _diff_text(left: str, right: str, verbose: int = 0) -> list[str]: """Return the explanation for the diff between text. Unless --verbose is used this will skip leading and trailing @@ -282,7 +282,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: """ from difflib import ndiff - explanation: List[str] = [] + explanation: list[str] = [] if verbose < 1: i = 0 # just in case left or right has zero length @@ -292,7 +292,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: if i > 42: i -= 10 # Provide some context explanation = [ - "Skipping %s identical leading characters in diff, use -v to show" % i + f"Skipping {i} identical leading characters in diff, use -v to show" ] left = left[i:] right = right[i:] @@ -325,9 +325,9 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: def _compare_eq_iterable( left: Iterable[Any], right: Iterable[Any], - highligher: _HighlightFunc, + highlighter: _HighlightFunc, verbose: int = 0, -) -> List[str]: +) -> list[str]: if verbose <= 0 and not running_on_ci(): return ["Use -v to get more diff"] # dynamic import to speedup pytest @@ -340,7 +340,7 @@ def _compare_eq_iterable( # "right" is the expected base against which we compare "left", # see https://github.com/pytest-dev/pytest/issues/3333 explanation.extend( - highligher( + highlighter( "\n".join( line.rstrip() for line in difflib.ndiff(right_formatting, left_formatting) @@ -356,9 +356,9 @@ def _compare_eq_sequence( right: Sequence[Any], highlighter: _HighlightFunc, verbose: int = 0, -) -> List[str]: +) -> list[str]: comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes) - explanation: List[str] = [] + explanation: list[str] = [] len_left = len(left) len_right = len(right) for i in range(min(len_left, len_right)): @@ -417,7 +417,7 @@ def _compare_eq_set( right: AbstractSet[Any], highlighter: _HighlightFunc, verbose: int = 0, -) -> List[str]: +) -> list[str]: explanation = [] explanation.extend(_set_one_sided_diff("left", left, right, highlighter)) explanation.extend(_set_one_sided_diff("right", right, left, highlighter)) @@ -429,7 +429,7 @@ def _compare_gt_set( right: AbstractSet[Any], highlighter: _HighlightFunc, verbose: int = 0, -) -> List[str]: +) -> list[str]: explanation = _compare_gte_set(left, right, highlighter) if not explanation: return ["Both sets are equal"] @@ -441,7 +441,7 @@ def _compare_lt_set( right: AbstractSet[Any], highlighter: _HighlightFunc, verbose: int = 0, -) -> List[str]: +) -> list[str]: explanation = _compare_lte_set(left, right, highlighter) if not explanation: return ["Both sets are equal"] @@ -453,7 +453,7 @@ def _compare_gte_set( right: AbstractSet[Any], highlighter: _HighlightFunc, verbose: int = 0, -) -> List[str]: +) -> list[str]: return _set_one_sided_diff("right", right, left, highlighter) @@ -462,7 +462,7 @@ def _compare_lte_set( right: AbstractSet[Any], highlighter: _HighlightFunc, verbose: int = 0, -) -> List[str]: +) -> list[str]: return _set_one_sided_diff("left", left, right, highlighter) @@ -471,7 +471,7 @@ def _set_one_sided_diff( set1: AbstractSet[Any], set2: AbstractSet[Any], highlighter: _HighlightFunc, -) -> List[str]: +) -> list[str]: explanation = [] diff = set1 - set2 if diff: @@ -486,14 +486,14 @@ def _compare_eq_dict( right: Mapping[Any, Any], highlighter: _HighlightFunc, verbose: int = 0, -) -> List[str]: - explanation: List[str] = [] +) -> list[str]: + explanation: list[str] = [] set_left = set(left) set_right = set(right) common = set_left.intersection(set_right) same = {k: left[k] for k in common if left[k] == right[k]} if same and verbose < 2: - explanation += ["Omitting %s identical items, use -vv to show" % len(same)] + explanation += [f"Omitting {len(same)} identical items, use -vv to show"] elif same: explanation += ["Common items:"] explanation += highlighter(pprint.pformat(same)).splitlines() @@ -531,7 +531,7 @@ def _compare_eq_dict( def _compare_eq_cls( left: Any, right: Any, highlighter: _HighlightFunc, verbose: int -) -> List[str]: +) -> list[str]: if not has_default_eq(left): return [] if isdatacls(left): @@ -560,7 +560,7 @@ def _compare_eq_cls( if same or diff: explanation += [""] if same and verbose < 2: - explanation.append("Omitting %s identical items, use -vv to show" % len(same)) + explanation.append(f"Omitting {len(same)} identical items, use -vv to show") elif same: explanation += ["Matching attributes:"] explanation += highlighter(pprint.pformat(same)).splitlines() @@ -584,13 +584,13 @@ def _compare_eq_cls( return explanation -def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]: +def _notin_text(term: str, text: str, verbose: int = 0) -> list[str]: index = text.find(term) head = text[:index] tail = text[index + len(term) :] correct_text = head + tail diff = _diff_text(text, correct_text, verbose) - newdiff = ["%s is contained here:" % saferepr(term, maxsize=42)] + newdiff = [f"{saferepr(term, maxsize=42)} is contained here:"] for line in diff: if line.startswith("Skipping"): continue diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index e9f66f1f44f..20bb262e05d 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -3,19 +3,17 @@ # This plugin was not named "cache" to avoid conflicts with the external # pytest-cache version. +from __future__ import annotations + import dataclasses +import errno import json import os from pathlib import Path import tempfile -from typing import Dict from typing import final from typing import Generator from typing import Iterable -from typing import List -from typing import Optional -from typing import Set -from typing import Union from .pathlib import resolve_from_str from .pathlib import rm_rf @@ -76,7 +74,7 @@ def __init__( self._config = config @classmethod - def for_config(cls, config: Config, *, _ispytest: bool = False) -> "Cache": + def for_config(cls, config: Config, *, _ispytest: bool = False) -> Cache: """Create the Cache instance for a Config. :meta private: @@ -213,25 +211,43 @@ def _ensure_cache_dir_and_supporting_files(self) -> None: dir=self._cachedir.parent, ) as newpath: path = Path(newpath) - with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f: + + # Reset permissions to the default, see #12308. + # Note: there's no way to get the current umask atomically, eek. + umask = os.umask(0o022) + os.umask(umask) + path.chmod(0o777 - umask) + + with open(path.joinpath("README.md"), "x", encoding="UTF-8") as f: f.write(README_CONTENT) - with open(path.joinpath(".gitignore"), "xt", encoding="UTF-8") as f: + with open(path.joinpath(".gitignore"), "x", encoding="UTF-8") as f: f.write("# Created by pytest automatically.\n*\n") with open(path.joinpath("CACHEDIR.TAG"), "xb") as f: f.write(CACHEDIR_TAG_CONTENT) - path.rename(self._cachedir) - # Create a directory in place of the one we just moved so that `TemporaryDirectory`'s - # cleanup doesn't complain. - # - # TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. See - # https://github.com/python/cpython/issues/74168. Note that passing delete=False would - # do the wrong thing in case of errors and isn't supported until python 3.12. - path.mkdir() + try: + path.rename(self._cachedir) + except OSError as e: + # If 2 concurrent pytests both race to the rename, the loser + # gets "Directory not empty" from the rename. In this case, + # everything is handled so just continue (while letting the + # temporary directory be cleaned up). + # On Windows, the error is a FileExistsError which translates to EEXIST. + if e.errno not in (errno.ENOTEMPTY, errno.EEXIST): + raise + else: + # Create a directory in place of the one we just moved so that + # `TemporaryDirectory`'s cleanup doesn't complain. + # + # TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. + # See https://github.com/python/cpython/issues/74168. Note that passing + # delete=False would do the wrong thing in case of errors and isn't supported + # until python 3.12. + path.mkdir() class LFPluginCollWrapper: - def __init__(self, lfplugin: "LFPlugin") -> None: + def __init__(self, lfplugin: LFPlugin) -> None: self.lfplugin = lfplugin self._collected_at_least_one_failure = False @@ -244,8 +260,8 @@ def pytest_make_collect_report( # Sort any lf-paths to the beginning. lf_paths = self.lfplugin._last_failed_paths - # Use stable sort to priorize last failed. - def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool: + # Use stable sort to prioritize last failed. + def sort_key(node: nodes.Item | nodes.Collector) -> bool: return node.path in lf_paths res.result = sorted( @@ -283,13 +299,13 @@ def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool: class LFPluginCollSkipfiles: - def __init__(self, lfplugin: "LFPlugin") -> None: + def __init__(self, lfplugin: LFPlugin) -> None: self.lfplugin = lfplugin @hookimpl def pytest_make_collect_report( self, collector: nodes.Collector - ) -> Optional[CollectReport]: + ) -> CollectReport | None: if isinstance(collector, File): if collector.path not in self.lfplugin._last_failed_paths: self.lfplugin._skipped_files += 1 @@ -308,9 +324,9 @@ def __init__(self, config: Config) -> None: active_keys = "lf", "failedfirst" self.active = any(config.getoption(key) for key in active_keys) assert config.cache - self.lastfailed: Dict[str, bool] = config.cache.get("cache/lastfailed", {}) - self._previously_failed_count: Optional[int] = None - self._report_status: Optional[str] = None + self.lastfailed: dict[str, bool] = config.cache.get("cache/lastfailed", {}) + self._previously_failed_count: int | None = None + self._report_status: str | None = None self._skipped_files = 0 # count skipped files during collection due to --lf if config.getoption("lf"): @@ -319,7 +335,7 @@ def __init__(self, config: Config) -> None: LFPluginCollWrapper(self), "lfplugin-collwrapper" ) - def get_last_failed_paths(self) -> Set[Path]: + def get_last_failed_paths(self) -> set[Path]: """Return a set with all Paths of the previously failed nodeids and their parents.""" rootpath = self.config.rootpath @@ -330,9 +346,9 @@ def get_last_failed_paths(self) -> Set[Path]: result.update(path.parents) return {x for x in result if x.exists()} - def pytest_report_collectionfinish(self) -> Optional[str]: + def pytest_report_collectionfinish(self) -> str | None: if self.active and self.config.getoption("verbose") >= 0: - return "run-last-failure: %s" % self._report_status + return f"run-last-failure: {self._report_status}" return None def pytest_runtest_logreport(self, report: TestReport) -> None: @@ -352,7 +368,7 @@ def pytest_collectreport(self, report: CollectReport) -> None: @hookimpl(wrapper=True, tryfirst=True) def pytest_collection_modifyitems( - self, config: Config, items: List[nodes.Item] + self, config: Config, items: list[nodes.Item] ) -> Generator[None, None, None]: res = yield @@ -424,13 +440,13 @@ def __init__(self, config: Config) -> None: @hookimpl(wrapper=True, tryfirst=True) def pytest_collection_modifyitems( - self, items: List[nodes.Item] + self, items: list[nodes.Item] ) -> Generator[None, None, None]: res = yield if self.active: - new_items: Dict[str, nodes.Item] = {} - other_items: Dict[str, nodes.Item] = {} + new_items: dict[str, nodes.Item] = {} + other_items: dict[str, nodes.Item] = {} for item in items: if item.nodeid not in self.cached_nodeids: new_items[item.nodeid] = item @@ -446,7 +462,7 @@ def pytest_collection_modifyitems( return res - def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]: + 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) def pytest_sessionfinish(self) -> None: @@ -523,7 +539,7 @@ def pytest_addoption(parser: Parser) -> None: ) -def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: if config.option.cacheshow and not config.option.help: from _pytest.main import wrap_session @@ -554,7 +570,7 @@ def cache(request: FixtureRequest) -> Cache: return request.config.cache -def pytest_report_header(config: Config) -> Optional[str]: +def pytest_report_header(config: Config) -> str | None: """Display cachedir with --cache-show and if non-default.""" if config.option.verbose > 0 or config.getini("cache_dir") != ".pytest_cache": assert config.cache is not None @@ -588,21 +604,21 @@ def cacheshow(config: Config, session: Session) -> int: dummy = object() basedir = config.cache._cachedir vdir = basedir / Cache._CACHE_PREFIX_VALUES - tw.sep("-", "cache values for %r" % glob) + tw.sep("-", f"cache values for {glob!r}") for valpath in sorted(x for x in vdir.rglob(glob) if x.is_file()): key = str(valpath.relative_to(vdir)) val = config.cache.get(key, dummy) if val is dummy: - tw.line("%s contains unreadable content, will be ignored" % key) + tw.line(f"{key} contains unreadable content, will be ignored") else: - tw.line("%s contains:" % key) + tw.line(f"{key} contains:") for line in pformat(val).splitlines(): tw.line(" " + line) ddir = basedir / Cache._CACHE_PREFIX_DIRS if ddir.is_dir(): contents = sorted(ddir.rglob(glob)) - tw.sep("-", "cache directories for %r" % glob) + tw.sep("-", f"cache directories for {glob!r}") for p in contents: # if p.is_dir(): # print("%s/" % p.relative_to(basedir)) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 3f6a2510348..c4dfcc27552 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Per-test stdout/stderr capturing mechanism.""" +from __future__ import annotations + import abc import collections import contextlib @@ -19,15 +21,14 @@ 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 -from typing import Tuple -from typing import Type from typing import TYPE_CHECKING -from typing import Union + + +if TYPE_CHECKING: + from typing_extensions import Self from _pytest.config import Config from _pytest.config import hookimpl @@ -213,7 +214,7 @@ def read(self, size: int = -1) -> str: def __next__(self) -> str: return self.readline() - def readlines(self, hint: Optional[int] = -1) -> List[str]: + def readlines(self, hint: int | None = -1) -> list[str]: raise OSError( "pytest: reading from stdin while output is captured! Consider using `-s`." ) @@ -245,7 +246,7 @@ def seekable(self) -> bool: def tell(self) -> int: raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()") - def truncate(self, size: Optional[int] = None) -> int: + def truncate(self, size: int | None = None) -> int: raise UnsupportedOperation("cannot truncate stdin") def write(self, data: str) -> int: @@ -257,14 +258,14 @@ def writelines(self, lines: Iterable[str]) -> None: def writable(self) -> bool: return False - def __enter__(self) -> "DontReadFromInput": + def __enter__(self) -> Self: return self def __exit__( self, - type: Optional[Type[BaseException]], - value: Optional[BaseException], - traceback: Optional[TracebackType], + type: type[BaseException] | None, + value: BaseException | None, + traceback: TracebackType | None, ) -> None: pass @@ -339,7 +340,7 @@ def writeorg(self, data: str) -> None: class SysCaptureBase(CaptureBase[AnyStr]): def __init__( - self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False + self, fd: int, tmpfile: TextIO | None = None, *, tee: bool = False ) -> None: name = patchsysdict[fd] self._old: TextIO = getattr(sys, name) @@ -370,7 +371,7 @@ def __repr__(self) -> str: self.tmpfile, ) - def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: + def _assert_state(self, op: str, states: tuple[str, ...]) -> None: assert ( self._state in states ), "cannot {} in state {!r}: expected one of {}".format( @@ -457,7 +458,7 @@ def __init__(self, targetfd: int) -> None: # Further complications are the need to support suspend() and the # possibility of FD reuse (e.g. the tmpfile getting the very same # target FD). The following approach is robust, I believe. - self.targetfd_invalid: Optional[int] = os.open(os.devnull, os.O_RDWR) + self.targetfd_invalid: int | None = os.open(os.devnull, os.O_RDWR) os.dup2(self.targetfd_invalid, targetfd) else: self.targetfd_invalid = None @@ -487,7 +488,7 @@ def __repr__(self) -> str: f"_state={self._state!r} tmpfile={self.tmpfile!r}>" ) - def _assert_state(self, op: str, states: Tuple[str, ...]) -> None: + def _assert_state(self, op: str, states: tuple[str, ...]) -> None: assert ( self._state in states ), "cannot {} in state {!r}: expected one of {}".format( @@ -609,13 +610,13 @@ class MultiCapture(Generic[AnyStr]): def __init__( self, - in_: Optional[CaptureBase[AnyStr]], - out: Optional[CaptureBase[AnyStr]], - err: Optional[CaptureBase[AnyStr]], + in_: CaptureBase[AnyStr] | None, + out: CaptureBase[AnyStr] | None, + err: CaptureBase[AnyStr] | None, ) -> None: - self.in_: Optional[CaptureBase[AnyStr]] = in_ - self.out: Optional[CaptureBase[AnyStr]] = out - self.err: Optional[CaptureBase[AnyStr]] = err + self.in_: CaptureBase[AnyStr] | None = in_ + self.out: CaptureBase[AnyStr] | None = out + self.err: CaptureBase[AnyStr] | None = err def __repr__(self) -> str: return ( @@ -632,7 +633,7 @@ def start_capturing(self) -> None: if self.err: self.err.start() - def pop_outerr_to_orig(self) -> Tuple[AnyStr, AnyStr]: + def pop_outerr_to_orig(self) -> tuple[AnyStr, AnyStr]: """Pop current snapshot out/err capture and flush to orig streams.""" out, err = self.readouterr() if out: @@ -725,8 +726,8 @@ class CaptureManager: def __init__(self, method: _CaptureMethod) -> None: self._method: Final = method - self._global_capturing: Optional[MultiCapture[str]] = None - self._capture_fixture: Optional[CaptureFixture[Any]] = None + self._global_capturing: MultiCapture[str] | None = None + self._capture_fixture: CaptureFixture[Any] | None = None def __repr__(self) -> str: return ( @@ -734,11 +735,11 @@ def __repr__(self) -> str: f"_capture_fixture={self._capture_fixture!r}>" ) - def is_capturing(self) -> Union[str, bool]: + def is_capturing(self) -> str | bool: if self.is_globally_capturing(): return "global" if self._capture_fixture: - return "fixture %s" % self._capture_fixture.request.fixturename + return f"fixture {self._capture_fixture.request.fixturename}" return False # Global capturing control @@ -782,7 +783,7 @@ def read_global_capture(self) -> CaptureResult[str]: # Fixture Control - def set_fixture(self, capture_fixture: "CaptureFixture[Any]") -> None: + def set_fixture(self, capture_fixture: CaptureFixture[Any]) -> None: if self._capture_fixture: current_fixture = self._capture_fixture.request.fixturename requested_fixture = capture_fixture.request.fixturename @@ -897,15 +898,15 @@ class CaptureFixture(Generic[AnyStr]): def __init__( self, - captureclass: Type[CaptureBase[AnyStr]], + captureclass: type[CaptureBase[AnyStr]], request: SubRequest, *, _ispytest: bool = False, ) -> None: check_ispytest(_ispytest) - self.captureclass: Type[CaptureBase[AnyStr]] = captureclass + self.captureclass: type[CaptureBase[AnyStr]] = captureclass self.request = request - self._capture: Optional[MultiCapture[AnyStr]] = None + self._capture: MultiCapture[AnyStr] | None = None self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER @@ -983,6 +984,7 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: Returns an instance of :class:`CaptureFixture[str] `. Example: + .. code-block:: python def test_output(capsys): @@ -1010,6 +1012,7 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, Returns an instance of :class:`CaptureFixture[bytes] `. Example: + .. code-block:: python def test_output(capsysbinary): @@ -1037,6 +1040,7 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]: Returns an instance of :class:`CaptureFixture[str] `. Example: + .. code-block:: python def test_system_echo(capfd): @@ -1064,6 +1068,7 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N Returns an instance of :class:`CaptureFixture[bytes] `. Example: + .. code-block:: python def test_system_echo(capfdbinary): diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 9d9411818ac..614848e0dba 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -53,7 +53,7 @@ def iscoroutinefunction(func: object) -> bool: def syntax, and doesn't contain yield), or a function decorated with @asyncio.coroutine. - Note: copied and modified from Python 3.5's builtin couroutines.py to avoid + Note: copied and modified from Python 3.5's builtin coroutines.py to avoid importing asyncio directly, which in turns also initializes the "logging" module as a side-effect (see issue #8). """ diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 306b14cce28..0c1850df503 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Command line options, ini-file and conftest.py processing.""" +from __future__ import annotations + import argparse import collections.abc import copy @@ -11,7 +13,7 @@ import importlib.metadata import inspect import os -from pathlib import Path +import pathlib import re import shlex import sys @@ -21,22 +23,16 @@ 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 IO from typing import Iterable from typing import Iterator -from typing import List -from typing import Optional from typing import Sequence -from typing import Set from typing import TextIO -from typing import Tuple from typing import Type from typing import TYPE_CHECKING -from typing import Union import warnings import pluggy @@ -54,7 +50,10 @@ import _pytest._code from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback +from _pytest._code.code import TracebackStyle from _pytest._io import TerminalWriter +from _pytest.config.argparsing import Argument +from _pytest.config.argparsing import Parser import _pytest.deprecated import _pytest.hookspec from _pytest.outcomes import fail @@ -71,9 +70,7 @@ if TYPE_CHECKING: - from .argparsing import Argument - from .argparsing import Parser - from _pytest._code.code import _TracebackStyle + from _pytest.cacheprovider import Cache from _pytest.terminal import TerminalReporter @@ -117,7 +114,7 @@ class ExitCode(enum.IntEnum): class ConftestImportFailure(Exception): def __init__( self, - path: Path, + path: pathlib.Path, *, cause: Exception, ) -> None: @@ -140,9 +137,9 @@ def filter_traceback_for_conftest_import_failure( def main( - args: Optional[Union[List[str], "os.PathLike[str]"]] = None, - plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, -) -> Union[int, ExitCode]: + args: list[str] | os.PathLike[str] | None = None, + plugins: Sequence[str | _PluggyPlugin] | None = None, +) -> int | ExitCode: """Perform an in-process test run. :param args: @@ -175,9 +172,7 @@ def main( return ExitCode.USAGE_ERROR else: try: - ret: Union[ExitCode, int] = config.hook.pytest_cmdline_main( - config=config - ) + ret: ExitCode | int = config.hook.pytest_cmdline_main(config=config) try: return ExitCode(ret) except ValueError: @@ -285,9 +280,9 @@ def directory_arg(path: str, optname: str) -> str: def get_config( - args: Optional[List[str]] = None, - plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, -) -> "Config": + args: list[str] | None = None, + plugins: Sequence[str | _PluggyPlugin] | None = None, +) -> Config: # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() config = Config( @@ -295,7 +290,7 @@ def get_config( invocation_params=Config.InvocationParams( args=args or (), plugins=plugins, - dir=Path.cwd(), + dir=pathlib.Path.cwd(), ), ) @@ -309,7 +304,7 @@ def get_config( return config -def get_plugin_manager() -> "PytestPluginManager": +def get_plugin_manager() -> PytestPluginManager: """Obtain a new instance of the :py:class:`pytest.PytestPluginManager`, with default plugins already loaded. @@ -321,9 +316,9 @@ def get_plugin_manager() -> "PytestPluginManager": def _prepareconfig( - args: Optional[Union[List[str], "os.PathLike[str]"]] = None, - plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] = None, -) -> "Config": + args: list[str] | os.PathLike[str] | None = None, + plugins: Sequence[str | _PluggyPlugin] | None = None, +) -> Config: if args is None: args = sys.argv[1:] elif isinstance(args, os.PathLike): @@ -352,7 +347,7 @@ def _prepareconfig( raise -def _get_directory(path: Path) -> Path: +def _get_directory(path: pathlib.Path) -> pathlib.Path: """Get the directory of a path - itself if already a directory.""" if path.is_file(): return path.parent @@ -363,14 +358,14 @@ def _get_directory(path: Path) -> Path: def _get_legacy_hook_marks( method: Any, hook_type: str, - opt_names: Tuple[str, ...], -) -> Dict[str, bool]: + opt_names: tuple[str, ...], +) -> dict[str, bool]: if TYPE_CHECKING: # abuse typeguard from importlib to avoid massive method type union thats lacking a alias assert inspect.isroutine(method) - known_marks: Set[str] = {m.name for m in getattr(method, "pytestmark", [])} - must_warn: List[str] = [] - opts: Dict[str, bool] = {} + known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])} + must_warn: list[str] = [] + opts: dict[str, bool] = {} for opt_name in opt_names: opt_attr = getattr(method, opt_name, AttributeError) if opt_attr is not AttributeError: @@ -409,13 +404,13 @@ def __init__(self) -> None: # -- State related to local conftest plugins. # All loaded conftest modules. - self._conftest_plugins: Set[types.ModuleType] = set() + self._conftest_plugins: set[types.ModuleType] = set() # All conftest modules applicable for a directory. # This includes the directory's own conftest modules as well # as those of its parent directories. - self._dirpath2confmods: Dict[Path, List[types.ModuleType]] = {} + self._dirpath2confmods: dict[pathlib.Path, list[types.ModuleType]] = {} # Cutoff directory above which conftests are no longer discovered. - self._confcutdir: Optional[Path] = None + self._confcutdir: pathlib.Path | None = None # If set, conftest loading is skipped. self._noconftest = False @@ -429,7 +424,7 @@ def __init__(self) -> None: # previously we would issue a warning when a plugin was skipped, but # since we refactored warnings as first citizens of Config, they are # just stored here to be used later. - self.skipped_plugins: List[Tuple[str, str]] = [] + self.skipped_plugins: list[tuple[str, str]] = [] self.add_hookspecs(_pytest.hookspec) self.register(self) @@ -455,14 +450,14 @@ def __init__(self) -> None: def parse_hookimpl_opts( self, plugin: _PluggyPlugin, name: str - ) -> Optional[HookimplOpts]: + ) -> HookimplOpts | None: """:meta private:""" # pytest hooks are always prefixed with "pytest_", # so we avoid accessing possibly non-readable attributes # (see issue #1073). if not name.startswith("pytest_"): return None - # Ignore names which can not be hooks. + # Ignore names which cannot be hooks. if name == "pytest_plugins": return None @@ -479,7 +474,7 @@ def parse_hookimpl_opts( method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper") ) - def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]: + def parse_hookspec_opts(self, module_or_class, name: str) -> HookspecOpts | None: """:meta private:""" opts = super().parse_hookspec_opts(module_or_class, name) if opts is None: @@ -492,9 +487,7 @@ def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOp ) return opts - def register( - self, plugin: _PluggyPlugin, name: Optional[str] = None - ) -> Optional[str]: + def register(self, plugin: _PluggyPlugin, name: str | None = None) -> str | None: if name in _pytest.deprecated.DEPRECATED_EXTERNAL_PLUGINS: warnings.warn( PytestConfigWarning( @@ -521,14 +514,14 @@ def register( def getplugin(self, name: str): # Support deprecated naming because plugins (xdist e.g.) use it. - plugin: Optional[_PluggyPlugin] = self.get_plugin(name) + plugin: _PluggyPlugin | None = self.get_plugin(name) return plugin def hasplugin(self, name: str) -> bool: """Return whether a plugin with the given name is registered.""" return bool(self.get_plugin(name)) - def pytest_configure(self, config: "Config") -> None: + def pytest_configure(self, config: Config) -> None: """:meta private:""" # XXX now that the pluginmanager exposes hookimpl(tryfirst...) # we should remove tryfirst/trylast as markers. @@ -551,13 +544,13 @@ def pytest_configure(self, config: "Config") -> None: # def _set_initial_conftests( self, - args: Sequence[Union[str, Path]], + args: Sequence[str | pathlib.Path], pyargs: bool, noconftest: bool, - rootpath: Path, - confcutdir: Optional[Path], - invocation_dir: Path, - importmode: Union[ImportMode, str], + rootpath: pathlib.Path, + confcutdir: pathlib.Path | None, + invocation_dir: pathlib.Path, + importmode: ImportMode | str, *, consider_namespace_packages: bool, ) -> None: @@ -574,8 +567,8 @@ def _set_initial_conftests( self._noconftest = noconftest self._using_pyargs = pyargs foundanchor = False - for intitial_path in args: - path = str(intitial_path) + for initial_path in args: + path = str(initial_path) # remove node-id syntax i = path.find("::") if i != -1: @@ -600,7 +593,7 @@ def _set_initial_conftests( consider_namespace_packages=consider_namespace_packages, ) - def _is_in_confcutdir(self, path: Path) -> bool: + def _is_in_confcutdir(self, path: pathlib.Path) -> bool: """Whether to consider the given path to load conftests from.""" if self._confcutdir is None: return True @@ -617,9 +610,9 @@ def _is_in_confcutdir(self, path: Path) -> bool: def _try_load_conftest( self, - anchor: Path, - importmode: Union[str, ImportMode], - rootpath: Path, + anchor: pathlib.Path, + importmode: str | ImportMode, + rootpath: pathlib.Path, *, consider_namespace_packages: bool, ) -> None: @@ -642,9 +635,9 @@ def _try_load_conftest( def _loadconftestmodules( self, - path: Path, - importmode: Union[str, ImportMode], - rootpath: Path, + path: pathlib.Path, + importmode: str | ImportMode, + rootpath: pathlib.Path, *, consider_namespace_packages: bool, ) -> None: @@ -672,15 +665,15 @@ def _loadconftestmodules( clist.append(mod) self._dirpath2confmods[directory] = clist - def _getconftestmodules(self, path: Path) -> Sequence[types.ModuleType]: + def _getconftestmodules(self, path: pathlib.Path) -> Sequence[types.ModuleType]: directory = self._get_directory(path) return self._dirpath2confmods.get(directory, ()) def _rget_with_confmod( self, name: str, - path: Path, - ) -> Tuple[types.ModuleType, Any]: + path: pathlib.Path, + ) -> tuple[types.ModuleType, Any]: modules = self._getconftestmodules(path) for mod in reversed(modules): try: @@ -691,9 +684,9 @@ def _rget_with_confmod( def _importconftest( self, - conftestpath: Path, - importmode: Union[str, ImportMode], - rootpath: Path, + conftestpath: pathlib.Path, + importmode: str | ImportMode, + rootpath: pathlib.Path, *, consider_namespace_packages: bool, ) -> types.ModuleType: @@ -745,7 +738,7 @@ def _importconftest( def _check_non_top_pytest_plugins( self, mod: types.ModuleType, - conftestpath: Path, + conftestpath: pathlib.Path, ) -> None: if ( hasattr(mod, "pytest_plugins") @@ -798,7 +791,7 @@ def consider_pluginarg(self, arg: str) -> None: if arg.startswith("no:"): name = arg[3:] if name in essential_plugins: - raise UsageError("plugin %s cannot be disabled" % name) + raise UsageError(f"plugin {name} cannot be disabled") # PR #4304: remove stepwise if cacheprovider is blocked. if name == "cacheprovider": @@ -831,7 +824,7 @@ def consider_module(self, mod: types.ModuleType) -> None: self._import_plugin_specs(getattr(mod, "pytest_plugins", [])) def _import_plugin_specs( - self, spec: Union[None, types.ModuleType, str, Sequence[str]] + self, spec: None | types.ModuleType | str | Sequence[str] ) -> None: plugins = _get_plugin_specs_as_list(spec) for import_spec in plugins: @@ -847,9 +840,9 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No # "terminal" or "capture". Those plugins are registered under their # basename for historic purposes but must be imported with the # _pytest prefix. - assert isinstance(modname, str), ( - "module name as text required, got %r" % modname - ) + assert isinstance( + modname, str + ), f"module name as text required, got {modname!r}" if self.is_blocked(modname) or self.get_plugin(modname) is not None: return @@ -876,8 +869,8 @@ def import_plugin(self, modname: str, consider_entry_points: bool = False) -> No def _get_plugin_specs_as_list( - specs: Union[None, types.ModuleType, str, Sequence[str]], -) -> List[str]: + specs: None | types.ModuleType | str | Sequence[str], +) -> list[str]: """Parse a plugins specification into a list of plugin names.""" # None means empty. if specs is None: @@ -892,8 +885,7 @@ def _get_plugin_specs_as_list( if isinstance(specs, collections.abc.Sequence): return list(specs) raise UsageError( - "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %r" - % specs + f"Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: {specs!r}" ) @@ -999,19 +991,19 @@ class InvocationParams: Plugins accessing ``InvocationParams`` must be aware of that. """ - args: Tuple[str, ...] + args: tuple[str, ...] """The command-line arguments as passed to :func:`pytest.main`.""" - plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] + plugins: Sequence[str | _PluggyPlugin] | None """Extra plugins, might be `None`.""" - dir: Path - """The directory from which :func:`pytest.main` was invoked.""" + dir: pathlib.Path + """The directory from which :func:`pytest.main` was invoked. :type: pathlib.Path""" def __init__( self, *, args: Iterable[str], - plugins: Optional[Sequence[Union[str, _PluggyPlugin]]], - dir: Path, + plugins: Sequence[str | _PluggyPlugin] | None, + dir: pathlib.Path, ) -> None: object.__setattr__(self, "args", tuple(args)) object.__setattr__(self, "plugins", plugins) @@ -1031,18 +1023,21 @@ class ArgsSource(enum.Enum): #: 'testpaths' configuration value. TESTPATHS = enum.auto() + # Set by cacheprovider plugin. + cache: Cache + def __init__( self, pluginmanager: PytestPluginManager, *, - invocation_params: Optional[InvocationParams] = None, + invocation_params: InvocationParams | None = None, ) -> None: from .argparsing import FILE_OR_DIR from .argparsing import Parser if invocation_params is None: invocation_params = self.InvocationParams( - args=(), plugins=None, dir=Path.cwd() + args=(), plugins=None, dir=pathlib.Path.cwd() ) self.option = argparse.Namespace() @@ -1080,25 +1075,20 @@ def __init__( self.trace = self.pluginmanager.trace.root.get("config") self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment] - self._inicache: Dict[str, Any] = {} + self._inicache: dict[str, Any] = {} self._override_ini: Sequence[str] = () - self._opt2dest: Dict[str, str] = {} - self._cleanup: List[Callable[[], None]] = [] + self._opt2dest: dict[str, str] = {} + self._cleanup: list[Callable[[], None]] = [] self.pluginmanager.register(self, "pytestconfig") self._configured = False self.hook.pytest_addoption.call_historic( kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) ) self.args_source = Config.ArgsSource.ARGS - self.args: List[str] = [] - - if TYPE_CHECKING: - from _pytest.cacheprovider import Cache - - self.cache: Optional[Cache] = None + self.args: list[str] = [] @property - def rootpath(self) -> Path: + def rootpath(self) -> pathlib.Path: """The path to the :ref:`rootdir `. :type: pathlib.Path @@ -1108,11 +1098,9 @@ def rootpath(self) -> Path: return self._rootpath @property - def inipath(self) -> Optional[Path]: + def inipath(self) -> pathlib.Path | None: """The path to the :ref:`configfile `. - :type: Optional[pathlib.Path] - .. versionadded:: 6.1 """ return self._inipath @@ -1139,15 +1127,15 @@ def _ensure_unconfigure(self) -> None: fin() def get_terminal_writer(self) -> TerminalWriter: - terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin( + terminalreporter: TerminalReporter | None = self.pluginmanager.get_plugin( "terminalreporter" ) assert terminalreporter is not None return terminalreporter._tw def pytest_cmdline_parse( - self, pluginmanager: PytestPluginManager, args: List[str] - ) -> "Config": + self, pluginmanager: PytestPluginManager, args: list[str] + ) -> Config: try: self.parse(args) except UsageError: @@ -1173,10 +1161,10 @@ def pytest_cmdline_parse( def notify_exception( self, excinfo: ExceptionInfo[BaseException], - option: Optional[argparse.Namespace] = None, + option: argparse.Namespace | None = None, ) -> None: if option and getattr(option, "fulltrace", False): - style: _TracebackStyle = "long" + style: TracebackStyle = "long" else: style = "native" excrepr = excinfo.getrepr( @@ -1185,7 +1173,7 @@ def notify_exception( res = self.hook.pytest_internalerror(excrepr=excrepr, excinfo=excinfo) if not any(res): for line in str(excrepr).split("\n"): - sys.stderr.write("INTERNALERROR> %s\n" % line) + sys.stderr.write(f"INTERNALERROR> {line}\n") sys.stderr.flush() def cwd_relative_nodeid(self, nodeid: str) -> str: @@ -1196,7 +1184,7 @@ def cwd_relative_nodeid(self, nodeid: str) -> str: return nodeid @classmethod - def fromdictargs(cls, option_dict, args) -> "Config": + def fromdictargs(cls, option_dict, args) -> Config: """Constructor usable for subprocesses.""" config = get_config(args) config.option.__dict__.update(option_dict) @@ -1205,7 +1193,7 @@ def fromdictargs(cls, option_dict, args) -> "Config": config.pluginmanager.consider_pluginarg(x) return config - def _processopt(self, opt: "Argument") -> None: + def _processopt(self, opt: Argument) -> None: for name in opt._short_opts + opt._long_opts: self._opt2dest[name] = opt.dest @@ -1214,7 +1202,7 @@ def _processopt(self, opt: "Argument") -> None: setattr(self.option, opt.dest, opt.default) @hookimpl(trylast=True) - def pytest_load_initial_conftests(self, early_config: "Config") -> None: + def pytest_load_initial_conftests(self, early_config: Config) -> None: # 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. @@ -1292,7 +1280,8 @@ def _mark_plugins_for_rewrite(self, hook) -> None: self.pluginmanager.rewrite_hook = hook if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): - # We don't autoload from setuptools entry points, no need to continue. + # We don't autoload from distribution package entry points, + # no need to continue. return package_files = ( @@ -1305,7 +1294,7 @@ def _mark_plugins_for_rewrite(self, hook) -> None: for name in _iter_rewritable_modules(package_files): hook.mark_rewrite(name) - def _validate_args(self, args: List[str], via: str) -> List[str]: + def _validate_args(self, args: list[str], via: str) -> list[str]: """Validate known args.""" self._parser._config_source_hint = via # type: ignore try: @@ -1320,13 +1309,13 @@ def _validate_args(self, args: List[str], via: str) -> List[str]: def _decide_args( self, *, - args: List[str], + args: list[str], pyargs: bool, - testpaths: List[str], - invocation_dir: Path, - rootpath: Path, + testpaths: list[str], + invocation_dir: pathlib.Path, + rootpath: pathlib.Path, warn: bool, - ) -> Tuple[List[str], ArgsSource]: + ) -> tuple[list[str], ArgsSource]: """Decide the args (initial paths/nodeids) to use given the relevant inputs. :param warn: Whether can issue warnings. @@ -1362,7 +1351,7 @@ def _decide_args( result = [str(invocation_dir)] return result, source - def _preparse(self, args: List[str], addopts: bool = True) -> None: + def _preparse(self, args: list[str], addopts: bool = True) -> None: if addopts: env_addopts = os.environ.get("PYTEST_ADDOPTS", "") if len(env_addopts): @@ -1383,8 +1372,8 @@ def _preparse(self, args: List[str], addopts: bool = True) -> None: self._consider_importhook(args) self.pluginmanager.consider_preparse(args, exclude_only=False) if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): - # Don't autoload from setuptools entry point. Only explicitly specified - # plugins are going to be loaded. + # Don't autoload from distribution package entry point. Only + # explicitly specified plugins are going to be loaded. self.pluginmanager.load_setuptools_entrypoints("pytest11") self.pluginmanager.consider_env() @@ -1435,7 +1424,7 @@ def _checkversion(self) -> None: if not isinstance(minver, str): raise pytest.UsageError( - "%s: 'minversion' must be a single value" % self.inipath + f"{self.inipath}: 'minversion' must be a single value" ) if Version(minver) > Version(pytest.__version__): @@ -1486,11 +1475,11 @@ def _warn_or_fail_if_strict(self, message: str) -> None: self.issue_config_time_warning(PytestConfigWarning(message), stacklevel=3) - def _get_unknown_ini_keys(self) -> List[str]: + def _get_unknown_ini_keys(self) -> list[str]: parser_inicfg = self._parser._inidict return [name for name in self.inicfg if name not in parser_inicfg] - def parse(self, args: List[str], addopts: bool = True) -> None: + def parse(self, args: list[str], addopts: bool = True) -> None: # Parse given cmdline arguments into this config object. assert ( self.args == [] @@ -1594,7 +1583,7 @@ def getini(self, name: str): # Meant for easy monkeypatching by legacypath plugin. # Can be inlined back (with no cover removed) once legacypath is gone. - def _getini_unknown_type(self, name: str, type: str, value: Union[str, List[str]]): + def _getini_unknown_type(self, name: str, type: str, value: str | list[str]): msg = f"unknown configuration type: {type}" raise ValueError(msg, value) # pragma: no cover @@ -1650,24 +1639,26 @@ def _getini(self, name: str): else: return self._getini_unknown_type(name, type, value) - def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]: + def _getconftest_pathlist( + self, name: str, path: pathlib.Path + ) -> list[pathlib.Path] | None: try: mod, relroots = self.pluginmanager._rget_with_confmod(name, path) except KeyError: return None assert mod.__file__ is not None - modpath = Path(mod.__file__).parent - values: List[Path] = [] + modpath = pathlib.Path(mod.__file__).parent + values: list[pathlib.Path] = [] for relroot in relroots: if isinstance(relroot, os.PathLike): - relroot = Path(relroot) + relroot = pathlib.Path(relroot) else: relroot = relroot.replace("/", os.sep) relroot = absolutepath(modpath / relroot) values.append(relroot) return values - def _get_override_ini_value(self, name: str) -> Optional[str]: + def _get_override_ini_value(self, name: str) -> str | None: value = None # override_ini is a list of "ini=value" options. # Always use the last item if multiple values are set for same ini-name, @@ -1722,7 +1713,7 @@ def getvalueorskip(self, name: str, path=None): VERBOSITY_TEST_CASES: Final = "test_cases" _VERBOSITY_INI_DEFAULT: Final = "auto" - def get_verbosity(self, verbosity_type: Optional[str] = None) -> int: + def get_verbosity(self, verbosity_type: str | None = None) -> int: r"""Retrieve the verbosity level for a fine-grained verbosity type. :param verbosity_type: Verbosity type to get level for. If a level is @@ -1737,6 +1728,7 @@ def get_verbosity(self, verbosity_type: Optional[str] = None) -> int: can be used to explicitly use the global verbosity level. Example: + .. code-block:: ini # content of pytest.ini @@ -1772,7 +1764,7 @@ def _verbosity_ini_name(verbosity_type: str) -> str: return f"verbosity_{verbosity_type}" @staticmethod - def _add_verbosity_ini(parser: "Parser", verbosity_type: str, help: str) -> None: + def _add_verbosity_ini(parser: Parser, verbosity_type: str, help: str) -> None: """Add a output verbosity configuration option for the given output type. :param parser: Parser for command line arguments and ini-file values. @@ -1828,7 +1820,7 @@ def _assertion_supported() -> bool: def create_terminal_writer( - config: Config, file: Optional[TextIO] = None + config: Config, file: TextIO | None = None ) -> TerminalWriter: """Create a TerminalWriter instance configured according to the options in the config object. @@ -1872,7 +1864,7 @@ def _strtobool(val: str) -> bool: @lru_cache(maxsize=50) def parse_warning_filter( arg: str, *, escape: bool -) -> Tuple["warnings._ActionKind", str, Type[Warning], str, int]: +) -> tuple[warnings._ActionKind, str, type[Warning], str, int]: """Parse a warnings filter string. This is copied from warnings._setoption with the following changes: @@ -1914,11 +1906,11 @@ def parse_warning_filter( parts.append("") action_, message, category_, module, lineno_ = (s.strip() for s in parts) try: - action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined] + action: warnings._ActionKind = warnings._getaction(action_) # type: ignore[attr-defined] except warnings._OptionError as e: raise UsageError(error_template.format(error=str(e))) from None try: - category: Type[Warning] = _resolve_warning_category(category_) + category: type[Warning] = _resolve_warning_category(category_) except Exception: exc_info = ExceptionInfo.from_current() exception_text = exc_info.getrepr(style="native") @@ -1941,7 +1933,7 @@ def parse_warning_filter( return action, message, category, module, lineno -def _resolve_warning_category(category: str) -> Type[Warning]: +def _resolve_warning_category(category: str) -> type[Warning]: """ Copied from warnings._getcategory, but changed so it lets exceptions (specially ImportErrors) propagate so we can get access to their tracebacks (#9218). diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 9006351af72..85aa4632702 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import argparse from gettext import gettext import os @@ -6,16 +8,12 @@ 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 Union import _pytest._io from _pytest.config.exceptions import UsageError @@ -41,32 +39,32 @@ class Parser: there's an error processing the command line arguments. """ - prog: Optional[str] = None + prog: str | None = None def __init__( self, - usage: Optional[str] = None, - processopt: Optional[Callable[["Argument"], None]] = None, + usage: str | None = None, + processopt: Callable[[Argument], None] | None = None, *, _ispytest: bool = False, ) -> None: check_ispytest(_ispytest) self._anonymous = OptionGroup("Custom options", parser=self, _ispytest=True) - self._groups: List[OptionGroup] = [] + self._groups: list[OptionGroup] = [] self._processopt = processopt self._usage = usage - self._inidict: Dict[str, Tuple[str, Optional[str], Any]] = {} - self._ininames: List[str] = [] - self.extra_info: Dict[str, Any] = {} + self._inidict: dict[str, tuple[str, str | None, Any]] = {} + self._ininames: list[str] = [] + self.extra_info: dict[str, Any] = {} - def processoption(self, option: "Argument") -> None: + def processoption(self, option: Argument) -> None: if self._processopt: if option.dest: self._processopt(option) def getgroup( - self, name: str, description: str = "", after: Optional[str] = None - ) -> "OptionGroup": + self, name: str, description: str = "", after: str | None = None + ) -> OptionGroup: """Get (or create) a named option Group. :param name: Name of the option group. @@ -108,8 +106,8 @@ def addoption(self, *opts: str, **attrs: Any) -> None: def parse( self, - args: Sequence[Union[str, "os.PathLike[str]"]], - namespace: Optional[argparse.Namespace] = None, + args: Sequence[str | os.PathLike[str]], + namespace: argparse.Namespace | None = None, ) -> argparse.Namespace: from _pytest._argcomplete import try_argcomplete @@ -118,7 +116,7 @@ def parse( strargs = [os.fspath(x) for x in args] return self.optparser.parse_args(strargs, namespace=namespace) - def _getparser(self) -> "MyOptionParser": + def _getparser(self) -> MyOptionParser: from _pytest._argcomplete import filescompleter optparser = MyOptionParser(self, self.extra_info, prog=self.prog) @@ -139,10 +137,10 @@ def _getparser(self) -> "MyOptionParser": def parse_setoption( self, - args: Sequence[Union[str, "os.PathLike[str]"]], + args: Sequence[str | os.PathLike[str]], option: argparse.Namespace, - namespace: Optional[argparse.Namespace] = None, - ) -> List[str]: + namespace: argparse.Namespace | None = None, + ) -> list[str]: parsedoption = self.parse(args, namespace=namespace) for name, value in parsedoption.__dict__.items(): setattr(option, name, value) @@ -150,8 +148,8 @@ def parse_setoption( def parse_known_args( self, - args: Sequence[Union[str, "os.PathLike[str]"]], - namespace: Optional[argparse.Namespace] = None, + args: Sequence[str | os.PathLike[str]], + namespace: argparse.Namespace | None = None, ) -> argparse.Namespace: """Parse the known arguments at this point. @@ -161,9 +159,9 @@ def parse_known_args( def parse_known_and_unknown_args( self, - args: Sequence[Union[str, "os.PathLike[str]"]], - namespace: Optional[argparse.Namespace] = None, - ) -> Tuple[argparse.Namespace, List[str]]: + args: Sequence[str | os.PathLike[str]], + namespace: argparse.Namespace | None = None, + ) -> tuple[argparse.Namespace, list[str]]: """Parse the known arguments at this point, and also return the remaining unknown arguments. @@ -179,9 +177,8 @@ def addini( self, name: str, help: str, - type: Optional[ - Literal["string", "paths", "pathlist", "args", "linelist", "bool"] - ] = None, + type: Literal["string", "paths", "pathlist", "args", "linelist", "bool"] + | None = None, default: Any = NOT_SET, ) -> None: """Register an ini-file option. @@ -224,7 +221,7 @@ def addini( def get_ini_default_for_type( - type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]], + type: Literal["string", "paths", "pathlist", "args", "linelist", "bool"] | None, ) -> Any: """ Used by addini to get the default value for a given ini-option type, when @@ -244,7 +241,7 @@ class ArgumentError(Exception): """Raised if an Argument instance is created with invalid or inconsistent arguments.""" - def __init__(self, msg: str, option: Union["Argument", str]) -> None: + def __init__(self, msg: str, option: Argument | str) -> None: self.msg = msg self.option_id = str(option) @@ -267,8 +264,8 @@ class Argument: def __init__(self, *names: str, **attrs: Any) -> None: """Store params in private vars for use in add_argument.""" self._attrs = attrs - self._short_opts: List[str] = [] - self._long_opts: List[str] = [] + self._short_opts: list[str] = [] + self._long_opts: list[str] = [] try: self.type = attrs["type"] except KeyError: @@ -279,7 +276,7 @@ def __init__(self, *names: str, **attrs: Any) -> None: except KeyError: pass self._set_opt_strings(names) - dest: Optional[str] = attrs.get("dest") + dest: str | None = attrs.get("dest") if dest: self.dest = dest elif self._long_opts: @@ -291,7 +288,7 @@ def __init__(self, *names: str, **attrs: Any) -> None: self.dest = "???" # Needed for the error repr. raise ArgumentError("need a long or short option", self) from e - def names(self) -> List[str]: + def names(self) -> list[str]: return self._short_opts + self._long_opts def attrs(self) -> Mapping[str, Any]: @@ -313,29 +310,29 @@ def _set_opt_strings(self, opts: Sequence[str]) -> None: for opt in opts: if len(opt) < 2: raise ArgumentError( - "invalid option string %r: " - "must be at least two characters long" % opt, + f"invalid option string {opt!r}: " + "must be at least two characters long", self, ) elif len(opt) == 2: if not (opt[0] == "-" and opt[1] != "-"): raise ArgumentError( - "invalid short option string %r: " - "must be of the form -x, (x any non-dash char)" % opt, + f"invalid short option string {opt!r}: " + "must be of the form -x, (x any non-dash char)", self, ) self._short_opts.append(opt) else: if not (opt[0:2] == "--" and opt[2] != "-"): raise ArgumentError( - "invalid long option string %r: " - "must start with --, followed by non-dash" % opt, + f"invalid long option string {opt!r}: " + "must start with --, followed by non-dash", self, ) self._long_opts.append(opt) def __repr__(self) -> str: - args: List[str] = [] + args: list[str] = [] if self._short_opts: args += ["_short_opts: " + repr(self._short_opts)] if self._long_opts: @@ -355,14 +352,14 @@ def __init__( self, name: str, description: str = "", - parser: Optional[Parser] = None, + parser: Parser | None = None, *, _ispytest: bool = False, ) -> None: check_ispytest(_ispytest) self.name = name self.description = description - self.options: List[Argument] = [] + self.options: list[Argument] = [] self.parser = parser def addoption(self, *opts: str, **attrs: Any) -> None: @@ -383,7 +380,7 @@ def addoption(self, *opts: str, **attrs: Any) -> None: name for opt in self.options for name in opt.names() ) if conflict: - raise ValueError("option names %s already added" % conflict) + raise ValueError(f"option names {conflict} already added") option = Argument(*opts, **attrs) self._addoption_instance(option, shortupper=False) @@ -391,7 +388,7 @@ def _addoption(self, *opts: str, **attrs: Any) -> None: option = Argument(*opts, **attrs) self._addoption_instance(option, shortupper=True) - def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None: + def _addoption_instance(self, option: Argument, shortupper: bool = False) -> None: if not shortupper: for opt in option._short_opts: if opt[0] == "-" and opt[1].islower(): @@ -405,8 +402,8 @@ class MyOptionParser(argparse.ArgumentParser): def __init__( self, parser: Parser, - extra_info: Optional[Dict[str, Any]] = None, - prog: Optional[str] = None, + extra_info: dict[str, Any] | None = None, + prog: str | None = None, ) -> None: self._parser = parser super().__init__( @@ -433,15 +430,17 @@ def error(self, message: str) -> NoReturn: # Type ignored because typeshed has a very complex type in the superclass. def parse_args( # type: ignore self, - args: Optional[Sequence[str]] = None, - namespace: Optional[argparse.Namespace] = None, + args: Sequence[str] | None = None, + namespace: argparse.Namespace | None = None, ) -> argparse.Namespace: """Allow splitting of positional arguments.""" parsed, unrecognized = self.parse_known_args(args, namespace) if unrecognized: for arg in unrecognized: if arg and arg[0] == "-": - lines = ["unrecognized arguments: %s" % (" ".join(unrecognized))] + lines = [ + "unrecognized arguments: {}".format(" ".join(unrecognized)) + ] for k, v in sorted(self.extra_info.items()): lines.append(f" {k}: {v}") self.error("\n".join(lines)) @@ -453,7 +452,7 @@ def parse_args( # type: ignore # disable long --argument abbreviations without breaking short flags. def _parse_optional( self, arg_string: str - ) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]: + ) -> tuple[argparse.Action | None, str, str | None] | None: if not arg_string: return None if arg_string[0] not in self.prefix_chars: @@ -505,7 +504,7 @@ def _format_action_invocation(self, action: argparse.Action) -> str: orgstr = super()._format_action_invocation(action) if orgstr and orgstr[0] != "-": # only optional arguments return orgstr - res: Optional[str] = getattr(action, "_formatted_action_invocation", None) + res: str | None = getattr(action, "_formatted_action_invocation", None) if res: return res options = orgstr.split(", ") @@ -514,13 +513,13 @@ def _format_action_invocation(self, action: argparse.Action) -> str: action._formatted_action_invocation = orgstr # type: ignore return orgstr return_list = [] - short_long: Dict[str, str] = {} + short_long: dict[str, str] = {} for option in options: if len(option) == 2 or option[2] == " ": continue if not option.startswith("--"): raise ArgumentError( - 'long optional argument without "--": [%s]' % (option), option + f'long optional argument without "--": [{option}]', option ) xxoption = option[2:] shortened = xxoption.replace("-", "") diff --git a/src/_pytest/config/exceptions.py b/src/_pytest/config/exceptions.py index 4031ea732f3..90108eca904 100644 --- a/src/_pytest/config/exceptions.py +++ b/src/_pytest/config/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from typing import final diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index 9909376de0f..ce4c990b810 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -1,13 +1,10 @@ +from __future__ import annotations + import os from pathlib import Path import sys -from typing import Dict from typing import Iterable -from typing import List -from typing import Optional from typing import Sequence -from typing import Tuple -from typing import Union import iniconfig @@ -32,7 +29,7 @@ def _parse_ini_config(path: Path) -> iniconfig.IniConfig: def load_config_dict_from_file( filepath: Path, -) -> Optional[Dict[str, Union[str, List[str]]]]: +) -> dict[str, str | list[str]] | None: """Load pytest configuration from the given file path, if supported. Return None if the file does not contain valid pytest configuration. @@ -77,7 +74,7 @@ def load_config_dict_from_file( # TOML supports richer data types than ini files (strings, arrays, floats, ints, etc), # however we need to convert all scalar values to str for compatibility with the rest # of the configuration system, which expects strings only. - def make_scalar(v: object) -> Union[str, List[str]]: + def make_scalar(v: object) -> str | list[str]: return v if isinstance(v, list) else str(v) return {k: make_scalar(v) for k, v in result.items()} @@ -88,7 +85,7 @@ def make_scalar(v: object) -> Union[str, List[str]]: def locate_config( invocation_dir: Path, args: Iterable[Path], -) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]: +) -> tuple[Path | None, Path | None, dict[str, str | list[str]]]: """Search in the list of arguments for a valid ini-file for pytest, and return a tuple of (rootdir, inifile, cfg-dict).""" config_names = [ @@ -101,7 +98,7 @@ def locate_config( args = [x for x in args if not str(x).startswith("-")] if not args: args = [invocation_dir] - found_pyproject_toml: Optional[Path] = None + found_pyproject_toml: Path | None = None for arg in args: argpath = absolutepath(arg) for base in (argpath, *argpath.parents): @@ -122,7 +119,7 @@ def get_common_ancestor( invocation_dir: Path, paths: Iterable[Path], ) -> Path: - common_ancestor: Optional[Path] = None + common_ancestor: Path | None = None for path in paths: if not path.exists(): continue @@ -144,7 +141,7 @@ def get_common_ancestor( return common_ancestor -def get_dirs_from_args(args: Iterable[str]) -> List[Path]: +def get_dirs_from_args(args: Iterable[str]) -> list[Path]: def is_option(x: str) -> bool: return x.startswith("-") @@ -171,11 +168,11 @@ def get_dir_from_path(path: Path) -> Path: def determine_setup( *, - inifile: Optional[str], + inifile: str | None, args: Sequence[str], - rootdir_cmd_arg: Optional[str], + rootdir_cmd_arg: str | None, invocation_dir: Path, -) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]: +) -> tuple[Path, Path | None, dict[str, str | list[str]]]: """Determine the rootdir, inifile and ini configuration values from the command line arguments. @@ -192,7 +189,7 @@ def determine_setup( dirs = get_dirs_from_args(args) if inifile: inipath_ = absolutepath(inifile) - inipath: Optional[Path] = inipath_ + inipath: Path | None = inipath_ inicfg = load_config_dict_from_file(inipath_) or {} if rootdir_cmd_arg is None: rootdir = inipath_.parent diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 6ed0c5c7aee..3e1463fff26 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -1,6 +1,9 @@ # mypy: allow-untyped-defs +# ruff: noqa: T100 """Interactive debugging with PDB, the Python Debugger.""" +from __future__ import annotations + import argparse import functools import sys @@ -8,16 +11,11 @@ from typing import Any from typing import Callable from typing import Generator -from typing import List -from typing import Optional -from typing import Tuple -from typing import Type -from typing import TYPE_CHECKING -from typing import Union import unittest from _pytest import outcomes from _pytest._code import ExceptionInfo +from _pytest.capture import CaptureManager from _pytest.config import Config from _pytest.config import ConftestImportFailure from _pytest.config import hookimpl @@ -26,14 +24,10 @@ from _pytest.config.exceptions import UsageError from _pytest.nodes import Node from _pytest.reports import BaseReport +from _pytest.runner import CallInfo -if TYPE_CHECKING: - from _pytest.capture import CaptureManager - from _pytest.runner import CallInfo - - -def _validate_usepdb_cls(value: str) -> Tuple[str, str]: +def _validate_usepdb_cls(value: str) -> tuple[str, str]: """Validate syntax of --pdbcls option.""" try: modname, classname = value.split(":") @@ -98,22 +92,22 @@ def fin() -> None: class pytestPDB: """Pseudo PDB that defers to the real pdb.""" - _pluginmanager: Optional[PytestPluginManager] = None - _config: Optional[Config] = None - _saved: List[ - Tuple[Callable[..., None], Optional[PytestPluginManager], Optional[Config]] + _pluginmanager: PytestPluginManager | None = None + _config: Config | None = None + _saved: list[ + tuple[Callable[..., None], PytestPluginManager | None, Config | None] ] = [] _recursive_debug = 0 - _wrapped_pdb_cls: Optional[Tuple[Type[Any], Type[Any]]] = None + _wrapped_pdb_cls: tuple[type[Any], type[Any]] | None = None @classmethod - def _is_capturing(cls, capman: Optional["CaptureManager"]) -> Union[str, bool]: + def _is_capturing(cls, capman: CaptureManager | None) -> str | bool: if capman: return capman.is_capturing() return False @classmethod - def _import_pdb_cls(cls, capman: Optional["CaptureManager"]): + def _import_pdb_cls(cls, capman: CaptureManager | None): if not cls._config: import pdb @@ -152,7 +146,7 @@ def _import_pdb_cls(cls, capman: Optional["CaptureManager"]): return wrapped_cls @classmethod - def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]): + def _get_pdb_wrapper_class(cls, pdb_cls, capman: CaptureManager | None): import _pytest.config class PytestPdbWrapper(pdb_cls): @@ -180,8 +174,7 @@ def do_continue(self, arg): else: tw.sep( ">", - "PDB continue (IO-capturing resumed for %s)" - % capturing, + f"PDB continue (IO-capturing resumed for {capturing})", ) assert capman is not None capman.resume() @@ -242,7 +235,7 @@ def _init_pdb(cls, method, *args, **kwargs): import _pytest.config if cls._pluginmanager is None: - capman: Optional[CaptureManager] = None + capman: CaptureManager | None = None else: capman = cls._pluginmanager.getplugin("capturemanager") if capman: @@ -285,7 +278,7 @@ def set_trace(cls, *args, **kwargs) -> None: class PdbInvoke: def pytest_exception_interact( - self, node: Node, call: "CallInfo[Any]", report: BaseReport + self, node: Node, call: CallInfo[Any], report: BaseReport ) -> None: capman = node.config.pluginmanager.getplugin("capturemanager") if capman: @@ -310,7 +303,7 @@ def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]: return (yield) -def wrap_pytest_function_for_tracing(pyfuncitem): +def wrap_pytest_function_for_tracing(pyfuncitem) -> None: """Change the Python function object of the given Function item by a wrapper which actually enters pdb before calling the python function itself, effectively leaving the user in the pdb prompt in the first @@ -322,14 +315,14 @@ def wrap_pytest_function_for_tracing(pyfuncitem): # python < 3.7.4) runcall's first param is `func`, which means we'd get # an exception if one of the kwargs to testfunction was called `func`. @functools.wraps(testfunction) - def wrapper(*args, **kwargs): + def wrapper(*args, **kwargs) -> None: func = functools.partial(testfunction, *args, **kwargs) _pdb.runcall(func) pyfuncitem.obj = wrapper -def maybe_wrap_pytest_function_for_tracing(pyfuncitem): +def maybe_wrap_pytest_function_for_tracing(pyfuncitem) -> None: """Wrap the given pytestfunct item for tracing support if --trace was given in the command line.""" if pyfuncitem.config.getvalue("trace"): diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 10811d158aa..a605c24e58f 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -9,6 +9,8 @@ in case of warnings which need to format their messages. """ +from __future__ import annotations + from warnings import warn from _pytest.warning_types import PytestDeprecationWarning diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 7fff99f37b5..cb46d9a3bb5 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Discover and run doctests in modules and test files.""" +from __future__ import annotations + import bdb from contextlib import contextmanager import functools @@ -13,17 +15,11 @@ import types from typing import Any from typing import Callable -from typing import Dict from typing import Generator from typing import Iterable -from typing import List -from typing import Optional from typing import Pattern from typing import Sequence -from typing import Tuple -from typing import Type from typing import TYPE_CHECKING -from typing import Union import warnings from _pytest import outcomes @@ -67,7 +63,7 @@ # Lazy definition of runner class RUNNER_CLASS = None # Lazy definition of output checker class -CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None +CHECKER_CLASS: type[doctest.OutputChecker] | None = None def pytest_addoption(parser: Parser) -> None: @@ -129,7 +125,7 @@ def pytest_unconfigure() -> None: def pytest_collect_file( file_path: Path, parent: Collector, -) -> Optional[Union["DoctestModule", "DoctestTextfile"]]: +) -> DoctestModule | DoctestTextfile | None: config = parent.config if file_path.suffix == ".py": if config.option.doctestmodules and not any( @@ -161,7 +157,7 @@ def _is_main_py(path: Path) -> bool: class ReprFailDoctest(TerminalRepr): def __init__( - self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]] + self, reprlocation_lines: Sequence[tuple[ReprFileLocation, Sequence[str]]] ) -> None: self.reprlocation_lines = reprlocation_lines @@ -173,12 +169,12 @@ def toterminal(self, tw: TerminalWriter) -> None: class MultipleDoctestFailures(Exception): - def __init__(self, failures: Sequence["doctest.DocTestFailure"]) -> None: + def __init__(self, failures: Sequence[doctest.DocTestFailure]) -> None: super().__init__() self.failures = failures -def _init_runner_class() -> Type["doctest.DocTestRunner"]: +def _init_runner_class() -> type[doctest.DocTestRunner]: import doctest class PytestDoctestRunner(doctest.DebugRunner): @@ -190,8 +186,8 @@ class PytestDoctestRunner(doctest.DebugRunner): def __init__( self, - checker: Optional["doctest.OutputChecker"] = None, - verbose: Optional[bool] = None, + checker: doctest.OutputChecker | None = None, + verbose: bool | None = None, optionflags: int = 0, continue_on_failure: bool = True, ) -> None: @@ -201,8 +197,8 @@ def __init__( def report_failure( self, out, - test: "doctest.DocTest", - example: "doctest.Example", + test: doctest.DocTest, + example: doctest.Example, got: str, ) -> None: failure = doctest.DocTestFailure(test, example, got) @@ -214,9 +210,9 @@ def report_failure( def report_unexpected_exception( self, out, - test: "doctest.DocTest", - example: "doctest.Example", - exc_info: Tuple[Type[BaseException], BaseException, types.TracebackType], + test: doctest.DocTest, + example: doctest.Example, + exc_info: tuple[type[BaseException], BaseException, types.TracebackType], ) -> None: if isinstance(exc_info[1], OutcomeException): raise exc_info[1] @@ -232,11 +228,11 @@ def report_unexpected_exception( def _get_runner( - checker: Optional["doctest.OutputChecker"] = None, - verbose: Optional[bool] = None, + checker: doctest.OutputChecker | None = None, + verbose: bool | None = None, optionflags: int = 0, continue_on_failure: bool = True, -) -> "doctest.DocTestRunner": +) -> doctest.DocTestRunner: # We need this in order to do a lazy import on doctest global RUNNER_CLASS if RUNNER_CLASS is None: @@ -255,9 +251,9 @@ class DoctestItem(Item): def __init__( self, name: str, - parent: "Union[DoctestTextfile, DoctestModule]", - runner: "doctest.DocTestRunner", - dtest: "doctest.DocTest", + parent: DoctestTextfile | DoctestModule, + runner: doctest.DocTestRunner, + dtest: doctest.DocTest, ) -> None: super().__init__(name, parent) self.runner = runner @@ -274,18 +270,18 @@ def __init__( @classmethod def from_parent( # type: ignore[override] cls, - parent: "Union[DoctestTextfile, DoctestModule]", + parent: DoctestTextfile | DoctestModule, *, name: str, - runner: "doctest.DocTestRunner", - dtest: "doctest.DocTest", - ) -> "Self": + runner: doctest.DocTestRunner, + dtest: doctest.DocTest, + ) -> Self: # incompatible signature due to imposed limits on subclass """The public named constructor.""" return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) def _initrequest(self) -> None: - self.funcargs: Dict[str, object] = {} + self.funcargs: dict[str, object] = {} self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type] def setup(self) -> None: @@ -298,7 +294,7 @@ def setup(self) -> None: def runtest(self) -> None: _check_all_skipped(self.dtest) self._disable_output_capturing_for_darwin() - failures: List["doctest.DocTestFailure"] = [] + failures: list[doctest.DocTestFailure] = [] # Type ignored because we change the type of `out` from what # doctest expects. self.runner.run(self.dtest, out=failures) # type: ignore[arg-type] @@ -320,12 +316,12 @@ def _disable_output_capturing_for_darwin(self) -> None: def repr_failure( # type: ignore[override] self, excinfo: ExceptionInfo[BaseException], - ) -> Union[str, TerminalRepr]: + ) -> str | TerminalRepr: import doctest - failures: Optional[ - Sequence[Union[doctest.DocTestFailure, doctest.UnexpectedException]] - ] = None + failures: ( + Sequence[doctest.DocTestFailure | doctest.UnexpectedException] | None + ) = None if isinstance( excinfo.value, (doctest.DocTestFailure, doctest.UnexpectedException) ): @@ -374,18 +370,18 @@ def repr_failure( # type: ignore[override] ).split("\n") else: inner_excinfo = ExceptionInfo.from_exc_info(failure.exc_info) - lines += ["UNEXPECTED EXCEPTION: %s" % repr(inner_excinfo.value)] + lines += [f"UNEXPECTED EXCEPTION: {inner_excinfo.value!r}"] lines += [ x.strip("\n") for x in traceback.format_exception(*failure.exc_info) ] reprlocation_lines.append((reprlocation, lines)) return ReprFailDoctest(reprlocation_lines) - def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: - return self.path, self.dtest.lineno, "[doctest] %s" % self.name + def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]: + return self.path, self.dtest.lineno, f"[doctest] {self.name}" -def _get_flag_lookup() -> Dict[str, int]: +def _get_flag_lookup() -> dict[str, int]: import doctest return dict( @@ -451,7 +447,7 @@ def collect(self) -> Iterable[DoctestItem]: ) -def _check_all_skipped(test: "doctest.DocTest") -> None: +def _check_all_skipped(test: doctest.DocTest) -> None: """Raise pytest.skip() if all examples in the given DocTest have the SKIP option set.""" import doctest @@ -477,7 +473,7 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]: real_unwrap = inspect.unwrap def _mock_aware_unwrap( - func: Callable[..., Any], *, stop: Optional[Callable[[Any], Any]] = None + func: Callable[..., Any], *, stop: Callable[[Any], Any] | None = None ) -> Any: try: if stop is None or stop is _is_mocked: @@ -505,43 +501,52 @@ def collect(self) -> Iterable[DoctestItem]: import doctest class MockAwareDocTestFinder(doctest.DocTestFinder): - """A hackish doctest finder that overrides stdlib internals to fix a stdlib bug. - - https://github.com/pytest-dev/pytest/issues/3456 - https://bugs.python.org/issue25532 - """ - - def _find_lineno(self, obj, source_lines): - """Doctest code does not take into account `@property`, this - is a hackish way to fix it. https://bugs.python.org/issue17446 - - Wrapped Doctests will need to be unwrapped so the correct - line number is returned. This will be reported upstream. #8796 - """ - if isinstance(obj, property): - obj = getattr(obj, "fget", obj) - - if hasattr(obj, "__wrapped__"): - # Get the main obj in case of it being wrapped - obj = inspect.unwrap(obj) - - # Type ignored because this is a private function. - return super()._find_lineno( # type:ignore[misc] - obj, - source_lines, - ) + py_ver_info_minor = sys.version_info[:2] + is_find_lineno_broken = ( + py_ver_info_minor < (3, 11) + or (py_ver_info_minor == (3, 11) and sys.version_info.micro < 9) + or (py_ver_info_minor == (3, 12) and sys.version_info.micro < 3) + ) + if is_find_lineno_broken: + + def _find_lineno(self, obj, source_lines): + """On older Pythons, doctest code does not take into account + `@property`. https://github.com/python/cpython/issues/61648 + + Moreover, wrapped Doctests need to be unwrapped so the correct + line number is returned. #8796 + """ + if isinstance(obj, property): + obj = getattr(obj, "fget", obj) + + if hasattr(obj, "__wrapped__"): + # Get the main obj in case of it being wrapped + obj = inspect.unwrap(obj) - def _find( - self, tests, obj, name, module, source_lines, globs, seen - ) -> None: - if _is_mocked(obj): - return - with _patch_unwrap_mock_aware(): # Type ignored because this is a private function. - super()._find( # type:ignore[misc] - tests, obj, name, module, source_lines, globs, seen + return super()._find_lineno( # type:ignore[misc] + obj, + source_lines, ) + if sys.version_info < (3, 10): + + def _find( + self, tests, obj, name, module, source_lines, globs, seen + ) -> None: + """Override _find to work around issue in stdlib. + + https://github.com/pytest-dev/pytest/issues/3456 + https://github.com/python/cpython/issues/69718 + """ + if _is_mocked(obj): + return # pragma: no cover + with _patch_unwrap_mock_aware(): + # Type ignored because this is a private function. + super()._find( # type:ignore[misc] + tests, obj, name, module, source_lines, globs, seen + ) + if sys.version_info < (3, 13): def _from_module(self, module, object): @@ -556,14 +561,11 @@ def _from_module(self, module, object): # Type ignored because this is a private function. return super()._from_module(module, object) # type: ignore[misc] - else: # pragma: no cover - pass - try: module = self.obj except Collector.CollectError: if self.config.getvalue("doctest_ignore_import_errors"): - skip("unable to import module %r" % self.path) + skip(f"unable to import module {self.path!r}") else: raise @@ -588,7 +590,7 @@ def _from_module(self, module, object): ) -def _init_checker_class() -> Type["doctest.OutputChecker"]: +def _init_checker_class() -> type[doctest.OutputChecker]: import doctest import re @@ -656,8 +658,8 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: return got offset = 0 for w, g in zip(wants, gots): - fraction: Optional[str] = w.group("fraction") - exponent: Optional[str] = w.group("exponent1") + fraction: str | None = w.group("fraction") + exponent: str | None = w.group("exponent1") if exponent is None: exponent = w.group("exponent2") precision = 0 if fraction is None else len(fraction) @@ -676,7 +678,7 @@ def _remove_unwanted_precision(self, want: str, got: str) -> str: return LiteralsOutputChecker -def _get_checker() -> "doctest.OutputChecker": +def _get_checker() -> doctest.OutputChecker: """Return a doctest.OutputChecker subclass that supports some additional options: @@ -735,7 +737,7 @@ def _get_report_choice(key: str) -> int: @fixture(scope="session") -def doctest_namespace() -> Dict[str, Any]: +def doctest_namespace() -> dict[str, Any]: """Fixture that returns a :py:class:`dict` that will be injected into the namespace of doctests. diff --git a/src/_pytest/faulthandler.py b/src/_pytest/faulthandler.py index 083bcb83739..07e60f03fc9 100644 --- a/src/_pytest/faulthandler.py +++ b/src/_pytest/faulthandler.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import sys from typing import Generator diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 09fd07422fc..7d0b40b150a 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import abc from collections import defaultdict from collections import deque @@ -8,6 +10,7 @@ import os from pathlib import Path import sys +import types from typing import AbstractSet from typing import Any from typing import Callable @@ -19,13 +22,13 @@ from typing import Generic from typing import Iterable from typing import Iterator -from typing import List +from typing import Mapping from typing import MutableMapping from typing import NoReturn from typing import Optional +from typing import OrderedDict from typing import overload from typing import Sequence -from typing import Set from typing import Tuple from typing import TYPE_CHECKING from typing import TypeVar @@ -57,6 +60,7 @@ from _pytest.deprecated import check_ispytest from _pytest.deprecated import MARKED_FIXTURE from _pytest.deprecated import YIELD_FIXTURE +from _pytest.main import Session from _pytest.mark import Mark from _pytest.mark import ParameterSet from _pytest.mark.structures import MarkDecorator @@ -75,9 +79,6 @@ if TYPE_CHECKING: - from typing import Deque - - from _pytest.main import Session from _pytest.python import CallSpec2 from _pytest.python import Function from _pytest.python import Metafunc @@ -104,26 +105,26 @@ None, # Cache key. object, - # Exception if raised. - BaseException, + # The exception and the original traceback. + Tuple[BaseException, Optional[types.TracebackType]], ], ] @dataclasses.dataclass(frozen=True) class PseudoFixtureDef(Generic[FixtureValue]): - cached_result: "_FixtureCachedResult[FixtureValue]" + cached_result: _FixtureCachedResult[FixtureValue] _scope: Scope -def pytest_sessionstart(session: "Session") -> None: +def pytest_sessionstart(session: Session) -> None: session._fixturemanager = FixtureManager(session) def get_scope_package( node: nodes.Item, - fixturedef: "FixtureDef[object]", -) -> Optional[nodes.Node]: + fixturedef: FixtureDef[object], +) -> nodes.Node | None: from _pytest.python import Package for parent in node.iter_parents(): @@ -132,7 +133,7 @@ def get_scope_package( return node.session -def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]: +def get_scope_node(node: nodes.Node, scope: Scope) -> nodes.Node | None: import _pytest.python if scope is Scope.Function: @@ -151,7 +152,7 @@ def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]: assert_never(scope) -def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: +def getfixturemarker(obj: object) -> FixtureFunctionMarker | None: """Return fixturemarker or None if it doesn't exist or raised exceptions.""" return cast( @@ -160,104 +161,109 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: ) +# Algorithm for sorting on a per-parametrized resource setup basis. +# It is called for Session scope first and performs sorting +# down to the lower scopes such as to minimize number of "high scope" +# setups and teardowns. + + @dataclasses.dataclass(frozen=True) class FixtureArgKey: argname: str param_index: int - scoped_item_path: Optional[Path] - item_cls: Optional[type] + scoped_item_path: Path | None + item_cls: type | None + +_V = TypeVar("_V") +OrderedSet = Dict[_V, None] -def get_parametrized_fixture_keys( + +def get_parametrized_fixture_argkeys( item: nodes.Item, scope: Scope ) -> Iterator[FixtureArgKey]: """Return list of keys for all parametrized arguments which match the specified scope.""" assert scope is not Scope.Function + try: callspec: CallSpec2 = item.callspec # type: ignore[attr-defined] except AttributeError: return + + item_cls = None + if scope is Scope.Session: + scoped_item_path = None + elif scope is Scope.Package: + # Package key = module's directory. + scoped_item_path = item.path.parent + elif scope is Scope.Module: + scoped_item_path = item.path + elif scope is Scope.Class: + scoped_item_path = item.path + item_cls = item.cls # type: ignore[attr-defined] + else: + assert_never(scope) + for argname in callspec.indices: if callspec._arg2scope[argname] != scope: continue - - item_cls = None - if scope is Scope.Session: - scoped_item_path = None - elif scope is Scope.Package: - scoped_item_path = item.path - elif scope is Scope.Module: - scoped_item_path = item.path - elif scope is Scope.Class: - scoped_item_path = item.path - item_cls = item.cls # type: ignore[attr-defined] - else: - assert_never(scope) - param_index = callspec.indices[argname] yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls) -# Algorithm for sorting on a per-parametrized resource setup basis. -# It is called for Session scope first and performs sorting -# down to the lower scopes such as to minimize number of "high scope" -# setups and teardowns. - - -def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]: - argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {} - items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {} +def reorder_items(items: Sequence[nodes.Item]) -> list[nodes.Item]: + argkeys_by_item: dict[Scope, dict[nodes.Item, OrderedSet[FixtureArgKey]]] = {} + items_by_argkey: dict[ + Scope, dict[FixtureArgKey, OrderedDict[nodes.Item, None]] + ] = {} for scope in HIGH_SCOPES: - scoped_argkeys_cache = argkeys_cache[scope] = {} - scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque) + scoped_argkeys_by_item = argkeys_by_item[scope] = {} + scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict) for item in items: - keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None) - if keys: - scoped_argkeys_cache[item] = keys - for key in keys: - scoped_items_by_argkey[key].append(item) - items_dict = dict.fromkeys(items, None) + argkeys = dict.fromkeys(get_parametrized_fixture_argkeys(item, scope)) + if argkeys: + scoped_argkeys_by_item[item] = argkeys + for argkey in argkeys: + scoped_items_by_argkey[argkey][item] = None + + items_set = dict.fromkeys(items) return list( - reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session) + reorder_items_atscope( + items_set, argkeys_by_item, items_by_argkey, Scope.Session + ) ) -def fix_cache_order( - item: nodes.Item, - argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]], - items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]], -) -> None: - for scope in HIGH_SCOPES: - for key in argkeys_cache[scope].get(item, []): - items_by_argkey[scope][key].appendleft(item) - - def reorder_items_atscope( - items: Dict[nodes.Item, None], - argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]], - items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]], + items: OrderedSet[nodes.Item], + argkeys_by_item: Mapping[Scope, Mapping[nodes.Item, OrderedSet[FixtureArgKey]]], + items_by_argkey: Mapping[ + Scope, Mapping[FixtureArgKey, OrderedDict[nodes.Item, None]] + ], scope: Scope, -) -> Dict[nodes.Item, None]: +) -> OrderedSet[nodes.Item]: if scope is Scope.Function or len(items) < 3: return items - ignore: Set[Optional[FixtureArgKey]] = set() - items_deque = deque(items) - items_done: Dict[nodes.Item, None] = {} + scoped_items_by_argkey = items_by_argkey[scope] - scoped_argkeys_cache = argkeys_cache[scope] + scoped_argkeys_by_item = argkeys_by_item[scope] + + ignore: set[FixtureArgKey] = set() + items_deque = deque(items) + items_done: OrderedSet[nodes.Item] = {} while items_deque: - no_argkey_group: Dict[nodes.Item, None] = {} + no_argkey_items: OrderedSet[nodes.Item] = {} slicing_argkey = None while items_deque: item = items_deque.popleft() - if item in items_done or item in no_argkey_group: + if item in items_done or item in no_argkey_items: continue argkeys = dict.fromkeys( - (k for k in scoped_argkeys_cache.get(item, []) if k not in ignore), None + k for k in scoped_argkeys_by_item.get(item, ()) if k not in ignore ) if not argkeys: - no_argkey_group[item] = None + no_argkey_items[item] = None else: slicing_argkey, _ = argkeys.popitem() # We don't have to remove relevant items from later in the @@ -266,16 +272,23 @@ def reorder_items_atscope( i for i in scoped_items_by_argkey[slicing_argkey] if i in items ] for i in reversed(matching_items): - fix_cache_order(i, argkeys_cache, items_by_argkey) items_deque.appendleft(i) + # Fix items_by_argkey order. + for other_scope in HIGH_SCOPES: + other_scoped_items_by_argkey = items_by_argkey[other_scope] + for argkey in argkeys_by_item[other_scope].get(i, ()): + other_scoped_items_by_argkey[argkey][i] = None + other_scoped_items_by_argkey[argkey].move_to_end( + i, last=False + ) break - if no_argkey_group: - no_argkey_group = reorder_items_atscope( - no_argkey_group, argkeys_cache, items_by_argkey, scope.next_lower() + if no_argkey_items: + reordered_no_argkey_items = reorder_items_atscope( + no_argkey_items, argkeys_by_item, items_by_argkey, scope.next_lower() ) - for item in no_argkey_group: - items_done[item] = None - ignore.add(slicing_argkey) + items_done.update(reordered_no_argkey_items) + if slicing_argkey is not None: + ignore.add(slicing_argkey) return items_done @@ -296,19 +309,19 @@ class FuncFixtureInfo: __slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs") # Fixture names that the item requests directly by function parameters. - argnames: Tuple[str, ...] + argnames: tuple[str, ...] # 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, ...] + 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] + 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]"]] + name2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] def prune_dependency_tree(self) -> None: """Recompute names_closure from initialnames and name2fixturedefs. @@ -321,11 +334,11 @@ def prune_dependency_tree(self) -> None: tree. In this way the dependency tree can get pruned, and the closure of argnames may get reduced. """ - closure: Set[str] = set() + closure: set[str] = set() working_set = set(self.initialnames) while working_set: argname = working_set.pop() - # Argname may be smth not included in the original names_closure, + # Argname may be something not included in the original names_closure, # in which case we ignore it. This currently happens with pseudo # FixtureDefs which wrap 'get_direct_param_fixture_func(request)'. # So they introduce the new dependency 'request' which might have @@ -347,10 +360,10 @@ class FixtureRequest(abc.ABC): def __init__( self, - pyfuncitem: "Function", - fixturename: Optional[str], - arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]], - fixture_defs: Dict[str, "FixtureDef[Any]"], + pyfuncitem: Function, + fixturename: str | None, + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]], + fixture_defs: dict[str, FixtureDef[Any]], *, _ispytest: bool = False, ) -> None: @@ -377,7 +390,7 @@ def __init__( self.param: Any @property - def _fixturemanager(self) -> "FixtureManager": + def _fixturemanager(self) -> FixtureManager: return self._pyfuncitem.session._fixturemanager @property @@ -393,13 +406,13 @@ def scope(self) -> _ScopeName: @abc.abstractmethod def _check_scope( self, - requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], + requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: raise NotImplementedError() @property - def fixturenames(self) -> List[str]: + def fixturenames(self) -> list[str]: """Names of all active fixtures in this request.""" result = list(self._pyfuncitem.fixturenames) result.extend(set(self._fixture_defs).difference(result)) @@ -464,7 +477,7 @@ def keywords(self) -> MutableMapping[str, Any]: return node.keywords @property - def session(self) -> "Session": + def session(self) -> Session: """Pytest session object.""" return self._pyfuncitem.session @@ -474,7 +487,7 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None: the last test within the requesting test context finished execution.""" raise NotImplementedError() - def applymarker(self, marker: Union[str, MarkDecorator]) -> None: + def applymarker(self, marker: str | MarkDecorator) -> None: """Apply a marker to a single test function invocation. This method is useful if you don't want to have a keyword/marker @@ -485,7 +498,7 @@ def applymarker(self, marker: Union[str, MarkDecorator]) -> None: """ self.node.add_marker(marker) - def raiseerror(self, msg: Optional[str]) -> NoReturn: + def raiseerror(self, msg: str | None) -> NoReturn: """Raise a FixtureLookupError exception. :param msg: @@ -522,7 +535,7 @@ def getfixturevalue(self, argname: str) -> Any: ) return fixturedef.cached_result[0] - def _iter_chain(self) -> Iterator["SubRequest"]: + def _iter_chain(self) -> Iterator[SubRequest]: """Yield all SubRequests in the chain, from self up. Note: does *not* yield the TopRequest. @@ -534,7 +547,7 @@ def _iter_chain(self) -> Iterator["SubRequest"]: def _get_active_fixturedef( self, argname: str - ) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]: + ) -> FixtureDef[object] | PseudoFixtureDef[object]: if argname == "request": cached_result = (self, [0], None) return PseudoFixtureDef(cached_result, Scope.Function) @@ -605,7 +618,7 @@ def _get_active_fixturedef( self._fixture_defs[argname] = fixturedef return fixturedef - def _check_fixturedef_without_param(self, fixturedef: "FixtureDef[object]") -> None: + def _check_fixturedef_without_param(self, fixturedef: FixtureDef[object]) -> None: """Check that this request is allowed to execute this fixturedef without a param.""" funcitem = self._pyfuncitem @@ -638,7 +651,7 @@ def _check_fixturedef_without_param(self, fixturedef: "FixtureDef[object]") -> N ) fail(msg, pytrace=False) - def _get_fixturestack(self) -> List["FixtureDef[Any]"]: + def _get_fixturestack(self) -> list[FixtureDef[Any]]: values = [request._fixturedef for request in self._iter_chain()] values.reverse() return values @@ -648,7 +661,7 @@ def _get_fixturestack(self) -> List["FixtureDef[Any]"]: class TopRequest(FixtureRequest): """The type of the ``request`` fixture in a test function.""" - def __init__(self, pyfuncitem: "Function", *, _ispytest: bool = False) -> None: + def __init__(self, pyfuncitem: Function, *, _ispytest: bool = False) -> None: super().__init__( fixturename=None, pyfuncitem=pyfuncitem, @@ -663,7 +676,7 @@ def _scope(self) -> Scope: def _check_scope( self, - requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], + requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: # TopRequest always has function scope so always valid. @@ -674,7 +687,7 @@ def node(self): return self._pyfuncitem def __repr__(self) -> str: - return "" % (self.node) + return f"" def _fillfixtures(self) -> None: item = self._pyfuncitem @@ -697,7 +710,7 @@ def __init__( scope: Scope, param: Any, param_index: int, - fixturedef: "FixtureDef[object]", + fixturedef: FixtureDef[object], *, _ispytest: bool = False, ) -> None: @@ -727,7 +740,7 @@ def node(self): scope = self._scope if scope is Scope.Function: # This might also be a non-function Item despite its attribute name. - node: Optional[nodes.Node] = self._pyfuncitem + node: nodes.Node | None = self._pyfuncitem elif scope is Scope.Package: node = get_scope_package(self._pyfuncitem, self._fixturedef) else: @@ -740,7 +753,7 @@ def node(self): def _check_scope( self, - requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]], + requested_fixturedef: FixtureDef[object] | PseudoFixtureDef[object], requested_scope: Scope, ) -> None: if isinstance(requested_fixturedef, PseudoFixtureDef): @@ -761,7 +774,7 @@ def _check_scope( pytrace=False, ) - def _format_fixturedef_line(self, fixturedef: "FixtureDef[object]") -> str: + def _format_fixturedef_line(self, fixturedef: FixtureDef[object]) -> str: factory = fixturedef.func path, lineno = getfslineno(factory) if isinstance(path, Path): @@ -778,15 +791,15 @@ class FixtureLookupError(LookupError): """Could not return a requested fixture (missing or invalid).""" def __init__( - self, argname: Optional[str], request: FixtureRequest, msg: Optional[str] = None + self, argname: str | None, request: FixtureRequest, msg: str | None = None ) -> None: self.argname = argname self.request = request self.fixturestack = request._get_fixturestack() self.msg = msg - def formatrepr(self) -> "FixtureLookupErrorRepr": - tblines: List[str] = [] + def formatrepr(self) -> FixtureLookupErrorRepr: + tblines: list[str] = [] addline = tblines.append stack = [self.request._pyfuncitem.obj] stack.extend(map(lambda x: x.func, self.fixturestack)) @@ -834,11 +847,11 @@ def formatrepr(self) -> "FixtureLookupErrorRepr": class FixtureLookupErrorRepr(TerminalRepr): def __init__( self, - filename: Union[str, "os.PathLike[str]"], + filename: str | os.PathLike[str], firstlineno: int, tblines: Sequence[str], errorstring: str, - argname: Optional[str], + argname: str | None, ) -> None: self.tblines = tblines self.errorstring = errorstring @@ -866,7 +879,7 @@ def toterminal(self, tw: TerminalWriter) -> None: def call_fixture_func( - fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs + fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs ) -> FixtureValue: if is_generator(fixturefunc): fixturefunc = cast( @@ -937,14 +950,12 @@ class FixtureDef(Generic[FixtureValue]): def __init__( self, config: Config, - baseid: Optional[str], + baseid: str | None, argname: str, - func: "_FixtureFunc[FixtureValue]", - scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None], - params: Optional[Sequence[object]], - ids: Optional[ - Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] - ] = None, + func: _FixtureFunc[FixtureValue], + scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None, + params: Sequence[object] | None, + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, *, _ispytest: bool = False, ) -> None: @@ -990,8 +1001,8 @@ def __init__( self.argnames: Final = getfuncargnames(func, name=argname) # 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: Final[List[Callable[[], object]]] = [] + self.cached_result: _FixtureCachedResult[FixtureValue] | None = None + self._finalizers: Final[list[Callable[[], object]]] = [] @property def scope(self) -> _ScopeName: @@ -1002,7 +1013,7 @@ def addfinalizer(self, finalizer: Callable[[], object]) -> None: self._finalizers.append(finalizer) def finish(self, request: SubRequest) -> None: - exceptions: List[BaseException] = [] + exceptions: list[BaseException] = [] while self._finalizers: fin = self._finalizers.pop() try: @@ -1042,15 +1053,21 @@ def execute(self, request: SubRequest) -> FixtureValue: requested_fixtures_that_should_finalize_us.append(fixturedef) # Check for (and return) cached value/exception. - my_cache_key = self.cache_key(request) if self.cached_result is not None: + request_cache_key = self.cache_key(request) cache_key = self.cached_result[1] - # note: comparison with `==` can fail (or be expensive) for e.g. - # numpy arrays (#6497). - if my_cache_key is cache_key: + try: + # Attempt to make a normal == check: this might fail for objects + # which do not implement the standard comparison (like numpy arrays -- #6497). + cache_hit = bool(request_cache_key == cache_key) + except (ValueError, RuntimeError): + # If the comparison raises, use 'is' as fallback. + cache_hit = request_cache_key is cache_key + + if cache_hit: if self.cached_result[2] is not None: - exc = self.cached_result[2] - raise exc + exc, exc_tb = self.cached_result[2] + raise exc.with_traceback(exc_tb) else: result = self.cached_result[0] return result @@ -1086,7 +1103,7 @@ def __repr__(self) -> str: def resolve_fixture_function( fixturedef: FixtureDef[FixtureValue], request: FixtureRequest -) -> "_FixtureFunc[FixtureValue]": +) -> _FixtureFunc[FixtureValue]: """Get the actual callable that can be called to obtain the fixture value.""" fixturefunc = fixturedef.func @@ -1126,7 +1143,7 @@ def pytest_fixture_setup( # 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) + fixturedef.cached_result = (None, my_cache_key, (e, e.__traceback__)) raise fixturedef.cached_result = (result, my_cache_key, None) return result @@ -1134,7 +1151,7 @@ def pytest_fixture_setup( def wrap_function_to_error_out_if_called_directly( function: FixtureFunction, - fixture_marker: "FixtureFunctionMarker", + fixture_marker: FixtureFunctionMarker, ) -> FixtureFunction: """Wrap the given fixture function so we can raise an error about it being called directly, instead of used as an argument in a test function.""" @@ -1160,13 +1177,11 @@ def result(*args, **kwargs): @final @dataclasses.dataclass(frozen=True) class FixtureFunctionMarker: - scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" - params: Optional[Tuple[object, ...]] + scope: _ScopeName | Callable[[str, Config], _ScopeName] + params: tuple[object, ...] | None autouse: bool = False - ids: Optional[ - Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] - ] = None - name: Optional[str] = None + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None + name: str | None = None _ispytest: dataclasses.InitVar[bool] = False @@ -1204,13 +1219,11 @@ def __call__(self, function: FixtureFunction) -> FixtureFunction: def fixture( fixture_function: FixtureFunction, *, - scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., - params: Optional[Iterable[object]] = ..., + scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., + params: Iterable[object] | None = ..., autouse: bool = ..., - ids: Optional[ - Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] - ] = ..., - name: Optional[str] = ..., + ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., + name: str | None = ..., ) -> FixtureFunction: ... @@ -1218,27 +1231,23 @@ def fixture( def fixture( fixture_function: None = ..., *, - scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ..., - params: Optional[Iterable[object]] = ..., + scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., + params: Iterable[object] | None = ..., autouse: bool = ..., - ids: Optional[ - Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] - ] = ..., - name: Optional[str] = None, + ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., + name: str | None = None, ) -> FixtureFunctionMarker: ... def fixture( - fixture_function: Optional[FixtureFunction] = None, + fixture_function: FixtureFunction | None = None, *, - scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function", - params: Optional[Iterable[object]] = None, + scope: _ScopeName | Callable[[str, Config], _ScopeName] = "function", + params: Iterable[object] | None = None, autouse: bool = False, - ids: Optional[ - Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]] - ] = None, - name: Optional[str] = None, -) -> Union[FixtureFunctionMarker, FixtureFunction]: + ids: Sequence[object | None] | Callable[[Any], object | None] | None = None, + name: str | None = None, +) -> FixtureFunctionMarker | FixtureFunction: """Decorator to mark a fixture factory function. This decorator can be used, with or without parameters, to define a @@ -1372,7 +1381,7 @@ def pytest_addoption(parser: Parser) -> None: ) -def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: if config.option.showfixtures: showfixtures(config) return 0 @@ -1382,7 +1391,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: return None -def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]: +def _get_direct_parametrize_args(node: nodes.Node) -> set[str]: """Return all direct parametrization arguments of a node, so we don't mistake them for fixtures. @@ -1391,7 +1400,7 @@ def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]: These things are done later as well when dealing with parametrization so this could be improved. """ - parametrize_argnames: Set[str] = set() + parametrize_argnames: set[str] = set() for marker in node.iter_markers(name="parametrize"): if not marker.kwargs.get("indirect", False): p_argnames, _ = ParameterSet._parse_parametrize_args( @@ -1401,7 +1410,7 @@ def _get_direct_parametrize_args(node: nodes.Node) -> Set[str]: return parametrize_argnames -def deduplicate_names(*seqs: Iterable[str]) -> Tuple[str, ...]: +def deduplicate_names(*seqs: Iterable[str]) -> tuple[str, ...]: """De-duplicate the sequence of names while keeping the original order.""" # Ideally we would use a set, but it does not preserve insertion order. return tuple(dict.fromkeys(name for seq in seqs for name in seq)) @@ -1438,17 +1447,17 @@ class FixtureManager: by a lookup of their FuncFixtureInfo. """ - def __init__(self, session: "Session") -> None: + def __init__(self, session: Session) -> None: self.session = session self.config: Config = session.config # 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() + 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: Final[Dict[str, List[str]]] = { + self._nodeid_autousenames: Final[dict[str, list[str]]] = { "": self.config.getini("usefixtures"), } session.config.pluginmanager.register(self, "funcmanage") @@ -1456,8 +1465,8 @@ def __init__(self, session: "Session") -> None: def getfixtureinfo( self, node: nodes.Item, - func: Optional[Callable[..., object]], - cls: Optional[type], + func: Callable[..., object] | None, + cls: type | None, ) -> FuncFixtureInfo: """Calculate the :class:`FuncFixtureInfo` for an item. @@ -1528,9 +1537,9 @@ def _getusefixturesnames(self, node: nodes.Item) -> Iterator[str]: def getfixtureclosure( self, parentnode: nodes.Node, - initialnames: Tuple[str, ...], + initialnames: tuple[str, ...], ignore_args: AbstractSet[str], - ) -> Tuple[List[str], Dict[str, Sequence[FixtureDef[Any]]]]: + ) -> tuple[list[str], dict[str, Sequence[FixtureDef[Any]]]]: # Collect the closure of all fixtures, starting with the given # fixturenames as the initial set. As we have to visit all # factory definitions anyway, we also return an arg2fixturedefs @@ -1540,7 +1549,7 @@ def getfixtureclosure( fixturenames_closure = list(initialnames) - arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {} + arg2fixturedefs: dict[str, Sequence[FixtureDef[Any]]] = {} lastlen = -1 while lastlen != len(fixturenames_closure): lastlen = len(fixturenames_closure) @@ -1567,7 +1576,7 @@ def sort_by_scope(arg_name: str) -> Scope: fixturenames_closure.sort(key=sort_by_scope, reverse=True) return fixturenames_closure, arg2fixturedefs - def pytest_generate_tests(self, metafunc: "Metafunc") -> None: + def pytest_generate_tests(self, metafunc: Metafunc) -> None: """Generate new tests based on parametrized fixtures used by the given metafunc""" def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]: @@ -1612,7 +1621,7 @@ def get_parametrize_mark_argnames(mark: Mark) -> Sequence[str]: # Try next super fixture, if any. - def pytest_collection_modifyitems(self, items: List[nodes.Item]) -> None: + def pytest_collection_modifyitems(self, items: list[nodes.Item]) -> None: # Separate parametrized setups. items[:] = reorder_items(items) @@ -1620,15 +1629,11 @@ def _register_fixture( self, *, name: str, - func: "_FixtureFunc[object]", - nodeid: Optional[str], - scope: Union[ - Scope, _ScopeName, Callable[[str, Config], _ScopeName], None - ] = "function", - params: Optional[Sequence[object]] = None, - ids: Optional[ - Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] - ] = None, + func: _FixtureFunc[object], + nodeid: str | None, + scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function", + params: Sequence[object] | None = None, + ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, autouse: bool = False, ) -> None: """Register a fixture @@ -1686,14 +1691,14 @@ def parsefactories( def parsefactories( self, node_or_obj: object, - nodeid: Optional[str], + nodeid: str | None, ) -> None: raise NotImplementedError() def parsefactories( self, - node_or_obj: Union[nodes.Node, object], - nodeid: Union[str, NotSetType, None] = NOTSET, + node_or_obj: nodes.Node | object, + nodeid: str | NotSetType | None = NOTSET, ) -> None: """Collect fixtures from a collection node or object. @@ -1701,7 +1706,7 @@ def parsefactories( If `node_or_object` is a collection node (with an underlying Python object), the node's object is traversed and the node's nodeid is used to - determine the fixtures' visibilty. `nodeid` must not be specified in + determine the fixtures' visibility. `nodeid` must not be specified in this case. If `node_or_object` is an object (e.g. a plugin), the object is @@ -1751,7 +1756,7 @@ def parsefactories( def getfixturedefs( self, argname: str, node: nodes.Node - ) -> Optional[Sequence[FixtureDef[Any]]]: + ) -> Sequence[FixtureDef[Any]] | None: """Get FixtureDefs for a fixture name which are applicable to a given node. @@ -1778,7 +1783,7 @@ def _matchfactories( yield fixturedef -def show_fixtures_per_test(config: Config) -> Union[int, ExitCode]: +def show_fixtures_per_test(config: Config) -> int | ExitCode: from _pytest.main import wrap_session return wrap_session(config, _show_fixtures_per_test) @@ -1796,7 +1801,7 @@ def _pretty_fixture_path(invocation_dir: Path, func) -> str: return bestrelpath(invocation_dir, loc) -def _show_fixtures_per_test(config: Config, session: "Session") -> None: +def _show_fixtures_per_test(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() @@ -1819,14 +1824,17 @@ def write_fixture(fixture_def: FixtureDef[object]) -> None: fixture_doc = inspect.getdoc(fixture_def.func) if fixture_doc: write_docstring( - tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc + tw, + fixture_doc.split("\n\n", maxsplit=1)[0] + if verbose <= 0 + else fixture_doc, ) else: tw.line(" no docstring available", red=True) def write_item(item: nodes.Item) -> None: # Not all items have _fixtureinfo attribute. - info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None) + info: FuncFixtureInfo | None = getattr(item, "_fixtureinfo", None) if info is None or not info.name2fixturedefs: # This test item does not use any fixtures. return @@ -1846,13 +1854,13 @@ def write_item(item: nodes.Item) -> None: write_item(session_item) -def showfixtures(config: Config) -> Union[int, ExitCode]: +def showfixtures(config: Config) -> int | ExitCode: from _pytest.main import wrap_session return wrap_session(config, _showfixtures_main) -def _showfixtures_main(config: Config, session: "Session") -> None: +def _showfixtures_main(config: Config, session: Session) -> None: import _pytest.config session.perform_collect() @@ -1863,7 +1871,7 @@ def _showfixtures_main(config: Config, session: "Session") -> None: fm = session._fixturemanager available = [] - seen: Set[Tuple[str, str]] = set() + seen: set[tuple[str, str]] = set() for argname, fixturedefs in fm._arg2fixturedefs.items(): assert fixturedefs is not None @@ -1896,12 +1904,14 @@ def _showfixtures_main(config: Config, session: "Session") -> None: continue tw.write(f"{argname}", green=True) if fixturedef.scope != "function": - tw.write(" [%s scope]" % fixturedef.scope, cyan=True) + tw.write(f" [{fixturedef.scope} scope]", cyan=True) tw.write(f" -- {prettypath}", yellow=True) tw.write("\n") doc = inspect.getdoc(fixturedef.func) if doc: - write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc) + write_docstring( + tw, doc.split("\n\n", maxsplit=1)[0] if verbose <= 0 else doc + ) else: tw.line(" no docstring available", red=True) tw.line() diff --git a/src/_pytest/freeze_support.py b/src/_pytest/freeze_support.py index e03a6d1753d..2ba6f9b8bcc 100644 --- a/src/_pytest/freeze_support.py +++ b/src/_pytest/freeze_support.py @@ -1,13 +1,13 @@ """Provides a function to report all internal modules for using freezing tools.""" +from __future__ import annotations + import types from typing import Iterator -from typing import List -from typing import Union -def freeze_includes() -> List[str]: +def freeze_includes() -> list[str]: """Return a list of module names used by pytest that should be included by cx_freeze.""" import _pytest @@ -17,7 +17,7 @@ def freeze_includes() -> List[str]: def _iter_all_modules( - package: Union[str, types.ModuleType], + package: str | types.ModuleType, prefix: str = "", ) -> Iterator[str]: """Iterate over the names of all modules that can be found in the given diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 37fbdf04d7e..1886d5c9342 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -1,13 +1,12 @@ # mypy: allow-untyped-defs """Version info, help messages, tracing configuration.""" +from __future__ import annotations + from argparse import Action import os import sys from typing import Generator -from typing import List -from typing import Optional -from typing import Union from _pytest.config import Config from _pytest.config import ExitCode @@ -121,11 +120,11 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]: ) config.trace.root.setwriter(debugfile.write) undo_tracing = config.pluginmanager.enable_tracing() - sys.stderr.write("writing pytest debug information to %s\n" % path) + sys.stderr.write(f"writing pytest debug information to {path}\n") def unset_tracing() -> None: debugfile.close() - sys.stderr.write("wrote pytest debug information to %s\n" % debugfile.name) + sys.stderr.write(f"wrote pytest debug information to {debugfile.name}\n") config.trace.root.setwriter(None) undo_tracing() @@ -147,7 +146,7 @@ def showversion(config: Config) -> None: sys.stdout.write(f"pytest {pytest.__version__}\n") -def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: if config.option.version > 0: showversion(config) return 0 @@ -162,7 +161,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: def showhelp(config: Config) -> None: import textwrap - reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin( + reporter: TerminalReporter | None = config.pluginmanager.get_plugin( "terminalreporter" ) assert reporter is not None @@ -185,7 +184,7 @@ def showhelp(config: Config) -> None: if help is None: raise TypeError(f"help argument cannot be None for {name}") spec = f"{name} ({type}):" - tw.write(" %s" % spec) + tw.write(f" {spec}") spec_len = len(spec) if spec_len > (indent_len - 3): # Display help starting at a new line. @@ -213,6 +212,12 @@ def showhelp(config: Config) -> None: tw.line() tw.line("Environment variables:") vars = [ + ( + "CI", + "When set (regardless of value), pytest knows it is running in a " + "CI process and does not truncate summary info", + ), + ("BUILD_NUMBER", "Equivalent to CI"), ("PYTEST_ADDOPTS", "Extra command line options"), ("PYTEST_PLUGINS", "Comma-separated plugins to load during startup"), ("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "Set to disable plugin auto-loading"), @@ -233,17 +238,16 @@ def showhelp(config: Config) -> None: for warningreport in reporter.stats.get("warnings", []): tw.line("warning : " + warningreport.message, red=True) - return conftest_options = [("pytest_plugins", "list of plugin names to load")] -def getpluginversioninfo(config: Config) -> List[str]: +def getpluginversioninfo(config: Config) -> list[str]: lines = [] plugininfo = config.pluginmanager.list_plugin_distinfo() if plugininfo: - lines.append("setuptools registered plugins:") + lines.append("registered third-party plugins:") for plugin, dist in plugininfo: loc = getattr(plugin, "__file__", repr(plugin)) content = f"{dist.project_name}-{dist.version} at {loc}" @@ -251,7 +255,7 @@ def getpluginversioninfo(config: Config) -> List[str]: return lines -def pytest_report_header(config: Config) -> List[str]: +def pytest_report_header(config: Config) -> list[str]: lines = [] if config.option.debug or config.option.traceconfig: lines.append(f"using: pytest-{pytest.__version__}") diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index acfe7eb9587..99614899994 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,17 +1,15 @@ # mypy: allow-untyped-defs +# ruff: noqa: T100 """Hook specifications for pytest plugins which are invoked by pytest itself and by builtin plugins.""" +from __future__ import annotations + from pathlib import Path from typing import Any -from typing import Dict -from typing import List from typing import Mapping -from typing import Optional from typing import Sequence -from typing import Tuple from typing import TYPE_CHECKING -from typing import Union from pluggy import HookspecMarker @@ -56,7 +54,7 @@ @hookspec(historic=True) -def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: +def pytest_addhooks(pluginmanager: PytestPluginManager) -> None: """Called at plugin registration time to allow adding new hooks via a call to :func:`pluginmanager.add_hookspecs(module_or_class, prefix) `. @@ -75,9 +73,9 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None: @hookspec(historic=True) def pytest_plugin_registered( - plugin: "_PluggyPlugin", + plugin: _PluggyPlugin, plugin_name: str, - manager: "PytestPluginManager", + manager: PytestPluginManager, ) -> None: """A new pytest plugin got registered. @@ -99,7 +97,7 @@ def pytest_plugin_registered( @hookspec(historic=True) -def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> None: +def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> None: """Register argparse-style options and ini-style config values, called once at the beginning of a test run. @@ -140,7 +138,7 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") -> @hookspec(historic=True) -def pytest_configure(config: "Config") -> None: +def pytest_configure(config: Config) -> None: """Allow plugins and conftest files to perform initial configuration. .. note:: @@ -165,8 +163,8 @@ def pytest_configure(config: "Config") -> None: @hookspec(firstresult=True) def pytest_cmdline_parse( - pluginmanager: "PytestPluginManager", args: List[str] -) -> Optional["Config"]: + pluginmanager: PytestPluginManager, args: list[str] +) -> Config | None: """Return an initialized :class:`~pytest.Config`, parsing the specified args. Stops at first non-None result, see :ref:`firstresult`. @@ -188,7 +186,7 @@ def pytest_cmdline_parse( def pytest_load_initial_conftests( - early_config: "Config", parser: "Parser", args: List[str] + early_config: Config, parser: Parser, args: list[str] ) -> None: """Called to implement the loading of :ref:`initial conftest files ` ahead of command line option parsing. @@ -205,7 +203,7 @@ def pytest_load_initial_conftests( @hookspec(firstresult=True) -def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]: +def pytest_cmdline_main(config: Config) -> ExitCode | int | None: """Called for performing the main command line action. The default implementation will invoke the configure hooks and @@ -229,7 +227,7 @@ def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]: @hookspec(firstresult=True) -def pytest_collection(session: "Session") -> Optional[object]: +def pytest_collection(session: Session) -> object | None: """Perform the collection phase for the given session. Stops at first non-None result, see :ref:`firstresult`. @@ -271,7 +269,7 @@ def pytest_collection(session: "Session") -> Optional[object]: def pytest_collection_modifyitems( - session: "Session", config: "Config", items: List["Item"] + session: Session, config: Config, items: list[Item] ) -> None: """Called after collection has been performed. May filter or re-order the items in-place. @@ -287,7 +285,7 @@ def pytest_collection_modifyitems( """ -def pytest_collection_finish(session: "Session") -> None: +def pytest_collection_finish(session: Session) -> None: """Called after collection has been performed and modified. :param session: The pytest session object. @@ -308,9 +306,14 @@ def pytest_collection_finish(session: "Session") -> None: }, ) def pytest_ignore_collect( - collection_path: Path, path: "LEGACY_PATH", config: "Config" -) -> Optional[bool]: - """Return True to prevent considering this path for collection. + collection_path: Path, path: LEGACY_PATH, config: Config +) -> bool | None: + """Return ``True`` to ignore this path for collection. + + Return ``None`` to let other plugins ignore the path for collection. + + Returning ``False`` will forcefully *not* ignore this path for collection, + without giving a chance for other plugins to ignore this path. This hook is consulted for all files and directories prior to calling more specific hooks. @@ -318,6 +321,7 @@ def pytest_ignore_collect( Stops at first non-None result, see :ref:`firstresult`. :param collection_path: The path to analyze. + :type collection_path: pathlib.Path :param path: The path to analyze (deprecated). :param config: The pytest config object. @@ -337,7 +341,7 @@ def pytest_ignore_collect( @hookspec(firstresult=True) -def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Collector]": +def pytest_collect_directory(path: Path, parent: Collector) -> Collector | None: """Create a :class:`~pytest.Collector` for the given directory, or None if not relevant. @@ -351,6 +355,7 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle Stops at first non-None result, see :ref:`firstresult`. :param path: The path to analyze. + :type path: pathlib.Path See :ref:`custom directory collectors` for a simple example of use of this hook. @@ -373,8 +378,8 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle }, ) def pytest_collect_file( - file_path: Path, path: "LEGACY_PATH", parent: "Collector" -) -> "Optional[Collector]": + file_path: Path, path: LEGACY_PATH, parent: Collector +) -> Collector | None: """Create a :class:`~pytest.Collector` for the given path, or None if not relevant. For best results, the returned collector should be a subclass of @@ -383,6 +388,7 @@ def pytest_collect_file( The new node needs to have the specified ``parent`` as a parent. :param file_path: The path to analyze. + :type file_path: pathlib.Path :param path: The path to collect (deprecated). .. versionchanged:: 7.0.0 @@ -401,7 +407,7 @@ def pytest_collect_file( # logging hooks for collection -def pytest_collectstart(collector: "Collector") -> None: +def pytest_collectstart(collector: Collector) -> None: """Collector starts collecting. :param collector: @@ -416,7 +422,7 @@ def pytest_collectstart(collector: "Collector") -> None: """ -def pytest_itemcollected(item: "Item") -> None: +def pytest_itemcollected(item: Item) -> None: """We just collected a test item. :param item: @@ -430,7 +436,7 @@ def pytest_itemcollected(item: "Item") -> None: """ -def pytest_collectreport(report: "CollectReport") -> None: +def pytest_collectreport(report: CollectReport) -> None: """Collector finished collecting. :param report: @@ -445,7 +451,7 @@ def pytest_collectreport(report: "CollectReport") -> None: """ -def pytest_deselected(items: Sequence["Item"]) -> None: +def pytest_deselected(items: Sequence[Item]) -> None: """Called for deselected test items, e.g. by keyword. May be called multiple times. @@ -461,7 +467,7 @@ def pytest_deselected(items: Sequence["Item"]) -> None: @hookspec(firstresult=True) -def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectReport]": +def pytest_make_collect_report(collector: Collector) -> CollectReport | None: """Perform :func:`collector.collect() ` and return a :class:`~pytest.CollectReport`. @@ -493,8 +499,8 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor }, ) def pytest_pycollect_makemodule( - module_path: Path, path: "LEGACY_PATH", parent -) -> Optional["Module"]: + module_path: Path, path: LEGACY_PATH, parent +) -> Module | None: """Return a :class:`pytest.Module` collector or None for the given path. This hook will be called for each matching test module path. @@ -504,6 +510,7 @@ def pytest_pycollect_makemodule( Stops at first non-None result, see :ref:`firstresult`. :param module_path: The path of the module to collect. + :type module_path: pathlib.Path :param path: The path of the module to collect (deprecated). .. versionchanged:: 7.0.0 @@ -523,8 +530,8 @@ def pytest_pycollect_makemodule( @hookspec(firstresult=True) def pytest_pycollect_makeitem( - collector: Union["Module", "Class"], name: str, obj: object -) -> Union[None, "Item", "Collector", List[Union["Item", "Collector"]]]: + collector: Module | Class, name: str, obj: object +) -> None | Item | Collector | list[Item | Collector]: """Return a custom item/collector for a Python object in a module, or None. Stops at first non-None result, see :ref:`firstresult`. @@ -548,7 +555,7 @@ def pytest_pycollect_makeitem( @hookspec(firstresult=True) -def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: +def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: """Call underlying test function. Stops at first non-None result, see :ref:`firstresult`. @@ -565,7 +572,7 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: """ -def pytest_generate_tests(metafunc: "Metafunc") -> None: +def pytest_generate_tests(metafunc: Metafunc) -> None: """Generate (multiple) parametrized calls to a test function. :param metafunc: @@ -581,9 +588,7 @@ def pytest_generate_tests(metafunc: "Metafunc") -> None: @hookspec(firstresult=True) -def pytest_make_parametrize_id( - config: "Config", val: object, argname: str -) -> Optional[str]: +def pytest_make_parametrize_id(config: Config, val: object, argname: str) -> str | None: """Return a user-friendly string representation of the given ``val`` that will be used by @pytest.mark.parametrize calls, or None if the hook doesn't know about ``val``. @@ -609,7 +614,7 @@ def pytest_make_parametrize_id( @hookspec(firstresult=True) -def pytest_runtestloop(session: "Session") -> Optional[object]: +def pytest_runtestloop(session: Session) -> object | None: """Perform the main runtest loop (after collection finished). The default hook implementation performs the runtest protocol for all items @@ -635,9 +640,7 @@ def pytest_runtestloop(session: "Session") -> Optional[object]: @hookspec(firstresult=True) -def pytest_runtest_protocol( - item: "Item", nextitem: "Optional[Item]" -) -> Optional[object]: +def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> object | None: """Perform the runtest protocol for a single test item. The default runtest protocol is this (see individual hooks for full details): @@ -650,7 +653,7 @@ def pytest_runtest_protocol( - ``pytest_runtest_logreport(report)`` - ``pytest_exception_interact(call, report)`` if an interactive exception occurred - - Call phase, if the the setup passed and the ``setuponly`` pytest option is not set: + - Call phase, if the setup passed and the ``setuponly`` pytest option is not set: - ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``) - ``report = pytest_runtest_makereport(item, call)`` - ``pytest_runtest_logreport(report)`` @@ -677,9 +680,7 @@ def pytest_runtest_protocol( """ -def pytest_runtest_logstart( - nodeid: str, location: Tuple[str, Optional[int], str] -) -> None: +def pytest_runtest_logstart(nodeid: str, location: tuple[str, int | None, str]) -> None: """Called at the start of running the runtest protocol for a single item. See :hook:`pytest_runtest_protocol` for a description of the runtest protocol. @@ -698,7 +699,7 @@ def pytest_runtest_logstart( def pytest_runtest_logfinish( - nodeid: str, location: Tuple[str, Optional[int], str] + nodeid: str, location: tuple[str, int | None, str] ) -> None: """Called at the end of running the runtest protocol for a single item. @@ -717,7 +718,7 @@ def pytest_runtest_logfinish( """ -def pytest_runtest_setup(item: "Item") -> None: +def pytest_runtest_setup(item: Item) -> None: """Called to perform the setup phase for a test item. The default implementation runs ``setup()`` on ``item`` and all of its @@ -736,7 +737,7 @@ def pytest_runtest_setup(item: "Item") -> None: """ -def pytest_runtest_call(item: "Item") -> None: +def pytest_runtest_call(item: Item) -> None: """Called to run the test for test item (the call phase). The default implementation calls ``item.runtest()``. @@ -752,7 +753,7 @@ def pytest_runtest_call(item: "Item") -> None: """ -def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None: +def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None: """Called to perform the teardown phase for a test item. The default implementation runs the finalizers and calls ``teardown()`` @@ -777,9 +778,7 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None: @hookspec(firstresult=True) -def pytest_runtest_makereport( - item: "Item", call: "CallInfo[None]" -) -> Optional["TestReport"]: +def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport | None: """Called to create a :class:`~pytest.TestReport` for each of the setup, call and teardown runtest phases of a test item. @@ -798,7 +797,7 @@ def pytest_runtest_makereport( """ -def pytest_runtest_logreport(report: "TestReport") -> None: +def pytest_runtest_logreport(report: TestReport) -> None: """Process the :class:`~pytest.TestReport` produced for each of the setup, call and teardown runtest phases of an item. @@ -814,9 +813,9 @@ def pytest_runtest_logreport(report: "TestReport") -> None: @hookspec(firstresult=True) def pytest_report_to_serializable( - config: "Config", - report: Union["CollectReport", "TestReport"], -) -> Optional[Dict[str, Any]]: + config: Config, + report: CollectReport | TestReport, +) -> dict[str, Any] | None: """Serialize the given report object into a data structure suitable for sending over the wire, e.g. converted to JSON. @@ -833,9 +832,9 @@ def pytest_report_to_serializable( @hookspec(firstresult=True) def pytest_report_from_serializable( - config: "Config", - data: Dict[str, Any], -) -> Optional[Union["CollectReport", "TestReport"]]: + config: Config, + data: dict[str, Any], +) -> CollectReport | TestReport | None: """Restore a report object previously serialized with :hook:`pytest_report_to_serializable`. @@ -856,11 +855,11 @@ def pytest_report_from_serializable( @hookspec(firstresult=True) def pytest_fixture_setup( - fixturedef: "FixtureDef[Any]", request: "SubRequest" -) -> Optional[object]: + fixturedef: FixtureDef[Any], request: SubRequest +) -> object | None: """Perform fixture setup execution. - :param fixturdef: + :param fixturedef: The fixture definition object. :param request: The fixture request object. @@ -884,13 +883,13 @@ def pytest_fixture_setup( def pytest_fixture_post_finalizer( - fixturedef: "FixtureDef[Any]", request: "SubRequest" + fixturedef: FixtureDef[Any], request: SubRequest ) -> None: """Called after fixture teardown, but before the cache is cleared, so the fixture result ``fixturedef.cached_result`` is still available (not ``None``). - :param fixturdef: + :param fixturedef: The fixture definition object. :param request: The fixture request object. @@ -909,7 +908,7 @@ def pytest_fixture_post_finalizer( # ------------------------------------------------------------------------- -def pytest_sessionstart(session: "Session") -> None: +def pytest_sessionstart(session: Session) -> None: """Called after the ``Session`` object has been created and before performing collection and entering the run test loop. @@ -923,8 +922,8 @@ def pytest_sessionstart(session: "Session") -> None: def pytest_sessionfinish( - session: "Session", - exitstatus: Union[int, "ExitCode"], + session: Session, + exitstatus: int | ExitCode, ) -> None: """Called after whole test run finished, right before returning the exit status to the system. @@ -938,7 +937,7 @@ def pytest_sessionfinish( """ -def pytest_unconfigure(config: "Config") -> None: +def pytest_unconfigure(config: Config) -> None: """Called before test process is exited. :param config: The pytest config object. @@ -956,8 +955,8 @@ def pytest_unconfigure(config: "Config") -> None: def pytest_assertrepr_compare( - config: "Config", op: str, left: object, right: object -) -> Optional[List[str]]: + config: Config, op: str, left: object, right: object +) -> list[str] | None: """Return explanation for comparisons in failing assert expressions. Return None for no custom explanation, otherwise return a list @@ -978,7 +977,7 @@ def pytest_assertrepr_compare( """ -def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> None: +def pytest_assertion_pass(item: Item, lineno: int, orig: str, expl: str) -> None: """Called whenever an assertion passes. .. versionadded:: 5.0 @@ -1025,12 +1024,13 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No }, ) def pytest_report_header( # type:ignore[empty-body] - config: "Config", start_path: Path, startdir: "LEGACY_PATH" -) -> Union[str, List[str]]: + config: Config, start_path: Path, startdir: LEGACY_PATH +) -> str | list[str]: """Return a string or list of strings to be displayed as header info for terminal reporting. :param config: The pytest config object. :param start_path: The starting dir. + :type start_path: pathlib.Path :param startdir: The starting dir (deprecated). .. note:: @@ -1060,11 +1060,11 @@ def pytest_report_header( # type:ignore[empty-body] }, ) def pytest_report_collectionfinish( # type:ignore[empty-body] - config: "Config", + config: Config, start_path: Path, - startdir: "LEGACY_PATH", - items: Sequence["Item"], -) -> Union[str, List[str]]: + startdir: LEGACY_PATH, + items: Sequence[Item], +) -> str | list[str]: """Return a string or list of strings to be displayed after collection has finished successfully. @@ -1074,6 +1074,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body] :param config: The pytest config object. :param start_path: The starting dir. + :type start_path: pathlib.Path :param startdir: The starting dir (deprecated). :param items: List of pytest items that are going to be executed; this list should not be modified. @@ -1098,8 +1099,8 @@ def pytest_report_collectionfinish( # type:ignore[empty-body] @hookspec(firstresult=True) def pytest_report_teststatus( # type:ignore[empty-body] - report: Union["CollectReport", "TestReport"], config: "Config" -) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]": + report: CollectReport | TestReport, config: Config +) -> TestShortLogReport | tuple[str, str, str | tuple[str, Mapping[str, bool]]]: """Return result-category, shortletter and verbose word for status reporting. @@ -1130,9 +1131,9 @@ def pytest_report_teststatus( # type:ignore[empty-body] def pytest_terminal_summary( - terminalreporter: "TerminalReporter", - exitstatus: "ExitCode", - config: "Config", + terminalreporter: TerminalReporter, + exitstatus: ExitCode, + config: Config, ) -> None: """Add a section to terminal summary reporting. @@ -1152,10 +1153,10 @@ def pytest_terminal_summary( @hookspec(historic=True) def pytest_warning_recorded( - warning_message: "warnings.WarningMessage", - when: "Literal['config', 'collect', 'runtest']", + warning_message: warnings.WarningMessage, + when: Literal["config", "collect", "runtest"], nodeid: str, - location: Optional[Tuple[str, int, str]], + location: tuple[str, int, str] | None, ) -> None: """Process a warning captured by the internal pytest warnings plugin. @@ -1196,8 +1197,8 @@ def pytest_warning_recorded( def pytest_markeval_namespace( # type:ignore[empty-body] - config: "Config", -) -> Dict[str, Any]: + config: Config, +) -> dict[str, Any]: """Called when constructing the globals dictionary used for evaluating string conditions in xfail/skipif markers. @@ -1225,9 +1226,9 @@ def pytest_markeval_namespace( # type:ignore[empty-body] def pytest_internalerror( - excrepr: "ExceptionRepr", - excinfo: "ExceptionInfo[BaseException]", -) -> Optional[bool]: + excrepr: ExceptionRepr, + excinfo: ExceptionInfo[BaseException], +) -> bool | None: """Called for internal errors. Return True to suppress the fallback handling of printing an @@ -1244,7 +1245,7 @@ def pytest_internalerror( def pytest_keyboard_interrupt( - excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]", + excinfo: ExceptionInfo[KeyboardInterrupt | Exit], ) -> None: """Called for keyboard interrupt. @@ -1258,9 +1259,9 @@ def pytest_keyboard_interrupt( def pytest_exception_interact( - node: Union["Item", "Collector"], - call: "CallInfo[Any]", - report: Union["CollectReport", "TestReport"], + node: Item | Collector, + call: CallInfo[Any], + report: CollectReport | TestReport, ) -> None: """Called when an exception was raised which can potentially be interactively handled. @@ -1289,7 +1290,7 @@ def pytest_exception_interact( """ -def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None: +def pytest_enter_pdb(config: Config, pdb: pdb.Pdb) -> None: """Called upon pdb.set_trace(). Can be used by plugins to take special action just before the python @@ -1305,7 +1306,7 @@ def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None: """ -def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None: +def pytest_leave_pdb(config: Config, pdb: pdb.Pdb) -> None: """Called when leaving pdb (e.g. with continue after pdb.set_trace()). Can be used by plugins to take special action just after the python diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 13fc9277aec..3a2cb59a6c1 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -8,18 +8,16 @@ https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd """ +from __future__ import annotations + from datetime import datetime +from datetime import timezone import functools import os import platform import re from typing import Callable -from typing import Dict -from typing import List from typing import Match -from typing import Optional -from typing import Tuple -from typing import Union import xml.etree.ElementTree as ET from _pytest import nodes @@ -53,9 +51,9 @@ def bin_xml_escape(arg: object) -> str: def repl(matchobj: Match[str]) -> str: i = ord(matchobj.group()) if i <= 0xFF: - return "#x%02X" % i + return f"#x{i:02X}" else: - return "#x%04X" % i + return f"#x{i:04X}" # The spec range of valid chars is: # Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF] @@ -89,15 +87,15 @@ def merge_family(left, right) -> None: class _NodeReporter: - def __init__(self, nodeid: Union[str, TestReport], xml: "LogXML") -> None: + def __init__(self, nodeid: str | TestReport, xml: LogXML) -> None: self.id = nodeid self.xml = xml self.add_stats = self.xml.add_stats self.family = self.xml.family self.duration = 0.0 - self.properties: List[Tuple[str, str]] = [] - self.nodes: List[ET.Element] = [] - self.attrs: Dict[str, str] = {} + self.properties: list[tuple[str, str]] = [] + self.nodes: list[ET.Element] = [] + self.attrs: dict[str, str] = {} def append(self, node: ET.Element) -> None: self.xml.add_stats(node.tag) @@ -109,7 +107,7 @@ def add_property(self, name: str, value: object) -> None: def add_attribute(self, name: str, value: object) -> None: self.attrs[str(name)] = bin_xml_escape(value) - def make_properties_node(self) -> Optional[ET.Element]: + def make_properties_node(self) -> ET.Element | None: """Return a Junit node containing custom properties, if any.""" if self.properties: properties = ET.Element("properties") @@ -124,7 +122,7 @@ def record_testreport(self, testreport: TestReport) -> None: classnames = names[:-1] if self.xml.prefix: classnames.insert(0, self.xml.prefix) - attrs: Dict[str, str] = { + attrs: dict[str, str] = { "classname": ".".join(classnames), "name": bin_xml_escape(names[-1]), "file": testreport.location[0], @@ -149,14 +147,14 @@ def record_testreport(self, testreport: TestReport) -> None: self.attrs = temp_attrs def to_xml(self) -> ET.Element: - testcase = ET.Element("testcase", self.attrs, time="%.3f" % self.duration) + testcase = ET.Element("testcase", self.attrs, time=f"{self.duration:.3f}") properties = self.make_properties_node() if properties is not None: testcase.append(properties) testcase.extend(self.nodes) return testcase - def _add_simple(self, tag: str, message: str, data: Optional[str] = None) -> None: + def _add_simple(self, tag: str, message: str, data: str | None = None) -> None: node = ET.Element(tag, message=message) node.text = bin_xml_escape(data) self.append(node) @@ -201,7 +199,7 @@ def append_failure(self, report: TestReport) -> None: self._add_simple("skipped", "xfail-marked test passes unexpectedly") else: assert report.longrepr is not None - reprcrash: Optional[ReprFileLocation] = getattr( + reprcrash: ReprFileLocation | None = getattr( report.longrepr, "reprcrash", None ) if reprcrash is not None: @@ -221,9 +219,7 @@ def append_collect_skipped(self, report: TestReport) -> None: def append_error(self, report: TestReport) -> None: assert report.longrepr is not None - reprcrash: Optional[ReprFileLocation] = getattr( - report.longrepr, "reprcrash", None - ) + reprcrash: ReprFileLocation | None = getattr(report.longrepr, "reprcrash", None) if reprcrash is not None: reason = reprcrash.message else: @@ -451,7 +447,7 @@ def pytest_unconfigure(config: Config) -> None: config.pluginmanager.unregister(xml) -def mangle_test_address(address: str) -> List[str]: +def mangle_test_address(address: str) -> list[str]: path, possible_open_bracket, params = address.partition("[") names = path.split("::") # Convert file path to dotted path. @@ -466,7 +462,7 @@ class LogXML: def __init__( self, logfile, - prefix: Optional[str], + prefix: str | None, suite_name: str = "pytest", logging: str = "no", report_duration: str = "total", @@ -481,17 +477,15 @@ def __init__( self.log_passing_tests = log_passing_tests self.report_duration = report_duration self.family = family - self.stats: Dict[str, int] = dict.fromkeys( + self.stats: dict[str, int] = dict.fromkeys( ["error", "passed", "failure", "skipped"], 0 ) - self.node_reporters: Dict[ - Tuple[Union[str, TestReport], object], _NodeReporter - ] = {} - self.node_reporters_ordered: List[_NodeReporter] = [] - self.global_properties: List[Tuple[str, str]] = [] + self.node_reporters: dict[tuple[str | TestReport, object], _NodeReporter] = {} + self.node_reporters_ordered: list[_NodeReporter] = [] + self.global_properties: list[tuple[str, str]] = [] # List of reports that failed on call but teardown is pending. - self.open_reports: List[TestReport] = [] + self.open_reports: list[TestReport] = [] self.cnt_double_fail_tests = 0 # Replaces convenience family with real family. @@ -510,8 +504,8 @@ def finalize(self, report: TestReport) -> None: if reporter is not None: reporter.finalize() - def node_reporter(self, report: Union[TestReport, str]) -> _NodeReporter: - nodeid: Union[str, TestReport] = getattr(report, "nodeid", report) + def node_reporter(self, report: TestReport | str) -> _NodeReporter: + nodeid: str | TestReport = getattr(report, "nodeid", report) # Local hack to handle xdist report order. workernode = getattr(report, "node", None) @@ -670,8 +664,10 @@ def pytest_sessionfinish(self) -> None: failures=str(self.stats["failure"]), skipped=str(self.stats["skipped"]), tests=str(numtests), - time="%.3f" % suite_time_delta, - timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(), + time=f"{suite_time_delta:.3f}", + timestamp=datetime.fromtimestamp(self.suite_start_time, timezone.utc) + .astimezone() + .isoformat(), hostname=platform.node(), ) global_properties = self._get_global_properties_node() @@ -691,7 +687,7 @@ def add_global_property(self, name: str, value: object) -> None: _check_record_param_type("name", name) self.global_properties.append((name, bin_xml_escape(value))) - def _get_global_properties_node(self) -> Optional[ET.Element]: + def _get_global_properties_node(self) -> ET.Element | None: """Return a Junit node containing custom properties, if any.""" if self.global_properties: properties = ET.Element("properties") diff --git a/src/_pytest/legacypath.py b/src/_pytest/legacypath.py index b28c89767fe..61476d68932 100644 --- a/src/_pytest/legacypath.py +++ b/src/_pytest/legacypath.py @@ -1,16 +1,15 @@ # mypy: allow-untyped-defs """Add backward compatibility support for the legacy py path type.""" +from __future__ import annotations + import dataclasses from pathlib import Path import shlex import subprocess from typing import Final from typing import final -from typing import List -from typing import Optional from typing import TYPE_CHECKING -from typing import Union from iniconfig import SectionWrapper @@ -50,8 +49,8 @@ class Testdir: __test__ = False - CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN - TimeoutExpired: "Final" = Pytester.TimeoutExpired + CLOSE_STDIN: Final = Pytester.CLOSE_STDIN + TimeoutExpired: Final = Pytester.TimeoutExpired def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) @@ -145,7 +144,7 @@ def copy_example(self, name=None) -> LEGACY_PATH: """See :meth:`Pytester.copy_example`.""" return legacy_path(self._pytester.copy_example(name)) - def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]: + def getnode(self, config: Config, arg) -> Item | Collector | None: """See :meth:`Pytester.getnode`.""" return self._pytester.getnode(config, arg) @@ -153,7 +152,7 @@ def getpathnode(self, path): """See :meth:`Pytester.getpathnode`.""" return self._pytester.getpathnode(path) - def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]: + def genitems(self, colitems: list[Item | Collector]) -> list[Item]: """See :meth:`Pytester.genitems`.""" return self._pytester.genitems(colitems) @@ -205,9 +204,7 @@ def getmodulecol(self, source, configargs=(), withinit=False): source, configargs=configargs, withinit=withinit ) - def collect_by_name( - self, modcol: Collector, name: str - ) -> Optional[Union[Item, Collector]]: + def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None: """See :meth:`Pytester.collect_by_name`.""" return self._pytester.collect_by_name(modcol, name) @@ -238,13 +235,11 @@ def runpytest_subprocess(self, *args, timeout=None) -> RunResult: """See :meth:`Pytester.runpytest_subprocess`.""" return self._pytester.runpytest_subprocess(*args, timeout=timeout) - def spawn_pytest( - self, string: str, expect_timeout: float = 10.0 - ) -> "pexpect.spawn": + def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn: """See :meth:`Pytester.spawn_pytest`.""" return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout) - def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn: """See :meth:`Pytester.spawn`.""" return self._pytester.spawn(cmd, expect_timeout=expect_timeout) @@ -374,7 +369,7 @@ def Config_rootdir(self: Config) -> LEGACY_PATH: return legacy_path(str(self.rootpath)) -def Config_inifile(self: Config) -> Optional[LEGACY_PATH]: +def Config_inifile(self: Config) -> LEGACY_PATH | None: """The path to the :ref:`configfile `. Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. @@ -384,7 +379,7 @@ def Config_inifile(self: Config) -> Optional[LEGACY_PATH]: return legacy_path(str(self.inipath)) if self.inipath else None -def Session_stardir(self: Session) -> LEGACY_PATH: +def Session_startdir(self: Session) -> LEGACY_PATH: """The path from which pytest was invoked. Prefer to use ``startpath`` which is a :class:`pathlib.Path`. @@ -394,9 +389,7 @@ def Session_stardir(self: Session) -> LEGACY_PATH: return legacy_path(self.startpath) -def Config__getini_unknown_type( - self, name: str, type: str, value: Union[str, List[str]] -): +def Config__getini_unknown_type(self, name: str, type: str, value: str | list[str]): if type == "pathlist": # TODO: This assert is probably not valid in all cases. assert self.inipath is not None @@ -439,7 +432,7 @@ def pytest_load_initial_conftests(early_config: Config) -> None: mp.setattr(Config, "inifile", property(Config_inifile), raising=False) # Add Session.startdir property. - mp.setattr(Session, "startdir", property(Session_stardir), raising=False) + mp.setattr(Session, "startdir", property(Session_startdir), raising=False) # Add pathlist configuration type. mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index af5e443ced1..44af8ff2041 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Access and control log capturing.""" +from __future__ import annotations + from contextlib import contextmanager from contextlib import nullcontext from datetime import datetime @@ -22,12 +24,8 @@ from typing import List from typing import Literal from typing import Mapping -from typing import Optional -from typing import Tuple -from typing import Type from typing import TYPE_CHECKING from typing import TypeVar -from typing import Union from _pytest import nodes from _pytest._io import TerminalWriter @@ -68,7 +66,7 @@ class DatetimeFormatter(logging.Formatter): :func:`time.strftime` in case of microseconds in format string. """ - def formatTime(self, record: LogRecord, datefmt: Optional[str] = None) -> str: + def formatTime(self, record: LogRecord, datefmt: str | None = None) -> str: if datefmt and "%f" in datefmt: ct = self.converter(record.created) tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone) @@ -100,7 +98,7 @@ def __init__(self, terminalwriter: TerminalWriter, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._terminalwriter = terminalwriter self._original_fmt = self._style._fmt - self._level_to_fmt_mapping: Dict[int, str] = {} + self._level_to_fmt_mapping: dict[int, str] = {} for level, color_opts in self.LOGLEVEL_COLOROPTS.items(): self.add_color_level(level, *color_opts) @@ -148,12 +146,12 @@ class PercentStyleMultiline(logging.PercentStyle): formats the message as if each line were logged separately. """ - def __init__(self, fmt: str, auto_indent: Union[int, str, bool, None]) -> None: + def __init__(self, fmt: str, auto_indent: int | str | bool | None) -> None: super().__init__(fmt) self._auto_indent = self._get_auto_indent(auto_indent) @staticmethod - def _get_auto_indent(auto_indent_option: Union[int, str, bool, None]) -> int: + def _get_auto_indent(auto_indent_option: int | str | bool | None) -> int: """Determine the current auto indentation setting. Specify auto indent behavior (on/off/fixed) by passing in @@ -348,7 +346,7 @@ class catching_logs(Generic[_HandlerType]): __slots__ = ("handler", "level", "orig_level") - def __init__(self, handler: _HandlerType, level: Optional[int] = None) -> None: + def __init__(self, handler: _HandlerType, level: int | None = None) -> None: self.handler = handler self.level = level @@ -364,9 +362,9 @@ def __enter__(self) -> _HandlerType: def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> None: root_logger = logging.getLogger() if self.level is not None: @@ -380,7 +378,7 @@ class LogCaptureHandler(logging_StreamHandler): def __init__(self) -> None: """Create a new log handler.""" super().__init__(StringIO()) - self.records: List[logging.LogRecord] = [] + self.records: list[logging.LogRecord] = [] def emit(self, record: logging.LogRecord) -> None: """Keep the log records in a list in addition to the log text.""" @@ -401,7 +399,7 @@ def handleError(self, record: logging.LogRecord) -> None: # The default behavior of logging is to print "Logging error" # to stderr with the call stack and some extra details. # pytest wants to make such mistakes visible during testing. - raise + raise # noqa: PLE0704 @final @@ -411,10 +409,10 @@ class LogCaptureFixture: def __init__(self, item: nodes.Node, *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) self._item = item - self._initial_handler_level: Optional[int] = None + self._initial_handler_level: int | None = None # Dict of log name -> log level. - self._initial_logger_levels: Dict[Optional[str], int] = {} - self._initial_disabled_logging_level: Optional[int] = None + self._initial_logger_levels: dict[str | None, int] = {} + self._initial_disabled_logging_level: int | None = None def _finalize(self) -> None: """Finalize the fixture. @@ -439,7 +437,7 @@ def handler(self) -> LogCaptureHandler: def get_records( self, when: Literal["setup", "call", "teardown"] - ) -> List[logging.LogRecord]: + ) -> list[logging.LogRecord]: """Get the logging records for one of the possible test phases. :param when: @@ -458,12 +456,12 @@ def text(self) -> str: return _remove_ansi_escape_sequences(self.handler.stream.getvalue()) @property - def records(self) -> List[logging.LogRecord]: + def records(self) -> list[logging.LogRecord]: """The list of log records.""" return self.handler.records @property - def record_tuples(self) -> List[Tuple[str, int, str]]: + def record_tuples(self) -> list[tuple[str, int, str]]: """A list of a stripped down version of log records intended for use in assertion comparison. @@ -474,7 +472,7 @@ def record_tuples(self) -> List[Tuple[str, int, str]]: return [(r.name, r.levelno, r.getMessage()) for r in self.records] @property - def messages(self) -> List[str]: + def messages(self) -> list[str]: """A list of format-interpolated log messages. Unlike 'records', which contains the format string and parameters for @@ -497,7 +495,7 @@ def clear(self) -> None: self.handler.clear() def _force_enable_logging( - self, level: Union[int, str], logger_obj: logging.Logger + self, level: int | str, logger_obj: logging.Logger ) -> int: """Enable the desired logging level if the global level was disabled via ``logging.disabled``. @@ -530,7 +528,7 @@ def _force_enable_logging( return original_disable_level - def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None: + def set_level(self, level: int | str, logger: str | None = None) -> None: """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. @@ -557,7 +555,7 @@ def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> Non @contextmanager def at_level( - self, level: Union[int, str], logger: Optional[str] = None + self, level: int | str, logger: str | None = None ) -> Generator[None, None, None]: """Context manager that sets the level for capturing of logs. After the end of the 'with' statement the level is restored to its original @@ -615,7 +613,7 @@ def caplog(request: FixtureRequest) -> Generator[LogCaptureFixture, None, None]: result._finalize() -def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[int]: +def get_log_level_for_setting(config: Config, *setting_names: str) -> int | None: for setting_name in setting_names: log_level = config.getoption(setting_name) if log_level is None: @@ -701,9 +699,9 @@ def __init__(self, config: Config) -> None: assert terminal_reporter is not None capture_manager = config.pluginmanager.get_plugin("capturemanager") # if capturemanager plugin is disabled, live logging still works. - self.log_cli_handler: Union[ - _LiveLoggingStreamHandler, _LiveLoggingNullHandler - ] = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) + self.log_cli_handler: ( + _LiveLoggingStreamHandler | _LiveLoggingNullHandler + ) = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) else: self.log_cli_handler = _LiveLoggingNullHandler() log_cli_formatter = self._create_formatter( @@ -714,7 +712,7 @@ def __init__(self, config: Config) -> None: self.log_cli_handler.setFormatter(log_cli_formatter) self._disable_loggers(loggers_to_disable=config.option.logger_disable) - def _disable_loggers(self, loggers_to_disable: List[str]) -> None: + def _disable_loggers(self, loggers_to_disable: list[str]) -> None: if not loggers_to_disable: return @@ -839,7 +837,7 @@ def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, Non def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]: self.log_cli_handler.set_when("setup") - empty: Dict[str, List[logging.LogRecord]] = {} + empty: dict[str, list[logging.LogRecord]] = {} item.stash[caplog_records_key] = empty yield from self._runtest_for(item, "setup") @@ -902,7 +900,7 @@ class _LiveLoggingStreamHandler(logging_StreamHandler): def __init__( self, terminal_reporter: TerminalReporter, - capture_manager: Optional[CaptureManager], + capture_manager: CaptureManager | None, ) -> None: super().__init__(stream=terminal_reporter) # type: ignore[arg-type] self.capture_manager = capture_manager @@ -914,7 +912,7 @@ def reset(self) -> None: """Reset the handler; should be called before the start of each test.""" self._first_record_emitted = False - def set_when(self, when: Optional[str]) -> None: + def set_when(self, when: str | None) -> None: """Prepare for the given test phase (setup/call/teardown).""" self._when = when self._section_name_shown = False diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 716d5cf783b..befc7ccce6e 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -1,5 +1,7 @@ """Core implementation of the testing process: init, session, runtest loop.""" +from __future__ import annotations + import argparse import dataclasses import fnmatch @@ -13,17 +15,12 @@ from typing import Callable from typing import Dict from typing import final -from typing import FrozenSet from typing import Iterable 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 Tuple from typing import TYPE_CHECKING -from typing import Union import warnings import pluggy @@ -38,7 +35,6 @@ from _pytest.config import UsageError from _pytest.config.argparsing import Parser from _pytest.config.compat import PathAwareHookProxy -from _pytest.fixtures import FixtureManager from _pytest.outcomes import exit from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath @@ -55,6 +51,8 @@ if TYPE_CHECKING: from typing import Self + from _pytest.fixtures import FixtureManager + def pytest_addoption(parser: Parser) -> None: parser.addini( @@ -270,8 +268,8 @@ def is_ancestor(base: Path, query: Path) -> bool: def wrap_session( - config: Config, doit: Callable[[Config, "Session"], Optional[Union[int, ExitCode]]] -) -> Union[int, ExitCode]: + config: Config, doit: Callable[[Config, Session], int | ExitCode | None] +) -> int | ExitCode: """Skeleton command line program.""" session = Session.from_config(config) session.exitstatus = ExitCode.OK @@ -290,7 +288,7 @@ def wrap_session( session.exitstatus = ExitCode.TESTS_FAILED except (KeyboardInterrupt, exit.Exception): excinfo = _pytest._code.ExceptionInfo.from_current() - exitstatus: Union[int, ExitCode] = ExitCode.INTERRUPTED + exitstatus: int | ExitCode = ExitCode.INTERRUPTED if isinstance(excinfo.value, exit.Exception): if excinfo.value.returncode is not None: exitstatus = excinfo.value.returncode @@ -328,11 +326,11 @@ def wrap_session( return session.exitstatus -def pytest_cmdline_main(config: Config) -> Union[int, ExitCode]: +def pytest_cmdline_main(config: Config) -> int | ExitCode: return wrap_session(config, _main) -def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]: +def _main(config: Config, session: Session) -> int | ExitCode | None: """Default command line protocol for initialization, session, running tests and reporting.""" config.hook.pytest_collection(session=session) @@ -345,11 +343,11 @@ def _main(config: Config, session: "Session") -> Optional[Union[int, ExitCode]]: return None -def pytest_collection(session: "Session") -> None: +def pytest_collection(session: Session) -> None: session.perform_collect() -def pytest_runtestloop(session: "Session") -> bool: +def pytest_runtestloop(session: Session) -> bool: if session.testsfailed and not session.config.option.continue_on_collection_errors: raise session.Interrupted( "%d error%s during collection" @@ -371,25 +369,26 @@ def pytest_runtestloop(session: "Session") -> bool: def _in_venv(path: Path) -> bool: """Attempt to detect if ``path`` is the root of a Virtual Environment by - checking for the existence of the appropriate activate script.""" - bindir = path.joinpath("Scripts" if sys.platform.startswith("win") else "bin") + checking for the existence of the pyvenv.cfg file. + + [https://peps.python.org/pep-0405/] + + For regression protection we also check for conda environments that do not include pyenv.cfg yet -- + https://github.com/conda/conda/issues/13337 is the conda issue tracking adding pyenv.cfg. + + Checking for the `conda-meta/history` file per https://github.com/pytest-dev/pytest/issues/12652#issuecomment-2246336902. + + """ try: - if not bindir.is_dir(): - return False + return ( + path.joinpath("pyvenv.cfg").is_file() + or path.joinpath("conda-meta", "history").is_file() + ) except OSError: return False - activates = ( - "activate", - "activate.csh", - "activate.fish", - "Activate", - "Activate.bat", - "Activate.ps1", - ) - return any(fname.name in activates for fname in bindir.iterdir()) -def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]: +def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: if collection_path.name == "__pycache__": return True @@ -429,11 +428,11 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo def pytest_collect_directory( path: Path, parent: nodes.Collector -) -> Optional[nodes.Collector]: +) -> nodes.Collector | None: return Dir.from_parent(parent, path=path) -def pytest_collection_modifyitems(items: List[nodes.Item], config: Config) -> None: +def pytest_collection_modifyitems(items: list[nodes.Item], config: Config) -> None: deselect_prefixes = tuple(config.getoption("deselect") or []) if not deselect_prefixes: return @@ -507,17 +506,18 @@ def from_parent( # type: ignore[override] parent: nodes.Collector, *, path: Path, - ) -> "Self": + ) -> Self: """The public constructor. :param parent: The parent collector of this Dir. :param path: The directory's path. + :type path: pathlib.Path """ return super().from_parent(parent=parent, path=path) - def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: config = self.config - col: Optional[nodes.Collector] + col: nodes.Collector | None cols: Sequence[nodes.Collector] ihook = self.ihook for direntry in scandir(self.path): @@ -552,7 +552,7 @@ class Session(nodes.Collector): _setupstate: SetupState # Set on the session by fixtures.pytest_sessionstart. _fixturemanager: FixtureManager - exitstatus: Union[int, ExitCode] + exitstatus: int | ExitCode def __init__(self, config: Config) -> None: super().__init__( @@ -566,22 +566,22 @@ def __init__(self, config: Config) -> None: ) self.testsfailed = 0 self.testscollected = 0 - self._shouldstop: Union[bool, str] = False - self._shouldfail: Union[bool, str] = False + self._shouldstop: bool | str = False + self._shouldfail: bool | str = False self.trace = config.trace.root.get("collection") - self._initialpaths: FrozenSet[Path] = frozenset() - self._initialpaths_with_parents: FrozenSet[Path] = frozenset() - self._notfound: List[Tuple[str, Sequence[nodes.Collector]]] = [] - self._initial_parts: List[CollectionArgument] = [] - self._collection_cache: Dict[nodes.Collector, CollectReport] = {} - self.items: List[nodes.Item] = [] + self._initialpaths: frozenset[Path] = frozenset() + self._initialpaths_with_parents: frozenset[Path] = frozenset() + self._notfound: list[tuple[str, Sequence[nodes.Collector]]] = [] + self._initial_parts: list[CollectionArgument] = [] + self._collection_cache: dict[nodes.Collector, CollectReport] = {} + self.items: list[nodes.Item] = [] - self._bestrelpathcache: Dict[Path, str] = _bestrelpath_cache(config.rootpath) + self._bestrelpathcache: dict[Path, str] = _bestrelpath_cache(config.rootpath) self.config.pluginmanager.register(self, name="session") @classmethod - def from_config(cls, config: Config) -> "Session": + def from_config(cls, config: Config) -> Session: session: Session = cls._create(config=config) return session @@ -595,11 +595,11 @@ def __repr__(self) -> str: ) @property - def shouldstop(self) -> Union[bool, str]: + def shouldstop(self) -> bool | str: return self._shouldstop @shouldstop.setter - def shouldstop(self, value: Union[bool, str]) -> None: + def shouldstop(self, value: bool | str) -> None: # The runner checks shouldfail and assumes that if it is set we are # definitely stopping, so prevent unsetting it. if value is False and self._shouldstop: @@ -613,11 +613,11 @@ def shouldstop(self, value: Union[bool, str]) -> None: self._shouldstop = value @property - def shouldfail(self) -> Union[bool, str]: + def shouldfail(self) -> bool | str: return self._shouldfail @shouldfail.setter - def shouldfail(self, value: Union[bool, str]) -> None: + def shouldfail(self, value: bool | str) -> None: # The runner checks shouldfail and assumes that if it is set we are # definitely stopping, so prevent unsetting it. if value is False and self._shouldfail: @@ -650,9 +650,7 @@ def pytest_collectstart(self) -> None: raise self.Interrupted(self.shouldstop) @hookimpl(tryfirst=True) - def pytest_runtest_logreport( - self, report: Union[TestReport, CollectReport] - ) -> None: + def pytest_runtest_logreport(self, report: TestReport | CollectReport) -> None: if report.failed and not hasattr(report, "wasxfail"): self.testsfailed += 1 maxfail = self.config.getvalue("maxfail") @@ -663,7 +661,7 @@ def pytest_runtest_logreport( def isinitpath( self, - path: Union[str, "os.PathLike[str]"], + path: str | os.PathLike[str], *, with_parents: bool = False, ) -> bool: @@ -685,7 +683,7 @@ def isinitpath( else: return path_ in self._initialpaths - def gethookproxy(self, fspath: "os.PathLike[str]") -> pluggy.HookRelay: + def gethookproxy(self, fspath: os.PathLike[str]) -> pluggy.HookRelay: # Optimization: Path(Path(...)) is much slower than isinstance. path = fspath if isinstance(fspath, Path) else Path(fspath) pm = self.config.pluginmanager @@ -705,7 +703,7 @@ def gethookproxy(self, fspath: "os.PathLike[str]") -> pluggy.HookRelay: def _collect_path( self, path: Path, - path_cache: Dict[Path, Sequence[nodes.Collector]], + path_cache: dict[Path, Sequence[nodes.Collector]], ) -> Sequence[nodes.Collector]: """Create a Collector for the given path. @@ -717,7 +715,7 @@ def _collect_path( if path.is_dir(): ihook = self.gethookproxy(path.parent) - col: Optional[nodes.Collector] = ihook.pytest_collect_directory( + col: nodes.Collector | None = ihook.pytest_collect_directory( path=path, parent=self ) cols: Sequence[nodes.Collector] = (col,) if col is not None else () @@ -735,17 +733,17 @@ def _collect_path( @overload def perform_collect( - self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ... + self, args: Sequence[str] | None = ..., genitems: Literal[True] = ... ) -> Sequence[nodes.Item]: ... @overload def perform_collect( - self, args: Optional[Sequence[str]] = ..., genitems: bool = ... - ) -> Sequence[Union[nodes.Item, nodes.Collector]]: ... + self, args: Sequence[str] | None = ..., genitems: bool = ... + ) -> Sequence[nodes.Item | nodes.Collector]: ... def perform_collect( - self, args: Optional[Sequence[str]] = None, genitems: bool = True - ) -> Sequence[Union[nodes.Item, nodes.Collector]]: + self, args: Sequence[str] | None = None, genitems: bool = True + ) -> Sequence[nodes.Item | nodes.Collector]: """Perform the collection phase for this session. This is called by the default :hook:`pytest_collection` hook @@ -771,10 +769,10 @@ def perform_collect( self._initial_parts = [] self._collection_cache = {} self.items = [] - items: Sequence[Union[nodes.Item, nodes.Collector]] = self.items + items: Sequence[nodes.Item | nodes.Collector] = self.items try: - initialpaths: List[Path] = [] - initialpaths_with_parents: List[Path] = [] + initialpaths: list[Path] = [] + initialpaths_with_parents: list[Path] = [] for arg in args: collection_argument = resolve_collection_argument( self.config.invocation_params.dir, @@ -829,7 +827,7 @@ def _collect_one_node( self, node: nodes.Collector, handle_dupes: bool = True, - ) -> Tuple[CollectReport, bool]: + ) -> tuple[CollectReport, bool]: if node in self._collection_cache and handle_dupes: rep = self._collection_cache[node] return rep, True @@ -838,11 +836,11 @@ def _collect_one_node( self._collection_cache[node] = rep return rep, False - def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: + def collect(self) -> Iterator[nodes.Item | nodes.Collector]: # This is a cache for the root directories of the initial paths. # We can't use collection_cache for Session because of its special # role as the bootstrapping collector. - path_cache: Dict[Path, Sequence[nodes.Collector]] = {} + path_cache: dict[Path, Sequence[nodes.Collector]] = {} pm = self.config.pluginmanager @@ -880,9 +878,9 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # and discarding all nodes which don't match the level's part. any_matched_in_initial_part = False notfound_collectors = [] - work: List[ - Tuple[Union[nodes.Collector, nodes.Item], List[Union[Path, str]]] - ] = [(self, [*paths, *names])] + work: list[tuple[nodes.Collector | nodes.Item, list[Path | str]]] = [ + (self, [*paths, *names]) + ] while work: matchnode, matchparts = work.pop() @@ -899,7 +897,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: # Collect this level of matching. # Collecting Session (self) is done directly to avoid endless # recursion to this function. - subnodes: Sequence[Union[nodes.Collector, nodes.Item]] + subnodes: Sequence[nodes.Collector | nodes.Item] if isinstance(matchnode, Session): assert isinstance(matchparts[0], Path) subnodes = matchnode._collect_path(matchparts[0], path_cache) @@ -959,9 +957,7 @@ def collect(self) -> Iterator[Union[nodes.Item, nodes.Collector]]: self.trace.root.indent -= 1 - def genitems( - self, node: Union[nodes.Item, nodes.Collector] - ) -> Iterator[nodes.Item]: + def genitems(self, node: nodes.Item | nodes.Collector) -> Iterator[nodes.Item]: self.trace("genitems", node) if isinstance(node, nodes.Item): node.ihook.pytest_itemcollected(item=node) @@ -981,7 +977,7 @@ def genitems( node.ihook.pytest_collectreport(report=rep) -def search_pypath(module_name: str) -> Optional[str]: +def search_pypath(module_name: str) -> str | None: """Search sys.path for the given a dotted module name, and return its file system path if found.""" try: @@ -1005,7 +1001,7 @@ class CollectionArgument: path: Path parts: Sequence[str] - module_name: Optional[str] + module_name: str | None def resolve_collection_argument( diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 77dabd95dec..a4f942c5ae3 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -1,12 +1,14 @@ """Generic mechanism for marking and selecting python functions.""" +from __future__ import annotations + +import collections import dataclasses from typing import AbstractSet from typing import Collection -from typing import List +from typing import Iterable from typing import Optional from typing import TYPE_CHECKING -from typing import Union from .expression import Expression from .expression import ParseError @@ -21,6 +23,7 @@ from _pytest.config import ExitCode from _pytest.config import hookimpl from _pytest.config import UsageError +from _pytest.config.argparsing import NOT_SET from _pytest.config.argparsing import Parser from _pytest.stash import StashKey @@ -44,8 +47,8 @@ def param( *values: object, - marks: Union[MarkDecorator, Collection[Union[MarkDecorator, Mark]]] = (), - id: Optional[str] = None, + marks: MarkDecorator | Collection[MarkDecorator | Mark] = (), + id: str | None = None, ) -> ParameterSet: """Specify a parameter in `pytest.mark.parametrize`_ calls or :ref:`parametrized fixtures `. @@ -78,7 +81,7 @@ def pytest_addoption(parser: Parser) -> None: default="", metavar="EXPRESSION", help="Only run tests which match the given substring expression. " - "An expression is a Python evaluatable expression " + "An expression is a Python evaluable expression " "where all names are substring-matched against test names " "and their parent classes. Example: -k 'test_method or test_" "other' matches all test functions and classes whose name " @@ -112,7 +115,7 @@ def pytest_addoption(parser: Parser) -> None: @hookimpl(tryfirst=True) -def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: import _pytest.config if config.option.markers: @@ -122,7 +125,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: parts = line.split(":", 1) name = parts[0] rest = parts[1] if len(parts) == 2 else "" - tw.write("@pytest.mark.%s:" % name, bold=True) + tw.write(f"@pytest.mark.{name}:", bold=True) tw.line(rest) tw.line() config._ensure_unconfigure() @@ -151,7 +154,7 @@ class KeywordMatcher: _names: AbstractSet[str] @classmethod - def from_item(cls, item: "Item") -> "KeywordMatcher": + def from_item(cls, item: Item) -> KeywordMatcher: mapped_names = set() # Add the names of the current item and any parent items, @@ -181,7 +184,9 @@ def from_item(cls, item: "Item") -> "KeywordMatcher": return cls(mapped_names) - def __call__(self, subname: str) -> bool: + def __call__(self, subname: str, /, **kwargs: str | int | bool | None) -> bool: + if kwargs: + raise UsageError("Keyword expressions do not support call parameters.") subname = subname.lower() names = (name.lower() for name in self._names) @@ -191,7 +196,7 @@ def __call__(self, subname: str) -> bool: return False -def deselect_by_keyword(items: "List[Item]", config: Config) -> None: +def deselect_by_keyword(items: list[Item], config: Config) -> None: keywordexpr = config.option.keyword.lstrip() if not keywordexpr: return @@ -218,29 +223,38 @@ class MarkMatcher: Tries to match on any marker names, attached to the given colitem. """ - __slots__ = ("own_mark_names",) + __slots__ = ("own_mark_name_mapping",) - own_mark_names: AbstractSet[str] + own_mark_name_mapping: dict[str, list[Mark]] @classmethod - def from_item(cls, item: "Item") -> "MarkMatcher": - mark_names = {mark.name for mark in item.iter_markers()} - return cls(mark_names) + def from_markers(cls, markers: Iterable[Mark]) -> MarkMatcher: + mark_name_mapping = collections.defaultdict(list) + for mark in markers: + mark_name_mapping[mark.name].append(mark) + return cls(mark_name_mapping) + + def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: + if not (matches := self.own_mark_name_mapping.get(name, [])): + return False + + for mark in matches: + if all(mark.kwargs.get(k, NOT_SET) == v for k, v in kwargs.items()): + return True - def __call__(self, name: str) -> bool: - return name in self.own_mark_names + return False -def deselect_by_mark(items: "List[Item]", config: Config) -> None: +def deselect_by_mark(items: list[Item], config: Config) -> None: matchexpr = config.option.markexpr if not matchexpr: return expr = _parse_expression(matchexpr, "Wrong expression passed to '-m'") - remaining: List[Item] = [] - deselected: List[Item] = [] + remaining: list[Item] = [] + deselected: list[Item] = [] for item in items: - if expr.evaluate(MarkMatcher.from_item(item)): + if expr.evaluate(MarkMatcher.from_markers(item.iter_markers())): remaining.append(item) else: deselected.append(item) @@ -256,7 +270,7 @@ def _parse_expression(expr: str, exc_message: str) -> Expression: raise UsageError(f"{exc_message}: {expr}: {e}") from None -def pytest_collection_modifyitems(items: "List[Item]", config: Config) -> None: +def pytest_collection_modifyitems(items: list[Item], config: Config) -> None: deselect_by_keyword(items, config) deselect_by_mark(items, config) diff --git a/src/_pytest/mark/expression.py b/src/_pytest/mark/expression.py index 78b7fda696b..89cc0e94d3b 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -5,26 +5,35 @@ expression: expr? EOF expr: and_expr ('or' and_expr)* and_expr: not_expr ('and' not_expr)* -not_expr: 'not' not_expr | '(' expr ')' | ident +not_expr: 'not' not_expr | '(' expr ')' | ident kwargs? + ident: (\w|:|\+|-|\.|\[|\]|\\|/)+ +kwargs: ('(' name '=' value ( ', ' name '=' value )* ')') +name: a valid ident, but not a reserved keyword +value: (unescaped) string literal | (-)?[0-9]+ | 'False' | 'True' | 'None' The semantics are: - Empty expression evaluates to False. -- ident evaluates to True of False according to a provided matcher function. +- ident evaluates to True or False according to a provided matcher function. - or/and/not evaluate according to the usual boolean semantics. +- ident with parentheses and keyword arguments evaluates to True or False according to a provided matcher function. """ +from __future__ import annotations + import ast import dataclasses import enum +import keyword import re import types -from typing import Callable from typing import Iterator +from typing import Literal from typing import Mapping from typing import NoReturn -from typing import Optional +from typing import overload +from typing import Protocol from typing import Sequence @@ -42,6 +51,9 @@ class TokenType(enum.Enum): NOT = "not" IDENT = "identifier" EOF = "end of input" + EQUAL = "=" + STRING = "string literal" + COMMA = "," @dataclasses.dataclass(frozen=True) @@ -85,6 +97,27 @@ def lex(self, input: str) -> Iterator[Token]: elif input[pos] == ")": yield Token(TokenType.RPAREN, ")", pos) pos += 1 + elif input[pos] == "=": + yield Token(TokenType.EQUAL, "=", pos) + pos += 1 + elif input[pos] == ",": + yield Token(TokenType.COMMA, ",", pos) + pos += 1 + elif (quote_char := input[pos]) in ("'", '"'): + end_quote_pos = input.find(quote_char, pos + 1) + if end_quote_pos == -1: + raise ParseError( + pos + 1, + f'closing quote "{quote_char}" is missing', + ) + value = input[pos : end_quote_pos + 1] + if (backslash_pos := input.find("\\")) != -1: + raise ParseError( + backslash_pos + 1, + r'escaping with "\" not supported in marker expression', + ) + yield Token(TokenType.STRING, value, pos) + pos += len(value) else: match = re.match(r"(:?\w|:|\+|-|\.|\[|\]|\\|/)+", input[pos:]) if match: @@ -105,7 +138,15 @@ def lex(self, input: str) -> Iterator[Token]: ) yield Token(TokenType.EOF, "", pos) - def accept(self, type: TokenType, *, reject: bool = False) -> Optional[Token]: + @overload + def accept(self, type: TokenType, *, reject: Literal[True]) -> Token: ... + + @overload + def accept( + self, type: TokenType, *, reject: Literal[False] = False + ) -> Token | None: ... + + def accept(self, type: TokenType, *, reject: bool = False) -> Token | None: if self.current.type is type: token = self.current if token.type is not TokenType.EOF: @@ -165,18 +206,87 @@ def not_expr(s: Scanner) -> ast.expr: return ret ident = s.accept(TokenType.IDENT) if ident: - return ast.Name(IDENT_PREFIX + ident.value, ast.Load()) + name = ast.Name(IDENT_PREFIX + ident.value, ast.Load()) + if s.accept(TokenType.LPAREN): + ret = ast.Call(func=name, args=[], keywords=all_kwargs(s)) + s.accept(TokenType.RPAREN, reject=True) + else: + ret = name + return ret + s.reject((TokenType.NOT, TokenType.LPAREN, TokenType.IDENT)) -class MatcherAdapter(Mapping[str, bool]): +BUILTIN_MATCHERS = {"True": True, "False": False, "None": None} + + +def single_kwarg(s: Scanner) -> ast.keyword: + keyword_name = s.accept(TokenType.IDENT, reject=True) + if not keyword_name.value.isidentifier(): + raise ParseError( + keyword_name.pos + 1, + f"not a valid python identifier {keyword_name.value}", + ) + if keyword.iskeyword(keyword_name.value): + raise ParseError( + keyword_name.pos + 1, + f"unexpected reserved python keyword `{keyword_name.value}`", + ) + s.accept(TokenType.EQUAL, reject=True) + + if value_token := s.accept(TokenType.STRING): + value: str | int | bool | None = value_token.value[1:-1] # strip quotes + else: + value_token = s.accept(TokenType.IDENT, reject=True) + if ( + (number := value_token.value).isdigit() + or number.startswith("-") + and number[1:].isdigit() + ): + value = int(number) + elif value_token.value in BUILTIN_MATCHERS: + value = BUILTIN_MATCHERS[value_token.value] + else: + raise ParseError( + value_token.pos + 1, + f'unexpected character/s "{value_token.value}"', + ) + + ret = ast.keyword(keyword_name.value, ast.Constant(value)) + return ret + + +def all_kwargs(s: Scanner) -> list[ast.keyword]: + ret = [single_kwarg(s)] + while s.accept(TokenType.COMMA): + ret.append(single_kwarg(s)) + return ret + + +class MatcherCall(Protocol): + def __call__(self, name: str, /, **kwargs: str | int | bool | None) -> bool: ... + + +@dataclasses.dataclass +class MatcherNameAdapter: + matcher: MatcherCall + name: str + + def __bool__(self) -> bool: + return self.matcher(self.name) + + def __call__(self, **kwargs: str | int | bool | None) -> bool: + return self.matcher(self.name, **kwargs) + + +class MatcherAdapter(Mapping[str, MatcherNameAdapter]): """Adapts a matcher function to a locals mapping as required by eval().""" - def __init__(self, matcher: Callable[[str], bool]) -> None: + def __init__(self, matcher: MatcherCall) -> None: self.matcher = matcher - def __getitem__(self, key: str) -> bool: - return self.matcher(key[len(IDENT_PREFIX) :]) + def __getitem__(self, key: str) -> MatcherNameAdapter: + return MatcherNameAdapter(matcher=self.matcher, name=key[len(IDENT_PREFIX) :]) def __iter__(self) -> Iterator[str]: raise NotImplementedError() @@ -197,7 +307,7 @@ def __init__(self, code: types.CodeType) -> None: self.code = code @classmethod - def compile(self, input: str) -> "Expression": + def compile(self, input: str) -> Expression: """Compile a match expression. :param input: The input expression - one line. @@ -210,7 +320,7 @@ def compile(self, input: str) -> "Expression": ) return Expression(code) - def evaluate(self, matcher: Callable[[str], bool]) -> bool: + def evaluate(self, matcher: MatcherCall) -> bool: """Evaluate the match expression. :param matcher: @@ -219,5 +329,5 @@ def evaluate(self, matcher: Callable[[str], bool]) -> bool: :returns: Whether the expression matches or not. """ - ret: bool = eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher)) + ret: bool = bool(eval(self.code, {"__builtins__": {}}, MatcherAdapter(matcher))) return ret diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index a6503bf1d46..92ade55f7c0 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import collections.abc import dataclasses import inspect @@ -8,16 +10,11 @@ from typing import final from typing import Iterable from typing import Iterator -from typing import List from typing import Mapping from typing import MutableMapping from typing import NamedTuple -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 @@ -31,6 +28,7 @@ from _pytest.deprecated import check_ispytest from _pytest.deprecated import MARKED_FIXTURE from _pytest.outcomes import fail +from _pytest.scope import _ScopeName from _pytest.warning_types import PytestUnknownMarkWarning @@ -47,7 +45,7 @@ def istestfunc(func) -> bool: def get_empty_parameterset_mark( config: Config, argnames: Sequence[str], func -) -> "MarkDecorator": +) -> MarkDecorator: from ..nodes import Collector fs, lineno = getfslineno(func) @@ -75,17 +73,17 @@ def get_empty_parameterset_mark( class ParameterSet(NamedTuple): - values: Sequence[Union[object, NotSetType]] - marks: Collection[Union["MarkDecorator", "Mark"]] - id: Optional[str] + values: Sequence[object | NotSetType] + marks: Collection[MarkDecorator | Mark] + id: str | None @classmethod def param( cls, *values: object, - marks: Union["MarkDecorator", Collection[Union["MarkDecorator", "Mark"]]] = (), - id: Optional[str] = None, - ) -> "ParameterSet": + marks: MarkDecorator | Collection[MarkDecorator | Mark] = (), + id: str | None = None, + ) -> ParameterSet: if isinstance(marks, MarkDecorator): marks = (marks,) else: @@ -100,9 +98,9 @@ def param( @classmethod def extract_from( cls, - parameterset: Union["ParameterSet", Sequence[object], object], + parameterset: ParameterSet | Sequence[object] | object, force_tuple: bool = False, - ) -> "ParameterSet": + ) -> ParameterSet: """Extract from an object or objects. :param parameterset: @@ -127,11 +125,11 @@ def extract_from( @staticmethod def _parse_parametrize_args( - argnames: Union[str, Sequence[str]], - argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], *args, **kwargs, - ) -> Tuple[Sequence[str], bool]: + ) -> tuple[Sequence[str], bool]: if isinstance(argnames, str): argnames = [x.strip() for x in argnames.split(",") if x.strip()] force_tuple = len(argnames) == 1 @@ -141,9 +139,9 @@ def _parse_parametrize_args( @staticmethod def _parse_parametrize_parameters( - argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], + argvalues: Iterable[ParameterSet | Sequence[object] | object], force_tuple: bool, - ) -> List["ParameterSet"]: + ) -> list[ParameterSet]: return [ ParameterSet.extract_from(x, force_tuple=force_tuple) for x in argvalues ] @@ -151,12 +149,12 @@ def _parse_parametrize_parameters( @classmethod def _for_parametrize( cls, - argnames: Union[str, Sequence[str]], - argvalues: Iterable[Union["ParameterSet", Sequence[object], object]], + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], func, config: Config, nodeid: str, - ) -> Tuple[Sequence[str], List["ParameterSet"]]: + ) -> tuple[Sequence[str], list[ParameterSet]]: argnames, force_tuple = cls._parse_parametrize_args(argnames, argvalues) parameters = cls._parse_parametrize_parameters(argvalues, force_tuple) del argvalues @@ -199,24 +197,24 @@ class Mark: #: Name of the mark. name: str #: Positional arguments of the mark decorator. - args: Tuple[Any, ...] + args: tuple[Any, ...] #: Keyword arguments of the mark decorator. kwargs: Mapping[str, Any] #: Source Mark for ids with parametrize Marks. - _param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False) + _param_ids_from: Mark | None = dataclasses.field(default=None, repr=False) #: Resolved/generated ids with parametrize Marks. - _param_ids_generated: Optional[Sequence[str]] = dataclasses.field( + _param_ids_generated: Sequence[str] | None = dataclasses.field( default=None, repr=False ) def __init__( self, name: str, - args: Tuple[Any, ...], + args: tuple[Any, ...], kwargs: Mapping[str, Any], - param_ids_from: Optional["Mark"] = None, - param_ids_generated: Optional[Sequence[str]] = None, + param_ids_from: Mark | None = None, + param_ids_generated: Sequence[str] | None = None, *, _ispytest: bool = False, ) -> None: @@ -232,7 +230,7 @@ def __init__( def _has_param_ids(self) -> bool: return "ids" in self.kwargs or len(self.args) >= 4 - def combined_with(self, other: "Mark") -> "Mark": + def combined_with(self, other: Mark) -> Mark: """Return a new Mark which is a combination of this Mark and another Mark. @@ -244,7 +242,7 @@ def combined_with(self, other: "Mark") -> "Mark": assert self.name == other.name # Remember source of ids with parametrize Marks. - param_ids_from: Optional[Mark] = None + param_ids_from: Mark | None = None if self.name == "parametrize": if other._has_param_ids(): param_ids_from = other @@ -315,7 +313,7 @@ def name(self) -> str: return self.mark.name @property - def args(self) -> Tuple[Any, ...]: + def args(self) -> tuple[Any, ...]: """Alias for mark.args.""" return self.mark.args @@ -329,7 +327,7 @@ def markname(self) -> str: """:meta private:""" return self.name # for backward-compat (2.4.1 had this attr) - def with_args(self, *args: object, **kwargs: object) -> "MarkDecorator": + def with_args(self, *args: object, **kwargs: object) -> MarkDecorator: """Return a MarkDecorator with extra arguments added. Unlike calling the MarkDecorator, with_args() can be used even @@ -346,7 +344,7 @@ def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap] pass @overload - def __call__(self, *args: object, **kwargs: object) -> "MarkDecorator": + def __call__(self, *args: object, **kwargs: object) -> MarkDecorator: pass def __call__(self, *args: object, **kwargs: object): @@ -361,10 +359,10 @@ def __call__(self, *args: object, **kwargs: object): def get_unpacked_marks( - obj: Union[object, type], + obj: object | type, *, consider_mro: bool = True, -) -> List[Mark]: +) -> list[Mark]: """Obtain the unpacked marks that are stored on an object. If obj is a class and consider_mro is true, return marks applied to @@ -394,7 +392,7 @@ def get_unpacked_marks( def normalize_mark_list( - mark_list: Iterable[Union[Mark, MarkDecorator]], + mark_list: Iterable[Mark | MarkDecorator], ) -> Iterable[Mark]: """ Normalize an iterable of Mark or MarkDecorator objects into a list of marks @@ -430,20 +428,19 @@ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None: # Typing for builtin pytest marks. This is cheating; it gives builtin marks # special privilege, and breaks modularity. But practicality beats purity... if TYPE_CHECKING: - from _pytest.scope import _ScopeName class _SkipMarkDecorator(MarkDecorator): @overload # type: ignore[override,no-overload-impl] def __call__(self, arg: Markable) -> Markable: ... @overload - def __call__(self, reason: str = ...) -> "MarkDecorator": ... + def __call__(self, reason: str = ...) -> MarkDecorator: ... class _SkipifMarkDecorator(MarkDecorator): def __call__( # type: ignore[override] self, - condition: Union[str, bool] = ..., - *conditions: Union[str, bool], + condition: str | bool = ..., + *conditions: str | bool, reason: str = ..., ) -> MarkDecorator: ... @@ -454,30 +451,25 @@ def __call__(self, arg: Markable) -> Markable: ... @overload def __call__( self, - condition: Union[str, bool] = False, - *conditions: Union[str, bool], + condition: str | bool = False, + *conditions: str | bool, reason: str = ..., run: bool = ..., - raises: Union[ - None, Type[BaseException], Tuple[Type[BaseException], ...] - ] = ..., + raises: None | type[BaseException] | tuple[type[BaseException], ...] = ..., strict: bool = ..., ) -> MarkDecorator: ... class _ParametrizeMarkDecorator(MarkDecorator): def __call__( # type: ignore[override] self, - argnames: Union[str, Sequence[str]], - argvalues: Iterable[Union[ParameterSet, Sequence[object], object]], + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], *, - indirect: Union[bool, Sequence[str]] = ..., - ids: Optional[ - Union[ - Iterable[Union[None, str, float, int, bool]], - Callable[[Any], Optional[object]], - ] - ] = ..., - scope: Optional[_ScopeName] = ..., + indirect: bool | Sequence[str] = ..., + ids: Iterable[None | str | float | int | bool] + | Callable[[Any], object | None] + | None = ..., + scope: _ScopeName | None = ..., ) -> MarkDecorator: ... class _UsefixturesMarkDecorator(MarkDecorator): @@ -517,8 +509,8 @@ def test_function(): def __init__(self, *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) - self._config: Optional[Config] = None - self._markers: Set[str] = set() + self._config: Config | None = None + self._markers: set[str] = set() def __getattr__(self, name: str) -> MarkDecorator: """Generate a new :class:`MarkDecorator` with the given name.""" @@ -552,9 +544,9 @@ def __getattr__(self, name: str) -> MarkDecorator: fail(f"Unknown '{name}' mark, did you mean 'parametrize'?") warnings.warn( - "Unknown pytest.mark.%s - is this a typo? You can register " + f"Unknown pytest.mark.{name} - is this a typo? You can register " "custom marks to avoid this warning - for details, see " - "https://docs.pytest.org/en/stable/how-to/mark.html" % name, + "https://docs.pytest.org/en/stable/how-to/mark.html", PytestUnknownMarkWarning, 2, ) @@ -569,7 +561,7 @@ def __getattr__(self, name: str) -> MarkDecorator: class NodeKeywords(MutableMapping[str, Any]): __slots__ = ("node", "parent", "_markers") - def __init__(self, node: "Node") -> None: + def __init__(self, node: Node) -> None: self.node = node self.parent = node.parent self._markers = {node.name: True} @@ -597,7 +589,7 @@ def __contains__(self, key: object) -> bool: def update( # type: ignore[override] self, - other: Union[Mapping[str, Any], Iterable[Tuple[str, Any]]] = (), + other: Mapping[str, Any] | Iterable[tuple[str, Any]] = (), **kwds: Any, ) -> None: self._markers.update(other) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 3f398df76b1..75b019a3be6 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Monkeypatching and mocking functionality.""" +from __future__ import annotations + from contextlib import contextmanager import os import re @@ -8,14 +10,10 @@ from typing import Any from typing import final from typing import Generator -from typing import List from typing import Mapping from typing import MutableMapping -from typing import Optional from typing import overload -from typing import Tuple from typing import TypeVar -from typing import Union import warnings from _pytest.fixtures import fixture @@ -30,7 +28,7 @@ @fixture -def monkeypatch() -> Generator["MonkeyPatch", None, None]: +def monkeypatch() -> Generator[MonkeyPatch, None, None]: """A convenient fixture for monkey-patching. The fixture provides these methods to modify objects, dictionaries, or @@ -97,7 +95,7 @@ def annotated_getattr(obj: object, name: str, ann: str) -> object: return obj -def derive_importpath(import_path: str, raising: bool) -> Tuple[str, object]: +def derive_importpath(import_path: str, raising: bool) -> tuple[str, object]: if not isinstance(import_path, str) or "." not in import_path: raise TypeError(f"must be absolute import path string, not {import_path!r}") module, attr = import_path.rsplit(".", 1) @@ -130,18 +128,19 @@ class MonkeyPatch: """ def __init__(self) -> None: - self._setattr: List[Tuple[object, str, object]] = [] - self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = [] - self._cwd: Optional[str] = None - self._savesyspath: Optional[List[str]] = None + self._setattr: list[tuple[object, str, object]] = [] + self._setitem: list[tuple[Mapping[Any, Any], object, object]] = [] + self._cwd: str | None = None + self._savesyspath: list[str] | None = None @classmethod @contextmanager - def context(cls) -> Generator["MonkeyPatch", None, None]: + def context(cls) -> Generator[MonkeyPatch, None, None]: """Context manager that returns a new :class:`MonkeyPatch` object which undoes any patching done inside the ``with`` block upon exit. Example: + .. code-block:: python import functools @@ -181,8 +180,8 @@ def setattr( def setattr( self, - target: Union[str, object], - name: Union[object, str], + target: str | object, + name: object | str, value: object = notset, raising: bool = True, ) -> None: @@ -253,8 +252,8 @@ def setattr( def delattr( self, - target: Union[object, str], - name: Union[str, Notset] = notset, + target: object | str, + name: str | Notset = notset, raising: bool = True, ) -> None: """Delete attribute ``name`` from ``target``. @@ -309,7 +308,7 @@ def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None: # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict del dic[name] # type: ignore[attr-defined] - def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None: + def setenv(self, name: str, value: str, prepend: str | None = None) -> None: """Set environment variable ``name`` to ``value``. If ``prepend`` is a character, read the current environment variable @@ -362,7 +361,7 @@ def syspath_prepend(self, path) -> None: invalidate_caches() - def chdir(self, path: Union[str, "os.PathLike[str]"]) -> None: + def chdir(self, path: str | os.PathLike[str]) -> None: """Change the current working directory to the specified path. :param path: diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 974d756a2be..bbde2664b90 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import abc from functools import cached_property from inspect import signature @@ -10,17 +12,11 @@ from typing import cast from typing import Iterable from typing import Iterator -from typing import List from typing import MutableMapping from typing import NoReturn -from typing import Optional from typing import overload -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 import warnings import pluggy @@ -30,6 +26,7 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import TerminalRepr from _pytest._code.code import Traceback +from _pytest._code.code import TracebackStyle from _pytest.compat import LEGACY_PATH from _pytest.config import Config from _pytest.config import ConftestImportFailure @@ -49,7 +46,6 @@ from typing import Self # Imported here due to circular import. - from _pytest._code.code import _TracebackStyle from _pytest.main import Session @@ -62,9 +58,9 @@ def _imply_path( - node_type: Type["Node"], - path: Optional[Path], - fspath: Optional[LEGACY_PATH], + node_type: type[Node], + path: Path | None, + fspath: LEGACY_PATH | None, ) -> Path: if fspath is not None: warnings.warn( @@ -109,7 +105,7 @@ def __call__(cls, *k, **kw) -> NoReturn: ).format(name=f"{cls.__module__}.{cls.__name__}") fail(msg, pytrace=False) - def _create(cls: Type[_T], *k, **kw) -> _T: + def _create(cls: type[_T], *k, **kw) -> _T: try: return super().__call__(*k, **kw) # type: ignore[no-any-return,misc] except TypeError: @@ -160,12 +156,12 @@ class Node(abc.ABC, metaclass=NodeMeta): def __init__( self, name: str, - parent: "Optional[Node]" = None, - config: Optional[Config] = None, - session: "Optional[Session]" = None, - fspath: Optional[LEGACY_PATH] = None, - path: Optional[Path] = None, - nodeid: Optional[str] = None, + parent: Node | None = None, + config: Config | None = None, + session: Session | None = None, + fspath: LEGACY_PATH | None = None, + path: Path | None = None, + nodeid: str | None = None, ) -> None: #: A unique name within the scope of the parent node. self.name: str = name @@ -199,10 +195,10 @@ def __init__( self.keywords: MutableMapping[str, Any] = NodeKeywords(self) #: The marker objects belonging to this node. - self.own_markers: List[Mark] = [] + self.own_markers: list[Mark] = [] #: Allow adding of extra keywords to use for matching. - self.extra_keyword_matches: Set[str] = set() + self.extra_keyword_matches: set[str] = set() if nodeid is not None: assert "::()" not in nodeid @@ -219,7 +215,7 @@ def __init__( self._store = self.stash @classmethod - def from_parent(cls, parent: "Node", **kw) -> "Self": + def from_parent(cls, parent: Node, **kw) -> Self: """Public constructor for Nodes. This indirection got introduced in order to enable removing @@ -295,31 +291,29 @@ def setup(self) -> None: def teardown(self) -> None: pass - def iter_parents(self) -> Iterator["Node"]: + def iter_parents(self) -> Iterator[Node]: """Iterate over all parent collectors starting from and including self up to the root of the collection tree. .. versionadded:: 8.1 """ - parent: Optional[Node] = self + parent: Node | None = self while parent is not None: yield parent parent = parent.parent - def listchain(self) -> List["Node"]: + def listchain(self) -> list[Node]: """Return a list of all parent collectors starting from the root of the collection tree down to and including self.""" chain = [] - item: Optional[Node] = self + item: Node | None = self while item is not None: chain.append(item) item = item.parent chain.reverse() return chain - def add_marker( - self, marker: Union[str, MarkDecorator], append: bool = True - ) -> None: + def add_marker(self, marker: str | MarkDecorator, append: bool = True) -> None: """Dynamically add a marker object to the node. :param marker: @@ -341,7 +335,7 @@ def add_marker( else: self.own_markers.insert(0, marker_.mark) - def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]: + def iter_markers(self, name: str | None = None) -> Iterator[Mark]: """Iterate over all markers of the node. :param name: If given, filter the results by the name attribute. @@ -350,8 +344,8 @@ def iter_markers(self, name: Optional[str] = None) -> Iterator[Mark]: return (x[1] for x in self.iter_markers_with_node(name=name)) def iter_markers_with_node( - self, name: Optional[str] = None - ) -> Iterator[Tuple["Node", Mark]]: + self, name: str | None = None + ) -> Iterator[tuple[Node, Mark]]: """Iterate over all markers of the node. :param name: If given, filter the results by the name attribute. @@ -363,14 +357,12 @@ def iter_markers_with_node( yield node, mark @overload - def get_closest_marker(self, name: str) -> Optional[Mark]: ... + def get_closest_marker(self, name: str) -> Mark | None: ... @overload def get_closest_marker(self, name: str, default: Mark) -> Mark: ... - def get_closest_marker( - self, name: str, default: Optional[Mark] = None - ) -> Optional[Mark]: + def get_closest_marker(self, name: str, default: Mark | None = None) -> Mark | None: """Return the first marker matching the name, from closest (for example function) to farther level (for example module level). @@ -379,14 +371,14 @@ def get_closest_marker( """ return next(self.iter_markers(name=name), default) - def listextrakeywords(self) -> Set[str]: + def listextrakeywords(self) -> set[str]: """Return a set of all extra keywords in self and any parents.""" - extra_keywords: Set[str] = set() + extra_keywords: set[str] = set() for item in self.listchain(): extra_keywords.update(item.extra_keyword_matches) return extra_keywords - def listnames(self) -> List[str]: + def listnames(self) -> list[str]: return [x.name for x in self.listchain()] def addfinalizer(self, fin: Callable[[], object]) -> None: @@ -398,7 +390,7 @@ def addfinalizer(self, fin: Callable[[], object]) -> None: """ self.session._setupstate.addfinalizer(fin, self) - def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]: + def getparent(self, cls: type[_NodeType]) -> _NodeType | None: """Get the closest parent node (including self) which is an instance of the given class. @@ -416,7 +408,7 @@ def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: def _repr_failure_py( self, excinfo: ExceptionInfo[BaseException], - style: "Optional[_TracebackStyle]" = None, + style: TracebackStyle | None = None, ) -> TerminalRepr: from _pytest.fixtures import FixtureLookupError @@ -428,7 +420,7 @@ def _repr_failure_py( if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() - tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] + tbfilter: bool | Callable[[ExceptionInfo[BaseException]], Traceback] if self.config.getoption("fulltrace", False): style = "long" tbfilter = False @@ -448,6 +440,8 @@ def _repr_failure_py( else: truncate_locals = True + truncate_args = False if self.config.getoption("verbose", 0) > 2 else True + # excinfo.getrepr() formats paths relative to the CWD if `abspath` is False. # It is possible for a fixture/test to change the CWD while this code runs, which # would then result in the user seeing confusing paths in the failure message. @@ -466,13 +460,14 @@ def _repr_failure_py( style=style, tbfilter=tbfilter, truncate_locals=truncate_locals, + truncate_args=truncate_args, ) def repr_failure( self, excinfo: ExceptionInfo[BaseException], - style: "Optional[_TracebackStyle]" = None, - ) -> Union[str, TerminalRepr]: + style: TracebackStyle | None = None, + ) -> str | TerminalRepr: """Return a representation of a collection or test failure. .. seealso:: :ref:`non-python tests` @@ -482,7 +477,7 @@ def repr_failure( return self._repr_failure_py(excinfo, style) -def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[int]]: +def get_fslocation_from_item(node: Node) -> tuple[str | Path, int | None]: """Try to extract the actual location from a node, depending on available attributes: * "location": a pair (path, lineno) @@ -492,7 +487,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i :rtype: A tuple of (str|Path, int) with filename and 0-based line number. """ # See Item.location. - location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None) + location: tuple[str, int | None, str] | None = getattr(node, "location", None) if location is not None: return location[:2] obj = getattr(node, "obj", None) @@ -512,14 +507,14 @@ class CollectError(Exception): """An error during collection, contains a custom message.""" @abc.abstractmethod - def collect(self) -> Iterable[Union["Item", "Collector"]]: + def collect(self) -> Iterable[Item | Collector]: """Collect children (items and collectors) for this collector.""" raise NotImplementedError("abstract") # TODO: This omits the style= parameter which breaks Liskov Substitution. def repr_failure( # type: ignore[override] self, excinfo: ExceptionInfo[BaseException] - ) -> Union[str, TerminalRepr]: + ) -> str | TerminalRepr: """Return a representation of a collection failure. :param excinfo: Exception information for the failure. @@ -548,7 +543,7 @@ def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: return excinfo.traceback -def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]: +def _check_initialpaths_for_relpath(session: Session, path: Path) -> str | None: for initial_path in session._initialpaths: if commonpath(path, initial_path) == initial_path: rel = str(path.relative_to(initial_path)) @@ -561,14 +556,14 @@ class FSCollector(Collector, abc.ABC): def __init__( self, - fspath: Optional[LEGACY_PATH] = None, - path_or_parent: Optional[Union[Path, Node]] = None, - path: Optional[Path] = None, - name: Optional[str] = None, - parent: Optional[Node] = None, - config: Optional[Config] = None, - session: Optional["Session"] = None, - nodeid: Optional[str] = None, + fspath: LEGACY_PATH | None = None, + path_or_parent: Path | Node | None = None, + path: Path | None = None, + name: str | None = None, + parent: Node | None = None, + config: Config | None = None, + session: Session | None = None, + nodeid: str | None = None, ) -> None: if path_or_parent: if isinstance(path_or_parent, Node): @@ -618,10 +613,10 @@ def from_parent( cls, parent, *, - fspath: Optional[LEGACY_PATH] = None, - path: Optional[Path] = None, + fspath: LEGACY_PATH | None = None, + path: Path | None = None, **kw, - ) -> "Self": + ) -> Self: """The public constructor.""" return super().from_parent(parent=parent, fspath=fspath, path=path, **kw) @@ -663,9 +658,9 @@ def __init__( self, name, parent=None, - config: Optional[Config] = None, - session: Optional["Session"] = None, - nodeid: Optional[str] = None, + config: Config | None = None, + session: Session | None = None, + nodeid: str | None = None, **kw, ) -> None: # The first two arguments are intentionally passed positionally, @@ -680,11 +675,11 @@ def __init__( nodeid=nodeid, **kw, ) - self._report_sections: List[Tuple[str, str, str]] = [] + self._report_sections: list[tuple[str, str, str]] = [] #: A list of tuples (name, value) that holds user defined properties #: for this test. - self.user_properties: List[Tuple[str, object]] = [] + self.user_properties: list[tuple[str, object]] = [] self._check_item_and_collector_diamond_inheritance() @@ -744,7 +739,7 @@ def add_report_section(self, when: str, key: str, content: str) -> None: if content: self._report_sections.append((when, key, content)) - def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: + def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]: """Get location information for this item for test reports. Returns a tuple with three elements: @@ -758,7 +753,7 @@ def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str return self.path, None, "" @cached_property - def location(self) -> Tuple[str, Optional[int], str]: + def location(self) -> tuple[str, int | None, str]: """ Returns a tuple of ``(relfspath, lineno, testname)`` for this item where ``relfspath`` is file path relative to ``config.rootpath`` diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 76d94accd0d..5b20803e586 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -1,12 +1,13 @@ """Exception classes and constants handling test outcomes as well as functions creating them.""" +from __future__ import annotations + import sys from typing import Any 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 @@ -18,7 +19,7 @@ class OutcomeException(BaseException): """OutcomeException and its subclass instances indicate and contain info about test and collection outcomes.""" - def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: + def __init__(self, msg: str | None = None, pytrace: bool = True) -> None: if msg is not None and not isinstance(msg, str): error_msg = ( # type: ignore[unreachable] "{} expected string as 'msg' parameter, got '{}' instead.\n" @@ -47,7 +48,7 @@ class Skipped(OutcomeException): def __init__( self, - msg: Optional[str] = None, + msg: str | None = None, pytrace: bool = True, allow_module_level: bool = False, *, @@ -70,7 +71,7 @@ class Exit(Exception): """Raised for immediate program exits (no tracebacks/summaries).""" def __init__( - self, msg: str = "unknown reason", returncode: Optional[int] = None + self, msg: str = "unknown reason", returncode: int | None = None ) -> None: self.msg = msg self.returncode = returncode @@ -104,7 +105,7 @@ def decorate(func: _F) -> _WithException[_F, _ET]: @_with_exception(Exit) def exit( reason: str = "", - returncode: Optional[int] = None, + returncode: int | None = None, ) -> NoReturn: """Exit testing process. @@ -114,6 +115,9 @@ def exit( :param returncode: Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`. + + :raises pytest.exit.Exception: + The exception that is raised. """ __tracebackhide__ = True raise Exit(reason, returncode) @@ -142,6 +146,9 @@ def skip( Defaults to False. + :raises pytest.skip.Exception: + The exception that is raised. + .. note:: It is better to use the :ref:`pytest.mark.skipif ref` marker when possible to declare a test to be skipped under certain conditions @@ -163,6 +170,9 @@ def fail(reason: str = "", pytrace: bool = True) -> NoReturn: :param pytrace: If False, msg represents the full failure information and no python traceback will be reported. + + :raises pytest.fail.Exception: + The exception that is raised. """ __tracebackhide__ = True raise Failed(msg=reason, pytrace=pytrace) @@ -188,6 +198,9 @@ def xfail(reason: str = "") -> NoReturn: It is better to use the :ref:`pytest.mark.xfail ref` marker when possible to declare a test to be xfailed under certain conditions like known bugs or missing features. + + :raises pytest.xfail.Exception: + The exception that is raised. """ __tracebackhide__ = True raise XFailed(reason) @@ -195,10 +208,10 @@ def xfail(reason: str = "") -> NoReturn: def importorskip( modname: str, - minversion: Optional[str] = None, - reason: Optional[str] = None, + minversion: str | None = None, + reason: str | None = None, *, - exc_type: Optional[Type[ImportError]] = None, + exc_type: type[ImportError] | None = None, ) -> Any: """Import and return the requested module ``modname``, or skip the current test if the module cannot be imported. @@ -227,6 +240,9 @@ def importorskip( :returns: The imported module. This should be assigned to its canonical name. + :raises pytest.skip.Exception: + If the module cannot be imported. + Example:: docutils = pytest.importorskip("docutils") @@ -252,8 +268,8 @@ def importorskip( else: warn_on_import_error = False - skipped: Optional[Skipped] = None - warning: Optional[Warning] = None + skipped: Skipped | None = None + warning: Warning | None = None with warnings.catch_warnings(): # Make sure to ignore ImportWarnings that might happen because diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 533d78c9a2a..69c011ed24a 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -1,10 +1,11 @@ # mypy: allow-untyped-defs """Submit failure or test session information to a pastebin service.""" +from __future__ import annotations + from io import StringIO import tempfile from typing import IO -from typing import Union from _pytest.config import Config from _pytest.config import create_terminal_writer @@ -65,10 +66,10 @@ def pytest_unconfigure(config: Config) -> None: # Write summary. tr.write_sep("=", "Sending information to Paste Service") pastebinurl = create_new_paste(sessionlog) - tr.write_line("pastebin session-log: %s\n" % pastebinurl) + tr.write_line(f"pastebin session-log: {pastebinurl}\n") -def create_new_paste(contents: Union[str, bytes]) -> str: +def create_new_paste(contents: str | bytes) -> str: """Create a new paste using the bpaste.net service. :contents: Paste contents string. @@ -85,7 +86,7 @@ def create_new_paste(contents: Union[str, bytes]) -> str: urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8") ) except OSError as exc_info: # urllib errors - return "bad response: %s" % exc_info + return f"bad response: {exc_info}" m = re.search(r'href="/raw/(\w+)"', response) if m: return f"{url}/show/{m.group(1)}" diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index e14c2acd328..e4dc4eddc9c 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import atexit import contextlib from enum import Enum @@ -24,16 +26,9 @@ from types import ModuleType from typing import Any from typing import Callable -from typing import Dict from typing import Iterable from typing import Iterator -from typing import List -from typing import Optional -from typing import Set -from typing import Tuple -from typing import Type from typing import TypeVar -from typing import Union import uuid import warnings @@ -71,12 +66,10 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath: def on_rm_rf_error( - func: Optional[Callable[..., Any]], + func: Callable[..., Any] | None, path: str, - excinfo: Union[ - BaseException, - Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]], - ], + excinfo: BaseException + | tuple[type[BaseException], BaseException, types.TracebackType | None], *, start_path: Path, ) -> bool: @@ -172,15 +165,15 @@ def rm_rf(path: Path) -> None: shutil.rmtree(str(path), onerror=onerror) -def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]: - """Find all elements in root that begin with the prefix, case insensitive.""" +def find_prefixed(root: Path, prefix: str) -> Iterator[os.DirEntry[str]]: + """Find all elements in root that begin with the prefix, case-insensitive.""" l_prefix = prefix.lower() for x in os.scandir(root): if x.name.lower().startswith(l_prefix): yield x -def extract_suffixes(iter: Iterable["os.DirEntry[str]"], prefix: str) -> Iterator[str]: +def extract_suffixes(iter: Iterable[os.DirEntry[str]], prefix: str) -> Iterator[str]: """Return the parts of the paths following the prefix. :param iter: Iterator over path names. @@ -204,9 +197,7 @@ def parse_num(maybe_num: str) -> int: return -1 -def _force_symlink( - root: Path, target: Union[str, PurePath], link_to: Union[str, Path] -) -> None: +def _force_symlink(root: Path, target: str | PurePath, link_to: str | Path) -> None: """Helper to create the current symlink. It's full of race conditions that are reasonably OK to ignore @@ -420,7 +411,7 @@ def resolve_from_str(input: str, rootpath: Path) -> Path: return rootpath.joinpath(input) -def fnmatch_ex(pattern: str, path: Union[str, "os.PathLike[str]"]) -> bool: +def fnmatch_ex(pattern: str, path: str | os.PathLike[str]) -> bool: """A port of FNMatcher from py.path.common which works with PurePath() instances. The difference between this algorithm and PurePath.match() is that the @@ -456,14 +447,14 @@ def fnmatch_ex(pattern: str, path: Union[str, "os.PathLike[str]"]) -> bool: return fnmatch.fnmatch(name, pattern) -def parts(s: str) -> Set[str]: +def parts(s: str) -> set[str]: parts = s.split(sep) return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} def symlink_or_skip( - src: Union["os.PathLike[str]", str], - dst: Union["os.PathLike[str]", str], + src: os.PathLike[str] | str, + dst: os.PathLike[str] | str, **kwargs: Any, ) -> None: """Make a symlink, or skip the test in case symlinks are not supported.""" @@ -491,9 +482,9 @@ class ImportPathMismatchError(ImportError): def import_path( - path: Union[str, "os.PathLike[str]"], + path: str | os.PathLike[str], *, - mode: Union[str, ImportMode] = ImportMode.prepend, + mode: str | ImportMode = ImportMode.prepend, root: Path, consider_namespace_packages: bool, ) -> ModuleType: @@ -618,7 +609,7 @@ def import_path( def _import_module_using_spec( module_name: str, module_path: Path, module_location: Path, *, insert_modules: bool -) -> Optional[ModuleType]: +) -> ModuleType | None: """ Tries to import a module by its canonical name, path to the .py file, and its parent location. @@ -641,7 +632,7 @@ def _import_module_using_spec( # Attempt to import the parent module, seems is our responsibility: # https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311 parent_module_name, _, name = module_name.rpartition(".") - parent_module: Optional[ModuleType] = None + parent_module: ModuleType | None = None if parent_module_name: parent_module = sys.modules.get(parent_module_name) if parent_module is None: @@ -680,9 +671,7 @@ def _import_module_using_spec( return None -def spec_matches_module_path( - module_spec: Optional[ModuleSpec], module_path: Path -) -> bool: +def spec_matches_module_path(module_spec: ModuleSpec | None, module_path: Path) -> bool: """Return true if the given ModuleSpec can be used to import the given module path.""" if module_spec is None or module_spec.origin is None: return False @@ -734,7 +723,7 @@ def module_name_from_path(path: Path, root: Path) -> str: return ".".join(path_parts) -def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> None: +def insert_missing_modules(modules: dict[str, ModuleType], module_name: str) -> None: """ Used by ``import_path`` to create intermediate modules when using mode=importlib. @@ -772,11 +761,11 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) -> module_name = ".".join(module_parts) -def resolve_package_path(path: Path) -> Optional[Path]: +def resolve_package_path(path: Path) -> Path | None: """Return the Python package path by looking for the last directory upwards which still contains an __init__.py. - Returns None if it can not be determined. + Returns None if it cannot be determined. """ result = None for parent in itertools.chain((path,), path.parents): @@ -791,7 +780,7 @@ def resolve_package_path(path: Path) -> Optional[Path]: def resolve_pkg_root_and_module_name( path: Path, *, consider_namespace_packages: bool = False -) -> Tuple[Path, str]: +) -> tuple[Path, str]: """ Return the path to the directory of the root package that contains the given Python file, and its module name: @@ -812,7 +801,7 @@ def resolve_pkg_root_and_module_name( Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files). """ - pkg_root: Optional[Path] = None + pkg_root: Path | None = None pkg_path = resolve_package_path(path) if pkg_path is not None: pkg_root = pkg_path.parent @@ -859,7 +848,7 @@ def is_importable(module_name: str, module_path: Path) -> bool: return spec_matches_module_path(spec, module_path) -def compute_module_name(root: Path, module_path: Path) -> Optional[str]: +def compute_module_name(root: Path, module_path: Path) -> str | None: """Compute a module name based on a path and a root anchor.""" try: path_without_suffix = module_path.with_suffix("") @@ -884,9 +873,9 @@ class CouldNotResolvePathError(Exception): def scandir( - path: Union[str, "os.PathLike[str]"], - sort_key: Callable[["os.DirEntry[str]"], object] = lambda entry: entry.name, -) -> List["os.DirEntry[str]"]: + path: str | os.PathLike[str], + sort_key: Callable[[os.DirEntry[str]], object] = lambda entry: entry.name, +) -> list[os.DirEntry[str]]: """Scan a directory recursively, in breadth-first order. The returned entries are sorted according to the given key. @@ -909,8 +898,8 @@ def scandir( def visit( - path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool] -) -> Iterator["os.DirEntry[str]"]: + path: str | os.PathLike[str], recurse: Callable[[os.DirEntry[str]], bool] +) -> Iterator[os.DirEntry[str]]: """Walk a directory recursively, in breadth-first order. The `recurse` predicate determines whether a directory is recursed. @@ -924,7 +913,7 @@ def visit( yield from visit(entry.path, recurse) -def absolutepath(path: "Union[str, os.PathLike[str]]") -> Path: +def absolutepath(path: str | os.PathLike[str]) -> Path: """Convert a path to an absolute path using os.path.abspath. Prefer this over Path.resolve() (see #6523). @@ -933,7 +922,7 @@ def absolutepath(path: "Union[str, os.PathLike[str]]") -> Path: return Path(os.path.abspath(path)) -def commonpath(path1: Path, path2: Path) -> Optional[Path]: +def commonpath(path1: Path, path2: Path) -> Path | None: """Return the common part shared with the other path, or None if there is no common part. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 23f44da69ca..5c6ce5e889f 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -4,6 +4,8 @@ PYTEST_DONT_REWRITE """ +from __future__ import annotations + import collections.abc import contextlib from fnmatch import fnmatch @@ -21,22 +23,16 @@ import traceback 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 from typing import TextIO -from typing import Tuple -from typing import Type from typing import TYPE_CHECKING -from typing import Union from weakref import WeakKeyDictionary from iniconfig import IniConfig @@ -123,7 +119,7 @@ def pytest_configure(config: Config) -> None: class LsofFdLeakChecker: - def get_open_files(self) -> List[Tuple[str, str]]: + def get_open_files(self) -> list[tuple[str, str]]: if sys.version_info >= (3, 11): # New in Python 3.11, ignores utf-8 mode encoding = locale.getencoding() @@ -182,13 +178,13 @@ def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object] leaked_files = [t for t in lines2 if t[0] in new_fds] if leaked_files: error = [ - "***** %s FD leakage detected" % len(leaked_files), + f"***** {len(leaked_files)} FD leakage detected", *(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), + f"***** {len(leaked_files)} FD leakage detected", "*** function {}:{}: {} ".format(*item.location), "See issue #2366", ] @@ -199,7 +195,7 @@ def pytest_runtest_protocol(self, item: Item) -> Generator[None, object, object] @fixture -def _pytest(request: FixtureRequest) -> "PytestArg": +def _pytest(request: FixtureRequest) -> PytestArg: """Return a helper which offers a gethookrecorder(hook) method which returns a HookRecorder instance which helps to make assertions about called hooks.""" @@ -210,13 +206,13 @@ class PytestArg: def __init__(self, request: FixtureRequest) -> None: self._request = request - def gethookrecorder(self, hook) -> "HookRecorder": + def gethookrecorder(self, hook) -> HookRecorder: hookrecorder = HookRecorder(hook._pm) self._request.addfinalizer(hookrecorder.finish_recording) return hookrecorder -def get_public_names(values: Iterable[str]) -> List[str]: +def get_public_names(values: Iterable[str]) -> list[str]: """Only return names from iterator values without a leading underscore.""" return [x for x in values if x[0] != "_"] @@ -265,8 +261,8 @@ def __init__( check_ispytest(_ispytest) self._pluginmanager = pluginmanager - self.calls: List[RecordedHookCall] = [] - self.ret: Optional[Union[int, ExitCode]] = None + self.calls: list[RecordedHookCall] = [] + self.ret: int | ExitCode | None = None def before(hook_name: str, hook_impls, kwargs) -> None: self.calls.append(RecordedHookCall(hook_name, kwargs)) @@ -279,17 +275,18 @@ def after(outcome, hook_name: str, hook_impls, kwargs) -> None: def finish_recording(self) -> None: self._undo_wrapping() - def getcalls(self, names: Union[str, Iterable[str]]) -> List[RecordedHookCall]: + def getcalls(self, names: str | Iterable[str]) -> list[RecordedHookCall]: """Get all recorded calls to hooks with the given names (or name).""" if isinstance(names, str): names = names.split() return [call for call in self.calls if call._name in names] - def assert_contains(self, entries: Sequence[Tuple[str, str]]) -> None: + def assert_contains(self, entries: Sequence[tuple[str, str]]) -> None: __tracebackhide__ = True i = 0 entries = list(entries) - backlocals = sys._getframe(1).f_locals + # Since Python 3.13, f_locals is not a dict, but eval requires a dict. + backlocals = dict(sys._getframe(1).f_locals) while entries: name, check = entries.pop(0) for ind, call in enumerate(self.calls[i:]): @@ -313,7 +310,7 @@ def popcall(self, name: str) -> RecordedHookCall: del self.calls[i] return call lines = [f"could not find call {name!r}, in:"] - lines.extend([" %s" % x for x in self.calls]) + lines.extend([f" {x}" for x in self.calls]) fail("\n".join(lines)) def getcall(self, name: str) -> RecordedHookCall: @@ -326,42 +323,42 @@ def getcall(self, name: str) -> RecordedHookCall: @overload def getreports( self, - names: "Literal['pytest_collectreport']", + names: Literal["pytest_collectreport"], ) -> Sequence[CollectReport]: ... @overload def getreports( self, - names: "Literal['pytest_runtest_logreport']", + names: Literal["pytest_runtest_logreport"], ) -> Sequence[TestReport]: ... @overload def getreports( self, - names: Union[str, Iterable[str]] = ( + names: str | Iterable[str] = ( "pytest_collectreport", "pytest_runtest_logreport", ), - ) -> Sequence[Union[CollectReport, TestReport]]: ... + ) -> Sequence[CollectReport | TestReport]: ... def getreports( self, - names: Union[str, Iterable[str]] = ( + names: str | Iterable[str] = ( "pytest_collectreport", "pytest_runtest_logreport", ), - ) -> Sequence[Union[CollectReport, TestReport]]: + ) -> Sequence[CollectReport | TestReport]: return [x.report for x in self.getcalls(names)] def matchreport( self, inamepart: str = "", - names: Union[str, Iterable[str]] = ( + names: str | Iterable[str] = ( "pytest_runtest_logreport", "pytest_collectreport", ), - when: Optional[str] = None, - ) -> Union[CollectReport, TestReport]: + when: str | None = None, + ) -> CollectReport | TestReport: """Return a testreport whose dotted import path matches.""" values = [] for rep in self.getreports(names=names): @@ -386,31 +383,31 @@ def matchreport( @overload def getfailures( self, - names: "Literal['pytest_collectreport']", + names: Literal["pytest_collectreport"], ) -> Sequence[CollectReport]: ... @overload def getfailures( self, - names: "Literal['pytest_runtest_logreport']", + names: Literal["pytest_runtest_logreport"], ) -> Sequence[TestReport]: ... @overload def getfailures( self, - names: Union[str, Iterable[str]] = ( + names: str | Iterable[str] = ( "pytest_collectreport", "pytest_runtest_logreport", ), - ) -> Sequence[Union[CollectReport, TestReport]]: ... + ) -> Sequence[CollectReport | TestReport]: ... def getfailures( self, - names: Union[str, Iterable[str]] = ( + names: str | Iterable[str] = ( "pytest_collectreport", "pytest_runtest_logreport", ), - ) -> Sequence[Union[CollectReport, TestReport]]: + ) -> Sequence[CollectReport | TestReport]: return [rep for rep in self.getreports(names) if rep.failed] def getfailedcollections(self) -> Sequence[CollectReport]: @@ -418,10 +415,10 @@ def getfailedcollections(self) -> Sequence[CollectReport]: def listoutcomes( self, - ) -> Tuple[ + ) -> tuple[ Sequence[TestReport], - Sequence[Union[CollectReport, TestReport]], - Sequence[Union[CollectReport, TestReport]], + Sequence[CollectReport | TestReport], + Sequence[CollectReport | TestReport], ]: passed = [] skipped = [] @@ -440,7 +437,7 @@ def listoutcomes( failed.append(rep) return passed, skipped, failed - def countoutcomes(self) -> List[int]: + def countoutcomes(self) -> list[int]: return [len(x) for x in self.listoutcomes()] def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: @@ -460,14 +457,14 @@ def clear(self) -> None: @fixture -def linecomp() -> "LineComp": +def linecomp() -> LineComp: """A :class: `LineComp` instance for checking that an input linearly contains a sequence of strings.""" return LineComp() @fixture(name="LineMatcher") -def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: +def LineMatcher_fixture(request: FixtureRequest) -> type[LineMatcher]: """A reference to the :class: `LineMatcher`. This is instantiable with a list of lines (without their trailing newlines). @@ -479,7 +476,7 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: @fixture def pytester( request: FixtureRequest, tmp_path_factory: TempPathFactory, monkeypatch: MonkeyPatch -) -> "Pytester": +) -> Pytester: """ Facilities to write tests/configuration files, execute pytest in isolation, and match against expected output, perfect for black-box testing of pytest plugins. @@ -523,13 +520,13 @@ class RunResult: def __init__( self, - ret: Union[int, ExitCode], - outlines: List[str], - errlines: List[str], + ret: int | ExitCode, + outlines: list[str], + errlines: list[str], duration: float, ) -> None: try: - self.ret: Union[int, ExitCode] = ExitCode(ret) + self.ret: int | ExitCode = ExitCode(ret) """The return value.""" except ValueError: self.ret = ret @@ -554,7 +551,7 @@ def __repr__(self) -> str: % (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration) ) - def parseoutcomes(self) -> Dict[str, int]: + def parseoutcomes(self) -> dict[str, int]: """Return a dictionary of outcome noun -> count from parsing the terminal output that the test process produced. @@ -567,7 +564,7 @@ def parseoutcomes(self) -> Dict[str, int]: return self.parse_summary_nouns(self.outlines) @classmethod - def parse_summary_nouns(cls, lines) -> Dict[str, int]: + def parse_summary_nouns(cls, lines) -> dict[str, int]: """Extract the nouns from a pytest terminal summary line. It always returns the plural noun for consistency:: @@ -598,8 +595,8 @@ def assert_outcomes( errors: int = 0, xpassed: int = 0, xfailed: int = 0, - warnings: Optional[int] = None, - deselected: Optional[int] = None, + warnings: int | None = None, + deselected: int | None = None, ) -> None: """ Assert that the specified outcomes appear with the respective @@ -625,7 +622,7 @@ def assert_outcomes( class SysModulesSnapshot: - def __init__(self, preserve: Optional[Callable[[str], bool]] = None) -> None: + def __init__(self, preserve: Callable[[str], bool] | None = None) -> None: self.__preserve = preserve self.__saved = dict(sys.modules) @@ -658,7 +655,7 @@ class Pytester: __test__ = False - CLOSE_STDIN: "Final" = NOTSET + CLOSE_STDIN: Final = NOTSET class TimeoutExpired(Exception): pass @@ -673,9 +670,9 @@ def __init__( ) -> None: check_ispytest(_ispytest) self._request = request - self._mod_collections: WeakKeyDictionary[ - Collector, List[Union[Item, Collector]] - ] = WeakKeyDictionary() + self._mod_collections: WeakKeyDictionary[Collector, list[Item | Collector]] = ( + WeakKeyDictionary() + ) if request.function: name: str = request.function.__name__ else: @@ -686,7 +683,7 @@ def __init__( #: :py:meth:`runpytest`. Initially this is an empty list but plugins can #: be added to the list. The type of items to add to the list depends on #: the method using them so refer to them for details. - self.plugins: List[Union[str, _PluggyPlugin]] = [] + self.plugins: list[str | _PluggyPlugin] = [] self._sys_path_snapshot = SysPathsSnapshot() self._sys_modules_snapshot = self.__take_sys_modules_snapshot() self._request.addfinalizer(self._finalize) @@ -754,18 +751,21 @@ def chdir(self) -> None: def _makefile( self, ext: str, - lines: Sequence[Union[Any, bytes]], - files: Dict[str, str], + lines: Sequence[Any | bytes], + files: dict[str, str], encoding: str = "utf-8", ) -> Path: items = list(files.items()) + if ext is None: + raise TypeError("ext must not be None") + if ext and not ext.startswith("."): raise ValueError( f"pytester.makefile expects a file extension, try .{ext} instead of {ext}" ) - def to_text(s: Union[Any, bytes]) -> str: + def to_text(s: Any | bytes) -> str: return s.decode(encoding) if isinstance(s, bytes) else str(s) if lines: @@ -801,6 +801,7 @@ def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: The first created file. Examples: + .. code-block:: python pytester.makefile(".txt", "line1", "line2") @@ -854,6 +855,7 @@ def makepyfile(self, *args, **kwargs) -> Path: existing files. Examples: + .. code-block:: python def test_something(pytester): @@ -873,6 +875,7 @@ def maketxtfile(self, *args, **kwargs) -> Path: existing files. Examples: + .. code-block:: python def test_something(pytester): @@ -885,9 +888,7 @@ def test_something(pytester): """ return self._makefile(".txt", args, kwargs) - def syspathinsert( - self, path: Optional[Union[str, "os.PathLike[str]"]] = None - ) -> None: + def syspathinsert(self, path: str | os.PathLike[str] | None = None) -> None: """Prepend a directory to sys.path, defaults to :attr:`path`. This is undone automatically when this object dies at the end of each @@ -901,19 +902,20 @@ def syspathinsert( self._monkeypatch.syspath_prepend(str(path)) - def mkdir(self, name: Union[str, "os.PathLike[str]"]) -> Path: + def mkdir(self, name: str | os.PathLike[str]) -> Path: """Create a new (sub)directory. :param name: The name of the directory, relative to the pytester path. :returns: The created directory. + :rtype: pathlib.Path """ p = self.path / name p.mkdir() return p - def mkpydir(self, name: Union[str, "os.PathLike[str]"]) -> Path: + def mkpydir(self, name: str | os.PathLike[str]) -> Path: """Create a new python package. This creates a (sub)directory with an empty ``__init__.py`` file so it @@ -924,13 +926,14 @@ def mkpydir(self, name: Union[str, "os.PathLike[str]"]) -> Path: p.joinpath("__init__.py").touch() return p - def copy_example(self, name: Optional[str] = None) -> Path: + def copy_example(self, name: str | None = None) -> Path: """Copy file from project's directory into the testdir. :param name: The name of the file to copy. :return: Path to the copied directory (inside ``self.path``). + :rtype: pathlib.Path """ example_dir_ = self._request.config.getini("pytester_example_dir") if example_dir_ is None: @@ -969,9 +972,7 @@ def copy_example(self, name: Optional[str] = None) -> Path: f'example "{example_path}" is not found as a file or directory' ) - def getnode( - self, config: Config, arg: Union[str, "os.PathLike[str]"] - ) -> Union[Collector, Item]: + def getnode(self, config: Config, arg: str | os.PathLike[str]) -> Collector | Item: """Get the collection node of a file. :param config: @@ -990,9 +991,7 @@ def getnode( config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res - def getpathnode( - self, path: Union[str, "os.PathLike[str]"] - ) -> Union[Collector, Item]: + def getpathnode(self, path: str | os.PathLike[str]) -> Collector | Item: """Return the collection node of a file. This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to @@ -1012,7 +1011,7 @@ def getpathnode( config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res - def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]: + def genitems(self, colitems: Sequence[Item | Collector]) -> list[Item]: """Generate all test items from a collection node. This recurses into the collection node and returns a list of all the @@ -1024,7 +1023,7 @@ def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]: The collected items. """ session = colitems[0].session - result: List[Item] = [] + result: list[Item] = [] for colitem in colitems: result.extend(session.genitems(colitem)) return result @@ -1058,7 +1057,7 @@ def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder: values = [*list(cmdlineargs), p] return self.inline_run(*values) - def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]: + def inline_genitems(self, *args) -> tuple[list[Item], HookRecorder]: """Run ``pytest.main(['--collect-only'])`` in-process. Runs the :py:func:`pytest.main` function to run all of pytest inside @@ -1071,7 +1070,7 @@ def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]: def inline_run( self, - *args: Union[str, "os.PathLike[str]"], + *args: str | os.PathLike[str], plugins=(), no_reraise_ctrlc: bool = False, ) -> HookRecorder: @@ -1141,7 +1140,7 @@ class reprec: # type: ignore finalizer() def runpytest_inprocess( - self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any + self, *args: str | os.PathLike[str], **kwargs: Any ) -> RunResult: """Return result of running pytest in-process, providing a similar interface to what self.runpytest() provides.""" @@ -1184,9 +1183,7 @@ class reprec: # type: ignore res.reprec = reprec # type: ignore return res - def runpytest( - self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any - ) -> RunResult: + def runpytest(self, *args: str | os.PathLike[str], **kwargs: Any) -> RunResult: """Run pytest inline or in a subprocess, depending on the command line option "--runpytest" and return a :py:class:`~pytest.RunResult`.""" new_args = self._ensure_basetemp(args) @@ -1197,17 +1194,19 @@ def runpytest( raise RuntimeError(f"Unrecognized runpytest option: {self._method}") def _ensure_basetemp( - self, args: Sequence[Union[str, "os.PathLike[str]"]] - ) -> List[Union[str, "os.PathLike[str]"]]: + self, args: Sequence[str | os.PathLike[str]] + ) -> list[str | os.PathLike[str]]: new_args = list(args) for x in new_args: if str(x).startswith("--basetemp"): break else: - new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp")) + new_args.append( + "--basetemp={}".format(self.path.parent.joinpath("basetemp")) + ) return new_args - def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config: + def parseconfig(self, *args: str | os.PathLike[str]) -> Config: """Return a new pytest :class:`pytest.Config` instance from given commandline args. @@ -1231,7 +1230,7 @@ def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config: self._request.addfinalizer(config._ensure_unconfigure) return config - def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config: + def parseconfigure(self, *args: str | os.PathLike[str]) -> Config: """Return a new pytest configured Config instance. Returns a new :py:class:`pytest.Config` instance like @@ -1243,7 +1242,7 @@ def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config: return config def getitem( - self, source: Union[str, "os.PathLike[str]"], funcname: str = "test_func" + self, source: str | os.PathLike[str], funcname: str = "test_func" ) -> Item: """Return the test item for a test function. @@ -1264,7 +1263,7 @@ def getitem( return item assert 0, f"{funcname!r} item not found in module:\n{source}\nitems: {items}" - def getitems(self, source: Union[str, "os.PathLike[str]"]) -> List[Item]: + def getitems(self, source: str | os.PathLike[str]) -> list[Item]: """Return all test items collected from the module. Writes the source to a Python file and runs pytest's collection on @@ -1275,7 +1274,7 @@ def getitems(self, source: Union[str, "os.PathLike[str]"]) -> List[Item]: def getmodulecol( self, - source: Union[str, "os.PathLike[str]"], + source: str | os.PathLike[str], configargs=(), *, withinit: bool = False, @@ -1307,9 +1306,7 @@ def getmodulecol( self.config = config = self.parseconfigure(path, *configargs) return self.getnode(config, path) - def collect_by_name( - self, modcol: Collector, name: str - ) -> Optional[Union[Item, Collector]]: + def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None: """Return the collection node for name from the module collection. Searches a module collection node for a collection node matching the @@ -1327,10 +1324,10 @@ def collect_by_name( def popen( self, - cmdargs: Sequence[Union[str, "os.PathLike[str]"]], - stdout: Union[int, TextIO] = subprocess.PIPE, - stderr: Union[int, TextIO] = subprocess.PIPE, - stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN, + cmdargs: Sequence[str | os.PathLike[str]], + stdout: int | TextIO = subprocess.PIPE, + stderr: int | TextIO = subprocess.PIPE, + stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN, **kw, ): """Invoke :py:class:`subprocess.Popen`. @@ -1365,9 +1362,9 @@ def popen( def run( self, - *cmdargs: Union[str, "os.PathLike[str]"], - timeout: Optional[float] = None, - stdin: Union[NotSetType, bytes, IO[Any], int] = CLOSE_STDIN, + *cmdargs: str | os.PathLike[str], + timeout: float | None = None, + stdin: NotSetType | bytes | IO[Any] | int = CLOSE_STDIN, ) -> RunResult: """Run a command with arguments. @@ -1395,8 +1392,10 @@ def run( - Otherwise, it is passed through to :py:class:`subprocess.Popen`. For further information in this case, consult the document of the ``stdin`` parameter in :py:class:`subprocess.Popen`. + :type stdin: _pytest.compat.NotSetType | bytes | IO[Any] | int :returns: The result. + """ __tracebackhide__ = True @@ -1453,10 +1452,10 @@ def _dump_lines(self, lines, fp): except UnicodeEncodeError: print(f"couldn't print to {fp} because of encoding") - def _getpytestargs(self) -> Tuple[str, ...]: + def _getpytestargs(self) -> tuple[str, ...]: return sys.executable, "-mpytest" - def runpython(self, script: "os.PathLike[str]") -> RunResult: + def runpython(self, script: os.PathLike[str]) -> RunResult: """Run a python script using sys.executable as interpreter.""" return self.run(sys.executable, script) @@ -1465,7 +1464,7 @@ def runpython_c(self, command: str) -> RunResult: return self.run(sys.executable, "-c", command) def runpytest_subprocess( - self, *args: Union[str, "os.PathLike[str]"], timeout: Optional[float] = None + self, *args: str | os.PathLike[str], timeout: float | None = None ) -> RunResult: """Run pytest as a subprocess with given arguments. @@ -1485,16 +1484,14 @@ def runpytest_subprocess( """ __tracebackhide__ = True p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700) - args = ("--basetemp=%s" % p, *args) + args = (f"--basetemp={p}", *args) plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: args = ("-p", plugins[0], *args) args = self._getpytestargs() + args return self.run(*args, timeout=timeout) - def spawn_pytest( - self, string: str, expect_timeout: float = 10.0 - ) -> "pexpect.spawn": + def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn: """Run pytest using pexpect. This makes sure to use the right pytest and sets up the temporary @@ -1508,7 +1505,7 @@ def spawn_pytest( cmd = f"{invoke} --basetemp={basetemp} {string}" return self.spawn(cmd, expect_timeout=expect_timeout) - def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn: """Run a command using pexpect. The pexpect child is returned. @@ -1553,9 +1550,9 @@ class LineMatcher: ``text.splitlines()``. """ - def __init__(self, lines: List[str]) -> None: + def __init__(self, lines: list[str]) -> None: self.lines = lines - self._log_output: List[str] = [] + self._log_output: list[str] = [] def __str__(self) -> str: """Return the entire original text. @@ -1565,7 +1562,7 @@ def __str__(self) -> str: """ return "\n".join(self.lines) - def _getlines(self, lines2: Union[str, Sequence[str], Source]) -> Sequence[str]: + def _getlines(self, lines2: str | Sequence[str] | Source) -> Sequence[str]: if isinstance(lines2, str): lines2 = Source(lines2) if isinstance(lines2, Source): @@ -1593,7 +1590,7 @@ def _match_lines_random( self._log("matched: ", repr(line)) break else: - msg = "line %r not found in output" % line + msg = f"line {line!r} not found in output" self._log(msg) self._fail(msg) @@ -1605,7 +1602,7 @@ def get_lines_after(self, fnline: str) -> Sequence[str]: for i, line in enumerate(self.lines): if fnline == line or fnmatch(line, fnline): return self.lines[i + 1 :] - raise ValueError("line %r not found in output" % fnline) + raise ValueError(f"line {fnline!r} not found in output") def _log(self, *args) -> None: self._log_output.append(" ".join(str(x) for x in args)) @@ -1690,7 +1687,7 @@ def _match_lines( started = True break elif match_func(nextline, line): - self._log("%s:" % match_nickname, repr(line)) + self._log(f"{match_nickname}:", repr(line)) self._log( "{:>{width}}".format("with:", width=wnick), repr(nextline) ) diff --git a/src/_pytest/pytester_assertions.py b/src/_pytest/pytester_assertions.py index d20c2bb5999..d543798f75a 100644 --- a/src/_pytest/pytester_assertions.py +++ b/src/_pytest/pytester_assertions.py @@ -4,21 +4,19 @@ # contain them itself, since it is imported by the `pytest` module, # hence cannot be subject to assertion rewriting, which requires a # module to not be already imported. -from typing import Dict -from typing import Optional +from __future__ import annotations + from typing import Sequence -from typing import Tuple -from typing import Union from _pytest.reports import CollectReport from _pytest.reports import TestReport def assertoutcome( - outcomes: Tuple[ + outcomes: tuple[ Sequence[TestReport], - Sequence[Union[CollectReport, TestReport]], - Sequence[Union[CollectReport, TestReport]], + Sequence[CollectReport | TestReport], + Sequence[CollectReport | TestReport], ], passed: int = 0, skipped: int = 0, @@ -37,15 +35,15 @@ def assertoutcome( def assert_outcomes( - outcomes: Dict[str, int], + outcomes: dict[str, int], passed: int = 0, skipped: int = 0, failed: int = 0, errors: int = 0, xpassed: int = 0, xfailed: int = 0, - warnings: Optional[int] = None, - deselected: Optional[int] = None, + warnings: int | None = None, + deselected: int | None = None, ) -> None: """Assert that the specified outcomes appear with the respective numbers (0 means it didn't occur) in the text output from a test run.""" diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 5e059f2c4e6..9182ce7dfe9 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Python test discovery, setup and run of test functions.""" +from __future__ import annotations + import abc from collections import Counter from collections import defaultdict @@ -20,16 +22,11 @@ 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 warnings import _pytest @@ -113,7 +110,7 @@ def pytest_addoption(parser: Parser) -> None: ) -def pytest_generate_tests(metafunc: "Metafunc") -> None: +def pytest_generate_tests(metafunc: Metafunc) -> None: for marker in metafunc.definition.iter_markers(name="parametrize"): metafunc.parametrize(*marker.args, **marker.kwargs, _param_mark=marker) @@ -153,7 +150,7 @@ def async_warn_and_skip(nodeid: str) -> None: @hookimpl(trylast=True) -def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: +def pytest_pyfunc_call(pyfuncitem: Function) -> object | None: testfunction = pyfuncitem.obj if is_async_function(testfunction): async_warn_and_skip(pyfuncitem.nodeid) @@ -174,14 +171,19 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]: def pytest_collect_directory( path: Path, parent: nodes.Collector -) -> Optional[nodes.Collector]: +) -> nodes.Collector | None: pkginit = path / "__init__.py" - if pkginit.is_file(): + try: + has_pkginit = pkginit.is_file() + except PermissionError: + # See https://github.com/pytest-dev/pytest/issues/12120#issuecomment-2106349096. + return None + if has_pkginit: return Package.from_parent(parent, path=path) return None -def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Optional["Module"]: +def pytest_collect_file(file_path: Path, parent: nodes.Collector) -> Module | None: if file_path.suffix == ".py": if not parent.session.isinitpath(file_path): if not path_matches_patterns( @@ -201,14 +203,14 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool: return any(fnmatch_ex(pattern, path) for pattern in patterns) -def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module": +def pytest_pycollect_makemodule(module_path: Path, parent) -> Module: return Module.from_parent(parent, path=module_path) @hookimpl(trylast=True) def pytest_pycollect_makeitem( - collector: Union["Module", "Class"], name: str, obj: object -) -> Union[None, nodes.Item, nodes.Collector, List[Union[nodes.Item, nodes.Collector]]]: + collector: Module | Class, name: str, obj: object +) -> None | nodes.Item | nodes.Collector | list[nodes.Item | nodes.Collector]: assert isinstance(collector, (Class, Module)), type(collector) # Nothing was collected elsewhere, let's do it here. if safe_isclass(obj): @@ -224,7 +226,7 @@ def pytest_pycollect_makeitem( filename, lineno = getfslineno(obj) warnings.warn_explicit( message=PytestCollectionWarning( - "cannot collect %r because it is not a function." % name + f"cannot collect {name!r} because it is not a function." ), category=None, filename=str(filename), @@ -315,7 +317,7 @@ def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> parts.reverse() return ".".join(parts) - def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: + def reportinfo(self) -> tuple[os.PathLike[str] | str, int | None, str]: # XXX caching? path, lineno = getfslineno(self.obj) modpath = self.getmodpath() @@ -368,7 +370,11 @@ def istestfunction(self, obj: object, name: str) -> bool: return False def istestclass(self, obj: object, name: str) -> bool: - return self.classnamefilter(name) or self.isnosetest(obj) + if not (self.classnamefilter(name) or self.isnosetest(obj)): + return False + if inspect.isabstract(obj): + return False + return True def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool: """Check if the given name matches the prefix or glob-pattern defined @@ -385,7 +391,7 @@ def _matches_prefix_or_glob_option(self, option_name: str, name: str) -> bool: return True return False - def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: if not getattr(self.obj, "__test__", True): return [] @@ -397,11 +403,11 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: # In each class, nodes should be definition ordered. # __dict__ is definition ordered. - seen: Set[str] = set() - dict_values: List[List[Union[nodes.Item, nodes.Collector]]] = [] + seen: set[str] = set() + dict_values: list[list[nodes.Item | nodes.Collector]] = [] ihook = self.ihook for dic in dicts: - values: List[Union[nodes.Item, nodes.Collector]] = [] + values: list[nodes.Item | nodes.Collector] = [] # Note: seems like the dict can change during iteration - # be careful not to remove the list() without consideration. for name, obj in list(dic.items()): @@ -428,7 +434,7 @@ def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: result.extend(values) return result - def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]: + def _genfunctions(self, name: str, funcobj) -> Iterator[Function]: modulecol = self.getparent(Module) assert modulecol is not None module = modulecol.obj @@ -539,7 +545,7 @@ class Module(nodes.File, PyCollector): def _getobj(self): return importtestmodule(self.path, self.config) - def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: self._register_setup_module_fixture() self._register_setup_function_fixture() self.session._fixturemanager.parsefactories(self) @@ -633,13 +639,13 @@ class Package(nodes.Directory): def __init__( self, - fspath: Optional[LEGACY_PATH], + fspath: LEGACY_PATH | None, parent: nodes.Collector, # NOTE: following args are unused: config=None, session=None, nodeid=None, - path: Optional[Path] = None, + path: Path | None = None, ) -> None: # NOTE: Could be just the following, but kept as-is for compat. # super().__init__(self, fspath, parent=parent) @@ -671,13 +677,13 @@ def setup(self) -> None: func = partial(_call_with_optional_argument, teardown_module, init_mod) self.addfinalizer(func) - def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: # Always collect __init__.py first. - def sort_key(entry: "os.DirEntry[str]") -> object: + def sort_key(entry: os.DirEntry[str]) -> object: return (entry.name != "__init__.py", entry.name) config = self.config - col: Optional[nodes.Collector] + col: nodes.Collector | None cols: Sequence[nodes.Collector] ihook = self.ihook for direntry in scandir(self.path, sort_key): @@ -711,12 +717,12 @@ def _call_with_optional_argument(func, arg) -> None: func() -def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]: +def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> object | None: """Return the attribute from the given object to be used as a setup/teardown xunit-style function, but only if not marked as a fixture to avoid calling it twice. """ for name in names: - meth: Optional[object] = getattr(obj, name, None) + meth: object | None = getattr(obj, name, None) if meth is not None and fixtures.getfixturemarker(meth) is None: return meth return None @@ -726,14 +732,14 @@ class Class(PyCollector): """Collector for test methods (and nested classes) in a Python class.""" @classmethod - def from_parent(cls, parent, *, name, obj=None, **kw) -> "Self": # type: ignore[override] + def from_parent(cls, parent, *, name, obj=None, **kw) -> Self: # type: ignore[override] """The public constructor.""" return super().from_parent(name=name, parent=parent, **kw) def newinstance(self): return self.obj() - def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: + def collect(self) -> Iterable[nodes.Item | nodes.Collector]: if not safe_getattr(self.obj, "__test__", True): return [] if hasinit(self.obj): @@ -863,21 +869,21 @@ class IdMaker: parametersets: Sequence[ParameterSet] # Optionally, a user-provided callable to make IDs for parameters in a # ParameterSet. - idfn: Optional[Callable[[Any], Optional[object]]] + idfn: Callable[[Any], object | None] | None # Optionally, explicit IDs for ParameterSets by index. - ids: Optional[Sequence[Optional[object]]] + ids: Sequence[object | None] | None # Optionally, the pytest config. # Used for controlling ASCII escaping, and for calling the # :hook:`pytest_make_parametrize_id` hook. - config: Optional[Config] + config: Config | None # Optionally, the ID of the node being parametrized. # Used only for clearer error messages. - nodeid: Optional[str] + nodeid: str | None # Optionally, the ID of the function being parametrized. # Used only for clearer error messages. - func_name: Optional[str] + func_name: str | None - def make_unique_parameterset_ids(self) -> List[str]: + def make_unique_parameterset_ids(self) -> list[str]: """Make a unique identifier for each ParameterSet, that may be used to identify the parametrization in a node ID. @@ -894,7 +900,7 @@ def make_unique_parameterset_ids(self) -> List[str]: # Record the number of occurrences of each ID. id_counts = Counter(resolved_ids) # Map the ID to its next suffix. - id_suffixes: Dict[str, int] = defaultdict(int) + id_suffixes: dict[str, int] = defaultdict(int) # Suffix non-unique IDs to make them unique. for index, id in enumerate(resolved_ids): if id_counts[id] > 1: @@ -941,9 +947,7 @@ def _idval(self, val: object, argname: str, idx: int) -> str: return idval return self._idval_from_argname(argname, idx) - def _idval_from_function( - self, val: object, argname: str, idx: int - ) -> Optional[str]: + def _idval_from_function(self, val: object, argname: str, idx: int) -> str | None: """Try to make an ID for a parameter in a ParameterSet using the user-provided id callable, if given.""" if self.idfn is None: @@ -959,17 +963,17 @@ def _idval_from_function( return None return self._idval_from_value(id) - def _idval_from_hook(self, val: object, argname: str) -> Optional[str]: + def _idval_from_hook(self, val: object, argname: str) -> str | None: """Try to make an ID for a parameter in a ParameterSet by calling the :hook:`pytest_make_parametrize_id` hook.""" if self.config: - id: Optional[str] = self.config.hook.pytest_make_parametrize_id( + id: str | None = self.config.hook.pytest_make_parametrize_id( config=self.config, val=val, argname=argname ) return id return None - def _idval_from_value(self, val: object) -> Optional[str]: + def _idval_from_value(self, val: object) -> str | None: """Try to make an ID for a parameter in a ParameterSet from its value, if the value type is supported.""" if isinstance(val, (str, bytes)): @@ -1027,15 +1031,15 @@ class CallSpec2: # arg name -> arg value which will be passed to a fixture or pseudo-fixture # of the same name. (indirect or direct parametrization respectively) - params: Dict[str, object] = dataclasses.field(default_factory=dict) + params: dict[str, object] = dataclasses.field(default_factory=dict) # arg name -> arg index. - indices: Dict[str, int] = dataclasses.field(default_factory=dict) + indices: dict[str, int] = dataclasses.field(default_factory=dict) # Used for sorting parametrized resources. _arg2scope: Mapping[str, Scope] = dataclasses.field(default_factory=dict) # Parts which will be added to the item's name in `[..]` separated by "-". _idlist: Sequence[str] = dataclasses.field(default_factory=tuple) # Marks which will be applied to the item. - marks: List[Mark] = dataclasses.field(default_factory=list) + marks: list[Mark] = dataclasses.field(default_factory=list) def setmulti( self, @@ -1043,10 +1047,10 @@ def setmulti( argnames: Iterable[str], valset: Iterable[object], id: str, - marks: Iterable[Union[Mark, MarkDecorator]], + marks: Iterable[Mark | MarkDecorator], scope: Scope, param_index: int, - ) -> "CallSpec2": + ) -> CallSpec2: params = self.params.copy() indices = self.indices.copy() arg2scope = dict(self._arg2scope) @@ -1094,7 +1098,7 @@ class Metafunc: def __init__( self, - definition: "FunctionDefinition", + definition: FunctionDefinition, fixtureinfo: fixtures.FuncFixtureInfo, config: Config, cls=None, @@ -1125,19 +1129,17 @@ def __init__( self._arg2fixturedefs = fixtureinfo.name2fixturedefs # Result of parametrize(). - self._calls: List[CallSpec2] = [] + self._calls: list[CallSpec2] = [] def parametrize( self, - argnames: Union[str, Sequence[str]], - argvalues: Iterable[Union[ParameterSet, Sequence[object], object]], - indirect: Union[bool, Sequence[str]] = False, - ids: Optional[ - Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]] - ] = None, - scope: Optional[_ScopeName] = None, + argnames: str | Sequence[str], + argvalues: Iterable[ParameterSet | Sequence[object] | object], + indirect: bool | Sequence[str] = False, + ids: Iterable[object | None] | Callable[[Any], object | None] | None = None, + scope: _ScopeName | None = None, *, - _param_mark: Optional[Mark] = None, + _param_mark: Mark | None = None, ) -> None: """Add new invocations to the underlying test function using the list of argvalues for the given argnames. Parametrization is performed @@ -1166,7 +1168,7 @@ def parametrize( If N argnames were specified, argvalues must be a list of N-tuples, where each tuple-element specifies a value for its respective argname. - + :type argvalues: Iterable[_pytest.mark.structures.ParameterSet | Sequence[object] | object] :param indirect: A list of arguments' names (subset of argnames) or a boolean. If True the list contains all names from the argnames. Each @@ -1266,7 +1268,7 @@ def parametrize( if node is None: name2pseudofixturedef = None else: - default: Dict[str, FixtureDef[Any]] = {} + default: dict[str, FixtureDef[Any]] = {} name2pseudofixturedef = node.stash.setdefault( name2pseudofixturedef_key, default ) @@ -1313,12 +1315,10 @@ def parametrize( def _resolve_parameter_set_ids( self, argnames: Sequence[str], - ids: Optional[ - Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]] - ], + ids: Iterable[object | None] | Callable[[Any], object | None] | None, parametersets: Sequence[ParameterSet], nodeid: str, - ) -> List[str]: + ) -> list[str]: """Resolve the actual ids for the given parameter sets. :param argnames: @@ -1356,10 +1356,10 @@ def _resolve_parameter_set_ids( def _validate_ids( self, - ids: Iterable[Optional[object]], + ids: Iterable[object | None], parametersets: Sequence[ParameterSet], func_name: str, - ) -> List[Optional[object]]: + ) -> list[object | None]: try: num_ids = len(ids) # type: ignore[arg-type] except TypeError: @@ -1379,8 +1379,8 @@ def _validate_ids( def _resolve_args_directness( self, argnames: Sequence[str], - indirect: Union[bool, Sequence[str]], - ) -> Dict[str, Literal["indirect", "direct"]]: + indirect: bool | Sequence[str], + ) -> dict[str, Literal["indirect", "direct"]]: """Resolve if each parametrized argument must be considered an indirect parameter to a fixture of the same name, or a direct parameter to the parametrized function, based on the ``indirect`` parameter of the @@ -1393,7 +1393,7 @@ def _resolve_args_directness( :returns A dict mapping each arg name to either "indirect" or "direct". """ - arg_directness: Dict[str, Literal["indirect", "direct"]] + arg_directness: dict[str, Literal["indirect", "direct"]] if isinstance(indirect, bool): arg_directness = dict.fromkeys( argnames, "indirect" if indirect else "direct" @@ -1418,7 +1418,7 @@ def _resolve_args_directness( def _validate_if_using_arg_names( self, argnames: Sequence[str], - indirect: Union[bool, Sequence[str]], + indirect: bool | Sequence[str], ) -> None: """Check if all argnames are being used, by default values, or directly/indirectly. @@ -1449,7 +1449,7 @@ def _validate_if_using_arg_names( def _find_parametrized_scope( argnames: Sequence[str], arg2fixturedefs: Mapping[str, Sequence[fixtures.FixtureDef[object]]], - indirect: Union[bool, Sequence[str]], + indirect: bool | Sequence[str], ) -> Scope: """Find the most appropriate scope for a parametrized call based on its arguments. @@ -1478,7 +1478,7 @@ def _find_parametrized_scope( return Scope.Function -def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -> str: +def _ascii_escaped_by_config(val: str | bytes, config: Config | None) -> str: if config is None: escape_option = False else: @@ -1527,13 +1527,13 @@ def __init__( self, name: str, parent, - config: Optional[Config] = None, - callspec: Optional[CallSpec2] = None, + config: Config | None = None, + callspec: CallSpec2 | None = None, callobj=NOTSET, - keywords: Optional[Mapping[str, Any]] = None, - session: Optional[Session] = None, - fixtureinfo: Optional[FuncFixtureInfo] = None, - originalname: Optional[str] = None, + keywords: Mapping[str, Any] | None = None, + session: Session | None = None, + fixtureinfo: FuncFixtureInfo | None = None, + originalname: str | None = None, ) -> None: super().__init__(name, parent, config=config, session=session) @@ -1576,12 +1576,12 @@ def __init__( # todo: determine sound type limitations @classmethod - def from_parent(cls, parent, **kw) -> "Self": + def from_parent(cls, parent, **kw) -> Self: """The public constructor.""" return super().from_parent(parent=parent, **kw) def _initrequest(self) -> None: - self.funcargs: Dict[str, object] = {} + self.funcargs: dict[str, object] = {} self._request = fixtures.TopRequest(self, _ispytest=True) @property @@ -1662,7 +1662,7 @@ def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback: def repr_failure( # type: ignore[override] self, excinfo: ExceptionInfo[BaseException], - ) -> Union[str, TerminalRepr]: + ) -> str | TerminalRepr: style = self.config.getoption("tbstyle", "auto") if style == "auto": style = "long" diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 7d89fdd809e..4174a55b589 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,19 +1,20 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from collections.abc import Collection from collections.abc import Sized from decimal import Decimal import math from numbers import Complex import pprint +import re from types import TracebackType 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 @@ -21,7 +22,6 @@ from typing import Type from typing import TYPE_CHECKING from typing import TypeVar -from typing import Union import _pytest._code from _pytest.outcomes import fail @@ -33,12 +33,12 @@ def _compare_approx( full_object: object, - message_data: Sequence[Tuple[str, str, str]], + message_data: Sequence[tuple[str, str, str]], number_of_elements: int, different_ids: Sequence[object], max_abs_diff: float, max_rel_diff: float, -) -> List[str]: +) -> list[str]: message_list = list(message_data) message_list.insert(0, ("Index", "Obtained", "Expected")) max_sizes = [0, 0, 0] @@ -79,7 +79,7 @@ def __init__(self, expected, rel=None, abs=None, nan_ok: bool = False) -> None: def __repr__(self) -> str: raise NotImplementedError - def _repr_compare(self, other_side: Any) -> List[str]: + def _repr_compare(self, other_side: Any) -> list[str]: return [ "comparison failed", f"Obtained: {other_side}", @@ -103,7 +103,7 @@ def __bool__(self): def __ne__(self, actual) -> bool: return not (actual == self) - def _approx_scalar(self, x) -> "ApproxScalar": + def _approx_scalar(self, x) -> ApproxScalar: if isinstance(x, Decimal): return ApproxDecimal(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok) @@ -129,6 +129,8 @@ def _recursive_sequence_map(f, x): if isinstance(x, (list, tuple)): seq_type = type(x) return seq_type(_recursive_sequence_map(f, xi) for xi in x) + elif _is_sequence_like(x): + return [_recursive_sequence_map(f, xi) for xi in x] else: return f(x) @@ -142,12 +144,12 @@ def __repr__(self) -> str: ) return f"approx({list_scalars!r})" - def _repr_compare(self, other_side: Union["ndarray", List[Any]]) -> List[str]: + def _repr_compare(self, other_side: ndarray | list[Any]) -> list[str]: import itertools import math def get_value_from_nested_list( - nested_list: List[Any], nd_index: Tuple[Any, ...] + nested_list: list[Any], nd_index: tuple[Any, ...] ) -> Any: """ Helper function to get the value out of a nested list, given an n-dimensional index. @@ -244,7 +246,7 @@ class ApproxMapping(ApproxBase): def __repr__(self) -> str: return f"approx({({k: self._approx_scalar(v) for k, v in self.expected.items()})!r})" - def _repr_compare(self, other_side: Mapping[object, float]) -> List[str]: + def _repr_compare(self, other_side: Mapping[object, float]) -> list[str]: import math approx_side_as_map = { @@ -319,7 +321,7 @@ def __repr__(self) -> str: seq_type = list return f"approx({seq_type(self._approx_scalar(x) for x in self.expected)!r})" - def _repr_compare(self, other_side: Sequence[float]) -> List[str]: + def _repr_compare(self, other_side: Sequence[float]) -> list[str]: import math if len(self.expected) != len(other_side): @@ -384,8 +386,8 @@ class ApproxScalar(ApproxBase): # Using Real should be better than this Union, but not possible yet: # https://github.com/python/typeshed/pull/3108 - DEFAULT_ABSOLUTE_TOLERANCE: Union[float, Decimal] = 1e-12 - DEFAULT_RELATIVE_TOLERANCE: Union[float, Decimal] = 1e-6 + DEFAULT_ABSOLUTE_TOLERANCE: float | Decimal = 1e-12 + DEFAULT_RELATIVE_TOLERANCE: float | Decimal = 1e-6 def __repr__(self) -> str: """Return a string communicating both the expected value and the @@ -454,7 +456,7 @@ def __eq__(self, actual) -> bool: return False # Return true if the two numbers are within the tolerance. - result: bool = abs(self.expected - actual) <= self.tolerance # type: ignore[arg-type] + result: bool = abs(self.expected - actual) <= self.tolerance return result # Ignore type because of https://github.com/python/mypy/issues/4266. @@ -715,17 +717,13 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: __tracebackhide__ = True if isinstance(expected, Decimal): - cls: Type[ApproxBase] = ApproxDecimal + cls: type[ApproxBase] = ApproxDecimal elif isinstance(expected, Mapping): cls = ApproxMapping elif _is_numpy_array(expected): expected = _as_numpy_array(expected) cls = ApproxNumpy - elif ( - hasattr(expected, "__getitem__") - and isinstance(expected, Sized) - and not isinstance(expected, (str, bytes)) - ): + elif _is_sequence_like(expected): cls = ApproxSequenceLike elif isinstance(expected, Collection) and not isinstance(expected, (str, bytes)): msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}" @@ -736,6 +734,14 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase: return cls(expected, rel, abs, nan_ok) +def _is_sequence_like(expected: object) -> bool: + return ( + hasattr(expected, "__getitem__") + and isinstance(expected, Sized) + and not isinstance(expected, (str, bytes)) + ) + + def _is_numpy_array(obj: object) -> bool: """ Return true if the given object is implicitly convertible to ndarray, @@ -744,7 +750,7 @@ def _is_numpy_array(obj: object) -> bool: return _as_numpy_array(obj) is not None -def _as_numpy_array(obj: object) -> Optional["ndarray"]: +def _as_numpy_array(obj: object) -> ndarray | None: """ Return an ndarray if the given object is implicitly convertible to ndarray, and numpy is already imported, otherwise None. @@ -770,15 +776,15 @@ def _as_numpy_array(obj: object) -> Optional["ndarray"]: @overload def raises( - expected_exception: Union[Type[E], Tuple[Type[E], ...]], + expected_exception: type[E] | tuple[type[E], ...], *, - match: Optional[Union[str, Pattern[str]]] = ..., -) -> "RaisesContext[E]": ... + match: str | Pattern[str] | None = ..., +) -> RaisesContext[E]: ... @overload def raises( - expected_exception: Union[Type[E], Tuple[Type[E], ...]], + expected_exception: type[E] | tuple[type[E], ...], func: Callable[..., Any], *args: Any, **kwargs: Any, @@ -786,8 +792,8 @@ def raises( def raises( - expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any -) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]: + expected_exception: type[E] | tuple[type[E], ...], *args: Any, **kwargs: Any +) -> RaisesContext[E] | _pytest._code.ExceptionInfo[E]: r"""Assert that a code block/function call raises an exception type, or one of its subclasses. :param expected_exception: @@ -935,7 +941,7 @@ def raises( f"any special code to say 'this should never raise an exception'." ) if isinstance(expected_exception, type): - expected_exceptions: Tuple[Type[E], ...] = (expected_exception,) + expected_exceptions: tuple[type[E], ...] = (expected_exception,) else: expected_exceptions = expected_exception for exc in expected_exceptions: @@ -947,7 +953,7 @@ def raises( message = f"DID NOT RAISE {expected_exception}" if not args: - match: Optional[Union[str, Pattern[str]]] = kwargs.pop("match", None) + match: str | Pattern[str] | None = kwargs.pop("match", None) if kwargs: msg = "Unexpected keyword arguments passed to pytest.raises: " msg += ", ".join(sorted(kwargs)) @@ -973,14 +979,22 @@ def raises( class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]): def __init__( self, - expected_exception: Union[Type[E], Tuple[Type[E], ...]], + expected_exception: type[E] | tuple[type[E], ...], message: str, - match_expr: Optional[Union[str, Pattern[str]]] = None, + match_expr: str | Pattern[str] | None = None, ) -> None: self.expected_exception = expected_exception self.message = message self.match_expr = match_expr - self.excinfo: Optional[_pytest._code.ExceptionInfo[E]] = None + self.excinfo: _pytest._code.ExceptionInfo[E] | None = None + if self.match_expr is not None: + re_error = None + try: + re.compile(self.match_expr) + except re.error as e: + re_error = e + if re_error is not None: + fail(f"Invalid regex pattern provided to 'match': {re_error}") def __enter__(self) -> _pytest._code.ExceptionInfo[E]: self.excinfo = _pytest._code.ExceptionInfo.for_later() @@ -988,9 +1002,9 @@ def __enter__(self) -> _pytest._code.ExceptionInfo[E]: def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> bool: __tracebackhide__ = True if exc_type is None: diff --git a/src/_pytest/python_path.py b/src/_pytest/python_path.py index cceabbca12a..6e33c8a39f2 100644 --- a/src/_pytest/python_path.py +++ b/src/_pytest/python_path.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys import pytest diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 63e7a4bd6dc..3fc00d94736 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Record warnings during test function execution.""" +from __future__ import annotations + from pprint import pformat import re from types import TracebackType @@ -9,14 +11,15 @@ 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 TYPE_CHECKING from typing import TypeVar -from typing import Union + + +if TYPE_CHECKING: + from typing_extensions import Self + import warnings from _pytest.deprecated import check_ispytest @@ -29,7 +32,7 @@ @fixture -def recwarn() -> Generator["WarningsRecorder", None, None]: +def recwarn() -> Generator[WarningsRecorder, None, None]: """Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions. See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information @@ -42,9 +45,7 @@ def recwarn() -> Generator["WarningsRecorder", None, None]: @overload -def deprecated_call( - *, match: Optional[Union[str, Pattern[str]]] = ... -) -> "WarningsRecorder": ... +def deprecated_call(*, match: str | Pattern[str] | None = ...) -> WarningsRecorder: ... @overload @@ -52,8 +53,8 @@ def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ... def deprecated_call( - func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any -) -> Union["WarningsRecorder", Any]: + func: Callable[..., Any] | None = None, *args: Any, **kwargs: Any +) -> WarningsRecorder | Any: """Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``. This function can be used as a context manager:: @@ -87,15 +88,15 @@ def deprecated_call( @overload def warns( - expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ..., + expected_warning: type[Warning] | tuple[type[Warning], ...] = ..., *, - match: Optional[Union[str, Pattern[str]]] = ..., -) -> "WarningsChecker": ... + match: str | Pattern[str] | None = ..., +) -> WarningsChecker: ... @overload def warns( - expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]], + expected_warning: type[Warning] | tuple[type[Warning], ...], func: Callable[..., T], *args: Any, **kwargs: Any, @@ -103,11 +104,11 @@ def warns( def warns( - expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning, + expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning, *args: Any, - match: Optional[Union[str, Pattern[str]]] = None, + match: str | Pattern[str] | None = None, **kwargs: Any, -) -> Union["WarningsChecker", Any]: +) -> WarningsChecker | Any: r"""Assert that code raises a particular class of warning. Specifically, the parameter ``expected_warning`` can be a warning class or tuple @@ -183,18 +184,18 @@ def __init__(self, *, _ispytest: bool = False) -> None: check_ispytest(_ispytest) super().__init__(record=True) self._entered = False - self._list: List[warnings.WarningMessage] = [] + self._list: list[warnings.WarningMessage] = [] @property - def list(self) -> List["warnings.WarningMessage"]: + def list(self) -> list[warnings.WarningMessage]: """The list of recorded warnings.""" return self._list - def __getitem__(self, i: int) -> "warnings.WarningMessage": + def __getitem__(self, i: int) -> warnings.WarningMessage: """Get a recorded warning by index.""" return self._list[i] - def __iter__(self) -> Iterator["warnings.WarningMessage"]: + def __iter__(self) -> Iterator[warnings.WarningMessage]: """Iterate through the recorded warnings.""" return iter(self._list) @@ -202,12 +203,12 @@ def __len__(self) -> int: """The number of recorded warnings.""" return len(self._list) - def pop(self, cls: Type[Warning] = Warning) -> "warnings.WarningMessage": + def pop(self, cls: type[Warning] = Warning) -> warnings.WarningMessage: """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 + best_idx: int | None = None for i, w in enumerate(self._list): if w.category == cls: return self._list.pop(i) # exact match, stop looking @@ -225,9 +226,7 @@ def clear(self) -> None: """Clear the list of recorded warnings.""" self._list[:] = [] - # Type ignored because it doesn't exactly warnings.catch_warnings.__enter__ - # -- it returns a List but we only emulate one. - def __enter__(self) -> "WarningsRecorder": # type: ignore + def __enter__(self) -> Self: if self._entered: __tracebackhide__ = True raise RuntimeError(f"Cannot enter {self!r} twice") @@ -240,9 +239,9 @@ def __enter__(self) -> "WarningsRecorder": # type: ignore def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> None: if not self._entered: __tracebackhide__ = True @@ -259,8 +258,8 @@ def __exit__( class WarningsChecker(WarningsRecorder): def __init__( self, - expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning, - match_expr: Optional[Union[str, Pattern[str]]] = None, + expected_warning: type[Warning] | tuple[type[Warning], ...] = Warning, + match_expr: str | Pattern[str] | None = None, *, _ispytest: bool = False, ) -> None: @@ -291,9 +290,9 @@ def matches(self, warning: warnings.WarningMessage) -> bool: def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> None: super().__exit__(exc_type, exc_val, exc_tb) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 70f3212ce7b..77cbf773e23 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,24 +1,20 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import dataclasses from io import StringIO import os 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 -from typing import Tuple -from typing import Type +from typing import Sequence from typing import TYPE_CHECKING -from typing import TypeVar -from typing import Union from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo @@ -35,10 +31,13 @@ from _pytest.config import Config from _pytest.nodes import Collector from _pytest.nodes import Item +from _pytest.outcomes import fail from _pytest.outcomes import skip if TYPE_CHECKING: + from typing_extensions import Self + from _pytest.runner import CallInfo @@ -54,16 +53,13 @@ def getworkerinfoline(node): return s -_R = TypeVar("_R", bound="BaseReport") - - class BaseReport: - when: Optional[str] - location: Optional[Tuple[str, Optional[int], str]] - longrepr: Union[ - None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr - ] - sections: List[Tuple[str, str]] + when: str | None + location: tuple[str, int | None, str] | None + longrepr: ( + None | ExceptionInfo[BaseException] | tuple[str, int, str] | str | TerminalRepr + ) + sections: list[tuple[str, str]] nodeid: str outcome: Literal["passed", "failed", "skipped"] @@ -94,7 +90,7 @@ def toterminal(self, out: TerminalWriter) -> None: s = "" out.line(s) - def get_sections(self, prefix: str) -> Iterator[Tuple[str, str]]: + def get_sections(self, prefix: str) -> Iterator[tuple[str, str]]: for name, content in self.sections: if name.startswith(prefix): yield prefix, content @@ -176,7 +172,7 @@ def count_towards_summary(self) -> bool: return True @property - def head_line(self) -> Optional[str]: + def head_line(self) -> str | None: """**Experimental** The head line shown with longrepr output for this report, more commonly during traceback representation during failures:: @@ -196,13 +192,28 @@ def head_line(self) -> Optional[str]: return domain return None - def _get_verbose_word(self, config: Config): + def _get_verbose_word_with_markup( + self, config: Config, default_markup: Mapping[str, bool] + ) -> tuple[str, Mapping[str, bool]]: _category, _short, verbose = config.hook.pytest_report_teststatus( report=self, config=config ) - return verbose - def _to_json(self) -> Dict[str, Any]: + if isinstance(verbose, str): + return verbose, default_markup + + if isinstance(verbose, Sequence) and len(verbose) == 2: + word, markup = verbose + if isinstance(word, str) and isinstance(markup, Mapping): + return word, markup + + fail( # pragma: no cover + "pytest_report_teststatus() hook (from a plugin) returned " + f"an invalid verbose value: {verbose!r}.\nExpected either a string " + "or a tuple of (word, markup)." + ) + + def _to_json(self) -> dict[str, Any]: """Return the contents of this report as a dict of builtin entries, suitable for serialization. @@ -213,7 +224,7 @@ def _to_json(self) -> Dict[str, Any]: return _report_to_json(self) @classmethod - def _from_json(cls: Type[_R], reportdict: Dict[str, object]) -> _R: + def _from_json(cls, reportdict: dict[str, object]) -> Self: """Create either a TestReport or CollectReport, depending on the calling class. It is the callers responsibility to know which class to pass here. @@ -227,15 +238,15 @@ def _from_json(cls: Type[_R], reportdict: Dict[str, object]) -> _R: def _report_unserialization_failure( - type_name: str, report_class: Type[BaseReport], reportdict + type_name: str, report_class: type[BaseReport], reportdict ) -> NoReturn: url = "https://github.com/pytest-dev/pytest/issues" stream = StringIO() pprint("-" * 100, stream=stream) - pprint("INTERNALERROR: Unknown entry type returned: %s" % type_name, stream=stream) - pprint("report_name: %s" % report_class, stream=stream) + pprint(f"INTERNALERROR: Unknown entry type returned: {type_name}", stream=stream) + pprint(f"report_name: {report_class}", stream=stream) pprint(reportdict, stream=stream) - pprint("Please report this bug at %s" % url, stream=stream) + pprint(f"Please report this bug at {url}", stream=stream) pprint("-" * 100, stream=stream) raise RuntimeError(stream.getvalue()) @@ -256,18 +267,20 @@ class TestReport(BaseReport): def __init__( self, nodeid: str, - location: Tuple[str, Optional[int], str], + location: tuple[str, int | None, str], keywords: Mapping[str, Any], outcome: Literal["passed", "failed", "skipped"], - longrepr: Union[ - None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr - ], + longrepr: None + | ExceptionInfo[BaseException] + | tuple[str, int, str] + | str + | TerminalRepr, when: Literal["setup", "call", "teardown"], - sections: Iterable[Tuple[str, str]] = (), + sections: Iterable[tuple[str, str]] = (), duration: float = 0, start: float = 0, stop: float = 0, - user_properties: Optional[Iterable[Tuple[str, object]]] = None, + user_properties: Iterable[tuple[str, object]] | None = None, **extra, ) -> None: #: Normalized collection nodeid. @@ -278,7 +291,7 @@ def __init__( #: collected one e.g. if a method is inherited from a different module. #: The filesystempath may be relative to ``config.rootdir``. #: The line number is 0-based. - self.location: Tuple[str, Optional[int], str] = location + self.location: tuple[str, int | None, str] = location #: A name -> value dictionary containing all keywords and #: markers associated with a test invocation. @@ -317,7 +330,7 @@ def __repr__(self) -> str: return f"<{self.__class__.__name__} {self.nodeid!r} when={self.when!r} outcome={self.outcome!r}>" @classmethod - def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": + def from_item_and_call(cls, item: Item, call: CallInfo[None]) -> TestReport: """Create and fill a TestReport with standard item and call info. :param item: The item. @@ -334,13 +347,13 @@ def from_item_and_call(cls, item: Item, call: "CallInfo[None]") -> "TestReport": sections = [] if not call.excinfo: outcome: Literal["passed", "failed", "skipped"] = "passed" - longrepr: Union[ - None, - ExceptionInfo[BaseException], - Tuple[str, int, str], - str, - TerminalRepr, - ] = None + longrepr: ( + None + | ExceptionInfo[BaseException] + | tuple[str, int, str] + | str + | TerminalRepr + ) = None else: if not isinstance(excinfo, ExceptionInfo): outcome = "failed" @@ -394,12 +407,14 @@ class CollectReport(BaseReport): def __init__( self, nodeid: str, - outcome: "Literal['passed', 'failed', 'skipped']", - longrepr: Union[ - None, ExceptionInfo[BaseException], Tuple[str, int, str], str, TerminalRepr - ], - result: Optional[List[Union[Item, Collector]]], - sections: Iterable[Tuple[str, str]] = (), + outcome: Literal["passed", "failed", "skipped"], + longrepr: None + | ExceptionInfo[BaseException] + | tuple[str, int, str] + | str + | TerminalRepr, + result: list[Item | Collector] | None, + sections: Iterable[tuple[str, str]] = (), **extra, ) -> None: #: Normalized collection nodeid. @@ -425,7 +440,7 @@ def __init__( @property def location( # type:ignore[override] self, - ) -> Optional[Tuple[str, Optional[int], str]]: + ) -> tuple[str, int | None, str] | None: return (self.fspath, None, self.fspath) def __repr__(self) -> str: @@ -441,8 +456,8 @@ def toterminal(self, out: TerminalWriter) -> None: def pytest_report_to_serializable( - report: Union[CollectReport, TestReport], -) -> Optional[Dict[str, Any]]: + report: CollectReport | TestReport, +) -> dict[str, Any] | None: if isinstance(report, (TestReport, CollectReport)): data = report._to_json() data["$report_type"] = report.__class__.__name__ @@ -452,8 +467,8 @@ def pytest_report_to_serializable( def pytest_report_from_serializable( - data: Dict[str, Any], -) -> Optional[Union[CollectReport, TestReport]]: + data: dict[str, Any], +) -> CollectReport | TestReport | None: if "$report_type" in data: if data["$report_type"] == "TestReport": return TestReport._from_json(data) @@ -465,7 +480,7 @@ def pytest_report_from_serializable( return None -def _report_to_json(report: BaseReport) -> Dict[str, Any]: +def _report_to_json(report: BaseReport) -> dict[str, Any]: """Return the contents of this report as a dict of builtin entries, suitable for serialization. @@ -473,8 +488,8 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]: """ def serialize_repr_entry( - entry: Union[ReprEntry, ReprEntryNative], - ) -> Dict[str, Any]: + entry: ReprEntry | ReprEntryNative, + ) -> dict[str, Any]: data = dataclasses.asdict(entry) for key, value in data.items(): if hasattr(value, "__dict__"): @@ -482,7 +497,7 @@ def serialize_repr_entry( entry_data = {"type": type(entry).__name__, "data": data} return entry_data - def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]: + def serialize_repr_traceback(reprtraceback: ReprTraceback) -> dict[str, Any]: result = dataclasses.asdict(reprtraceback) result["reprentries"] = [ serialize_repr_entry(x) for x in reprtraceback.reprentries @@ -490,18 +505,18 @@ def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]: return result def serialize_repr_crash( - reprcrash: Optional[ReprFileLocation], - ) -> Optional[Dict[str, Any]]: + reprcrash: ReprFileLocation | None, + ) -> dict[str, Any] | None: if reprcrash is not None: return dataclasses.asdict(reprcrash) else: return None - def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]: + def serialize_exception_longrepr(rep: BaseReport) -> dict[str, Any]: assert rep.longrepr is not None # TODO: Investigate whether the duck typing is really necessary here. longrepr = cast(ExceptionRepr, rep.longrepr) - result: Dict[str, Any] = { + result: dict[str, Any] = { "reprcrash": serialize_repr_crash(longrepr.reprcrash), "reprtraceback": serialize_repr_traceback(longrepr.reprtraceback), "sections": longrepr.sections, @@ -538,7 +553,7 @@ def serialize_exception_longrepr(rep: BaseReport) -> Dict[str, Any]: return d -def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]: +def _report_kwargs_from_json(reportdict: dict[str, Any]) -> dict[str, Any]: """Return **kwargs that can be used to construct a TestReport or CollectReport instance. @@ -559,7 +574,7 @@ def deserialize_repr_entry(entry_data): if data["reprlocals"]: reprlocals = ReprLocals(data["reprlocals"]["lines"]) - reprentry: Union[ReprEntry, ReprEntryNative] = ReprEntry( + reprentry: ReprEntry | ReprEntryNative = ReprEntry( lines=data["lines"], reprfuncargs=reprfuncargs, reprlocals=reprlocals, @@ -578,7 +593,7 @@ def deserialize_repr_traceback(repr_traceback_dict): ] return ReprTraceback(**repr_traceback_dict) - def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]): + def deserialize_repr_crash(repr_crash_dict: dict[str, Any] | None): if repr_crash_dict is not None: return ReprFileLocation(**repr_crash_dict) else: @@ -605,8 +620,8 @@ def deserialize_repr_crash(repr_crash_dict: Optional[Dict[str, Any]]): description, ) ) - exception_info: Union[ExceptionChainRepr, ReprExceptionInfo] = ( - ExceptionChainRepr(chain) + exception_info: ExceptionChainRepr | ReprExceptionInfo = ExceptionChainRepr( + chain ) else: exception_info = ReprExceptionInfo( diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 9bc544ea742..716c4948f4a 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -1,23 +1,20 @@ # mypy: allow-untyped-defs """Basic collect and runtest protocol implementations.""" +from __future__ import annotations + import bdb import dataclasses import os import sys +import types 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 from typing import TYPE_CHECKING from typing import TypeVar -from typing import Union from .reports import BaseReport from .reports import CollectErrorRepr @@ -71,7 +68,7 @@ def pytest_addoption(parser: Parser) -> None: ) -def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: +def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None: durations = terminalreporter.config.option.durations durations_min = terminalreporter.config.option.durations_min verbose = terminalreporter.config.getvalue("verbose") @@ -89,7 +86,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: if not durations: tr.write_sep("=", "slowest durations") else: - tr.write_sep("=", "slowest %s durations" % durations) + tr.write_sep("=", f"slowest {durations} durations") dlist = dlist[:durations] for i, rep in enumerate(dlist): @@ -102,15 +99,15 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None: tr.write_line(f"{rep.duration:02.2f}s {rep.when:<8} {rep.nodeid}") -def pytest_sessionstart(session: "Session") -> None: +def pytest_sessionstart(session: Session) -> None: session._setupstate = SetupState() -def pytest_sessionfinish(session: "Session") -> None: +def pytest_sessionfinish(session: Session) -> None: session._setupstate.teardown_exact(None) -def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool: +def pytest_runtest_protocol(item: Item, nextitem: Item | None) -> bool: ihook = item.ihook ihook.pytest_runtest_logstart(nodeid=item.nodeid, location=item.location) runtestprotocol(item, nextitem=nextitem) @@ -119,8 +116,8 @@ def pytest_runtest_protocol(item: Item, nextitem: Optional[Item]) -> bool: def runtestprotocol( - item: Item, log: bool = True, nextitem: Optional[Item] = None -) -> List[TestReport]: + item: Item, log: bool = True, nextitem: Item | None = None +) -> list[TestReport]: hasrequest = hasattr(item, "_request") if hasrequest and not item._request: # type: ignore[attr-defined] # This only happens if the item is re-run, as is done by @@ -133,6 +130,10 @@ def runtestprotocol( show_test_item(item) if not item.config.getoption("setuponly", False): reports.append(call_and_report(item, "call", log)) + # If the session is about to fail or stop, teardown everything - this is + # necessary to correctly report fixture teardown errors (see #11706) + if item.session.shouldfail or item.session.shouldstop: + nextitem = None reports.append(call_and_report(item, "teardown", log, nextitem=nextitem)) # After all teardown hooks have been called # want funcargs and request info to go away. @@ -166,7 +167,7 @@ def pytest_runtest_call(item: Item) -> None: del sys.last_value del sys.last_traceback if sys.version_info >= (3, 12, 0): - del sys.last_exc # type: ignore[attr-defined] + del sys.last_exc except AttributeError: pass try: @@ -176,21 +177,21 @@ def pytest_runtest_call(item: Item) -> None: sys.last_type = type(e) sys.last_value = e if sys.version_info >= (3, 12, 0): - sys.last_exc = e # type: ignore[attr-defined] + sys.last_exc = e assert e.__traceback__ is not None # Skip *this* frame sys.last_traceback = e.__traceback__.tb_next - raise e + raise -def pytest_runtest_teardown(item: Item, nextitem: Optional[Item]) -> None: +def pytest_runtest_teardown(item: Item, nextitem: Item | None) -> None: _update_current_test_var(item, "teardown") item.session._setupstate.teardown_exact(nextitem) _update_current_test_var(item, None) def _update_current_test_var( - item: Item, when: Optional[Literal["setup", "call", "teardown"]] + item: Item, when: Literal["setup", "call", "teardown"] | None ) -> None: """Update :envvar:`PYTEST_CURRENT_TEST` to reflect the current item and stage. @@ -206,7 +207,7 @@ def _update_current_test_var( os.environ.pop(var_name) -def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: +def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None: if report.when in ("setup", "teardown"): if report.failed: # category, shortletter, verbose-word @@ -234,7 +235,7 @@ def call_and_report( runtest_hook = ihook.pytest_runtest_teardown else: assert False, f"Unhandled runtest hook case: {when}" - reraise: Tuple[Type[BaseException], ...] = (Exit,) + reraise: tuple[type[BaseException], ...] = (Exit,) if not item.config.getoption("usepdb", False): reraise += (KeyboardInterrupt,) call = CallInfo.from_call( @@ -248,7 +249,7 @@ def call_and_report( return report -def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> bool: +def check_interactive_exception(call: CallInfo[object], report: BaseReport) -> bool: """Check whether the call raised an exception that should be reported as interactive.""" if call.excinfo is None: @@ -271,9 +272,9 @@ def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) -> class CallInfo(Generic[TResult]): """Result/Exception info of a function invocation.""" - _result: Optional[TResult] + _result: TResult | None #: The captured exception of the call, if it raised. - excinfo: Optional[ExceptionInfo[BaseException]] + excinfo: ExceptionInfo[BaseException] | None #: The system time when the call started, in seconds since the epoch. start: float #: The system time when the call ended, in seconds since the epoch. @@ -285,8 +286,8 @@ class CallInfo(Generic[TResult]): def __init__( self, - result: Optional[TResult], - excinfo: Optional[ExceptionInfo[BaseException]], + result: TResult | None, + excinfo: ExceptionInfo[BaseException] | None, start: float, stop: float, duration: float, @@ -320,14 +321,13 @@ def from_call( cls, func: Callable[[], TResult], when: Literal["collect", "setup", "call", "teardown"], - reraise: Optional[ - Union[Type[BaseException], Tuple[Type[BaseException], ...]] - ] = None, - ) -> "CallInfo[TResult]": + reraise: type[BaseException] | tuple[type[BaseException], ...] | None = None, + ) -> CallInfo[TResult]: """Call func, wrapping the result in a CallInfo. :param func: The function to call. Called without arguments. + :type func: Callable[[], _pytest.runner.TResult] :param when: The phase in which the function is called. :param reraise: @@ -338,7 +338,7 @@ def from_call( start = timing.time() precise_start = timing.perf_counter() try: - result: Optional[TResult] = func() + result: TResult | None = func() except BaseException: excinfo = ExceptionInfo.from_current() if reraise is not None and isinstance(excinfo.value, reraise): @@ -369,7 +369,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> TestReport: def pytest_make_collect_report(collector: Collector) -> CollectReport: - def collect() -> List[Union[Item, Collector]]: + def collect() -> list[Item | Collector]: # Before collecting, if this is a Directory, load the conftests. # If a conftest import fails to load, it is considered a collection # error of the Directory collector. This is why it's done inside of the @@ -388,8 +388,10 @@ def collect() -> List[Union[Item, Collector]]: return list(collector.collect()) - call = CallInfo.from_call(collect, "collect") - longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None + call = CallInfo.from_call( + collect, "collect", reraise=(KeyboardInterrupt, SystemExit) + ) + longrepr: None | tuple[str, int, str] | str | TerminalRepr = None if not call.excinfo: outcome: Literal["passed", "skipped", "failed"] = "passed" else: @@ -483,13 +485,13 @@ class SetupState: def __init__(self) -> None: # The stack is in the dict insertion order. - self.stack: Dict[ + self.stack: dict[ Node, - Tuple[ + tuple[ # Node's finalizers. - List[Callable[[], object]], - # Node's exception, if its setup raised. - Optional[Union[OutcomeException, Exception]], + list[Callable[[], object]], + # Node's exception and original traceback, if its setup raised. + tuple[OutcomeException | Exception, types.TracebackType | None] | None, ], ] = {} @@ -502,7 +504,7 @@ def setup(self, item: Item) -> None: for col, (finalizers, exc) in self.stack.items(): assert col in needed_collectors, "previous item was not torn down properly" if exc: - raise exc + raise exc[0].with_traceback(exc[1]) for col in needed_collectors[len(self.stack) :]: assert col not in self.stack @@ -511,8 +513,8 @@ def setup(self, item: Item) -> None: try: col.setup() except TEST_OUTCOME as exc: - self.stack[col] = (self.stack[col][0], exc) - raise exc + self.stack[col] = (self.stack[col][0], (exc, exc.__traceback__)) + raise def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: """Attach a finalizer to the given node. @@ -524,7 +526,7 @@ def addfinalizer(self, finalizer: Callable[[], object], node: Node) -> None: assert node in self.stack, (node, self.stack) self.stack[node][0].append(finalizer) - def teardown_exact(self, nextitem: Optional[Item]) -> None: + def teardown_exact(self, nextitem: Item | None) -> None: """Teardown the current stack up until reaching nodes that nextitem also descends from. @@ -532,7 +534,7 @@ def teardown_exact(self, nextitem: Optional[Item]) -> None: stack is torn down. """ needed_collectors = nextitem and nextitem.listchain() or [] - exceptions: List[BaseException] = [] + exceptions: list[BaseException] = [] while self.stack: if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: break diff --git a/src/_pytest/scope.py b/src/_pytest/scope.py index 2c6e23208f2..976a3ba242e 100644 --- a/src/_pytest/scope.py +++ b/src/_pytest/scope.py @@ -8,10 +8,11 @@ Also this makes the module light to import, as it should. """ +from __future__ import annotations + from enum import Enum from functools import total_ordering from typing import Literal -from typing import Optional _ScopeName = Literal["session", "package", "module", "class", "function"] @@ -38,29 +39,29 @@ class Scope(Enum): Package: _ScopeName = "package" Session: _ScopeName = "session" - def next_lower(self) -> "Scope": + def next_lower(self) -> Scope: """Return the next lower scope.""" index = _SCOPE_INDICES[self] if index == 0: raise ValueError(f"{self} is the lower-most scope") return _ALL_SCOPES[index - 1] - def next_higher(self) -> "Scope": + def next_higher(self) -> Scope: """Return the next higher scope.""" index = _SCOPE_INDICES[self] if index == len(_SCOPE_INDICES) - 1: raise ValueError(f"{self} is the upper-most scope") return _ALL_SCOPES[index + 1] - def __lt__(self, other: "Scope") -> bool: + def __lt__(self, other: Scope) -> bool: self_index = _SCOPE_INDICES[self] other_index = _SCOPE_INDICES[other] return self_index < other_index @classmethod def from_user( - cls, scope_name: _ScopeName, descr: str, where: Optional[str] = None - ) -> "Scope": + cls, scope_name: _ScopeName, descr: str, where: str | None = None + ) -> Scope: """ Given a scope name from the user, return the equivalent Scope enum. Should be used whenever we want to convert a user provided scope name to its enum object. diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 39ab28b466b..de297f408d3 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -1,6 +1,6 @@ +from __future__ import annotations + from typing import Generator -from typing import Optional -from typing import Union from _pytest._io.saferepr import saferepr from _pytest.config import Config @@ -96,7 +96,7 @@ def _show_fixture_action( @pytest.hookimpl(tryfirst=True) -def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: if config.option.setuponly: config.option.setupshow = True return None diff --git a/src/_pytest/setupplan.py b/src/_pytest/setupplan.py index 13c0df84ea1..4e124cce243 100644 --- a/src/_pytest/setupplan.py +++ b/src/_pytest/setupplan.py @@ -1,5 +1,4 @@ -from typing import Optional -from typing import Union +from __future__ import annotations from _pytest.config import Config from _pytest.config import ExitCode @@ -23,7 +22,7 @@ def pytest_addoption(parser: Parser) -> None: @pytest.hookimpl(tryfirst=True) def pytest_fixture_setup( fixturedef: FixtureDef[object], request: SubRequest -) -> Optional[object]: +) -> object | None: # Will return a dummy fixture if the setuponly option is provided. if request.config.option.setupplan: my_cache_key = fixturedef.cache_key(request) @@ -33,7 +32,7 @@ def pytest_fixture_setup( @pytest.hookimpl(tryfirst=True) -def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]: +def pytest_cmdline_main(config: Config) -> int | ExitCode | None: if config.option.setupplan: config.option.setuponly = True config.option.setupshow = True diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 188dcae3f1c..08fcb283eb2 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Support for skip/xfail functions and markers.""" +from __future__ import annotations + from collections.abc import Mapping import dataclasses import os @@ -9,8 +11,6 @@ import traceback from typing import Generator from typing import Optional -from typing import Tuple -from typing import Type from _pytest.config import Config from _pytest.config import hookimpl @@ -84,7 +84,7 @@ def nop(*args, **kwargs): ) -def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, str]: +def evaluate_condition(item: Item, mark: Mark, condition: object) -> tuple[bool, str]: """Evaluate a single skipif/xfail condition. If an old-style string condition is given, it is eval()'d, otherwise the @@ -117,7 +117,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, result = eval(condition_code, globals_) except SyntaxError as exc: msglines = [ - "Error evaluating %r condition" % mark.name, + f"Error evaluating {mark.name!r} condition", " " + condition, " " + " " * (exc.offset or 0) + "^", "SyntaxError: invalid syntax", @@ -125,7 +125,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, fail("\n".join(msglines), pytrace=False) except Exception as exc: msglines = [ - "Error evaluating %r condition" % mark.name, + f"Error evaluating {mark.name!r} condition", " " + condition, *traceback.format_exception_only(type(exc), exc), ] @@ -137,7 +137,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, result = bool(condition) except Exception as exc: msglines = [ - "Error evaluating %r condition as a boolean" % mark.name, + f"Error evaluating {mark.name!r} condition as a boolean", *traceback.format_exception_only(type(exc), exc), ] fail("\n".join(msglines), pytrace=False) @@ -149,7 +149,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool, else: # XXX better be checked at collection time msg = ( - "Error evaluating %r: " % mark.name + f"Error evaluating {mark.name!r}: " + "you need to specify reason=STRING when using booleans as conditions." ) fail(msg, pytrace=False) @@ -164,7 +164,7 @@ class Skip: reason: str = "unconditional skip" -def evaluate_skip_marks(item: Item) -> Optional[Skip]: +def evaluate_skip_marks(item: Item) -> Skip | None: """Evaluate skip and skipif marks on item, returning Skip if triggered.""" for mark in item.iter_markers(name="skipif"): if "condition" not in mark.kwargs: @@ -201,10 +201,10 @@ class Xfail: reason: str run: bool strict: bool - raises: Optional[Tuple[Type[BaseException], ...]] + raises: tuple[type[BaseException], ...] | None -def evaluate_xfail_marks(item: Item) -> Optional[Xfail]: +def evaluate_xfail_marks(item: Item) -> Xfail | None: """Evaluate xfail marks on item, returning Xfail if triggered.""" for mark in item.iter_markers(name="xfail"): run = mark.kwargs.get("run", True) @@ -292,7 +292,7 @@ def pytest_runtest_makereport( return rep -def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str]]: +def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str] | None: if hasattr(report, "wasxfail"): if report.skipped: return "xfailed", "x", "XFAIL" diff --git a/src/_pytest/stash.py b/src/_pytest/stash.py index a4b829fc6dd..6a9ff884e04 100644 --- a/src/_pytest/stash.py +++ b/src/_pytest/stash.py @@ -1,9 +1,9 @@ +from __future__ import annotations + from typing import Any from typing import cast -from typing import Dict from typing import Generic from typing import TypeVar -from typing import Union __all__ = ["Stash", "StashKey"] @@ -70,7 +70,7 @@ class Stash: __slots__ = ("_storage",) def __init__(self) -> None: - self._storage: Dict[StashKey[Any], object] = {} + self._storage: dict[StashKey[Any], object] = {} def __setitem__(self, key: StashKey[T], value: T) -> None: """Set a value for key.""" @@ -83,7 +83,7 @@ def __getitem__(self, key: StashKey[T]) -> T: """ return cast(T, self._storage[key]) - def get(self, key: StashKey[T], default: D) -> Union[T, D]: + def get(self, key: StashKey[T], default: D) -> T | D: """Get the value for key, or return default if the key wasn't set before.""" try: diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 3ebebc288f8..bd906ce63c1 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,18 +1,13 @@ -from typing import List -from typing import Optional -from typing import TYPE_CHECKING +from __future__ import annotations from _pytest import nodes +from _pytest.cacheprovider import Cache from _pytest.config import Config from _pytest.config.argparsing import Parser from _pytest.main import Session from _pytest.reports import TestReport -import pytest -if TYPE_CHECKING: - from _pytest.cacheprovider import Cache - STEPWISE_CACHE_DIR = "cache/stepwise" @@ -37,10 +32,9 @@ def pytest_addoption(parser: Parser) -> None: ) -@pytest.hookimpl def pytest_configure(config: Config) -> None: if config.option.stepwise_skip: - # allow --stepwise-skip to work on it's own merits. + # allow --stepwise-skip to work on its own merits. config.option.stepwise = True if config.getoption("stepwise"): config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") @@ -60,18 +54,18 @@ def pytest_sessionfinish(session: Session) -> None: class StepwisePlugin: def __init__(self, config: Config) -> None: self.config = config - self.session: Optional[Session] = None + self.session: Session | None = None self.report_status = "" assert config.cache is not None self.cache: Cache = config.cache - self.lastfailed: Optional[str] = self.cache.get(STEPWISE_CACHE_DIR, None) + self.lastfailed: str | None = self.cache.get(STEPWISE_CACHE_DIR, None) self.skip: bool = config.getoption("stepwise_skip") def pytest_sessionstart(self, session: Session) -> None: self.session = session def pytest_collection_modifyitems( - self, config: Config, items: List[nodes.Item] + self, config: Config, items: list[nodes.Item] ) -> None: if not self.lastfailed: self.report_status = "no previously failed tests, not skipping." @@ -118,7 +112,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: if report.nodeid == self.lastfailed: self.lastfailed = None - def pytest_report_collectionfinish(self) -> Optional[str]: + def pytest_report_collectionfinish(self) -> str | None: if self.config.getoption("verbose") >= 0 and self.report_status: return f"stepwise: {self.report_status}" return None diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 724d5c54d2f..8c722124d04 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -4,6 +4,8 @@ This is a good source for looking at the various reporting hooks. """ +from __future__ import annotations + import argparse from collections import Counter import dataclasses @@ -17,20 +19,14 @@ from typing import Any from typing import Callable 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 from typing import Sequence -from typing import Set from typing import TextIO -from typing import Tuple from typing import TYPE_CHECKING -from typing import Union import warnings import pluggy @@ -90,7 +86,7 @@ def __init__( dest: str, default: object = None, required: bool = False, - help: Optional[str] = None, + help: str | None = None, ) -> None: super().__init__( option_strings=option_strings, @@ -105,8 +101,8 @@ def __call__( self, parser: argparse.ArgumentParser, namespace: argparse.Namespace, - values: Union[str, Sequence[object], None], - option_string: Optional[str] = None, + values: str | Sequence[object] | None, + option_string: str | None = None, ) -> None: new_count = getattr(namespace, self.dest, 0) - 1 setattr(namespace, self.dest, new_count) @@ -131,7 +127,7 @@ class TestShortLogReport(NamedTuple): category: str letter: str - word: Union[str, Tuple[str, Mapping[str, bool]]] + word: str | tuple[str, Mapping[str, bool]] def pytest_addoption(parser: Parser) -> None: @@ -158,6 +154,13 @@ def pytest_addoption(parser: Parser) -> None: dest="no_summary", help="Disable summary", ) + group._addoption( + "--no-fold-skipped", + action="store_false", + dest="fold_skipped", + default=True, + help="Do not fold skipped tests in short summary.", + ) group._addoption( "-q", "--quiet", @@ -216,6 +219,13 @@ def pytest_addoption(parser: Parser) -> None: choices=["auto", "long", "short", "no", "line", "native"], help="Traceback print mode (auto/long/short/line/native/no)", ) + group._addoption( + "--xfail-tb", + action="store_true", + dest="xfail_tb", + default=False, + help="Show tracebacks for xfail (as long as --tb != no)", + ) group._addoption( "--show-capture", action="store", @@ -304,7 +314,7 @@ def getreportopt(config: Config) -> str: @hookimpl(trylast=True) # after _pytest.runner -def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]: +def pytest_report_teststatus(report: BaseReport) -> tuple[str, str, str]: letter = "F" if report.passed: letter = "." @@ -332,12 +342,12 @@ class WarningReport: """ message: str - nodeid: Optional[str] = None - fslocation: Optional[Tuple[str, int]] = None + nodeid: str | None = None + fslocation: tuple[str, int] | None = None count_towards_summary: ClassVar = True - def get_location(self, config: Config) -> Optional[str]: + def get_location(self, config: Config) -> str | None: """Return the more user-friendly information about the location of a warning, or None.""" if self.nodeid: return self.nodeid @@ -350,31 +360,32 @@ def get_location(self, config: Config) -> Optional[str]: @final class TerminalReporter: - def __init__(self, config: Config, file: Optional[TextIO] = None) -> None: + def __init__(self, config: Config, file: TextIO | None = None) -> None: import _pytest.config self.config = config self._numcollected = 0 - self._session: Optional[Session] = None - self._showfspath: Optional[bool] = None + self._session: Session | None = None + self._showfspath: bool | None = None - self.stats: Dict[str, List[Any]] = {} - self._main_color: Optional[str] = None - self._known_types: Optional[List[str]] = None + self.stats: dict[str, list[Any]] = {} + self._main_color: str | None = None + self._known_types: list[str] | None = None self.startpath = config.invocation_params.dir if file is None: file = sys.stdout self._tw = _pytest.config.create_terminal_writer(config, file) self._screen_width = self._tw.fullwidth - self.currentfspath: Union[None, Path, str, int] = None + self.currentfspath: None | Path | str | int = None self.reportchars = getreportopt(config) + self.foldskipped = config.option.fold_skipped self.hasmarkup = self._tw.hasmarkup self.isatty = file.isatty() - self._progress_nodeids_reported: Set[str] = set() + self._progress_nodeids_reported: set[str] = set() self._show_progress_info = self._determine_show_progress_info() - self._collect_report_last_write: Optional[float] = None - self._already_displayed_warnings: Optional[int] = None - self._keyboardinterrupt_memo: Optional[ExceptionRepr] = None + self._collect_report_last_write: float | None = None + self._already_displayed_warnings: int | None = None + self._keyboardinterrupt_memo: ExceptionRepr | None = None def _determine_show_progress_info(self) -> Literal["progress", "count", False]: """Return whether we should display progress information based on the current config.""" @@ -421,7 +432,7 @@ def showfspath(self) -> bool: return self._showfspath @showfspath.setter - def showfspath(self, value: Optional[bool]) -> None: + def showfspath(self, value: bool | None) -> None: self._showfspath = value @property @@ -432,7 +443,7 @@ def hasopt(self, char: str) -> bool: char = {"xfailed": "x", "skipped": "s"}.get(char, char) return char in self.reportchars - def write_fspath_result(self, nodeid: str, res, **markup: bool) -> None: + def write_fspath_result(self, nodeid: str, res: str, **markup: bool) -> None: fspath = self.config.rootpath / nodeid.split("::")[0] if self.currentfspath is None or fspath != self.currentfspath: if self.currentfspath is not None and self._show_progress_info: @@ -485,7 +496,7 @@ def write(self, content: str, *, flush: bool = False, **markup: bool) -> None: def flush(self) -> None: self._tw.flush() - def write_line(self, line: Union[str, bytes], **markup: bool) -> None: + def write_line(self, line: str | bytes, **markup: bool) -> None: if not isinstance(line, str): line = str(line, errors="replace") self.ensure_newline() @@ -512,8 +523,8 @@ def rewrite(self, line: str, **markup: bool) -> None: def write_sep( self, sep: str, - title: Optional[str] = None, - fullwidth: Optional[int] = None, + title: str | None = None, + fullwidth: int | None = None, **markup: bool, ) -> None: self.ensure_newline() @@ -563,12 +574,13 @@ def pytest_deselected(self, items: Sequence[Item]) -> None: self._add_stats("deselected", items) def pytest_runtest_logstart( - self, nodeid: str, location: Tuple[str, Optional[int], str] + self, nodeid: str, location: tuple[str, int | None, str] ) -> None: + fspath, lineno, domain = location # Ensure that the path is printed before the # 1st test of a module starts running. if self.showlongtestinfo: - line = self._locationline(nodeid, *location) + line = self._locationline(nodeid, fspath, lineno, domain) self.write_ensure_prefix(line, "") self.flush() elif self.showfspath: @@ -591,7 +603,6 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: if not letter and not word: # Probably passed setup/teardown. return - running_xdist = hasattr(rep, "node") if markup is None: was_xfail = hasattr(report, "wasxfail") if rep.passed and not was_xfail: @@ -604,11 +615,20 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: markup = {"yellow": True} else: markup = {} + self._progress_nodeids_reported.add(rep.nodeid) if self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0: self._tw.write(letter, **markup) + # When running in xdist, the logreport and logfinish of multiple + # items are interspersed, e.g. `logreport`, `logreport`, + # `logfinish`, `logfinish`. To avoid the "past edge" calculation + # from getting confused and overflowing (#7166), do the past edge + # printing here and not in logfinish, except for the 100% which + # should only be printed after all teardowns are finished. + if self._show_progress_info and not self._is_last_item: + self._write_progress_information_if_past_edge() else: - self._progress_nodeids_reported.add(rep.nodeid) line = self._locationline(rep.nodeid, *rep.location) + running_xdist = hasattr(rep, "node") if not running_xdist: self.write_ensure_prefix(line, word, **markup) if rep.skipped or hasattr(report, "wasxfail"): @@ -631,7 +651,7 @@ def pytest_runtest_logreport(self, report: TestReport) -> None: self._write_progress_information_filling_space() else: self.ensure_newline() - self._tw.write("[%s]" % rep.node.gateway.id) + self._tw.write(f"[{rep.node.gateway.id}]") if self._show_progress_info: self._tw.write( self._get_progress_information_message() + " ", cyan=True @@ -648,39 +668,29 @@ def _is_last_item(self) -> bool: assert self._session is not None return len(self._progress_nodeids_reported) == self._session.testscollected - def pytest_runtest_logfinish(self, nodeid: str) -> None: - assert self._session + @hookimpl(wrapper=True) + def pytest_runtestloop(self) -> Generator[None, object, object]: + result = yield + + # Write the final/100% progress -- deferred until the loop is complete. if ( self.config.get_verbosity(Config.VERBOSITY_TEST_CASES) <= 0 and self._show_progress_info + and self._progress_nodeids_reported ): - if self._show_progress_info == "count": - num_tests = self._session.testscollected - progress_length = len(f" [{num_tests}/{num_tests}]") - else: - progress_length = len(" [100%]") + self._write_progress_information_filling_space() - self._progress_nodeids_reported.add(nodeid) - - if self._is_last_item: - self._write_progress_information_filling_space() - else: - main_color, _ = self._get_main_color() - w = self._width_of_current_line - past_edge = w + progress_length + 1 >= self._screen_width - if past_edge: - msg = self._get_progress_information_message() - self._tw.write(msg + "\n", **{main_color: True}) + return result def _get_progress_information_message(self) -> str: assert self._session collected = self._session.testscollected if self._show_progress_info == "count": if collected: - progress = self._progress_nodeids_reported + progress = len(self._progress_nodeids_reported) counter_format = f"{{:{len(str(collected))}d}}" format_string = f" [{counter_format}/{{}}]" - return format_string.format(len(progress), collected) + return format_string.format(progress, collected) return f" [ {collected} / {collected} ]" else: if collected: @@ -689,6 +699,20 @@ def _get_progress_information_message(self) -> str: ) return " [100%]" + def _write_progress_information_if_past_edge(self) -> None: + w = self._width_of_current_line + if self._show_progress_info == "count": + assert self._session + num_tests = self._session.testscollected + progress_length = len(f" [{num_tests}/{num_tests}]") + else: + progress_length = len(" [100%]") + past_edge = w + progress_length + 1 >= self._screen_width + if past_edge: + main_color, _ = self._get_main_color() + msg = self._get_progress_information_message() + self._tw.write(msg + "\n", **{main_color: True}) + def _write_progress_information_filling_space(self) -> None: color, _ = self._get_main_color() msg = self._get_progress_information_message() @@ -757,7 +781,7 @@ def report_collect(self, final: bool = False) -> None: self.write_line(line) @hookimpl(trylast=True) - def pytest_sessionstart(self, session: "Session") -> None: + def pytest_sessionstart(self, session: Session) -> None: self._session = session self._sessionstarttime = timing.time() if not self.showheader: @@ -784,7 +808,7 @@ def pytest_sessionstart(self, session: "Session") -> None: self._write_report_lines_from_hooks(lines) def _write_report_lines_from_hooks( - self, lines: Sequence[Union[str, Sequence[str]]] + self, lines: Sequence[str | Sequence[str]] ) -> None: for line_or_lines in reversed(lines): if isinstance(line_or_lines, str): @@ -793,22 +817,24 @@ def _write_report_lines_from_hooks( for line in line_or_lines: self.write_line(line) - def pytest_report_header(self, config: Config) -> List[str]: + def pytest_report_header(self, config: Config) -> list[str]: result = [f"rootdir: {config.rootpath}"] if config.inipath: result.append("configfile: " + bestrelpath(config.rootpath, config.inipath)) if config.args_source == Config.ArgsSource.TESTPATHS: - testpaths: List[str] = config.getini("testpaths") + testpaths: list[str] = config.getini("testpaths") result.append("testpaths: {}".format(", ".join(testpaths))) plugininfo = config.pluginmanager.list_plugin_distinfo() if plugininfo: - result.append("plugins: %s" % ", ".join(_plugin_nameversions(plugininfo))) + result.append( + "plugins: {}".format(", ".join(_plugin_nameversions(plugininfo))) + ) return result - def pytest_collection_finish(self, session: "Session") -> None: + def pytest_collection_finish(self, session: Session) -> None: self.report_collect(True) lines = self.config.hook.pytest_report_collectionfinish( @@ -841,7 +867,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: for item in items: self._tw.line(item.nodeid) return - stack: List[Node] = [] + stack: list[Node] = [] indent = "" for item in items: needed_collectors = item.listchain()[1:] # strip root node @@ -862,7 +888,7 @@ def _printcollecteditems(self, items: Sequence[Item]) -> None: @hookimpl(wrapper=True) def pytest_sessionfinish( - self, session: "Session", exitstatus: Union[int, ExitCode] + self, session: Session, exitstatus: int | ExitCode ) -> Generator[None, None, None]: result = yield self._tw.line("") @@ -926,7 +952,7 @@ def _report_keyboardinterrupt(self) -> None: ) def _locationline( - self, nodeid: str, fspath: str, lineno: Optional[int], domain: str + self, nodeid: str, fspath: str, lineno: int | None, domain: str ) -> str: def mkrel(nodeid: str) -> str: line = self.config.cwd_relative_nodeid(nodeid) @@ -937,7 +963,7 @@ def mkrel(nodeid: str) -> str: line += "[".join(values) return line - # collect_fspath comes from testid which has a "/"-normalized path. + # fspath comes from testid which has a "/"-normalized path. if fspath: res = mkrel(nodeid) if self.verbosity >= 2 and nodeid.split("::")[0] != fspath.replace( @@ -971,7 +997,7 @@ def getreports(self, name: str): def summary_warnings(self) -> None: if self.hasopt("w"): - all_warnings: Optional[List[WarningReport]] = self.stats.get("warnings") + all_warnings: list[WarningReport] | None = self.stats.get("warnings") if not all_warnings: return @@ -984,11 +1010,11 @@ def summary_warnings(self) -> None: if not warning_reports: return - reports_grouped_by_message: Dict[str, List[WarningReport]] = {} + reports_grouped_by_message: dict[str, list[WarningReport]] = {} for wr in warning_reports: reports_grouped_by_message.setdefault(wr.message, []).append(wr) - def collapsed_location_report(reports: List[WarningReport]) -> str: + def collapsed_location_report(reports: list[WarningReport]) -> str: locations = [] for w in reports: location = w.get_location(self.config) @@ -1034,7 +1060,7 @@ def summary_passes_combined( ) -> None: if self.config.option.tbstyle != "no": if self.hasopt(needed_opt): - reports: List[TestReport] = self.getreports(which_reports) + reports: list[TestReport] = self.getreports(which_reports) if not reports: return self.write_sep("=", sep_title) @@ -1045,7 +1071,7 @@ def summary_passes_combined( self._outrep_summary(rep) self._handle_teardown_sections(rep.nodeid) - def _get_teardown_reports(self, nodeid: str) -> List[TestReport]: + def _get_teardown_reports(self, nodeid: str) -> list[TestReport]: reports = self.getreports("") return [ report @@ -1071,21 +1097,29 @@ def print_teardown_sections(self, rep: TestReport) -> None: self._tw.line(content) def summary_failures(self) -> None: - self.summary_failures_combined("failed", "FAILURES") + style = self.config.option.tbstyle + self.summary_failures_combined("failed", "FAILURES", style=style) def summary_xfailures(self) -> None: - self.summary_failures_combined("xfailed", "XFAILURES", "x") + show_tb = self.config.option.xfail_tb + style = self.config.option.tbstyle if show_tb else "no" + self.summary_failures_combined("xfailed", "XFAILURES", style=style) def summary_failures_combined( - self, which_reports: str, sep_title: str, needed_opt: Optional[str] = None + self, + which_reports: str, + sep_title: str, + *, + style: str, + needed_opt: str | None = None, ) -> None: - if self.config.option.tbstyle != "no": + if style != "no": if not needed_opt or self.hasopt(needed_opt): - reports: List[BaseReport] = self.getreports(which_reports) + reports: list[BaseReport] = self.getreports(which_reports) if not reports: return self.write_sep("=", sep_title) - if self.config.option.tbstyle == "line": + if style == "line": for rep in reports: line = self._getcrashline(rep) self.write_line(line) @@ -1098,7 +1132,7 @@ def summary_failures_combined( def summary_errors(self) -> None: if self.config.option.tbstyle != "no": - reports: List[BaseReport] = self.getreports("error") + reports: list[BaseReport] = self.getreports("error") if not reports: return self.write_sep("=", "ERRORS") @@ -1165,7 +1199,7 @@ def short_test_summary(self) -> None: if not self.reportchars: return - def show_simple(lines: List[str], *, stat: str) -> None: + def show_simple(lines: list[str], *, stat: str) -> None: failed = self.stats.get(stat, []) if not failed: return @@ -1177,13 +1211,13 @@ def show_simple(lines: List[str], *, stat: str) -> None: ) lines.append(line) - def show_xfailed(lines: List[str]) -> None: + def show_xfailed(lines: list[str]) -> None: xfailed = self.stats.get("xfailed", []) for rep in xfailed: - verbose_word = rep._get_verbose_word(self.config) - markup_word = self._tw.markup( - verbose_word, **{_color_for_type["warnings"]: True} + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) nodeid = _get_node_id_with_markup(self._tw, self.config, rep) line = f"{markup_word} {nodeid}" reason = rep.wasxfail @@ -1192,13 +1226,13 @@ def show_xfailed(lines: List[str]) -> None: lines.append(line) - def show_xpassed(lines: List[str]) -> None: + def show_xpassed(lines: list[str]) -> None: xpassed = self.stats.get("xpassed", []) for rep in xpassed: - verbose_word = rep._get_verbose_word(self.config) - markup_word = self._tw.markup( - verbose_word, **{_color_for_type["warnings"]: True} + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) nodeid = _get_node_id_with_markup(self._tw, self.config, rep) line = f"{markup_word} {nodeid}" reason = rep.wasxfail @@ -1206,15 +1240,15 @@ def show_xpassed(lines: List[str]) -> None: line += " - " + str(reason) lines.append(line) - def show_skipped(lines: List[str]) -> None: - skipped: List[CollectReport] = self.stats.get("skipped", []) + def show_skipped_folded(lines: list[str]) -> None: + skipped: list[CollectReport] = self.stats.get("skipped", []) fskips = _folded_skips(self.startpath, skipped) if skipped else [] if not fskips: return - verbose_word = skipped[0]._get_verbose_word(self.config) - markup_word = self._tw.markup( - verbose_word, **{_color_for_type["warnings"]: True} + verbose_word, verbose_markup = skipped[0]._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) prefix = "Skipped: " for num, fspath, lineno, reason in fskips: if reason.startswith(prefix): @@ -1226,7 +1260,32 @@ def show_skipped(lines: List[str]) -> None: else: lines.append("%s [%d] %s: %s" % (markup_word, num, fspath, reason)) - REPORTCHAR_ACTIONS: Mapping[str, Callable[[List[str]], None]] = { + def show_skipped_unfolded(lines: list[str]) -> None: + skipped: list[CollectReport] = self.stats.get("skipped", []) + + for rep in skipped: + assert rep.longrepr is not None + assert isinstance(rep.longrepr, tuple), (rep, rep.longrepr) + assert len(rep.longrepr) == 3, (rep, rep.longrepr) + + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + self.config, {_color_for_type["warnings"]: True} + ) + markup_word = self._tw.markup(verbose_word, **verbose_markup) + nodeid = _get_node_id_with_markup(self._tw, self.config, rep) + line = f"{markup_word} {nodeid}" + reason = rep.longrepr[2] + if reason: + line += " - " + str(reason) + lines.append(line) + + def show_skipped(lines: list[str]) -> None: + if self.foldskipped: + show_skipped_folded(lines) + else: + show_skipped_unfolded(lines) + + REPORTCHAR_ACTIONS: Mapping[str, Callable[[list[str]], None]] = { "x": show_xfailed, "X": show_xpassed, "f": partial(show_simple, stat="failed"), @@ -1235,7 +1294,7 @@ def show_skipped(lines: List[str]) -> None: "E": partial(show_simple, stat="error"), } - lines: List[str] = [] + lines: list[str] = [] for char in self.reportchars: action = REPORTCHAR_ACTIONS.get(char) if action: # skipping e.g. "P" (passed with output) here. @@ -1246,7 +1305,7 @@ def show_skipped(lines: List[str]) -> None: for line in lines: self.write_line(line) - def _get_main_color(self) -> Tuple[str, List[str]]: + def _get_main_color(self) -> tuple[str, list[str]]: if self._main_color is None or self._known_types is None or self._is_last_item: self._set_main_color() assert self._main_color @@ -1266,7 +1325,7 @@ def _determine_main_color(self, unknown_type_seen: bool) -> str: return main_color def _set_main_color(self) -> None: - unknown_types: List[str] = [] + unknown_types: list[str] = [] for found_type in self.stats: if found_type: # setup/teardown reports have an empty key, ignore them if found_type not in KNOWN_TYPES and found_type not in unknown_types: @@ -1274,7 +1333,7 @@ def _set_main_color(self) -> None: self._known_types = list(KNOWN_TYPES) + unknown_types self._main_color = self._determine_main_color(bool(unknown_types)) - def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]: + def build_summary_stats_line(self) -> tuple[list[tuple[str, dict[str, bool]]], str]: """ Build the parts used in the last summary stats line. @@ -1299,14 +1358,14 @@ def build_summary_stats_line(self) -> Tuple[List[Tuple[str, Dict[str, bool]]], s else: return self._build_normal_summary_stats_line() - def _get_reports_to_display(self, key: str) -> List[Any]: + def _get_reports_to_display(self, key: str) -> list[Any]: """Get test/collection reports for the given status key, such as `passed` or `error`.""" reports = self.stats.get(key, []) return [x for x in reports if getattr(x, "count_towards_summary", True)] def _build_normal_summary_stats_line( self, - ) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]: + ) -> tuple[list[tuple[str, dict[str, bool]]], str]: main_color, known_types = self._get_main_color() parts = [] @@ -1325,7 +1384,7 @@ def _build_normal_summary_stats_line( def _build_collect_only_summary_stats_line( self, - ) -> Tuple[List[Tuple[str, Dict[str, bool]]], str]: + ) -> tuple[list[tuple[str, dict[str, bool]]], str]: deselected = len(self._get_reports_to_display("deselected")) errors = len(self._get_reports_to_display("error")) @@ -1366,7 +1425,7 @@ def _get_node_id_with_markup(tw: TerminalWriter, config: Config, rep: BaseReport return path -def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str]: +def _format_trimmed(format: str, msg: str, available_width: int) -> str | None: """Format msg into format, ellipsizing it if doesn't fit in available_width. Returns None if even the ellipsis can't fit. @@ -1392,11 +1451,13 @@ def _format_trimmed(format: str, msg: str, available_width: int) -> Optional[str def _get_line_with_reprcrash_message( - config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: Dict[str, bool] + config: Config, rep: BaseReport, tw: TerminalWriter, word_markup: dict[str, bool] ) -> str: """Get summary line for a report, trying to add reprcrash message.""" - verbose_word = rep._get_verbose_word(config) - word = tw.markup(verbose_word, **word_markup) + verbose_word, verbose_markup = rep._get_verbose_word_with_markup( + config, word_markup + ) + word = tw.markup(verbose_word, **verbose_markup) node = _get_node_id_with_markup(tw, config, rep) line = f"{word} {node}" @@ -1422,8 +1483,8 @@ def _get_line_with_reprcrash_message( def _folded_skips( startpath: Path, skipped: Sequence[CollectReport], -) -> List[Tuple[int, str, Optional[int], str]]: - d: Dict[Tuple[str, Optional[int], str], List[CollectReport]] = {} +) -> list[tuple[int, str, int | None, str]]: + d: dict[tuple[str, int | None, str], list[CollectReport]] = {} for event in skipped: assert event.longrepr is not None assert isinstance(event.longrepr, tuple), (event, event.longrepr) @@ -1440,11 +1501,11 @@ def _folded_skips( and "skip" in keywords and "pytestmark" not in keywords ): - key: Tuple[str, Optional[int], str] = (fspath, None, reason) + key: tuple[str, int | None, str] = (fspath, None, reason) else: key = (fspath, lineno, reason) d.setdefault(key, []).append(event) - values: List[Tuple[int, str, Optional[int], str]] = [] + values: list[tuple[int, str, int | None, str]] = [] for key, events in d.items(): values.append((len(events), *key)) return values @@ -1459,7 +1520,7 @@ def _folded_skips( _color_for_type_default = "yellow" -def pluralize(count: int, noun: str) -> Tuple[int, str]: +def pluralize(count: int, noun: str) -> tuple[int, str]: # No need to pluralize words such as `failed` or `passed`. if noun not in ["error", "warnings", "test"]: return count, noun @@ -1472,8 +1533,8 @@ def pluralize(count: int, noun: str) -> Tuple[int, str]: return count, noun + "s" if count != 1 else noun -def _plugin_nameversions(plugininfo) -> List[str]: - values: List[str] = [] +def _plugin_nameversions(plugininfo) -> list[str]: + values: list[str] = [] for plugin, dist in plugininfo: # Gets us name and version! name = f"{dist.project_name}-{dist.version}" diff --git a/src/_pytest/threadexception.py b/src/_pytest/threadexception.py index 09faf661b91..d78c32c852f 100644 --- a/src/_pytest/threadexception.py +++ b/src/_pytest/threadexception.py @@ -1,16 +1,21 @@ +from __future__ import annotations + import threading import traceback from types import TracebackType from typing import Any from typing import Callable from typing import Generator -from typing import Optional -from typing import Type +from typing import TYPE_CHECKING import warnings import pytest +if TYPE_CHECKING: + from typing_extensions import Self + + # Copied from cpython/Lib/test/support/threading_helper.py, with modifications. class catch_threading_exception: """Context manager catching threading.Thread exception using @@ -34,22 +39,22 @@ class catch_threading_exception: """ def __init__(self) -> None: - self.args: Optional["threading.ExceptHookArgs"] = None - self._old_hook: Optional[Callable[["threading.ExceptHookArgs"], Any]] = None + self.args: threading.ExceptHookArgs | None = None + self._old_hook: Callable[[threading.ExceptHookArgs], Any] | None = None - def _hook(self, args: "threading.ExceptHookArgs") -> None: + def _hook(self, args: threading.ExceptHookArgs) -> None: self.args = args - def __enter__(self) -> "catch_threading_exception": + def __enter__(self) -> Self: self._old_hook = threading.excepthook threading.excepthook = self._hook return self def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> None: assert self._old_hook is not None threading.excepthook = self._old_hook diff --git a/src/_pytest/timing.py b/src/_pytest/timing.py index 0541dc8e0a1..b23c7f69e2d 100644 --- a/src/_pytest/timing.py +++ b/src/_pytest/timing.py @@ -6,6 +6,8 @@ Fixture "mock_timing" also interacts with this module for pytest's own tests. """ +from __future__ import annotations + from time import perf_counter from time import sleep from time import time diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 72efed3e87a..91109ea69ef 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Support for providing temporary directories to test functions.""" +from __future__ import annotations + import dataclasses import os from pathlib import Path @@ -12,8 +14,6 @@ from typing import final from typing import Generator from typing import Literal -from typing import Optional -from typing import Union from .pathlib import cleanup_dead_symlinks from .pathlib import LOCK_TIMEOUT @@ -46,20 +46,20 @@ class TempPathFactory: The base directory can be configured using the ``--basetemp`` option. """ - _given_basetemp: Optional[Path] + _given_basetemp: Path | None # pluggy TagTracerSub, not currently exposed, so Any. _trace: Any - _basetemp: Optional[Path] + _basetemp: Path | None _retention_count: int _retention_policy: RetentionType def __init__( self, - given_basetemp: Optional[Path], + given_basetemp: Path | None, retention_count: int, retention_policy: RetentionType, trace, - basetemp: Optional[Path] = None, + basetemp: Path | None = None, *, _ispytest: bool = False, ) -> None: @@ -82,7 +82,7 @@ def from_config( config: Config, *, _ispytest: bool = False, - ) -> "TempPathFactory": + ) -> TempPathFactory: """Create a factory according to pytest configuration. :meta private: @@ -198,7 +198,7 @@ def getbasetemp(self) -> Path: return basetemp -def get_user() -> Optional[str]: +def get_user() -> str | None: """Return the current user name, or None if getuser() does not work in the current environment (see #1010).""" try: @@ -286,7 +286,7 @@ def tmp_path( del request.node.stash[tmppath_result_key] -def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]): +def pytest_sessionfinish(session, exitstatus: int | ExitCode): """After each session, remove base directory if all the tests passed, the policy is "failed", and the basetemp is not specified by a user. """ @@ -317,6 +317,6 @@ def pytest_runtest_makereport( ) -> Generator[None, TestReport, TestReport]: rep = yield assert rep.when is not None - empty: Dict[str, bool] = {} + empty: dict[str, bool] = {} 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 8f1791bf744..aefea1333d9 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -1,6 +1,9 @@ # mypy: allow-untyped-defs """Discover and run std-library "unittest" style tests.""" +from __future__ import annotations + +import inspect import sys import traceback import types @@ -8,8 +11,6 @@ from typing import Callable from typing import Generator from typing import Iterable -from typing import List -from typing import Optional from typing import Tuple from typing import Type from typing import TYPE_CHECKING @@ -40,23 +41,29 @@ import twisted.trial.unittest - _SysExcInfoType = Union[ - Tuple[Type[BaseException], BaseException, types.TracebackType], - Tuple[None, None, None], - ] + +_SysExcInfoType = Union[ + Tuple[Type[BaseException], BaseException, types.TracebackType], + Tuple[None, None, None], +] def pytest_pycollect_makeitem( - collector: Union[Module, Class], name: str, obj: object -) -> Optional["UnitTestCase"]: - # Has unittest been imported and is obj a subclass of its TestCase? + collector: Module | Class, name: str, obj: object +) -> UnitTestCase | None: try: + # Has unittest been imported? ut = sys.modules["unittest"] + # Is obj a subclass of unittest.TestCase? # Type ignored because `ut` is an opaque module. if not issubclass(obj, ut.TestCase): # type: ignore return None except Exception: return None + # Is obj a concrete class? + # Abstract classes can't be instantiated so no point collecting them. + if inspect.isabstract(obj): + return None # Yes, so let's collect it. return UnitTestCase.from_parent(collector, name=name, obj=obj) @@ -74,7 +81,7 @@ def newinstance(self): # it. return self.obj("runTest") - def collect(self) -> Iterable[Union[Item, Collector]]: + def collect(self) -> Iterable[Item | Collector]: from unittest import TestLoader cls = self.obj @@ -194,7 +201,7 @@ def unittest_setup_method_fixture( class TestCaseFunction(Function): nofuncargs = True - _excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None + _excinfo: list[_pytest._code.ExceptionInfo[BaseException]] | None = None def _getinstance(self): assert isinstance(self.parent, UnitTestCase) @@ -208,20 +215,21 @@ def _testcase(self): def setup(self) -> None: # A bound method to be called during teardown() if set (see 'runtest()'). - self._explicit_tearDown: Optional[Callable[[], None]] = None + self._explicit_tearDown: Callable[[], None] | None = None super().setup() def teardown(self) -> None: - super().teardown() if self._explicit_tearDown is not None: self._explicit_tearDown() self._explicit_tearDown = None self._obj = None + del self._instance + super().teardown() - def startTest(self, testcase: "unittest.TestCase") -> None: + def startTest(self, testcase: unittest.TestCase) -> None: pass - def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None: + def _addexcinfo(self, rawexcinfo: _SysExcInfoType) -> None: # Unwrap potential exception info (see twisted trial support below). rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo) try: @@ -257,7 +265,7 @@ def _addexcinfo(self, rawexcinfo: "_SysExcInfoType") -> None: self.__dict__.setdefault("_excinfo", []).append(excinfo) def addError( - self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType" + self, testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType ) -> None: try: if isinstance(rawexcinfo[1], exit.Exception): @@ -267,11 +275,11 @@ def addError( self._addexcinfo(rawexcinfo) def addFailure( - self, testcase: "unittest.TestCase", rawexcinfo: "_SysExcInfoType" + self, testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType ) -> None: self._addexcinfo(rawexcinfo) - def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None: + def addSkip(self, testcase: unittest.TestCase, reason: str) -> None: try: raise pytest.skip.Exception(reason, _use_item_location=True) except skip.Exception: @@ -279,8 +287,8 @@ def addSkip(self, testcase: "unittest.TestCase", reason: str) -> None: def addExpectedFailure( self, - testcase: "unittest.TestCase", - rawexcinfo: "_SysExcInfoType", + testcase: unittest.TestCase, + rawexcinfo: _SysExcInfoType, reason: str = "", ) -> None: try: @@ -290,8 +298,8 @@ def addExpectedFailure( def addUnexpectedSuccess( self, - testcase: "unittest.TestCase", - reason: Optional["twisted.trial.unittest.Todo"] = None, + testcase: unittest.TestCase, + reason: twisted.trial.unittest.Todo | None = None, ) -> None: msg = "Unexpected success" if reason: @@ -302,13 +310,13 @@ def addUnexpectedSuccess( except fail.Exception: self._addexcinfo(sys.exc_info()) - def addSuccess(self, testcase: "unittest.TestCase") -> None: + def addSuccess(self, testcase: unittest.TestCase) -> None: pass - def stopTest(self, testcase: "unittest.TestCase") -> None: + def stopTest(self, testcase: unittest.TestCase) -> None: pass - def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None: + def addDuration(self, testcase: unittest.TestCase, elapsed: float) -> None: pass def runtest(self) -> None: diff --git a/src/_pytest/unraisableexception.py b/src/_pytest/unraisableexception.py index f649267abf1..c191703a3de 100644 --- a/src/_pytest/unraisableexception.py +++ b/src/_pytest/unraisableexception.py @@ -1,16 +1,21 @@ +from __future__ import annotations + import sys import traceback from types import TracebackType from typing import Any from typing import Callable from typing import Generator -from typing import Optional -from typing import Type +from typing import TYPE_CHECKING import warnings import pytest +if TYPE_CHECKING: + from typing_extensions import Self + + # Copied from cpython/Lib/test/support/__init__.py, with modifications. class catch_unraisable_exception: """Context manager catching unraisable exception using sys.unraisablehook. @@ -34,24 +39,24 @@ class catch_unraisable_exception: """ def __init__(self) -> None: - self.unraisable: Optional["sys.UnraisableHookArgs"] = None - self._old_hook: Optional[Callable[["sys.UnraisableHookArgs"], Any]] = None + self.unraisable: sys.UnraisableHookArgs | None = None + self._old_hook: Callable[[sys.UnraisableHookArgs], Any] | None = None - def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None: + def _hook(self, unraisable: sys.UnraisableHookArgs) -> None: # Storing unraisable.object can resurrect an object which is being # finalized. Storing unraisable.exc_value creates a reference cycle. self.unraisable = unraisable - def __enter__(self) -> "catch_unraisable_exception": + def __enter__(self) -> Self: self._old_hook = sys.unraisablehook sys.unraisablehook = self._hook return self def __exit__( self, - exc_type: Optional[Type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, ) -> None: assert self._old_hook is not None sys.unraisablehook = self._old_hook diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index a5884f29582..4ab14e48c92 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -1,10 +1,11 @@ +from __future__ import annotations + import dataclasses import inspect from types import FunctionType from typing import Any from typing import final from typing import Generic -from typing import Type from typing import TypeVar import warnings @@ -72,7 +73,7 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning): __module__ = "pytest" @classmethod - def simple(cls, apiname: str) -> "PytestExperimentalApiWarning": + def simple(cls, apiname: str) -> PytestExperimentalApiWarning: return cls(f"{apiname} is an experimental api that may change over time") @@ -132,7 +133,7 @@ class UnformattedWarning(Generic[_W]): as opposed to a direct message. """ - category: Type["_W"] + category: type[_W] template: str def format(self, **kwargs: Any) -> _W: diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 22590892f8d..5c59e55c5db 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -1,9 +1,10 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from contextlib import contextmanager import sys from typing import Generator from typing import Literal -from typing import Optional import warnings from _pytest.config import apply_warning_filters @@ -28,7 +29,7 @@ def catch_warnings_for_item( config: Config, ihook, when: Literal["config", "collect", "runtest"], - item: Optional[Item], + item: Item | None, ) -> Generator[None, None, None]: """Context manager that catches warnings generated in the contained execution block. @@ -142,7 +143,7 @@ def pytest_sessionfinish(session: Session) -> Generator[None, None, None]: @pytest.hookimpl(wrapper=True) def pytest_load_initial_conftests( - early_config: "Config", + early_config: Config, ) -> Generator[None, None, None]: with catch_warnings_for_item( config=early_config, ihook=early_config.hook, when="config", item=None diff --git a/src/py.py b/src/py.py index d1c39d203a8..5c661e66c1f 100644 --- a/src/py.py +++ b/src/py.py @@ -1,6 +1,8 @@ # shim for pylib going away # if pylib is installed this file will get skipped # (`py/__init__.py` has higher precedence) +from __future__ import annotations + import sys import _pytest._py.error as error diff --git a/src/pytest/__init__.py b/src/pytest/__init__.py index c6b6de827e9..90abcdab036 100644 --- a/src/pytest/__init__.py +++ b/src/pytest/__init__.py @@ -1,6 +1,8 @@ # PYTHON_ARGCOMPLETE_OK """pytest: unit and functional testing with Python.""" +from __future__ import annotations + from _pytest import __version__ from _pytest import version_tuple from _pytest._code import ExceptionInfo diff --git a/src/pytest/__main__.py b/src/pytest/__main__.py index e4cb67d5dd5..cccab5d57b8 100644 --- a/src/pytest/__main__.py +++ b/src/pytest/__main__.py @@ -1,5 +1,7 @@ """The pytest entry point.""" +from __future__ import annotations + import pytest diff --git a/testing/_py/test_local.py b/testing/_py/test_local.py index ad2526571e6..4a95e2d0cd9 100644 --- a/testing/_py/test_local.py +++ b/testing/_py/test_local.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import contextlib import multiprocessing import os @@ -207,7 +209,7 @@ def test_visit_norecurse(self, path1): @pytest.mark.parametrize( "fil", - ["*dir", "*dir", pytest.mark.skip("sys.version_info <" " (3,6)")(b"*dir")], + ["*dir", "*dir", pytest.mark.skip("sys.version_info < (3,6)")(b"*dir")], ) def test_visit_filterfunc_is_string(self, path1, fil): lst = [] @@ -551,7 +553,7 @@ 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_text("%s" % i, encoding="utf-8") + file_.write_text(f"{i}", encoding="utf-8") actual = int(file_.read_text(encoding="utf-8")) assert ( actual == i @@ -563,9 +565,9 @@ def batch_make_numbered_dirs(rootdir, repeats): class TestLocalPath(CommonFSTests): def test_join_normpath(self, tmpdir): assert tmpdir.join(".") == tmpdir - p = tmpdir.join("../%s" % tmpdir.basename) + p = tmpdir.join(f"../{tmpdir.basename}") assert p == tmpdir - p = tmpdir.join("..//%s/" % tmpdir.basename) + p = tmpdir.join(f"..//{tmpdir.basename}/") assert p == tmpdir @skiponwin32 @@ -667,7 +669,7 @@ def test_tilde_expansion(self, monkeypatch, tmpdir): assert p == os.path.expanduser("~") @pytest.mark.skipif( - not sys.platform.startswith("win32"), reason="case insensitive only on windows" + not sys.platform.startswith("win32"), reason="case-insensitive only on windows" ) def test_eq_hash_are_case_insensitive_on_windows(self): a = local("/some/path") @@ -722,7 +724,7 @@ def test_write_and_ensure(self, path1): @pytest.mark.parametrize("bin", (False, True)) def test_dump(self, tmpdir, bin): - path = tmpdir.join("dumpfile%s" % int(bin)) + path = tmpdir.join(f"dumpfile{int(bin)}") try: d = {"answer": 42} path.dump(d, bin=bin) @@ -898,7 +900,7 @@ def test_sysfind_bat_exe_before(self, tmpdir, monkeypatch): class TestExecution: pytestmark = skiponwin32 - def test_sysfind_no_permisson_ignored(self, monkeypatch, tmpdir): + def test_sysfind_no_permission_ignored(self, monkeypatch, tmpdir): noperm = tmpdir.ensure("noperm", dir=True) monkeypatch.setenv("PATH", str(noperm), prepend=":") noperm.chmod(0) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 8f001bc2401..01d911e8ca4 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import dataclasses import importlib.metadata import os @@ -400,7 +402,7 @@ def test_docstring_on_hookspec(self) -> None: for name, value in vars(hookspec).items(): if name.startswith("pytest_"): - assert value.__doc__, "no docstring for %s" % name + assert value.__doc__, f"no docstring for {name}" def test_initialization_error_issue49(self, pytester: Pytester) -> None: pytester.makeconftest( @@ -973,7 +975,7 @@ def test_calls_showall(self, pytester: Pytester, mock_timing) -> None: for x in tested: for y in ("call",): # 'setup', 'call', 'teardown': for line in result.stdout.lines: - if ("test_%s" % x) in line and y in line: + if (f"test_{x}") in line and y in line: break else: raise AssertionError(f"not found {x} {y}") @@ -986,7 +988,7 @@ def test_calls_showall_verbose(self, pytester: Pytester, mock_timing) -> None: for x in "123": for y in ("call",): # 'setup', 'call', 'teardown': for line in result.stdout.lines: - if ("test_%s" % x) in line and y in line: + if (f"test_{x}") in line and y in line: break else: raise AssertionError(f"not found {x} {y}") @@ -1464,14 +1466,21 @@ def my_fixture(self, request): } ) - subprocess.run([sys.executable, "setup.py", "develop"], check=True) + subprocess.run( + [sys.executable, "-Im", "pip", "install", "-e", "."], + check=True, + ) try: # We are using subprocess.run rather than pytester.run on purpose. # pytester.run is adding the current directory to PYTHONPATH which avoids # the bug. We also use pytest rather than python -m pytest for the same # PYTHONPATH reason. subprocess.run( - ["pytest", "my_package"], capture_output=True, check=True, text=True + ["pytest", "my_package"], + capture_output=True, + check=True, + encoding="utf-8", + text=True, ) except subprocess.CalledProcessError as exc: raise AssertionError( diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 57ab4cdfddb..7ae5ad46100 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import re import sys from types import FrameType diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index dd4bd22c8b8..fc60ae9ac99 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs from __future__ import annotations +import fnmatch import importlib import io import operator @@ -10,6 +11,7 @@ import sys import textwrap from typing import Any +from typing import cast from typing import TYPE_CHECKING import _pytest._code @@ -26,7 +28,7 @@ if TYPE_CHECKING: - from _pytest._code.code import _TracebackStyle + from _pytest._code.code import TracebackStyle if sys.version_info < (3, 11): from exceptiongroup import ExceptionGroup @@ -237,7 +239,7 @@ def f(n): n += 1 f(n) - excinfo = pytest.raises(RuntimeError, f, 8) + excinfo = pytest.raises(RecursionError, f, 8) traceback = excinfo.traceback recindex = traceback.recursionindex() assert recindex == 3 @@ -373,7 +375,10 @@ def test_excinfo_no_sourcecode(): except ValueError: excinfo = _pytest._code.ExceptionInfo.from_current() s = str(excinfo.traceback[-1]) - assert s == " File '':1 in \n ???\n" + # TODO: Since Python 3.13b1 under pytest-xdist, the * is `import + # sys;exec(eval(sys.stdin.readline()))` (execnet bootstrap code) + # instead of `???` like before. Is this OK? + fnmatch.fnmatch(s, " File '':1 in \n *\n") def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None: @@ -708,6 +713,29 @@ def test_repr_local_truncated(self) -> None: assert full_reprlocals.lines assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]" + def test_repr_args_not_truncated(self, importasmod) -> None: + mod = importasmod( + """ + def func1(m): + raise ValueError("hello\\nworld") + """ + ) + excinfo = pytest.raises(ValueError, mod.func1, "m" * 500) + excinfo.traceback = excinfo.traceback.filter(excinfo) + entry = excinfo.traceback[-1] + p = FormattedExcinfo(funcargs=True, truncate_args=True) + reprfuncargs = p.repr_args(entry) + assert reprfuncargs is not None + arg1 = cast(str, reprfuncargs.args[0][1]) + assert len(arg1) < 500 + assert "..." in arg1 + # again without truncate + p = FormattedExcinfo(funcargs=True, truncate_args=False) + reprfuncargs = p.repr_args(entry) + assert reprfuncargs is not None + assert reprfuncargs.args[0] == ("m", repr("m" * 500)) + assert "..." not in cast(str, reprfuncargs.args[0][1]) + def test_repr_tracebackentry_lines(self, importasmod) -> None: mod = importasmod( """ @@ -897,7 +925,7 @@ def entry(): ) 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) @@ -1024,7 +1052,7 @@ def entry(): ) 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) @@ -1406,7 +1434,7 @@ def g(): mod.f() # emulate the issue described in #1984 - attr = "__%s__" % reason + attr = f"__{reason}__" getattr(excinfo.value, attr).__traceback__ = None r = excinfo.getrepr() @@ -1515,7 +1543,7 @@ def test(tmp_path): result.stderr.no_fnmatch_line("*INTERNALERROR*") -def test_regression_nagative_line_index(pytester: Pytester) -> None: +def test_regression_negative_line_index(pytester: Pytester) -> None: """ With Python 3.10 alphas, there was an INTERNALERROR reported in https://github.com/pytest-dev/pytest/pull/8227 diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 12ea27b3517..a00259976c4 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -255,7 +255,7 @@ def g(): assert str(g_source).strip() == "def g():\n pass # pragma: no cover" -def test_getfuncsource_with_multine_string() -> None: +def test_getfuncsource_with_multiline_string() -> None: def f(): c = """while True: pass @@ -370,7 +370,11 @@ class B: pass B.__name__ = B.__qualname__ = "B2" - assert getfslineno(B)[1] == -1 + # Since Python 3.13 this started working. + if sys.version_info >= (3, 13): + assert getfslineno(B)[1] != -1 + else: + assert getfslineno(B)[1] == -1 def test_code_of_object_instance_with_call() -> None: diff --git a/testing/conftest.py b/testing/conftest.py index b7e2d6111af..24e5d183094 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,9 +1,10 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import dataclasses import re import sys from typing import Generator -from typing import List from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester @@ -190,22 +191,22 @@ class ColorMapping: NO_COLORS = {k: "" for k in COLORS.keys()} @classmethod - def format(cls, lines: List[str]) -> List[str]: + def format(cls, lines: list[str]) -> list[str]: """Straightforward replacement of color names to their ASCII codes.""" return [line.format(**cls.COLORS) for line in lines] @classmethod - def format_for_fnmatch(cls, lines: List[str]) -> List[str]: + def format_for_fnmatch(cls, lines: list[str]) -> list[str]: """Replace color names for use with LineMatcher.fnmatch_lines""" return [line.format(**cls.COLORS).replace("[", "[[]") for line in lines] @classmethod - def format_for_rematch(cls, lines: List[str]) -> List[str]: + def format_for_rematch(cls, lines: list[str]) -> list[str]: """Replace color names for use with LineMatcher.re_match_lines""" return [line.format(**cls.RE_COLORS) for line in lines] @classmethod - def strip_colors(cls, lines: List[str]) -> List[str]: + def strip_colors(cls, lines: list[str]) -> list[str]: """Entirely remove every color code""" return [line.format(**cls.NO_COLORS) for line in lines] diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 9e83a49d554..5d0e69c58c1 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from pathlib import Path import re import sys diff --git a/testing/example_scripts/acceptance/fixture_mock_integration.py b/testing/example_scripts/acceptance/fixture_mock_integration.py index d802a7f8728..e612ae01e66 100644 --- a/testing/example_scripts/acceptance/fixture_mock_integration.py +++ b/testing/example_scripts/acceptance/fixture_mock_integration.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Reproduces issue #3774""" +from __future__ import annotations + from unittest import mock import pytest diff --git a/testing/example_scripts/collect/collect_init_tests/tests/__init__.py b/testing/example_scripts/collect/collect_init_tests/tests/__init__.py index 58c41942d1c..5e30bb15883 100644 --- a/testing/example_scripts/collect/collect_init_tests/tests/__init__.py +++ b/testing/example_scripts/collect/collect_init_tests/tests/__init__.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_init(): pass diff --git a/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py b/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py index d88c001c2cc..3cb8f1be095 100644 --- a/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py +++ b/testing/example_scripts/collect/collect_init_tests/tests/test_foo.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_foo(): pass diff --git a/testing/example_scripts/collect/package_infinite_recursion/conftest.py b/testing/example_scripts/collect/package_infinite_recursion/conftest.py index bba5db8b2fd..c2d2b918874 100644 --- a/testing/example_scripts/collect/package_infinite_recursion/conftest.py +++ b/testing/example_scripts/collect/package_infinite_recursion/conftest.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def pytest_ignore_collect(collection_path): return False diff --git a/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py b/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py index 2809d0cc689..38c51e586fc 100644 --- a/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py +++ b/testing/example_scripts/collect/package_infinite_recursion/tests/test_basic.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test(): pass 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 58c41942d1c..5e30bb15883 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 @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + 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 d88c001c2cc..3cb8f1be095 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,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_foo(): pass diff --git a/testing/example_scripts/config/collect_pytest_prefix/conftest.py b/testing/example_scripts/config/collect_pytest_prefix/conftest.py index 2da4ffe2fed..5e0ab54411b 100644 --- a/testing/example_scripts/config/collect_pytest_prefix/conftest.py +++ b/testing/example_scripts/config/collect_pytest_prefix/conftest.py @@ -1,2 +1,5 @@ +from __future__ import annotations + + class pytest_something: pass diff --git a/testing/example_scripts/config/collect_pytest_prefix/test_foo.py b/testing/example_scripts/config/collect_pytest_prefix/test_foo.py index d88c001c2cc..3cb8f1be095 100644 --- a/testing/example_scripts/config/collect_pytest_prefix/test_foo.py +++ b/testing/example_scripts/config/collect_pytest_prefix/test_foo.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_foo(): pass diff --git a/testing/example_scripts/conftest_usageerror/conftest.py b/testing/example_scripts/conftest_usageerror/conftest.py index 64bbeefac1d..a6690bdc303 100644 --- a/testing/example_scripts/conftest_usageerror/conftest.py +++ b/testing/example_scripts/conftest_usageerror/conftest.py @@ -1,4 +1,7 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def pytest_configure(config): import pytest diff --git a/testing/example_scripts/customdirectory/conftest.py b/testing/example_scripts/customdirectory/conftest.py index fe1c743a686..4718d7d5be3 100644 --- a/testing/example_scripts/customdirectory/conftest.py +++ b/testing/example_scripts/customdirectory/conftest.py @@ -1,5 +1,7 @@ # mypy: allow-untyped-defs # content of conftest.py +from __future__ import annotations + import json import pytest diff --git a/testing/example_scripts/customdirectory/tests/test_first.py b/testing/example_scripts/customdirectory/tests/test_first.py index 890ca3dea38..06f40ca4733 100644 --- a/testing/example_scripts/customdirectory/tests/test_first.py +++ b/testing/example_scripts/customdirectory/tests/test_first.py @@ -1,4 +1,7 @@ # mypy: allow-untyped-defs # content of test_first.py +from __future__ import annotations + + def test_1(): pass diff --git a/testing/example_scripts/customdirectory/tests/test_second.py b/testing/example_scripts/customdirectory/tests/test_second.py index 42108d5da84..79bcc099e65 100644 --- a/testing/example_scripts/customdirectory/tests/test_second.py +++ b/testing/example_scripts/customdirectory/tests/test_second.py @@ -1,4 +1,7 @@ # mypy: allow-untyped-defs # content of test_second.py +from __future__ import annotations + + def test_2(): pass diff --git a/testing/example_scripts/customdirectory/tests/test_third.py b/testing/example_scripts/customdirectory/tests/test_third.py index ede0f3e6025..5af476ad44d 100644 --- a/testing/example_scripts/customdirectory/tests/test_third.py +++ b/testing/example_scripts/customdirectory/tests/test_third.py @@ -1,4 +1,7 @@ # mypy: allow-untyped-defs # content of test_third.py +from __future__ import annotations + + def test_3(): pass diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_dataclasses.py index d96c90a91bd..18180b99f2d 100644 --- a/testing/example_scripts/dataclasses/test_compare_dataclasses.py +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass from dataclasses import field diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py index 7479c66c1be..0dcc7ab2802 100644 --- a/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_field_comparison_off.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass from dataclasses import field diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py index 4737ef904e0..4985c69ff30 100644 --- a/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_verbose.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass from dataclasses import field diff --git a/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py b/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py index e026fe3d192..b787cb39ee2 100644 --- a/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py +++ b/testing/example_scripts/dataclasses/test_compare_dataclasses_with_custom_eq.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass from dataclasses import field diff --git a/testing/example_scripts/dataclasses/test_compare_initvar.py b/testing/example_scripts/dataclasses/test_compare_initvar.py index d687fc22530..fc589e1fde4 100644 --- a/testing/example_scripts/dataclasses/test_compare_initvar.py +++ b/testing/example_scripts/dataclasses/test_compare_initvar.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from dataclasses import dataclass from dataclasses import InitVar diff --git a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py index 801aa0a732e..885edd7d9d7 100644 --- a/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py +++ b/testing/example_scripts/dataclasses/test_compare_recursive_dataclasses.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from dataclasses import dataclass diff --git a/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py b/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py index 0a4820c69ba..b45a6772c59 100644 --- a/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py +++ b/testing/example_scripts/dataclasses/test_compare_two_different_dataclasses.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from dataclasses import dataclass from dataclasses import field diff --git a/testing/example_scripts/doctest/main_py/__main__.py b/testing/example_scripts/doctest/main_py/__main__.py index c8a124f5416..3a0f6bed1d6 100644 --- a/testing/example_scripts/doctest/main_py/__main__.py +++ b/testing/example_scripts/doctest/main_py/__main__.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_this_is_ignored(): assert True diff --git a/testing/example_scripts/doctest/main_py/test_normal_module.py b/testing/example_scripts/doctest/main_py/test_normal_module.py index 26a4d90bc89..8c150da5c02 100644 --- a/testing/example_scripts/doctest/main_py/test_normal_module.py +++ b/testing/example_scripts/doctest/main_py/test_normal_module.py @@ -1,4 +1,7 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_doc(): """ >>> 10 > 5 diff --git a/testing/example_scripts/fixtures/custom_item/conftest.py b/testing/example_scripts/fixtures/custom_item/conftest.py index fe1ae620aa6..274ab97d01b 100644 --- a/testing/example_scripts/fixtures/custom_item/conftest.py +++ b/testing/example_scripts/fixtures/custom_item/conftest.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/custom_item/foo/test_foo.py b/testing/example_scripts/fixtures/custom_item/foo/test_foo.py index 2809d0cc689..38c51e586fc 100644 --- a/testing/example_scripts/fixtures/custom_item/foo/test_foo.py +++ b/testing/example_scripts/fixtures/custom_item/foo/test_foo.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test(): pass diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py index 3a5d3ac33fe..94eaa3e0796 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/conftest.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py index d0c4bdbdfd9..cb3f9fbf469 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub1/test_in_sub1.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_1(arg1): pass diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py index a1f3b2d58b9..112d1e05f27 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/conftest.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py index 45e9744786a..3dea97f544c 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_conftest_funcargs_only_available_in_subdir/sub2/test_in_sub2.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_2(arg2): pass diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py b/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py index 84e5256f070..d90961ae3c4 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_detect_recursive_dependency_error.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py index 7f1769beb9b..b4fcc17bfc7 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/conftest.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py index ad26fdd8cdc..b933b70edf3 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/conftest.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py index 9ee74a47186..d31ab971f2b 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/pkg/test_spam.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_spam(spam): assert spam == "spamspam" diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py index 7f1769beb9b..b4fcc17bfc7 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/conftest.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py index fa688f0a844..2d6d7faef61 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/test_extend_fixture_conftest_module.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py index f78a57c322b..45e5deaafea 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_module_class.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py b/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py index 12e0e3e91d4..1c7a710cd0c 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_basic.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py b/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py index 8b6e8697e06..96f0cacfafd 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_classlevel.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py b/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py index 40587cf2bd1..b78ca04b3ab 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookup_modulelevel.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py b/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py index 0cc8446d8ee..0dd782e4285 100644 --- a/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py +++ b/testing/example_scripts/fixtures/fill_fixtures/test_funcarg_lookupfails.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/test_fixture_named_request.py b/testing/example_scripts/fixtures/test_fixture_named_request.py index a2ab7ee330d..db88bcdabb9 100644 --- a/testing/example_scripts/fixtures/test_fixture_named_request.py +++ b/testing/example_scripts/fixtures/test_fixture_named_request.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py b/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py index 0f316f0e449..0559905cea4 100644 --- a/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py +++ b/testing/example_scripts/fixtures/test_getfixturevalue_dynamic.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/issue88_initial_file_multinodes/conftest.py b/testing/example_scripts/issue88_initial_file_multinodes/conftest.py index bde5c0711ac..2e88c5ad5a9 100644 --- a/testing/example_scripts/issue88_initial_file_multinodes/conftest.py +++ b/testing/example_scripts/issue88_initial_file_multinodes/conftest.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py b/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py index dd18e1741f0..b10f874e78d 100644 --- a/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py +++ b/testing/example_scripts/issue88_initial_file_multinodes/test_hello.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_hello(): pass diff --git a/testing/example_scripts/issue_519.py b/testing/example_scripts/issue_519.py index 39766164490..138c07e95be 100644 --- a/testing/example_scripts/issue_519.py +++ b/testing/example_scripts/issue_519.py @@ -1,7 +1,7 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pprint -from typing import List -from typing import Tuple import pytest @@ -16,7 +16,7 @@ def pytest_generate_tests(metafunc): @pytest.fixture(scope="session") def checked_order(): - order: List[Tuple[str, str, str]] = [] + order: list[tuple[str, str, str]] = [] yield order pprint.pprint(order) diff --git a/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py b/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py index d95ad0a83d9..c98e58316eb 100644 --- a/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py +++ b/testing/example_scripts/marks/marks_considered_keywords/test_marks_as_keywords.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/perf_examples/collect_stats/generate_folders.py b/testing/example_scripts/perf_examples/collect_stats/generate_folders.py index 17085e50b54..3b580aa341a 100644 --- a/testing/example_scripts/perf_examples/collect_stats/generate_folders.py +++ b/testing/example_scripts/perf_examples/collect_stats/generate_folders.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import argparse import pathlib diff --git a/testing/example_scripts/perf_examples/collect_stats/template_test.py b/testing/example_scripts/perf_examples/collect_stats/template_test.py index f50eb65525c..d9449485db6 100644 --- a/testing/example_scripts/perf_examples/collect_stats/template_test.py +++ b/testing/example_scripts/perf_examples/collect_stats/template_test.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_x(): pass diff --git a/testing/example_scripts/tmpdir/tmp_path_fixture.py b/testing/example_scripts/tmpdir/tmp_path_fixture.py index 4aa35faa0b6..503ead473e7 100644 --- a/testing/example_scripts/tmpdir/tmp_path_fixture.py +++ b/testing/example_scripts/tmpdir/tmp_path_fixture.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py b/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py index d66b66df5b7..733202915e4 100644 --- a/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py +++ b/testing/example_scripts/unittest/test_parametrized_fixture_error_message.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import unittest import pytest diff --git a/testing/example_scripts/unittest/test_setup_skip.py b/testing/example_scripts/unittest/test_setup_skip.py index 7550a097576..52ff96ea8be 100644 --- a/testing/example_scripts/unittest/test_setup_skip.py +++ b/testing/example_scripts/unittest/test_setup_skip.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Skipping an entire subclass with unittest.skip() should *not* call setUp from a base class.""" +from __future__ import annotations + import unittest diff --git a/testing/example_scripts/unittest/test_setup_skip_class.py b/testing/example_scripts/unittest/test_setup_skip_class.py index 48f7e476f40..fe431d8e794 100644 --- a/testing/example_scripts/unittest/test_setup_skip_class.py +++ b/testing/example_scripts/unittest/test_setup_skip_class.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Skipping an entire subclass with unittest.skip() should *not* call setUpClass from a base class.""" +from __future__ import annotations + import unittest diff --git a/testing/example_scripts/unittest/test_setup_skip_module.py b/testing/example_scripts/unittest/test_setup_skip_module.py index eee4263d22b..07fd96c9cef 100644 --- a/testing/example_scripts/unittest/test_setup_skip_module.py +++ b/testing/example_scripts/unittest/test_setup_skip_module.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """setUpModule is always called, even if all tests in the module are skipped""" +from __future__ import annotations + import unittest diff --git a/testing/example_scripts/unittest/test_unittest_asyncio.py b/testing/example_scripts/unittest/test_unittest_asyncio.py index a82ddaebcc3..8792492b38d 100644 --- a/testing/example_scripts/unittest/test_unittest_asyncio.py +++ b/testing/example_scripts/unittest/test_unittest_asyncio.py @@ -1,9 +1,10 @@ # mypy: allow-untyped-defs -from typing import List +from __future__ import annotations + from unittest import IsolatedAsyncioTestCase -teardowns: List[None] = [] +teardowns: list[None] = [] class AsyncArguments(IsolatedAsyncioTestCase): diff --git a/testing/example_scripts/unittest/test_unittest_asynctest.py b/testing/example_scripts/unittest/test_unittest_asynctest.py index e9b10171e8d..8a93366b9a3 100644 --- a/testing/example_scripts/unittest/test_unittest_asynctest.py +++ b/testing/example_scripts/unittest/test_unittest_asynctest.py @@ -1,13 +1,14 @@ # mypy: allow-untyped-defs """Issue #7110""" +from __future__ import annotations + import asyncio -from typing import List import asynctest -teardowns: List[None] = [] +teardowns: list[None] = [] class Test(asynctest.TestCase): diff --git a/testing/example_scripts/unittest/test_unittest_plain_async.py b/testing/example_scripts/unittest/test_unittest_plain_async.py index 2a4a66509a4..ea1ae371551 100644 --- a/testing/example_scripts/unittest/test_unittest_plain_async.py +++ b/testing/example_scripts/unittest/test_unittest_plain_async.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import unittest diff --git a/testing/example_scripts/warnings/test_group_warnings_by_message.py b/testing/example_scripts/warnings/test_group_warnings_by_message.py index be64a1ff2c8..ee3bc2bbee4 100644 --- a/testing/example_scripts/warnings/test_group_warnings_by_message.py +++ b/testing/example_scripts/warnings/test_group_warnings_by_message.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import warnings import pytest diff --git a/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py b/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py index 95fa795efe0..cc514bafbe9 100644 --- a/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py +++ b/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_1.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import warnings import pytest diff --git a/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py b/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py index 5204fde8a85..33d5ce8ce34 100644 --- a/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py +++ b/testing/example_scripts/warnings/test_group_warnings_by_message_summary/test_2.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from test_1 import func diff --git a/testing/examples/test_issue519.py b/testing/examples/test_issue519.py index 7b9c109889e..80f78d843a2 100644 --- a/testing/examples/test_issue519.py +++ b/testing/examples/test_issue519.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from _pytest.pytester import Pytester diff --git a/testing/freeze/create_executable.py b/testing/freeze/create_executable.py index fbfda2e5d94..2015d22c7c0 100644 --- a/testing/freeze/create_executable.py +++ b/testing/freeze/create_executable.py @@ -1,5 +1,8 @@ """Generate an executable with pytest runner embedded using PyInstaller.""" +from __future__ import annotations + + if __name__ == "__main__": import subprocess diff --git a/testing/freeze/runtests_script.py b/testing/freeze/runtests_script.py index ef63a2d15b9..286c98ac539 100644 --- a/testing/freeze/runtests_script.py +++ b/testing/freeze/runtests_script.py @@ -3,6 +3,9 @@ pytest main(). """ +from __future__ import annotations + + if __name__ == "__main__": import sys diff --git a/testing/freeze/tests/test_trivial.py b/testing/freeze/tests/test_trivial.py index 425f29a649c..000ca97310c 100644 --- a/testing/freeze/tests/test_trivial.py +++ b/testing/freeze/tests/test_trivial.py @@ -1,4 +1,7 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_upper(): assert "foo".upper() == "FOO" diff --git a/testing/freeze/tox_run.py b/testing/freeze/tox_run.py index 7fd63cf1218..38c1e75cf10 100644 --- a/testing/freeze/tox_run.py +++ b/testing/freeze/tox_run.py @@ -3,6 +3,9 @@ directory. """ +from __future__ import annotations + + if __name__ == "__main__": import os import sys @@ -10,4 +13,4 @@ executable = os.path.join(os.getcwd(), "dist", "runtests_script", "runtests_script") if sys.platform.startswith("win"): executable += ".exe" - sys.exit(os.system("%s tests" % executable)) + sys.exit(os.system(f"{executable} tests")) diff --git a/testing/io/test_pprint.py b/testing/io/test_pprint.py index 15fe6611280..1326ef34b2e 100644 --- a/testing/io/test_pprint.py +++ b/testing/io/test_pprint.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from collections import ChainMap from collections import Counter from collections import defaultdict diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 5d270f1756c..075d40cdf44 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr_unlimited @@ -144,7 +146,7 @@ def test_big_repr(): def test_repr_on_newstyle() -> None: class Function: def __repr__(self): - return "<%s>" % (self.name) # type: ignore[attr-defined] + return f"<{self.name}>" # type: ignore[attr-defined] assert saferepr(Function()) diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index afa8d5cae87..043c2d1d904 100644 --- a/testing/io/test_terminalwriter.py +++ b/testing/io/test_terminalwriter.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import io import os from pathlib import Path @@ -6,7 +8,6 @@ import shutil import sys from typing import Generator -from typing import Optional from unittest import mock from _pytest._io import terminalwriter @@ -166,7 +167,7 @@ def test_attr_hasmarkup() -> None: assert "\x1b[0m" in s -def assert_color(expected: bool, default: Optional[bool] = None) -> None: +def assert_color(expected: bool, default: bool | None = None) -> None: file = io.StringIO() if default is None: default = not expected diff --git a/testing/io/test_wcwidth.py b/testing/io/test_wcwidth.py index 82503b8300c..9ff1ad06e60 100644 --- a/testing/io/test_wcwidth.py +++ b/testing/io/test_wcwidth.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from _pytest._io.wcwidth import wcswidth from _pytest._io.wcwidth import wcwidth import pytest diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index 2e16913f099..0603eaba218 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -1,5 +1,7 @@ # mypy: disable-error-code="attr-defined" # mypy: disallow-untyped-defs +from __future__ import annotations + import logging from typing import Iterator @@ -117,7 +119,7 @@ def test2(caplog): result.stdout.no_fnmatch_line("*log from test2*") -def test_change_level_undos_handler_level(pytester: Pytester) -> None: +def test_change_level_undoes_handler_level(pytester: Pytester) -> None: """Ensure that 'set_level' is undone after the end of the test (handler). Issue #7569. Tests the handler level specifically. @@ -302,7 +304,15 @@ def logging_during_setup_and_teardown( assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"] -def test_caplog_captures_for_all_stages( +def private_assert_caplog_records_is_setup_call( + caplog: pytest.LogCaptureFixture, +) -> None: + # This reaches into private API, don't use this type of thing in real tests! + caplog_records = caplog._item.stash[caplog_records_key] + assert set(caplog_records) == {"setup", "call"} + + +def test_captures_for_all_stages( caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None ) -> None: assert not caplog.records @@ -312,9 +322,7 @@ def test_caplog_captures_for_all_stages( assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] - # This reaches into private API, don't use this type of thing in real tests! - caplog_records = caplog._item.stash[caplog_records_key] - assert set(caplog_records) == {"setup", "call"} + private_assert_caplog_records_is_setup_call(caplog) def test_clear_for_call_stage( @@ -323,21 +331,18 @@ def test_clear_for_call_stage( logger.info("a_call_log") assert [x.message for x in caplog.get_records("call")] == ["a_call_log"] assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] - caplog_records = caplog._item.stash[caplog_records_key] - assert set(caplog_records) == {"setup", "call"} + private_assert_caplog_records_is_setup_call(caplog) caplog.clear() assert caplog.get_records("call") == [] assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] - caplog_records = caplog._item.stash[caplog_records_key] - assert set(caplog_records) == {"setup", "call"} + private_assert_caplog_records_is_setup_call(caplog) logging.info("a_call_log_after_clear") assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"] assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"] - caplog_records = caplog._item.stash[caplog_records_key] - assert set(caplog_records) == {"setup", "call"} + private_assert_caplog_records_is_setup_call(caplog) def test_ini_controls_global_log_level(pytester: Pytester) -> None: @@ -363,11 +368,11 @@ def test_log_level_override(request, caplog): ) result = pytester.runpytest() - # make sure that that we get a '0' exit code for the testsuite + # make sure that we get a '0' exit code for the testsuite assert result.ret == 0 -def test_caplog_can_override_global_log_level(pytester: Pytester) -> None: +def test_can_override_global_log_level(pytester: Pytester) -> None: pytester.makepyfile( """ import pytest @@ -406,7 +411,7 @@ def test_log_level_override(request, caplog): assert result.ret == 0 -def test_caplog_captures_despite_exception(pytester: Pytester) -> None: +def test_captures_despite_exception(pytester: Pytester) -> None: pytester.makepyfile( """ import pytest diff --git a/testing/logging/test_formatter.py b/testing/logging/test_formatter.py index 37971293726..cfe3bee68c4 100644 --- a/testing/logging/test_formatter.py +++ b/testing/logging/test_formatter.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging from typing import Any diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 7e592febf56..cf54788e246 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import io import os import re diff --git a/testing/plugins_integration/bdd_wallet.py b/testing/plugins_integration/bdd_wallet.py index 2bdb1545424..d748028842a 100644 --- a/testing/plugins_integration/bdd_wallet.py +++ b/testing/plugins_integration/bdd_wallet.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from pytest_bdd import given from pytest_bdd import scenario from pytest_bdd import then diff --git a/testing/plugins_integration/django_settings.py b/testing/plugins_integration/django_settings.py index 0715f476531..e36e554db9a 100644 --- a/testing/plugins_integration/django_settings.py +++ b/testing/plugins_integration/django_settings.py @@ -1 +1,4 @@ +from __future__ import annotations + + SECRET_KEY = "mysecret" diff --git a/testing/plugins_integration/pytest_anyio_integration.py b/testing/plugins_integration/pytest_anyio_integration.py index 383d7a0b5db..41ffad18a6e 100644 --- a/testing/plugins_integration/pytest_anyio_integration.py +++ b/testing/plugins_integration/pytest_anyio_integration.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import anyio import pytest diff --git a/testing/plugins_integration/pytest_asyncio_integration.py b/testing/plugins_integration/pytest_asyncio_integration.py index b216c4beecd..cef67f83ea6 100644 --- a/testing/plugins_integration/pytest_asyncio_integration.py +++ b/testing/plugins_integration/pytest_asyncio_integration.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import asyncio import pytest diff --git a/testing/plugins_integration/pytest_mock_integration.py b/testing/plugins_integration/pytest_mock_integration.py index 5494c44270a..a49129cf0c9 100644 --- a/testing/plugins_integration/pytest_mock_integration.py +++ b/testing/plugins_integration/pytest_mock_integration.py @@ -1,3 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + + def test_mocker(mocker): mocker.MagicMock() diff --git a/testing/plugins_integration/pytest_rerunfailures_integration.py b/testing/plugins_integration/pytest_rerunfailures_integration.py new file mode 100644 index 00000000000..449661f7294 --- /dev/null +++ b/testing/plugins_integration/pytest_rerunfailures_integration.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +import unittest + + +class MyTestCase(unittest.TestCase): + first_time = True + + def test_fail_the_first_time(self) -> None: + """Regression test for issue #12424.""" + if self.first_time: + type(self).first_time = False + self.fail() diff --git a/testing/plugins_integration/pytest_trio_integration.py b/testing/plugins_integration/pytest_trio_integration.py index 60f48ec609b..eceac5076a9 100644 --- a/testing/plugins_integration/pytest_trio_integration.py +++ b/testing/plugins_integration/pytest_trio_integration.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import trio import pytest diff --git a/testing/plugins_integration/pytest_twisted_integration.py b/testing/plugins_integration/pytest_twisted_integration.py index 0dbf5faeb8a..4f386bf1b9f 100644 --- a/testing/plugins_integration/pytest_twisted_integration.py +++ b/testing/plugins_integration/pytest_twisted_integration.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest_twisted from twisted.internet.task import deferLater diff --git a/testing/plugins_integration/requirements.txt b/testing/plugins_integration/requirements.txt index 9e152f1191b..4c1efcf32ed 100644 --- a/testing/plugins_integration/requirements.txt +++ b/testing/plugins_integration/requirements.txt @@ -1,9 +1,7 @@ -anyio[curio,trio]==4.3.0 -django==5.0.4 -pytest-asyncio==0.23.6 -# Temporarily not installed until pytest-bdd is fixed: -# https://github.com/pytest-dev/pytest/pull/11785 -# pytest-bdd==7.0.1 +anyio[curio,trio]==4.4.0 +django==5.0.7 +pytest-asyncio==0.23.7 +pytest-bdd==7.2.0 pytest-cov==5.0.0 pytest-django==4.8.0 pytest-flakes==4.0.5 @@ -11,7 +9,7 @@ pytest-html==4.1.1 pytest-mock==3.14.0 pytest-rerunfailures==14.0 pytest-sugar==1.0.0 -pytest-trio==0.7.0 -pytest-twisted==1.14.1 +pytest-trio==0.8.0 +pytest-twisted==1.14.2 twisted==24.3.0 pytest-xvfb==3.0.0 diff --git a/testing/plugins_integration/simple_integration.py b/testing/plugins_integration/simple_integration.py index 48089afcc7e..ed504ae4bf1 100644 --- a/testing/plugins_integration/simple_integration.py +++ b/testing/plugins_integration/simple_integration.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import pytest diff --git a/testing/python/approx.py b/testing/python/approx.py index 968e8828512..69743cdbe17 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from contextlib import contextmanager from decimal import Decimal from fractions import Fraction @@ -6,7 +8,6 @@ import operator from operator import eq from operator import ne -from typing import Optional from _pytest.pytester import Pytester from _pytest.python_api import _recursive_sequence_map @@ -415,9 +416,7 @@ def test_zero_tolerance(self): (-1e100, -1e100), ], ) - def test_negative_tolerance( - self, rel: Optional[float], abs: Optional[float] - ) -> None: + def test_negative_tolerance(self, rel: float | None, abs: float | None) -> None: # Negative tolerances are not allowed. with pytest.raises(ValueError): 1.1 == approx(1, rel, abs) @@ -954,6 +953,43 @@ def test_allow_ordered_sequences_only(self) -> None: with pytest.raises(TypeError, match="only supports ordered sequences"): assert {1, 2, 3} == approx({1, 2, 3}) + def test_strange_sequence(self): + """https://github.com/pytest-dev/pytest/issues/11797""" + a = MyVec3(1, 2, 3) + b = MyVec3(0, 1, 2) + + # this would trigger the error inside the test + pytest.approx(a, abs=0.5)._repr_compare(b) + + assert b == pytest.approx(a, abs=2) + assert b != pytest.approx(a, abs=0.5) + + +class MyVec3: # incomplete + """sequence like""" + + _x: int + _y: int + _z: int + + def __init__(self, x: int, y: int, z: int): + self._x, self._y, self._z = x, y, z + + def __repr__(self) -> str: + return f"" + + def __len__(self) -> int: + return 3 + + def __getitem__(self, key: int) -> int: + if key == 0: + return self._x + if key == 1: + return self._y + if key == 2: + return self._z + raise IndexError(key) + class TestRecursiveSequenceMap: def test_map_over_scalar(self): @@ -981,3 +1017,6 @@ def test_map_over_mixed_sequence(self): (5, 8), [(7)], ] + + def test_map_over_sequence_like(self): + assert _recursive_sequence_map(int, MyVec3(1, 2, 3)) == [1, 2, 3] diff --git a/testing/python/collect.py b/testing/python/collect.py index 745550f0775..06386611279 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1,9 +1,10 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os import sys import textwrap from typing import Any -from typing import Dict import _pytest._code from _pytest.config import ExitCode @@ -36,9 +37,9 @@ def test_import_duplicate(self, pytester: Pytester) -> None: [ "*import*mismatch*", "*imported*test_whatever*", - "*%s*" % p1, + f"*{p1}*", "*not the same*", - "*%s*" % p2, + f"*{p2}*", "*HINT*", ] ) @@ -262,6 +263,32 @@ def prop(self): result = pytester.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED + def test_abstract_class_is_not_collected(self, pytester: Pytester) -> None: + """Regression test for #12275 (non-unittest version).""" + pytester.makepyfile( + """ + import abc + + class TestBase(abc.ABC): + @abc.abstractmethod + def abstract1(self): pass + + @abc.abstractmethod + def abstract2(self): pass + + def test_it(self): pass + + class TestPartial(TestBase): + def abstract1(self): pass + + class TestConcrete(TestPartial): + def abstract2(self): pass + """ + ) + result = pytester.runpytest() + assert result.ret == ExitCode.OK + result.assert_outcomes(passed=1) + class TestFunction: def test_getmodulecollector(self, pytester: Pytester) -> None: @@ -1103,7 +1130,7 @@ def test_filter_traceback_generated_code(self) -> None: tb = None try: - ns: Dict[str, Any] = {} + ns: dict[str, Any] = {} exec("def foo(): raise ValueError", ns) ns["foo"]() except ValueError: diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 12ca6e92630..8d2646309a8 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os from pathlib import Path import sys @@ -74,6 +76,16 @@ class B(A): assert getfuncargnames(B.static, cls=B) == ("arg1", "arg2") +@pytest.mark.skipif( + sys.version_info >= (3, 13), + reason="""\ +In python 3.13, this will raise FutureWarning: +functools.partial will be a method descriptor in future Python versions; +wrap it in staticmethod() if you want to preserve the old behavior + +But the wrapped 'functools.partial' is tested by 'test_getfuncargnames_staticmethod_partial' below. +""", +) def test_getfuncargnames_partial(): """Check getfuncargnames for methods defined with functools.partial (#5701)""" import functools @@ -933,7 +945,7 @@ def test_request_subrequest_addfinalizer_exceptions( ) -> None: """ Ensure exceptions raised during teardown by finalizers are suppressed - until all finalizers are called, then re-reaised together in an + until all finalizers are called, then re-raised together in an exception group (#2440) """ pytester.makepyfile( @@ -1545,6 +1557,38 @@ def test_printer_2(self): result = pytester.runpytest() result.stdout.fnmatch_lines(["* 2 passed in *"]) + def test_parameterized_fixture_caching(self, pytester: Pytester) -> None: + """Regression test for #12600.""" + pytester.makepyfile( + """ + import pytest + from itertools import count + + CACHE_MISSES = count(0) + + def pytest_generate_tests(metafunc): + if "my_fixture" in metafunc.fixturenames: + # Use unique objects for parametrization (as opposed to small strings + # and small integers which are singletons). + metafunc.parametrize("my_fixture", [[1], [2]], indirect=True) + + @pytest.fixture(scope='session') + def my_fixture(request): + next(CACHE_MISSES) + + def test1(my_fixture): + pass + + def test2(my_fixture): + pass + + def teardown_module(): + assert next(CACHE_MISSES) == 2 + """ + ) + result = pytester.runpytest() + result.stdout.no_fnmatch_line("* ERROR at teardown *") + class TestFixtureManagerParseFactories: @pytest.fixture @@ -2219,6 +2263,25 @@ def test_check(): reprec = pytester.inline_run("-s") reprec.assertoutcome(passed=2) + def test_reordering_catastrophic_performance(self, pytester: Pytester) -> None: + """Check that a certain high-scope parametrization pattern doesn't cause + a catasrophic slowdown. + + Regression test for #12355. + """ + pytester.makepyfile(""" + import pytest + + params = tuple("abcdefghijklmnopqrstuvwxyz") + @pytest.mark.parametrize(params, [range(len(params))] * 3, scope="module") + def test_parametrize(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z): + pass + """) + + result = pytester.runpytest() + + result.assert_outcomes(passed=3) + class TestFixtureMarker: def test_parametrize(self, pytester: Pytester) -> None: @@ -2268,18 +2331,17 @@ def test_override_parametrized_fixture_issue_979( This was a regression introduced in the fix for #736. """ pytester.makepyfile( - """ + f""" import pytest @pytest.fixture(params=[1, 2]) def fixt(request): return request.param - @pytest.mark.parametrize(%s, [(3, 'x'), (4, 'x')]) + @pytest.mark.parametrize({param_args}, [(3, 'x'), (4, 'x')]) def test_foo(fixt, val): pass """ - % param_args ) reprec = pytester.inline_run() reprec.assertoutcome(passed=2) @@ -3397,6 +3459,28 @@ def test_something(): ["*def gen(qwe123):*", "*fixture*qwe123*not found*", "*1 error*"] ) + def test_cached_exception_doesnt_get_longer(self, pytester: Pytester) -> None: + """Regression test for #12204.""" + pytester.makepyfile( + """ + import pytest + @pytest.fixture(scope="session") + def bad(): 1 / 0 + + def test_1(bad): pass + def test_2(bad): pass + def test_3(bad): pass + """ + ) + + result = pytester.runpytest_inprocess("--tb=native") + assert result.ret == ExitCode.TESTS_FAILED + failures = result.reprec.getfailures() # type: ignore[attr-defined] + assert len(failures) == 3 + lines1 = failures[1].longrepr.reprtraceback.reprentries[0].lines + lines2 = failures[2].longrepr.reprtraceback.reprentries[0].lines + assert len(lines1) == len(lines2) + class TestShowFixtures: def test_funcarg_compat(self, pytester: Pytester) -> None: @@ -4253,6 +4337,39 @@ def test_func(self, f2, f1, m2): request = TopRequest(items[0], _ispytest=True) assert request.fixturenames == "s1 p1 m1 m2 c1 f2 f1".split() + def test_parametrized_package_scope_reordering(self, pytester: Pytester) -> None: + """A paramaterized package-scoped fixture correctly reorders items to + minimize setups & teardowns. + + Regression test for #12328. + """ + pytester.makepyfile( + __init__="", + conftest=""" + import pytest + @pytest.fixture(scope="package", params=["a", "b"]) + def fix(request): + return request.param + """, + test_1="def test1(fix): pass", + test_2="def test2(fix): pass", + ) + + result = pytester.runpytest("--setup-plan") + assert result.ret == ExitCode.OK + result.stdout.fnmatch_lines( + [ + " SETUP P fix['a']", + " test_1.py::test1[a] (fixtures used: fix, request)", + " test_2.py::test2[a] (fixtures used: fix, request)", + " TEARDOWN P fix['a']", + " SETUP P fix['b']", + " test_1.py::test1[b] (fixtures used: fix, request)", + " test_2.py::test2[b] (fixtures used: fix, request)", + " TEARDOWN P fix['b']", + ], + ) + def test_multiple_packages(self, pytester: Pytester) -> None: """Complex test involving multiple package fixtures. Make sure teardowns are executed in order. @@ -4441,7 +4558,7 @@ def test_fixture_named_request(pytester: Pytester) -> None: result.stdout.fnmatch_lines( [ "*'request' is a reserved word for fixtures, use another name:", - " *test_fixture_named_request.py:6", + " *test_fixture_named_request.py:8", ] ) diff --git a/testing/python/integration.py b/testing/python/integration.py index 219ebf9cec8..c52a683a322 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from _pytest._code import getfslineno from _pytest.fixtures import getfixturemarker from _pytest.pytester import Pytester @@ -163,7 +165,7 @@ def mock_basename(path): @mock.patch("os.path.abspath") @mock.patch("os.path.normpath") @mock.patch("os.path.basename", new=mock_basename) - def test_someting(normpath, abspath, tmp_path): + def test_something(normpath, abspath, tmp_path): abspath.return_value = "this" os.path.normpath(os.path.abspath("hello")) normpath.assert_any_call("this") @@ -176,7 +178,7 @@ def test_someting(normpath, abspath, tmp_path): funcnames = [ call.report.location[2] for call in calls if call.report.when == "call" ] - assert funcnames == ["T.test_hello", "test_someting"] + assert funcnames == ["T.test_hello", "test_something"] def test_mock_sorting(self, pytester: Pytester) -> None: pytest.importorskip("mock", "1.0.1") diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 3d0058fa0a7..2dd85607e71 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import dataclasses import itertools import re @@ -8,11 +10,7 @@ from typing import cast from typing import Dict from typing import Iterator -from typing import List -from typing import Optional from typing import Sequence -from typing import Tuple -from typing import Union import hypothesis from hypothesis import strategies @@ -35,7 +33,7 @@ def Metafunc(self, func, config=None) -> python.Metafunc: # on the funcarg level, so we don't need a full blown # initialization. class FuncFixtureInfoMock: - name2fixturedefs: Dict[str, List[fixtures.FixtureDef[object]]] = {} + name2fixturedefs: dict[str, list[fixtures.FixtureDef[object]]] = {} def __init__(self, names): self.names_closure = names @@ -101,7 +99,7 @@ class Exc(Exception): def __repr__(self): return "Exc(from_gen)" - def gen() -> Iterator[Union[int, None, Exc]]: + def gen() -> Iterator[int | None | Exc]: yield 0 yield None yield Exc() @@ -346,7 +344,7 @@ def getini(self, name): option = "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" - values: List[Tuple[str, Any, str]] = [ + values: list[tuple[str, Any, str]] = [ ("ação", MockConfig({option: True}), "ação"), ("ação", MockConfig({option: False}), "a\\xe7\\xe3o"), ] @@ -516,7 +514,7 @@ def test_idmaker_enum(self) -> None: def test_idmaker_idfn(self) -> None: """#351""" - def ids(val: object) -> Optional[str]: + def ids(val: object) -> str | None: if isinstance(val, Exception): return repr(val) return None @@ -579,7 +577,7 @@ def getini(self, name): option = "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" - values: List[Tuple[Any, str]] = [ + values: list[tuple[Any, str]] = [ (MockConfig({option: True}), "ação"), (MockConfig({option: False}), "a\\xe7\\xe3o"), ] @@ -617,7 +615,7 @@ def getini(self, name): option = "disable_test_id_escaping_and_forfeit_all_rights_to_community_support" - values: List[Tuple[Any, str]] = [ + values: list[tuple[Any, str]] = [ (MockConfig({option: True}), "ação"), (MockConfig({option: False}), "a\\xe7\\xe3o"), ] @@ -1748,9 +1746,9 @@ def test_parametrize_some_arguments_auto_scope( self, pytester: Pytester, monkeypatch ) -> None: """Integration test for (#3941)""" - class_fix_setup: List[object] = [] + class_fix_setup: list[object] = [] monkeypatch.setattr(sys, "class_fix_setup", class_fix_setup, raising=False) - func_fix_setup: List[object] = [] + func_fix_setup: list[object] = [] monkeypatch.setattr(sys, "func_fix_setup", func_fix_setup, raising=False) pytester.makepyfile( diff --git a/testing/python/raises.py b/testing/python/raises.py index 929865e31a0..2011c81615e 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import re import sys @@ -130,6 +132,26 @@ def test_division(example_input, expectation): result = pytester.runpytest() result.stdout.fnmatch_lines(["*2 failed*"]) + def test_raises_with_invalid_regex(self, pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + def test_invalid_regex(): + with pytest.raises(ValueError, match="invalid regex character ["): + raise ValueError() + """ + ) + result = pytester.runpytest() + result.stdout.fnmatch_lines( + [ + "*Invalid regex pattern provided to 'match': unterminated character set at position 24*", + ] + ) + result.stdout.no_fnmatch_line("*Traceback*") + result.stdout.no_fnmatch_line("*File*") + result.stdout.no_fnmatch_line("*line*") + def test_noclass(self) -> None: with pytest.raises(TypeError): pytest.raises("wrong", lambda: None) # type: ignore[call-overload] diff --git a/testing/python/show_fixtures_per_test.py b/testing/python/show_fixtures_per_test.py index f756dca41c7..c860b61e21b 100644 --- a/testing/python/show_fixtures_per_test.py +++ b/testing/python/show_fixtures_per_test.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from _pytest.pytester import Pytester diff --git a/testing/test_argcomplete.py b/testing/test_argcomplete.py index 0c41c0286a4..5d1513b6206 100644 --- a/testing/test_argcomplete.py +++ b/testing/test_argcomplete.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from pathlib import Path import subprocess import sys diff --git a/testing/test_assertion.py b/testing/test_assertion.py index ef4e36644d9..69ca0f73ff2 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1,11 +1,11 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import sys import textwrap from typing import Any -from typing import List from typing import MutableSequence from typing import NamedTuple -from typing import Optional import attr @@ -19,7 +19,7 @@ import pytest -def mock_config(verbose: int = 0, assertion_override: Optional[int] = None): +def mock_config(verbose: int = 0, assertion_override: int | None = None): class TerminalWriter: def _highlight(self, source, lexer="python"): return source @@ -28,7 +28,7 @@ class Config: def get_terminal_writer(self): return TerminalWriter() - def get_verbosity(self, verbosity_type: Optional[str] = None) -> int: + def get_verbosity(self, verbosity_type: str | None = None) -> int: if verbosity_type is None: return verbose if verbosity_type == _Config.VERBOSITY_ASSERTIONS: @@ -101,7 +101,7 @@ def test(check_first): """, } pytester.makepyfile(**contents) - result = pytester.runpytest_subprocess("--assert=%s" % mode) + result = pytester.runpytest_subprocess(f"--assert={mode}") if mode == "plain": expected = "E AssertionError" elif mode == "rewrite": @@ -163,7 +163,7 @@ def test_foo(check_first): """, } pytester.makepyfile(**contents) - result = pytester.runpytest_subprocess("--assert=%s" % mode) + result = pytester.runpytest_subprocess(f"--assert={mode}") if mode == "plain": expected = "E AssertionError" elif mode == "rewrite": @@ -223,7 +223,7 @@ def test_installed_plugin_rewrite( ) -> None: monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) # Make sure the hook is installed early enough so that plugins - # installed via setuptools are rewritten. + # installed via distribution package are rewritten. pytester.mkdir("hampkg") contents = { "hampkg/__init__.py": """\ @@ -280,7 +280,7 @@ def test2(check_first2): } pytester.makepyfile(**contents) result = pytester.run( - sys.executable, "mainwrapper.py", "-s", "--assert=%s" % mode + sys.executable, "mainwrapper.py", "-s", f"--assert={mode}" ) if mode == "plain": expected = "E AssertionError" @@ -369,12 +369,12 @@ def test_check(list): result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"]) -def callop(op: str, left: Any, right: Any, verbose: int = 0) -> Optional[List[str]]: +def callop(op: str, left: Any, right: Any, verbose: int = 0) -> list[str] | None: config = mock_config(verbose=verbose) return plugin.pytest_assertrepr_compare(config, op, left, right) -def callequal(left: Any, right: Any, verbose: int = 0) -> Optional[List[str]]: +def callequal(left: Any, right: Any, verbose: int = 0) -> list[str] | None: return callop("==", left, right, verbose) @@ -1316,7 +1316,7 @@ class TestTruncateExplanation: LINES_IN_TRUNCATION_MSG = 2 def test_doesnt_truncate_when_input_is_empty_list(self) -> None: - expl: List[str] = [] + expl: list[str] = [] result = truncate._truncate_explanation(expl, max_lines=8, max_chars=100) assert result == expl @@ -2045,3 +2045,36 @@ def test_long_text_fail(): f"E AssertionError: assert 'hello world' in '{long_text}'", ] ) + + +def test_full_output_vvv(pytester: Pytester) -> None: + pytester.makepyfile( + r""" + def crash_helper(m): + assert 1 == 2 + def test_vvv(): + crash_helper(500 * "a") + """ + ) + result = pytester.runpytest("") + # without -vvv, the passed args are truncated + expected_non_vvv_arg_line = "m = 'aaaaaaaaaaaaaaa*..aaaaaaaaaaaa*" + result.stdout.fnmatch_lines( + [ + expected_non_vvv_arg_line, + "test_full_output_vvv.py:2: AssertionError", + ], + ) + # double check that the untruncated part is not in the output + expected_vvv_arg_line = "m = '{}'".format(500 * "a") + result.stdout.no_fnmatch_line(expected_vvv_arg_line) + + # but with "-vvv" the args are not truncated + result = pytester.runpytest("-vvv") + result.stdout.fnmatch_lines( + [ + expected_vvv_arg_line, + "test_full_output_vvv.py:2: AssertionError", + ] + ) + result.stdout.no_fnmatch_line(expected_non_vvv_arg_line) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 7acc8cdf1d9..5ee40ee6568 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import ast import errno from functools import partial @@ -8,16 +10,13 @@ import os from pathlib import Path import py_compile +import re import stat import sys import textwrap from typing import cast -from typing import Dict from typing import Generator -from typing import List from typing import Mapping -from typing import Optional -from typing import Set from unittest import mock import zipfile @@ -26,6 +25,7 @@ from _pytest.assertion import util from _pytest.assertion.rewrite import _get_assertion_exprs from _pytest.assertion.rewrite import _get_maxsize_for_saferepr +from _pytest.assertion.rewrite import _saferepr from _pytest.assertion.rewrite import AssertionRewritingHook from _pytest.assertion.rewrite import get_cache_dir from _pytest.assertion.rewrite import PYC_TAIL @@ -45,13 +45,13 @@ def rewrite(src: str) -> ast.Module: def getmsg( - f, extra_ns: Optional[Mapping[str, object]] = None, *, must_pass: bool = False -) -> Optional[str]: + f, extra_ns: Mapping[str, object] | None = None, *, must_pass: bool = False +) -> str | None: """Rewrite the assertions in f, run it, and get the failure message.""" src = "\n".join(_pytest._code.Code.from_function(f).source().lines) mod = rewrite(src) code = compile(mod, "", "exec") - ns: Dict[str, object] = {} + ns: dict[str, object] = {} if extra_ns is not None: ns.update(extra_ns) exec(code, ns) @@ -130,6 +130,7 @@ def test_location_is_set(self) -> None: if isinstance(node, ast.Import): continue for n in [node, *ast.iter_child_nodes(node)]: + assert isinstance(n, (ast.stmt, ast.expr)) assert n.lineno == 3 assert n.col_offset == 0 assert n.end_lineno == 6 @@ -308,9 +309,7 @@ def test_foo(): ) result = pytester.runpytest() assert result.ret == 1 - result.stdout.fnmatch_lines( - ["*AssertionError*%s*" % repr((1, 2)), "*assert 1 == 2*"] - ) + result.stdout.fnmatch_lines([f"*AssertionError*{(1, 2)!r}*", "*assert 1 == 2*"]) def test_assertion_message_expr(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -908,7 +907,7 @@ def test_optimized(): assert test_optimized.__doc__ is None""" ) p = make_numbered_dir(root=Path(pytester.path), prefix="runpytest-") - tmp = "--basetemp=%s" % p + tmp = f"--basetemp={p}" with monkeypatch.context() as mp: mp.setenv("PYTHONOPTIMIZE", "2") mp.delenv("PYTHONDONTWRITEBYTECODE", raising=False) @@ -1639,8 +1638,8 @@ def hook( """ import importlib.machinery - self.find_spec_calls: List[str] = [] - self.initial_paths: Set[Path] = set() + self.find_spec_calls: list[str] = [] + self.initial_paths: set[Path] = set() class StubSession: _initialpaths = self.initial_paths @@ -1974,6 +1973,11 @@ def fake_mkdir(p, exist_ok=False, *, exc): monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err)) assert not try_makedirs(p) + err = OSError() + err.errno = errno.ENOSYS + monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err)) + assert not try_makedirs(p) + # unhandled OSError should raise err = OSError() err.errno = errno.ECHILD @@ -2034,7 +2038,9 @@ def test_foo(): assert test_foo_pyc.is_file() # normal file: not touched by pytest, normal cache tag - bar_init_pyc = get_cache_dir(bar_init) / f"__init__.{sys.implementation.cache_tag}.pyc" + bar_init_pyc = ( + get_cache_dir(bar_init) / f"__init__.{sys.implementation.cache_tag}.pyc" + ) assert bar_init_pyc.is_file() @@ -2055,7 +2061,7 @@ class TestReprSizeVerbosity: ) def test_get_maxsize_for_saferepr(self, verbose: int, expected_size) -> None: class FakeConfig: - def get_verbosity(self, verbosity_type: Optional[str] = None) -> int: + def get_verbosity(self, verbosity_type: str | None = None) -> int: return verbose config = FakeConfig() @@ -2101,3 +2107,26 @@ def test_foo(): ) result = pytester.runpytest() assert result.ret == 0 + + +class TestSafereprUnbounded: + class Help: + def bound_method(self): # pragma: no cover + pass + + def test_saferepr_bound_method(self): + """saferepr() of a bound method should show only the method name""" + assert _saferepr(self.Help().bound_method) == "bound_method" + + def test_saferepr_unbounded(self): + """saferepr() of an unbound method should still show the full information""" + obj = self.Help() + # using id() to fetch memory address fails on different platforms + pattern = re.compile( + rf"<{Path(__file__).stem}.{self.__class__.__name__}.Help object at 0x[0-9a-fA-F]*>", + ) + assert pattern.match(_saferepr(obj)) + assert ( + _saferepr(self.Help) + == f"" + ) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 304e5414abc..72b4265cf75 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from enum import auto from enum import Enum import os @@ -5,9 +7,7 @@ import shutil from typing import Any from typing import Generator -from typing import List from typing import Sequence -from typing import Tuple from _pytest.compat import assert_never from _pytest.config import ExitCode @@ -31,6 +31,21 @@ def test_config_cache_mkdir(self, pytester: Pytester) -> None: p = config.cache.mkdir("name") assert p.is_dir() + def test_cache_dir_permissions(self, pytester: Pytester) -> None: + """The .pytest_cache directory should have world-readable permissions + (depending on umask). + + Regression test for #12308. + """ + pytester.makeini("[pytest]") + config = pytester.parseconfigure() + assert config.cache is not None + p = config.cache.mkdir("name") + assert p.is_dir() + # Instead of messing with umask, make sure .pytest_cache has the same + # permissions as the default that `mkdir` gives `p`. + assert (p.parent.stat().st_mode & 0o777) == (p.stat().st_mode & 0o777) + def test_config_cache_dataerror(self, pytester: Pytester) -> None: pytester.makeini("[pytest]") config = pytester.parseconfigure() @@ -43,7 +58,7 @@ def test_config_cache_dataerror(self, pytester: Pytester) -> None: assert val == -2 @pytest.mark.filterwarnings("ignore:could not create cache path") - def test_cache_writefail_cachfile_silent(self, pytester: Pytester) -> None: + def test_cache_writefail_cachefile_silent(self, pytester: Pytester) -> None: pytester.makeini("[pytest]") pytester.path.joinpath(".pytest_cache").write_text( "gone wrong", encoding="utf-8" @@ -179,7 +194,7 @@ def test_custom_cache_dir_with_env_var( assert pytester.path.joinpath("custom_cache_dir").is_dir() -@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "/tox_env_dir"))) +@pytest.mark.parametrize("env", ((), ("TOX_ENV_DIR", "mydir/tox-env"))) def test_cache_reportheader( env: Sequence[str], pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: @@ -191,7 +206,7 @@ def test_cache_reportheader( monkeypatch.delenv("TOX_ENV_DIR", raising=False) expected = ".pytest_cache" result = pytester.runpytest("-v") - result.stdout.fnmatch_lines(["cachedir: %s" % expected]) + result.stdout.fnmatch_lines([f"cachedir: {expected}"]) def test_cache_reportheader_external_abspath( @@ -564,7 +579,7 @@ def test_pass(): def rlf( fail_import: int, fail_run: int, args: Sequence[str] = () - ) -> Tuple[Any, Any]: + ) -> tuple[Any, Any]: monkeypatch.setenv("FAILIMPORT", str(fail_import)) monkeypatch.setenv("FAILTEST", str(fail_run)) @@ -678,7 +693,7 @@ def test_lf_and_ff_prints_no_needless_message( else: assert "rerun previous" in result.stdout.str() - def get_cached_last_failed(self, pytester: Pytester) -> List[str]: + def get_cached_last_failed(self, pytester: Pytester) -> list[str]: config = pytester.parseconfigure() assert config.cache is not None return sorted(config.cache.get("cache/lastfailed", {})) @@ -1148,7 +1163,7 @@ def test_1(): assert 1 ) p1.write_text( - "def test_1(): assert 1\n" "def test_2(): assert 1\n", encoding="utf-8" + "def test_1(): assert 1\ndef test_2(): assert 1\n", encoding="utf-8" ) os.utime(p1, ns=(p1.stat().st_atime_ns, int(1e9))) diff --git a/testing/test_capture.py b/testing/test_capture.py index 0521c3b6b04..fe6bd7d14fa 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import contextlib import io from io import UnsupportedOperation @@ -103,16 +105,15 @@ def test_init_capturing(self): def test_capturing_unicode(pytester: Pytester, method: str) -> None: obj = "'b\u00f6y'" pytester.makepyfile( - """\ + f"""\ # taken from issue 227 from nosetests def test_unicode(): import sys print(sys.stdout) - print(%s) + print({obj}) """ - % obj ) - result = pytester.runpytest("--capture=%s" % method) + result = pytester.runpytest(f"--capture={method}") result.stdout.fnmatch_lines(["*1 passed*"]) @@ -124,7 +125,7 @@ def test_unicode(): print('b\\u00f6y') """ ) - result = pytester.runpytest("--capture=%s" % method) + result = pytester.runpytest(f"--capture={method}") result.stdout.fnmatch_lines(["*1 passed*"]) diff --git a/testing/test_collection.py b/testing/test_collection.py index 1491ec85990..aba8f8ea48d 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,12 +1,14 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os from pathlib import Path +from pathlib import PurePath import pprint import shutil import sys import tempfile import textwrap -from typing import List from _pytest.assertion.util import running_on_ci from _pytest.config import ExitCode @@ -151,20 +153,17 @@ def test_ignored_certain_directories(self, pytester: Pytester) -> None: assert "test_notfound" not in s assert "test_found" in s - @pytest.mark.parametrize( - "fname", - ( - "activate", - "activate.csh", - "activate.fish", - "Activate", - "Activate.bat", - "Activate.ps1", - ), + known_environment_types = pytest.mark.parametrize( + "env_path", + [ + pytest.param(PurePath("pyvenv.cfg"), id="venv"), + pytest.param(PurePath("conda-meta", "history"), id="conda"), + ], ) - def test_ignored_virtualenvs(self, pytester: Pytester, fname: str) -> None: - bindir = "Scripts" if sys.platform.startswith("win") else "bin" - ensure_file(pytester.path / "virtual" / bindir / fname) + + @known_environment_types + def test_ignored_virtualenvs(self, pytester: Pytester, env_path: PurePath) -> None: + ensure_file(pytester.path / "virtual" / env_path) testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py") testfile.write_text("def test_hello(): pass", encoding="utf-8") @@ -178,23 +177,12 @@ def test_ignored_virtualenvs(self, pytester: Pytester, fname: str) -> None: result = pytester.runpytest("virtual") assert "test_invenv" in result.stdout.str() - @pytest.mark.parametrize( - "fname", - ( - "activate", - "activate.csh", - "activate.fish", - "Activate", - "Activate.bat", - "Activate.ps1", - ), - ) + @known_environment_types def test_ignored_virtualenvs_norecursedirs_precedence( - self, pytester: Pytester, fname: str + self, pytester: Pytester, env_path ) -> None: - bindir = "Scripts" if sys.platform.startswith("win") else "bin" # norecursedirs takes priority - ensure_file(pytester.path / ".virtual" / bindir / fname) + ensure_file(pytester.path / ".virtual" / env_path) testfile = ensure_file(pytester.path / ".virtual" / "test_invenv.py") testfile.write_text("def test_hello(): pass", encoding="utf-8") result = pytester.runpytest("--collect-in-virtualenv") @@ -203,27 +191,14 @@ def test_ignored_virtualenvs_norecursedirs_precedence( result = pytester.runpytest("--collect-in-virtualenv", ".virtual") assert "test_invenv" in result.stdout.str() - @pytest.mark.parametrize( - "fname", - ( - "activate", - "activate.csh", - "activate.fish", - "Activate", - "Activate.bat", - "Activate.ps1", - ), - ) - def test__in_venv(self, pytester: Pytester, fname: str) -> None: + @known_environment_types + def test__in_venv(self, pytester: Pytester, env_path: PurePath) -> None: """Directly test the virtual env detection function""" - bindir = "Scripts" if sys.platform.startswith("win") else "bin" - # no bin/activate, not a virtualenv + # no env path, not a env base_path = pytester.mkdir("venv") assert _in_venv(base_path) is False - # with bin/activate, totally a virtualenv - bin_path = base_path.joinpath(bindir) - bin_path.mkdir() - bin_path.joinpath(fname).touch() + # with env path, totally a env + ensure_file(base_path.joinpath(env_path)) assert _in_venv(base_path) is True def test_custom_norecursedirs(self, pytester: Pytester) -> None: @@ -275,14 +250,31 @@ def test_testpaths_ini(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> No # collects the tests for dirname in ("a", "b", "c"): items, reprec = pytester.inline_genitems(tmp_path.joinpath(dirname)) - assert [x.name for x in items] == ["test_%s" % dirname] + assert [x.name for x in items] == [f"test_{dirname}"] # changing cwd to each subdirectory and running pytest without # arguments collects the tests in that directory normally for dirname in ("a", "b", "c"): monkeypatch.chdir(pytester.path.joinpath(dirname)) items, reprec = pytester.inline_genitems() - assert [x.name for x in items] == ["test_%s" % dirname] + assert [x.name for x in items] == [f"test_{dirname}"] + + def test_missing_permissions_on_unselected_directory_doesnt_crash( + self, pytester: Pytester + ) -> None: + """Regression test for #12120.""" + test = pytester.makepyfile(test="def test(): pass") + bad = pytester.mkdir("bad") + try: + bad.chmod(0) + + result = pytester.runpytest(test) + finally: + bad.chmod(750) + bad.rmdir() + + assert result.ret == ExitCode.OK + result.assert_outcomes(passed=1) class TestCollectPluginHookRelay: @@ -518,7 +510,7 @@ def test_collect_topdir(self, pytester: Pytester) -> None: assert len(colitems) == 1 assert colitems[0].path == topdir - def get_reported_items(self, hookrec: HookRecorder) -> List[Item]: + def get_reported_items(self, hookrec: HookRecorder) -> list[Item]: """Return pytest.Item instances reported by the pytest_collectreport hook""" calls = hookrec.getcalls("pytest_collectreport") return [ @@ -572,7 +564,7 @@ def test_method(self): def test_collect_custom_nodes_multi_id(self, pytester: Pytester) -> None: p = pytester.makepyfile("def test_func(): pass") pytester.makeconftest( - """ + f""" import pytest class SpecialItem(pytest.Item): def runtest(self): @@ -581,10 +573,9 @@ class SpecialFile(pytest.File): def collect(self): return [SpecialItem.from_parent(name="check", parent=self)] def pytest_collect_file(file_path, parent): - if file_path.name == %r: + if file_path.name == {p.name!r}: return SpecialFile.from_parent(path=file_path, parent=parent) """ - % p.name ) id = p.name @@ -862,7 +853,7 @@ def runtest(self): result = pytester.runpytest() assert result.ret == 0 result.stdout.fnmatch_lines(["*2 passed*"]) - res = pytester.runpytest("%s::item2" % p.name) + res = pytester.runpytest(f"{p.name}::item2") res.stdout.fnmatch_lines(["*1 passed*"]) @@ -1444,7 +1435,7 @@ def test_nodeid(request): symlink_to_sub = out_of_tree.joinpath("symlink_to_sub") symlink_or_skip(sub, symlink_to_sub) os.chdir(sub) - result = pytester.runpytest("-vs", "--rootdir=%s" % sub, symlink_to_sub) + result = pytester.runpytest("-vs", f"--rootdir={sub}", symlink_to_sub) result.stdout.fnmatch_lines( [ # Should not contain "sub/"! @@ -1857,3 +1848,33 @@ def test_do_not_collect_symlink_siblings( # Ensure we collect it only once if we pass the symlinked directory. result = pytester.runpytest(symlink_path, "-sv") result.assert_outcomes(passed=1) + + +@pytest.mark.parametrize( + "exception_class, msg", + [ + (KeyboardInterrupt, "*!!! KeyboardInterrupt !!!*"), + (SystemExit, "INTERNALERROR> SystemExit"), + ], +) +def test_respect_system_exceptions( + pytester: Pytester, + exception_class: type[BaseException], + msg: str, +): + head = "Before exception" + tail = "After exception" + ensure_file(pytester.path / "test_eggs.py").write_text( + f"print('{head}')", encoding="UTF-8" + ) + ensure_file(pytester.path / "test_ham.py").write_text( + f"raise {exception_class.__name__}()", encoding="UTF-8" + ) + ensure_file(pytester.path / "test_spam.py").write_text( + f"print('{tail}')", encoding="UTF-8" + ) + + result = pytester.runpytest_subprocess("-s") + result.stdout.fnmatch_lines([f"*{head}*"]) + result.stdout.fnmatch_lines([msg]) + result.stdout.no_fnmatch_line(f"*{tail}*") diff --git a/testing/test_compat.py b/testing/test_compat.py index c898af7c531..2c6b0269c27 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,11 +1,12 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import enum from functools import cached_property from functools import partial from functools import wraps import sys from typing import TYPE_CHECKING -from typing import Union from _pytest.compat import _PytestWrapper from _pytest.compat import assert_never @@ -94,7 +95,7 @@ def foo(x): assert get_real_func(partial(foo)) is foo -@pytest.mark.skipif(sys.version_info >= (3, 11), reason="couroutine removed") +@pytest.mark.skipif(sys.version_info >= (3, 11), reason="coroutine removed") def test_is_generator_asyncio(pytester: Pytester) -> None: pytester.makepyfile( """ @@ -216,7 +217,7 @@ def prop(self) -> int: def test_assert_never_union() -> None: - x: Union[int, str] = 10 + x: int | str = 10 if isinstance(x, int): pass diff --git a/testing/test_config.py b/testing/test_config.py index 147c2cb851c..232839399e2 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import dataclasses import importlib.metadata import os @@ -7,12 +9,7 @@ import sys import textwrap from typing import Any -from typing import Dict -from typing import List from typing import Sequence -from typing import Tuple -from typing import Type -from typing import Union import _pytest._code from _pytest.config import _get_plugin_specs_as_list @@ -67,13 +64,12 @@ def test_setupcfg_uses_toolpytest_with_pytest(self, pytester: Pytester) -> None: p1 = pytester.makepyfile("def test(): pass") pytester.makefile( ".cfg", - setup=""" + setup=f""" [tool:pytest] - testpaths=%s + testpaths={p1.name} [pytest] testpaths=ignored - """ - % p1.name, + """, ) result = pytester.runpytest() result.stdout.fnmatch_lines(["configfile: setup.cfg", "* 1 passed in *"]) @@ -217,7 +213,7 @@ def test_toml_parse_error(self, pytester: Pytester) -> None: def test_confcutdir_default_without_configfile(self, pytester: Pytester) -> None: # If --confcutdir is not specified, and there is no configfile, default - # to the roothpath. + # to the rootpath. sub = pytester.mkdir("sub") os.chdir(sub) config = pytester.parseconfigure() @@ -634,7 +630,7 @@ def test_absolute_win32_path(self, pytester: Pytester) -> None: class TestConfigAPI: def test_config_trace(self, pytester: Pytester) -> None: config = pytester.parseconfig() - values: List[str] = [] + values: list[str] = [] config.trace.root.setwriter(values.append) config.trace("hello") assert len(values) == 1 @@ -838,11 +834,10 @@ def pytest_addoption(parser): ) if str_val != "no-ini": pytester.makeini( - """ + f""" [pytest] - strip=%s + strip={str_val} """ - % str_val ) config = pytester.parseconfig() assert config.getini("strip") is bool_val @@ -998,7 +993,7 @@ def test_basic_behavior(self, _sys_snapshot) -> None: def test_invocation_params_args(self, _sys_snapshot) -> None: """Show that fromdictargs can handle args in their "orig" format""" - option_dict: Dict[str, object] = {} + option_dict: dict[str, object] = {} args = ["-vvvv", "-s", "a", "b"] config = Config.fromdictargs(option_dict, args) @@ -1212,7 +1207,7 @@ def distributions(): def test_disable_plugin_autoload( pytester: Pytester, monkeypatch: MonkeyPatch, - parse_args: Union[Tuple[str, str], Tuple[()]], + parse_args: tuple[str, str] | tuple[()], should_load: bool, ) -> None: class DummyEntryPoint: @@ -1290,8 +1285,8 @@ def test_invalid_options_show_extra_information(pytester: Pytester) -> None: result.stderr.fnmatch_lines( [ "*error: unrecognized arguments: --invalid-option*", - "* inifile: %s*" % pytester.path.joinpath("tox.ini"), - "* rootdir: %s*" % pytester.path, + "* inifile: {}*".format(pytester.path.joinpath("tox.ini")), + f"* rootdir: {pytester.path}*", ] ) @@ -1306,7 +1301,7 @@ def test_invalid_options_show_extra_information(pytester: Pytester) -> None: ], ) def test_consider_args_after_options_for_rootdir( - pytester: Pytester, args: List[str] + pytester: Pytester, args: list[str] ) -> None: """ Consider all arguments in the command-line for rootdir @@ -1423,8 +1418,8 @@ def pytest_load_initial_conftests(self): def test_get_plugin_specs_as_list() -> None: def exp_match(val: object) -> str: return ( - "Plugins may be specified as a sequence or a ','-separated string of plugin names. Got: %s" - % re.escape(repr(val)) + f"Plugins may be specified as a sequence or a ','-separated string " + f"of plugin names. Got: {re.escape(repr(val))}" ) with pytest.raises(pytest.UsageError, match=exp_match({"foo"})): @@ -1740,7 +1735,7 @@ def pytest_addoption(parser): ) pytester.makepyfile( r""" - def test_overriden(pytestconfig): + def test_overridden(pytestconfig): config_paths = pytestconfig.getini("paths") print(config_paths) for cpf in config_paths: @@ -1837,10 +1832,10 @@ def test_addopts_before_initini( self, monkeypatch: MonkeyPatch, _config_for_test, _sys_snapshot ) -> None: cache_dir = ".custom_cache" - monkeypatch.setenv("PYTEST_ADDOPTS", "-o cache_dir=%s" % cache_dir) + monkeypatch.setenv("PYTEST_ADDOPTS", f"-o cache_dir={cache_dir}") config = _config_for_test config._preparse([], addopts=True) - assert config._override_ini == ["cache_dir=%s" % cache_dir] + assert config._override_ini == [f"cache_dir={cache_dir}"] def test_addopts_from_env_not_concatenated( self, monkeypatch: MonkeyPatch, _config_for_test @@ -2048,7 +2043,7 @@ class DummyPlugin: ) def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None: p = pytester.makepyfile("def test(): pass") - result = pytester.runpytest(str(p), "-pno:%s" % plugin) + result = pytester.runpytest(str(p), f"-pno:{plugin}") if plugin == "python": assert result.ret == ExitCode.USAGE_ERROR @@ -2065,7 +2060,7 @@ def test_config_blocked_default_plugins(pytester: Pytester, plugin: str) -> None result.stdout.fnmatch_lines(["* 1 passed in *"]) p = pytester.makepyfile("def test(): assert 0") - result = pytester.runpytest(str(p), "-pno:%s" % plugin) + result = pytester.runpytest(str(p), f"-pno:{plugin}") assert result.ret == ExitCode.TESTS_FAILED if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 failed in *"]) @@ -2243,7 +2238,7 @@ def test_strtobool() -> None: ], ) def test_parse_warning_filter( - arg: str, escape: bool, expected: Tuple[str, str, Type[Warning], str, int] + arg: str, escape: bool, expected: tuple[str, str, type[Warning], str, int] ) -> None: assert parse_warning_filter(arg, escape=escape) == expected diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 3116dfe2584..d51846f2f5f 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,14 +1,13 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os from pathlib import Path import textwrap from typing import cast -from typing import Dict from typing import Generator from typing import List -from typing import Optional from typing import Sequence -from typing import Union from _pytest.config import ExitCode from _pytest.config import PytestPluginManager @@ -27,8 +26,8 @@ def ConftestWithSetinitial(path) -> PytestPluginManager: def conftest_setinitial( conftest: PytestPluginManager, - args: Sequence[Union[str, Path]], - confcutdir: Optional[Path] = None, + args: Sequence[str | Path], + confcutdir: Path | None = None, ) -> None: conftest._set_initial_conftests( args=args, @@ -70,7 +69,7 @@ def test_basic_init(self, basedir: Path) -> None: ) assert conftest._rget_with_confmod("a", p)[1] == 1 - def test_immediate_initialiation_and_incremental_are_the_same( + def test_immediate_initialization_and_incremental_are_the_same( self, basedir: Path ) -> None: conftest = PytestPluginManager() @@ -280,7 +279,7 @@ def pytest_addoption(parser): ), encoding="utf-8", ) - result = pytester.runpytest("-h", "--confcutdir=%s" % x, x) + result = pytester.runpytest("-h", f"--confcutdir={x}", x) result.stdout.fnmatch_lines(["*--xyz*"]) result.stdout.no_fnmatch_line("*warning: could not load initial*") @@ -380,7 +379,7 @@ def fixture(): """ ), } - pytester.makepyfile(**{"real/%s" % k: v for k, v in source.items()}) + pytester.makepyfile(**{f"real/{k}": v for k, v in source.items()}) # Create a build directory that contains symlinks to actual files # but doesn't symlink actual directories. @@ -396,13 +395,13 @@ def fixture(): @pytest.mark.skipif( os.path.normcase("x") != os.path.normcase("X"), - reason="only relevant for case insensitive file systems", + reason="only relevant for case-insensitive file systems", ) def test_conftest_badcase(pytester: Pytester) -> None: """Check conftest.py loading when directory casing is wrong (#5792).""" pytester.path.joinpath("JenkinsRoot/test").mkdir(parents=True) source = {"setup.py": "", "test/__init__.py": "", "test/conftest.py": ""} - pytester.makepyfile(**{"JenkinsRoot/%s" % k: v for k, v in source.items()}) + pytester.makepyfile(**{f"JenkinsRoot/{k}": v for k, v in source.items()}) os.chdir(pytester.path.joinpath("jenkinsroot/test")) result = pytester.runpytest() @@ -536,7 +535,7 @@ def pytest_addoption(parser): class TestConftestVisibility: - def _setup_tree(self, pytester: Pytester) -> Dict[str, Path]: # for issue616 + def _setup_tree(self, pytester: Pytester) -> dict[str, Path]: # for issue616 # example mostly taken from: # https://mail.python.org/pipermail/pytest-dev/2014-September/002617.html runner = pytester.mkdir("empty") @@ -638,9 +637,9 @@ def test_parsefactories_relative_node_ids( ) -> None: """#616""" dirs = self._setup_tree(pytester) - print("pytest run in cwd: %s" % (dirs[chdir].relative_to(pytester.path))) - print("pytestarg : %s" % testarg) - print("expected pass : %s" % expect_ntests_passed) + print(f"pytest run in cwd: {dirs[chdir].relative_to(pytester.path)}") + print(f"pytestarg : {testarg}") + print(f"expected pass : {expect_ntests_passed}") os.chdir(dirs[chdir]) reprec = pytester.inline_run( testarg, @@ -699,7 +698,7 @@ def out_of_reach(): pass args = [str(src)] if confcutdir: - args = ["--confcutdir=%s" % root.joinpath(confcutdir)] + args = [f"--confcutdir={root.joinpath(confcutdir)}"] result = pytester.runpytest(*args) match = "" if passed: diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 53ebadbdba4..37032f92354 100644 --- a/testing/test_debugging.py +++ b/testing/test_debugging.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import sys -from typing import List import _pytest._code from _pytest.debugging import _validate_usepdb_cls @@ -35,7 +36,7 @@ def runpdb_and_get_report(pytester: Pytester, source: str): @pytest.fixture -def custom_pdb_calls() -> List[str]: +def custom_pdb_calls() -> list[str]: called = [] # install dummy debugger class and track which methods were called on it @@ -221,7 +222,7 @@ def test_not_called_due_to_quit(): pass """ ) - child = pytester.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") child.expect("captured stdout") child.expect("get rekt") child.expect("captured stderr") @@ -246,7 +247,7 @@ def test_1(): assert False """ ) - child = pytester.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") child.expect("Pdb") output = child.before.decode("utf8") child.sendeof() @@ -283,7 +284,7 @@ def test_1(): assert False """ ) - child = pytester.spawn_pytest("--show-capture=all --pdb -p no:logging %s" % p1) + child = pytester.spawn_pytest(f"--show-capture=all --pdb -p no:logging {p1}") child.expect("get rekt") output = child.before.decode("utf8") assert "captured log" not in output @@ -303,7 +304,7 @@ def test_1(): pytest.raises(ValueError, globalfunc) """ ) - child = pytester.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") child.expect(".*def test_1") child.expect(".*pytest.raises.*globalfunc") child.expect("Pdb") @@ -320,7 +321,7 @@ def test_pdb_interaction_on_collection_issue181(self, pytester: Pytester) -> Non xxx """ ) - child = pytester.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") # child.expect(".*import pytest.*") child.expect("Pdb") child.sendline("c") @@ -335,7 +336,7 @@ def pytest_runtest_protocol(): """ ) p1 = pytester.makepyfile("def test_func(): pass") - child = pytester.spawn_pytest("--pdb %s" % p1) + child = pytester.spawn_pytest(f"--pdb {p1}") child.expect("Pdb") # INTERNALERROR is only displayed once via terminal reporter. @@ -461,7 +462,7 @@ def test_1(capsys, caplog): assert 0 """ ) - child = pytester.spawn_pytest("--pdb %s" % str(p1)) + child = pytester.spawn_pytest(f"--pdb {p1!s}") child.send("caplog.record_tuples\n") child.expect_exact( "[('test_pdb_with_caplog_on_pdb_invocation', 30, 'some_warning')]" @@ -501,7 +502,7 @@ def function_1(): ''' """ ) - child = pytester.spawn_pytest("--doctest-modules --pdb %s" % p1) + child = pytester.spawn_pytest(f"--doctest-modules --pdb {p1}") child.expect("Pdb") assert "UNEXPECTED EXCEPTION: AssertionError()" in child.before.decode("utf8") @@ -528,7 +529,7 @@ def function_1(): ) # NOTE: does not use pytest.set_trace, but Python's patched pdb, # therefore "-s" is required. - child = pytester.spawn_pytest("--doctest-modules --pdb -s %s" % p1) + child = pytester.spawn_pytest(f"--doctest-modules --pdb -s {p1}") child.expect("Pdb") child.sendline("q") rest = child.read().decode("utf8") @@ -621,7 +622,7 @@ def test_1(): pytest.fail("expected_failure") """ ) - child = pytester.spawn_pytest("--pdbcls=mytest:CustomPdb %s" % str(p1)) + child = pytester.spawn_pytest(f"--pdbcls=mytest:CustomPdb {p1!s}") child.expect(r"PDB set_trace \(IO-capturing turned off\)") child.expect(r"\n\(Pdb") child.sendline("debug foo()") @@ -658,7 +659,7 @@ def test_1(): pytest.set_trace() """ ) - child = pytester.spawn_pytest("-s %s" % p1) + child = pytester.spawn_pytest(f"-s {p1}") child.expect(r">>> PDB set_trace >>>") child.expect("Pdb") child.sendline("c") @@ -854,7 +855,7 @@ def test_post_mortem(): self.flush(child) def test_pdb_custom_cls( - self, pytester: Pytester, custom_pdb_calls: List[str] + self, pytester: Pytester, custom_pdb_calls: list[str] ) -> None: p1 = pytester.makepyfile("""xxx """) result = pytester.runpytest_inprocess( @@ -880,7 +881,7 @@ def test_pdb_validate_usepdb_cls(self): assert _validate_usepdb_cls("pdb:DoesNotExist") == ("pdb", "DoesNotExist") def test_pdb_custom_cls_without_pdb( - self, pytester: Pytester, custom_pdb_calls: List[str] + self, pytester: Pytester, custom_pdb_calls: list[str] ) -> None: p1 = pytester.makepyfile("""xxx """) result = pytester.runpytest_inprocess("--pdbcls=_pytest:_CustomPdb", p1) @@ -914,7 +915,7 @@ def test_foo(): """ ) monkeypatch.setenv("PYTHONPATH", str(pytester.path)) - child = pytester.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1)) + child = pytester.spawn_pytest(f"--pdbcls=custom_pdb:CustomPdb {p1!s}") child.expect("__init__") child.expect("custom set_trace>") @@ -1122,7 +1123,7 @@ def test_func_kw(myparam, request, func="func_kw"): def test_trace_after_runpytest(pytester: Pytester) -> None: - """Test that debugging's pytest_configure is re-entrant.""" + """Test that debugging's pytest_configure is reentrant.""" p1 = pytester.makepyfile( """ from _pytest.debugging import pytestPDB @@ -1153,7 +1154,7 @@ def test_inner(): def test_quit_with_swallowed_SystemExit(pytester: Pytester) -> None: - """Test that debugging's pytest_configure is re-entrant.""" + """Test that debugging's pytest_configure is reentrant.""" p1 = pytester.makepyfile( """ def call_pdb_set_trace(): @@ -1208,8 +1209,7 @@ def test_inner({fixture}): child.expect("Pdb") before = child.before.decode("utf8") assert ( - "> PDB set_trace (IO-capturing turned off for fixture %s) >" % (fixture) - in before + f"> PDB set_trace (IO-capturing turned off for fixture {fixture}) >" in before ) # Test that capturing is really suspended. @@ -1225,7 +1225,7 @@ def test_inner({fixture}): TestPDB.flush(child) assert child.exitstatus == 0 assert "= 1 passed in" in rest - assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest + assert f"> PDB continue (IO-capturing resumed for fixture {fixture}) >" in rest def test_pdbcls_via_local_module(pytester: Pytester) -> None: diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 58fce244f45..4aa4876c711 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,10 +1,11 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import inspect from pathlib import Path import sys import textwrap from typing import Callable -from typing import Optional from _pytest.doctest import _get_checker from _pytest.doctest import _is_main_py @@ -224,11 +225,7 @@ def test_doctest_unexpected_exception(self, pytester: Pytester): "Traceback (most recent call last):", ' File "*/doctest.py", line *, in __run', " *", - *( - (" *^^^^*",) - if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11) - else () - ), + *((" *^^^^*", " *", " *") if sys.version_info >= (3, 13) else ()), ' File "", line 1, in ', "ZeroDivisionError: division by zero", "*/test_doctest_unexpected_exception.txt:2: UnexpectedException", @@ -385,7 +382,7 @@ def some_property(self): "*= FAILURES =*", "*_ [[]doctest[]] test_doctest_linedata_on_property.Sample.some_property _*", "004 ", - "005 >>> Sample().some_property", + "005 *>>> Sample().some_property", "Expected:", " 'another thing'", "Got:", @@ -396,7 +393,7 @@ def some_property(self): ] ) - def test_doctest_no_linedata_on_overriden_property(self, pytester: Pytester): + def test_doctest_no_linedata_on_overridden_property(self, pytester: Pytester): pytester.makepyfile( """ class Sample(object): @@ -414,7 +411,7 @@ def some_property(self): result.stdout.fnmatch_lines( [ "*= FAILURES =*", - "*_ [[]doctest[]] test_doctest_no_linedata_on_overriden_property.Sample.some_property _*", + "*_ [[]doctest[]] test_doctest_no_linedata_on_overridden_property.Sample.some_property _*", "EXAMPLE LOCATION UNKNOWN, not showing all tests of that example", "[?][?][?] >>> Sample().some_property", "Expected:", @@ -422,7 +419,7 @@ def some_property(self): "Got:", " 'something'", "", - "*/test_doctest_no_linedata_on_overriden_property.py:None: DocTestFailure", + "*/test_doctest_no_linedata_on_overridden_property.py:None: DocTestFailure", "*= 1 failed in *", ] ) @@ -1160,7 +1157,7 @@ def makeit(doctest): pytester.maketxtfile(doctest) else: assert mode == "module" - pytester.makepyfile('"""\n%s"""' % doctest) + pytester.makepyfile(f'"""\n{doctest}"""') return makeit @@ -1599,7 +1596,7 @@ def __getattr__(self, _): "stop", [None, _is_mocked, lambda f: None, lambda f: False, lambda f: True] ) def test_warning_on_unwrap_of_broken_object( - stop: Optional[Callable[[object], object]], + stop: Callable[[object], object] | None, ) -> None: bad_instance = Broken() assert inspect.unwrap.__module__ == "inspect" diff --git a/testing/test_entry_points.py b/testing/test_entry_points.py index 68e3a8a92e4..543f3252b22 100644 --- a/testing/test_entry_points.py +++ b/testing/test_entry_points.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import importlib.metadata diff --git a/testing/test_error_diffs.py b/testing/test_error_diffs.py index f290eb1679f..741a6ca82d0 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -5,6 +5,8 @@ """ +from __future__ import annotations + from _pytest.pytester import Pytester import pytest diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index a3363de9816..c416e81d2d9 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import io import sys @@ -101,7 +103,7 @@ def test_timeout(): result = pytester.runpytest_subprocess(*args) tb_output = "most recent call first" if enabled: - result.stderr.fnmatch_lines(["*%s*" % tb_output]) + result.stderr.fnmatch_lines([f"*{tb_output}*"]) else: assert tb_output not in result.stderr.str() result.stdout.fnmatch_lines(["*1 passed*"]) diff --git a/testing/test_findpaths.py b/testing/test_findpaths.py index 260b9d07c9c..9532f1eef75 100644 --- a/testing/test_findpaths.py +++ b/testing/test_findpaths.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os from pathlib import Path from textwrap import dedent diff --git a/testing/test_helpconfig.py b/testing/test_helpconfig.py index 4906ef5c8f0..7fcf5804ace 100644 --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from _pytest.config import ExitCode from _pytest.pytester import Pytester import pytest @@ -10,7 +12,7 @@ def test_version_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None: assert result.ret == 0 result.stdout.fnmatch_lines([f"*pytest*{pytest.__version__}*imported from*"]) if pytestconfig.pluginmanager.list_plugin_distinfo(): - result.stdout.fnmatch_lines(["*setuptools registered plugins:", "*at*"]) + result.stdout.fnmatch_lines(["*registered third-party plugins:", "*at*"]) def test_version_less_verbose(pytester: Pytester, pytestconfig, monkeypatch) -> None: diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 3b92d65bdb9..fd1fecb54f1 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,14 +1,13 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from datetime import datetime +from datetime import timezone import os from pathlib import Path import platform from typing import cast -from typing import List -from typing import Optional -from typing import Tuple from typing import TYPE_CHECKING -from typing import Union from xml.dom import minidom import xmlschema @@ -39,12 +38,12 @@ def __init__(self, pytester: Pytester, schema: xmlschema.XMLSchema) -> None: self.schema = schema def __call__( - self, *args: Union[str, "os.PathLike[str]"], family: Optional[str] = "xunit1" - ) -> Tuple[RunResult, "DomNode"]: + self, *args: str | os.PathLike[str], family: str | None = "xunit1" + ) -> tuple[RunResult, DomNode]: if family: args = ("-o", "junit_family=" + family, *args) xml_path = self.pytester.path.joinpath("junit.xml") - result = self.pytester.runpytest("--junitxml=%s" % xml_path, *args) + result = self.pytester.runpytest(f"--junitxml={xml_path}", *args) if family == "xunit2": with xml_path.open(encoding="utf-8") as f: self.schema.validate(f) @@ -220,11 +219,11 @@ def test_pass(): pass """ ) - start_time = datetime.now() + start_time = datetime.now(timezone.utc) result, dom = run_and_parse(family=xunit_family) node = dom.find_first_by_tag("testsuite") - timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f") - assert start_time <= timestamp < datetime.now() + timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f%z") + assert start_time <= timestamp < datetime.now(timezone.utc) def test_timing_function( self, pytester: Pytester, run_and_parse: RunAndParse, mock_timing @@ -520,7 +519,7 @@ def test_fail(): ) result, dom = run_and_parse( - "-o", "junit_logging=%s" % junit_logging, family=xunit_family + "-o", f"junit_logging={junit_logging}", family=xunit_family ) assert result.ret, "Expected ret > 0" node = dom.find_first_by_tag("testsuite") @@ -605,11 +604,11 @@ def test_func(arg1): for index, char in enumerate("<&'"): tnode = node.find_nth_by_tag("testcase", index) tnode.assert_attr( - classname="test_failure_escape", name="test_func[%s]" % char + classname="test_failure_escape", name=f"test_func[{char}]" ) sysout = tnode.find_first_by_tag("system-out") text = sysout.text - assert "%s\n" % char in text + assert f"{char}\n" in text @parametrize_families def test_junit_prefixing( @@ -694,7 +693,7 @@ def test_fail(): assert 0 """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") if junit_logging in ["system-err", "out-err", "all"]: @@ -764,13 +763,12 @@ def test_collect_error( def test_unicode(self, pytester: Pytester, run_and_parse: RunAndParse) -> None: value = "hx\xc4\x85\xc4\x87\n" pytester.makepyfile( - """\ + f"""\ # coding: latin1 def test_hello(): - print(%r) + print({value!r}) assert 0 """ - % value ) result, dom = run_and_parse() assert result.ret == 1 @@ -805,7 +803,7 @@ def test_pass(): print('hello-stdout') """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") if junit_logging == "no": @@ -829,7 +827,7 @@ def test_pass(): sys.stderr.write('hello-stderr') """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") if junit_logging == "no": @@ -858,7 +856,7 @@ def test_function(arg): pass """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") if junit_logging == "no": @@ -888,7 +886,7 @@ def test_function(arg): pass """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") if junit_logging == "no": @@ -919,7 +917,7 @@ def test_function(arg): sys.stdout.write('hello-stdout call') """ ) - result, dom = run_and_parse("-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse("-o", f"junit_logging={junit_logging}") node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") if junit_logging == "no": @@ -941,7 +939,7 @@ def test_mangle_test_address() -> None: def test_dont_configure_on_workers(tmp_path: Path) -> None: - gotten: List[object] = [] + gotten: list[object] = [] class FakeConfig: if TYPE_CHECKING: @@ -1002,7 +1000,7 @@ def repr_failure(self, excinfo): @pytest.mark.parametrize("junit_logging", ["no", "system-out"]) def test_nullbyte(pytester: Pytester, junit_logging: str) -> None: - # A null byte can not occur in XML (see section 2.2 of the spec) + # A null byte cannot occur in XML (see section 2.2 of the spec) pytester.makepyfile( """ import sys @@ -1013,7 +1011,7 @@ def test_print_nullbyte(): """ ) xmlf = pytester.path.joinpath("junit.xml") - pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) + pytester.runpytest(f"--junitxml={xmlf}", "-o", f"junit_logging={junit_logging}") text = xmlf.read_text(encoding="utf-8") assert "\x00" not in text if junit_logging == "system-out": @@ -1035,7 +1033,7 @@ def test_print_nullbyte(): """ ) xmlf = pytester.path.joinpath("junit.xml") - pytester.runpytest("--junitxml=%s" % xmlf, "-o", "junit_logging=%s" % junit_logging) + pytester.runpytest(f"--junitxml={xmlf}", "-o", f"junit_logging={junit_logging}") text = xmlf.read_text(encoding="utf-8") if junit_logging == "system-out": assert "#x0" in text @@ -1071,9 +1069,9 @@ def test_invalid_xml_escape() -> None: for i in invalid: got = bin_xml_escape(chr(i)) if i <= 0xFF: - expected = "#x%02X" % i + expected = f"#x{i:02X}" else: - expected = "#x%04X" % i + expected = f"#x{i:04X}" assert got == expected for i in valid: assert chr(i) == bin_xml_escape(chr(i)) @@ -1184,7 +1182,7 @@ def test_unicode_issue368(pytester: Pytester) -> None: class Report(BaseReport): longrepr = ustr - sections: List[Tuple[str, str]] = [] + sections: list[tuple[str, str]] = [] nodeid = "something" location = "tests/filename.py", 42, "TestClass.method" when = "teardown" @@ -1496,7 +1494,7 @@ def test_global_properties(pytester: Pytester, xunit_family: str) -> None: log = LogXML(str(path), None, family=xunit_family) class Report(BaseReport): - sections: List[Tuple[str, str]] = [] + sections: list[tuple[str, str]] = [] nodeid = "test_node_id" log.pytest_sessionstart() @@ -1532,7 +1530,7 @@ def test_url_property(pytester: Pytester) -> None: class Report(BaseReport): longrepr = "FooBarBaz" - sections: List[Tuple[str, str]] = [] + sections: list[tuple[str, str]] = [] nodeid = "something" location = "tests/filename.py", 42, "TestClass.method" url = test_url @@ -1748,7 +1746,7 @@ def test_func(): """ ) result, dom = run_and_parse( - "-o", "junit_logging=%s" % junit_logging, family=xunit_family + "-o", f"junit_logging={junit_logging}", family=xunit_family ) assert result.ret == 1 node = dom.find_first_by_tag("testcase") diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index 49e620c1138..72854e4e5c0 100644 --- a/testing/test_legacypath.py +++ b/testing/test_legacypath.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from pathlib import Path from _pytest.compat import LEGACY_PATH @@ -79,7 +81,7 @@ def test_1(tmpdir): assert os.path.realpath(str(tmpdir)) == str(tmpdir) """ ) - result = pytester.runpytest("-s", p, "--basetemp=%s/bt" % linktemp) + result = pytester.runpytest("-s", p, f"--basetemp={linktemp}/bt") assert not result.ret @@ -155,7 +157,7 @@ def pytest_addoption(parser): ) pytester.makepyfile( r""" - def test_overriden(pytestconfig): + def test_overridden(pytestconfig): config_paths = pytestconfig.getini("paths") print(config_paths) for cpf in config_paths: diff --git a/testing/test_link_resolve.py b/testing/test_link_resolve.py index 0461cd75554..0557dae669d 100644 --- a/testing/test_link_resolve.py +++ b/testing/test_link_resolve.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from contextlib import contextmanager import os.path from pathlib import Path diff --git a/testing/test_main.py b/testing/test_main.py index 345aa1e62cf..94eac02ce63 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -1,10 +1,10 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import argparse import os from pathlib import Path import re -import sys -from typing import Optional from _pytest.config import ExitCode from _pytest.config import UsageError @@ -45,32 +45,18 @@ def pytest_internalerror(excrepr, excinfo): assert result.ret == ExitCode.INTERNAL_ERROR assert result.stdout.lines[0] == "INTERNALERROR> Traceback (most recent call last):" - end_lines = ( - result.stdout.lines[-4:] - if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11) - else result.stdout.lines[-3:] - ) + end_lines = result.stdout.lines[-3:] if exc == SystemExit: assert end_lines == [ f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart', 'INTERNALERROR> raise SystemExit("boom")', - *( - ("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",) - if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11) - else () - ), "INTERNALERROR> SystemExit: boom", ] else: assert end_lines == [ f'INTERNALERROR> File "{c1}", line 4, in pytest_sessionstart', 'INTERNALERROR> raise ValueError("boom")', - *( - ("INTERNALERROR> ^^^^^^^^^^^^^^^^^^^^^^^^",) - if (3, 11, 0, "beta", 4) > sys.version_info >= (3, 11) - else () - ), "INTERNALERROR> ValueError: boom", ] if returncode is False: @@ -81,7 +67,7 @@ def pytest_internalerror(excrepr, excinfo): @pytest.mark.parametrize("returncode", (None, 42)) def test_wrap_session_exit_sessionfinish( - returncode: Optional[int], pytester: Pytester + returncode: int | None, pytester: Pytester ) -> None: pytester.makeconftest( f""" diff --git a/testing/test_mark.py b/testing/test_mark.py index 2896afa4532..89eef7920cf 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1,8 +1,8 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os import sys -from typing import List -from typing import Optional from unittest import mock from _pytest.config import ExitCode @@ -214,7 +214,7 @@ def test_hello(): ], ) def test_mark_option( - expr: str, expected_passed: List[Optional[str]], pytester: Pytester + expr: str, expected_passed: list[str | None], pytester: Pytester ) -> None: pytester.makepyfile( """ @@ -233,12 +233,60 @@ def test_two(): assert passed_str == expected_passed +@pytest.mark.parametrize( + ("expr", "expected_passed"), + [ + ("car(color='red')", ["test_one"]), + ("car(color='red') or car(color='blue')", ["test_one", "test_two"]), + ("car and not car(temp=5)", ["test_one", "test_three"]), + ("car(temp=4)", ["test_one"]), + ("car(temp=4) or car(temp=5)", ["test_one", "test_two"]), + ("car(temp=4) and car(temp=5)", []), + ("car(temp=-5)", ["test_three"]), + ("car(ac=True)", ["test_one"]), + ("car(ac=False)", ["test_two"]), + ("car(ac=None)", ["test_three"]), # test NOT_NONE_SENTINEL + ], + ids=str, +) +def test_mark_option_with_kwargs( + expr: str, expected_passed: list[str | None], pytester: Pytester +) -> None: + pytester.makepyfile( + """ + import pytest + @pytest.mark.car + @pytest.mark.car(ac=True) + @pytest.mark.car(temp=4) + @pytest.mark.car(color="red") + def test_one(): + pass + @pytest.mark.car + @pytest.mark.car(ac=False) + @pytest.mark.car(temp=5) + @pytest.mark.car(color="blue") + def test_two(): + pass + @pytest.mark.car + @pytest.mark.car(ac=None) + @pytest.mark.car(temp=-5) + def test_three(): + pass + + """ + ) + rec = pytester.inline_run("-m", expr) + passed, skipped, fail = rec.listoutcomes() + passed_str = [x.nodeid.split("::")[-1] for x in passed] + assert passed_str == expected_passed + + @pytest.mark.parametrize( ("expr", "expected_passed"), [("interface", ["test_interface"]), ("not interface", ["test_nointer"])], ) def test_mark_option_custom( - expr: str, expected_passed: List[str], pytester: Pytester + expr: str, expected_passed: list[str], pytester: Pytester ) -> None: pytester.makeconftest( """ @@ -276,7 +324,7 @@ def test_nointer(): ], ) def test_keyword_option_custom( - expr: str, expected_passed: List[str], pytester: Pytester + expr: str, expected_passed: list[str], pytester: Pytester ) -> None: pytester.makepyfile( """ @@ -314,7 +362,7 @@ def test_keyword_option_considers_mark(pytester: Pytester) -> None: ], ) def test_keyword_option_parametrize( - expr: str, expected_passed: List[str], pytester: Pytester + expr: str, expected_passed: list[str], pytester: Pytester ) -> None: pytester.makepyfile( """ @@ -372,6 +420,10 @@ def test_func(arg): "not or", "at column 5: expected not OR left parenthesis OR identifier; got or", ), + ( + "nonexistent_mark(non_supported='kwarg')", + "Keyword expressions do not support call parameters", + ), ], ) def test_keyword_option_wrong_arguments( @@ -895,7 +947,7 @@ def test_ddd(): pass ) monkeypatch.chdir(pytester.path / "suite") - def get_collected_names(*args: str) -> List[str]: + def get_collected_names(*args: str) -> list[str]: _, rec = pytester.inline_genitems(*args) calls = rec.getcalls("pytest_collection_finish") assert len(calls) == 1 @@ -930,7 +982,7 @@ def test_aliases(self) -> None: @pytest.mark.parametrize("mark", [None, "", "skip", "xfail"]) def test_parameterset_for_parametrize_marks( - pytester: Pytester, mark: Optional[str] + pytester: Pytester, mark: str | None ) -> None: if mark is not None: pytester.makeini( diff --git a/testing/test_mark_expression.py b/testing/test_mark_expression.py index a7a9cf3044a..a61a9f21560 100644 --- a/testing/test_mark_expression.py +++ b/testing/test_mark_expression.py @@ -1,12 +1,17 @@ +from __future__ import annotations + from typing import Callable +from typing import cast +from _pytest.mark import MarkMatcher from _pytest.mark.expression import Expression +from _pytest.mark.expression import MatcherCall from _pytest.mark.expression import ParseError import pytest def evaluate(input: str, matcher: Callable[[str], bool]) -> bool: - return Expression.compile(input).evaluate(matcher) + return Expression.compile(input).evaluate(cast(MatcherCall, matcher)) def test_empty_is_false() -> None: @@ -61,7 +66,7 @@ def test_basic(expr: str, expected: bool) -> None: ("not not not not not true", False), ), ) -def test_syntax_oddeties(expr: str, expected: bool) -> None: +def test_syntax_oddities(expr: str, expected: bool) -> None: matcher = {"true": True, "false": False}.__getitem__ assert evaluate(expr, matcher) is expected @@ -151,6 +156,8 @@ def test_syntax_errors(expr: str, column: int, message: str) -> None: "1234", "1234abcd", "1234and", + "1234or", + "1234not", "notandor", "not_and_or", "not[and]or", @@ -193,3 +200,120 @@ def test_valid_idents(ident: str) -> None: def test_invalid_idents(ident: str) -> None: with pytest.raises(ParseError): evaluate(ident, lambda ident: True) + + +@pytest.mark.parametrize( + "expr, expected_error_msg", + ( + ("mark(True=False)", "unexpected reserved python keyword `True`"), + ("mark(def=False)", "unexpected reserved python keyword `def`"), + ("mark(class=False)", "unexpected reserved python keyword `class`"), + ("mark(if=False)", "unexpected reserved python keyword `if`"), + ("mark(else=False)", "unexpected reserved python keyword `else`"), + ("mark(valid=False, def=1)", "unexpected reserved python keyword `def`"), + ("mark(1)", "not a valid python identifier 1"), + ("mark(var:=False", "not a valid python identifier var:"), + ("mark(1=2)", "not a valid python identifier 1"), + ("mark(/=2)", "not a valid python identifier /"), + ("mark(var==", "expected identifier; got ="), + ("mark(var)", "expected =; got right parenthesis"), + ("mark(var=none)", 'unexpected character/s "none"'), + ("mark(var=1.1)", 'unexpected character/s "1.1"'), + ("mark(var=')", """closing quote "'" is missing"""), + ('mark(var=")', 'closing quote """ is missing'), + ("""mark(var="')""", 'closing quote """ is missing'), + ("""mark(var='")""", """closing quote "'" is missing"""), + ( + r"mark(var='\hugo')", + r'escaping with "\\" not supported in marker expression', + ), + ("mark(empty_list=[])", r'unexpected character/s "\[\]"'), + ("'str'", "expected not OR left parenthesis OR identifier; got string literal"), + ), +) +def test_invalid_kwarg_name_or_value( + expr: str, expected_error_msg: str, mark_matcher: MarkMatcher +) -> None: + with pytest.raises(ParseError, match=expected_error_msg): + assert evaluate(expr, mark_matcher) + + +@pytest.fixture(scope="session") +def mark_matcher() -> MarkMatcher: + markers = [ + pytest.mark.number_mark(a=1, b=2, c=3, d=999_999).mark, + pytest.mark.builtin_matchers_mark(x=True, y=False, z=None).mark, + pytest.mark.str_mark( # pylint: disable-next=non-ascii-name + m="M", space="with space", empty="", aaאבגדcc="aaאבגדcc", אבגד="אבגד" + ).mark, + ] + + return MarkMatcher.from_markers(markers) + + +@pytest.mark.parametrize( + "expr, expected", + ( + # happy cases + ("number_mark(a=1)", True), + ("number_mark(b=2)", True), + ("number_mark(a=1,b=2)", True), + ("number_mark(a=1, b=2)", True), + ("number_mark(d=999999)", True), + ("number_mark(a = 1,b= 2, c = 3)", True), + # sad cases + ("number_mark(a=6)", False), + ("number_mark(b=6)", False), + ("number_mark(a=1,b=6)", False), + ("number_mark(a=6,b=2)", False), + ("number_mark(a = 1,b= 2, c = 6)", False), + ("number_mark(a='1')", False), + ), +) +def test_keyword_expressions_with_numbers( + expr: str, expected: bool, mark_matcher: MarkMatcher +) -> None: + assert evaluate(expr, mark_matcher) is expected + + +@pytest.mark.parametrize( + "expr, expected", + ( + ("builtin_matchers_mark(x=True)", True), + ("builtin_matchers_mark(x=False)", False), + ("builtin_matchers_mark(y=True)", False), + ("builtin_matchers_mark(y=False)", True), + ("builtin_matchers_mark(z=None)", True), + ("builtin_matchers_mark(z=False)", False), + ("builtin_matchers_mark(z=True)", False), + ("builtin_matchers_mark(z=0)", False), + ("builtin_matchers_mark(z=1)", False), + ), +) +def test_builtin_matchers_keyword_expressions( + expr: str, expected: bool, mark_matcher: MarkMatcher +) -> None: + assert evaluate(expr, mark_matcher) is expected + + +@pytest.mark.parametrize( + "expr, expected", + ( + ("str_mark(m='M')", True), + ('str_mark(m="M")', True), + ("str_mark(aaאבגדcc='aaאבגדcc')", True), + ("str_mark(אבגד='אבגד')", True), + ("str_mark(space='with space')", True), + ("str_mark(empty='')", True), + ('str_mark(empty="")', True), + ("str_mark(m='wrong')", False), + ("str_mark(aaאבגדcc='wrong')", False), + ("str_mark(אבגד='wrong')", False), + ("str_mark(m='')", False), + ('str_mark(m="")', False), + ), +) +def test_str_keyword_expressions( + expr: str, expected: bool, mark_matcher: MarkMatcher +) -> None: + assert evaluate(expr, mark_matcher) is expected diff --git a/testing/test_meta.py b/testing/test_meta.py index 40ed95d6b47..e7d836f7ace 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -4,16 +4,17 @@ namespace being set, which is critical for the initialization of xdist. """ +from __future__ import annotations + import pkgutil import subprocess import sys -from typing import List import _pytest import pytest -def _modules() -> List[str]: +def _modules() -> list[str]: pytest_pkg: str = _pytest.__path__ # type: ignore return sorted( n diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index 12be774beca..079d8ff60ad 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -1,12 +1,12 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os from pathlib import Path import re import sys import textwrap -from typing import Dict from typing import Generator -from typing import Type from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester @@ -135,7 +135,7 @@ def test_setitem() -> None: def test_setitem_deleted_meanwhile() -> None: - d: Dict[str, object] = {} + d: dict[str, object] = {} monkeypatch = MonkeyPatch() monkeypatch.setitem(d, "x", 2) del d["x"] @@ -160,7 +160,7 @@ def test_setenv_deleted_meanwhile(before: bool) -> None: def test_delitem() -> None: - d: Dict[str, object] = {"x": 1} + d: dict[str, object] = {"x": 1} monkeypatch = MonkeyPatch() monkeypatch.delitem(d, "x") assert "x" not in d @@ -360,7 +360,7 @@ class SampleInherit(Sample): [Sample, SampleInherit], ids=["new", "new-inherit"], ) -def test_issue156_undo_staticmethod(Sample: Type[Sample]) -> None: +def test_issue156_undo_staticmethod(Sample: type[Sample]) -> None: monkeypatch = MonkeyPatch() monkeypatch.setattr(Sample, "hello", None) @@ -442,7 +442,7 @@ def test_syspath_prepend_with_namespace_packages( lib = ns.joinpath(dirname) lib.mkdir() lib.joinpath("__init__.py").write_text( - "def check(): return %r" % dirname, encoding="utf-8" + f"def check(): return {dirname!r}", encoding="utf-8" ) monkeypatch.syspath_prepend("hello") diff --git a/testing/test_nodes.py b/testing/test_nodes.py index a3caf471f70..f039acf243b 100644 --- a/testing/test_nodes.py +++ b/testing/test_nodes.py @@ -1,8 +1,9 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from pathlib import Path import re from typing import cast -from typing import Type import warnings from _pytest import nodes @@ -73,7 +74,7 @@ def runtest(self): "warn_type, msg", [(DeprecationWarning, "deprecated"), (PytestWarning, "pytest")] ) def test_node_warn_is_no_longer_only_pytest_warnings( - pytester: Pytester, warn_type: Type[Warning], msg: str + pytester: Pytester, warn_type: type[Warning], msg: str ) -> None: items = pytester.getitems( """ diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index e959dfd631b..14e2b5f69fb 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import argparse import locale import os diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 651a04da84a..8fdd60bac75 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -1,8 +1,8 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import email.message import io -from typing import List -from typing import Union from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester @@ -11,8 +11,8 @@ class TestPasteCapture: @pytest.fixture - def pastebinlist(self, monkeypatch, request) -> List[Union[str, bytes]]: - pastebinlist: List[Union[str, bytes]] = [] + def pastebinlist(self, monkeypatch, request) -> list[str | bytes]: + pastebinlist: list[str | bytes] = [] plugin = request.config.pluginmanager.getplugin("pastebin") monkeypatch.setattr(plugin, "create_new_paste", pastebinlist.append) return pastebinlist @@ -171,7 +171,7 @@ def test_create_new_paste(self, pastebin, mocked_urlopen) -> None: assert type(data) is bytes lexer = "text" assert url == "https://bpa.st" - assert "lexer=%s" % lexer in data.decode() + assert f"lexer={lexer}" in data.decode() assert "code=full-paste-contents" in data.decode() assert "expiry=1week" in data.decode() diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 688d13f2f05..81aba25f78f 100644 --- a/testing/test_pathlib.py +++ b/testing/test_pathlib.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import errno import importlib.abc import importlib.machinery @@ -12,9 +14,7 @@ from typing import Any from typing import Generator from typing import Iterator -from typing import Optional from typing import Sequence -from typing import Tuple import unittest.mock from _pytest.monkeypatch import MonkeyPatch @@ -865,7 +865,7 @@ def test_my_test(): def create_installed_doctests_and_tests_dir( self, path: Path, monkeypatch: MonkeyPatch - ) -> Tuple[Path, Path, Path]: + ) -> tuple[Path, Path, Path]: """ Create a directory structure where the application code is installed in a virtual environment, and the tests are in an outside ".tests" directory. @@ -1267,8 +1267,8 @@ def setup_imports_tracking(self, monkeypatch: MonkeyPatch) -> None: monkeypatch.setattr(sys, "pytest_namespace_packages_test", [], raising=False) def setup_directories( - self, tmp_path: Path, monkeypatch: Optional[MonkeyPatch], pytester: Pytester - ) -> Tuple[Path, Path]: + self, tmp_path: Path, monkeypatch: MonkeyPatch | None, pytester: Pytester + ) -> tuple[Path, Path]: # Use a code to guard against modules being imported more than once. # This is a safeguard in case future changes break this invariant. code = dedent( @@ -1438,7 +1438,7 @@ class CustomImporter(importlib.abc.MetaPathFinder): def find_spec( self, name: str, path: Any = None, target: Any = None - ) -> Optional[importlib.machinery.ModuleSpec]: + ) -> importlib.machinery.ModuleSpec | None: if name == "com": spec = importlib.machinery.ModuleSpec("com", loader=None) spec.submodule_search_locations = [str(com_root_2), str(com_root_1)] diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index da43364f643..db85124bf0d 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -1,9 +1,10 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os import shutil import sys import types -from typing import List from _pytest.config import Config from _pytest.config import ExitCode @@ -152,7 +153,7 @@ def pytest_plugin_registered(self): saveindent.append(pytestpm.trace.root.indent) raise ValueError() - values: List[str] = [] + values: list[str] = [] pytestpm.trace.root.setwriter(values.append) undo = pytestpm.enable_tracing() try: @@ -420,7 +421,7 @@ def test_consider_conftest_deps( pytestpm.consider_conftest(mod, registration_name="unused") -class TestPytestPluginManagerBootstrapming: +class TestPytestPluginManagerBootstrapping: def test_preparse_args(self, pytestpm: PytestPluginManager) -> None: pytest.raises( ImportError, lambda: pytestpm.consider_preparse(["xyz", "-p", "hello123"]) @@ -446,7 +447,7 @@ def test_plugin_prevent_register(self, pytestpm: PytestPluginManager) -> None: assert len(l2) == len(l1) assert 42 not in l2 - def test_plugin_prevent_register_unregistered_alredy_registered( + def test_plugin_prevent_register_unregistered_already_registered( self, pytestpm: PytestPluginManager ) -> None: pytestpm.register(42, name="abc") diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 9c6081a56db..87714b4708f 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -1,10 +1,11 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os import subprocess import sys import time from types import ModuleType -from typing import List from _pytest.config import ExitCode from _pytest.config import PytestPluginManager @@ -227,7 +228,7 @@ def test_inline_run_test_module_not_cleaned_up(self, pytester: Pytester) -> None def spy_factory(self): class SysModulesSnapshotSpy: - instances: List["SysModulesSnapshotSpy"] = [] + instances: list[SysModulesSnapshotSpy] = [] def __init__(self, preserve=None) -> None: SysModulesSnapshotSpy.instances.append(self) @@ -399,7 +400,7 @@ def test_preserve_container(self, monkeypatch: MonkeyPatch, path_type) -> None: original_data = list(getattr(sys, path_type)) original_other = getattr(sys, other_path_type) original_other_data = list(original_other) - new: List[object] = [] + new: list[object] = [] snapshot = SysPathsSnapshot() monkeypatch.setattr(sys, path_type, new) snapshot.restore() diff --git a/testing/test_python_path.py b/testing/test_python_path.py index 73a8725680f..1db02252d22 100644 --- a/testing/test_python_path.py +++ b/testing/test_python_path.py @@ -1,9 +1,9 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import sys from textwrap import dedent from typing import Generator -from typing import List -from typing import Optional from _pytest.pytester import Pytester import pytest @@ -91,8 +91,8 @@ def test_clean_up(pytester: Pytester) -> None: pytester.makefile(".ini", pytest="[pytest]\npythonpath=I_SHALL_BE_REMOVED\n") pytester.makepyfile(test_foo="""def test_foo(): pass""") - before: Optional[List[str]] = None - after: Optional[List[str]] = None + before: list[str] | None = None + after: list[str] | None = None class Plugin: @pytest.hookimpl(wrapper=True, tryfirst=True) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 27ee9aa72f0..384f2b66a15 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -1,9 +1,7 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import sys -from typing import List -from typing import Optional -from typing import Type -from typing import Union import warnings import pytest @@ -54,7 +52,7 @@ class ChildOfChildWarning(ChildWarning): pass @staticmethod - def raise_warnings_from_list(_warnings: List[Type[Warning]]): + def raise_warnings_from_list(_warnings: list[type[Warning]]): for warn in _warnings: warnings.warn(f"Warning {warn().__repr__()}", warn) @@ -134,7 +132,7 @@ def test_invalid_enter_exit(self) -> None: class TestDeprecatedCall: """test pytest.deprecated_call()""" - def dep(self, i: int, j: Optional[int] = None) -> int: + def dep(self, i: int, j: int | None = None) -> int: if i == 0: warnings.warn("is deprecated", DeprecationWarning, stacklevel=1) return 42 @@ -563,7 +561,7 @@ def test_raise_type_error_on_invalid_warning() -> None: pytest.param(Warning(), id="Warning"), ], ) -def test_no_raise_type_error_on_valid_warning(message: Union[str, Warning]) -> None: +def test_no_raise_type_error_on_valid_warning(message: str | Warning) -> None: """Check pytest.warns validates warning messages are strings (#10865) or Warning instances (#11959).""" with pytest.warns(Warning): diff --git a/testing/test_reports.py b/testing/test_reports.py index c6baeebc9dd..3e314d2aade 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from typing import Sequence -from typing import Union from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionRepr @@ -100,14 +101,13 @@ def test_repr_entry(): rep_entries = rep.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries - for i in range(len(a_entries)): - rep_entry = rep_entries[i] + assert len(rep_entries) == len(a_entries) # python < 3.10 zip(strict=True) + for a_entry, rep_entry in zip(a_entries, rep_entries): assert isinstance(rep_entry, ReprEntry) assert rep_entry.reprfileloc is not None assert rep_entry.reprfuncargs is not None assert rep_entry.reprlocals is not None - a_entry = a_entries[i] assert isinstance(a_entry, ReprEntry) assert a_entry.reprfileloc is not None assert a_entry.reprfuncargs is not None @@ -146,9 +146,10 @@ def test_repr_entry_native(): rep_entries = rep.longrepr.reprtraceback.reprentries a_entries = a.longrepr.reprtraceback.reprentries - for i in range(len(a_entries)): - assert isinstance(rep_entries[i], ReprEntryNative) - assert rep_entries[i].lines == a_entries[i].lines + assert len(rep_entries) == len(a_entries) # python < 3.10 zip(strict=True) + for rep_entry, a_entry in zip(rep_entries, a_entries): + assert isinstance(rep_entry, ReprEntryNative) + assert rep_entry.lines == a_entry.lines def test_itemreport_outcomes(self, pytester: Pytester) -> None: # This test came originally from test_remote.py in xdist (ca03269). @@ -294,8 +295,8 @@ def test_a(): reprec = pytester.inline_run() if report_class is TestReport: - reports: Union[Sequence[TestReport], Sequence[CollectReport]] = ( - reprec.getreports("pytest_runtest_logreport") + reports: Sequence[TestReport] | Sequence[CollectReport] = reprec.getreports( + "pytest_runtest_logreport" ) # we have 3 reports: setup/call/teardown assert len(reports) == 3 diff --git a/testing/test_runner.py b/testing/test_runner.py index 8b41ec28a38..1b59ff78633 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -1,14 +1,12 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from functools import partial import inspect import os from pathlib import Path import sys import types -from typing import Dict -from typing import List -from typing import Tuple -from typing import Type import warnings from _pytest import outcomes @@ -142,6 +140,43 @@ def raiser(exc): assert isinstance(func.exceptions[0], TypeError) # type: ignore assert isinstance(func.exceptions[1], ValueError) # type: ignore + def test_cached_exception_doesnt_get_longer(self, pytester: Pytester) -> None: + """Regression test for #12204 (the "BTW" case).""" + pytester.makepyfile(test="") + # If the collector.setup() raises, all collected items error with this + # exception. + pytester.makeconftest( + """ + import pytest + + class MyItem(pytest.Item): + def runtest(self) -> None: pass + + class MyBadCollector(pytest.Collector): + def collect(self): + return [ + MyItem.from_parent(self, name="one"), + MyItem.from_parent(self, name="two"), + MyItem.from_parent(self, name="three"), + ] + + def setup(self): + 1 / 0 + + def pytest_collect_file(file_path, parent): + if file_path.name == "test.py": + return MyBadCollector.from_parent(parent, name='bad') + """ + ) + + result = pytester.runpytest_inprocess("--tb=native") + assert result.ret == ExitCode.TESTS_FAILED + failures = result.reprec.getfailures() # type: ignore[attr-defined] + assert len(failures) == 3 + lines1 = failures[1].longrepr.reprtraceback.reprentries[0].lines + lines2 = failures[2].longrepr.reprtraceback.reprentries[0].lines + assert len(lines1) == len(lines2) + class BaseFunctionalTests: def test_passfunction(self, pytester: Pytester) -> None: @@ -409,7 +444,7 @@ def test_func(): # assert rep.outcome.when == "setup" # assert rep.outcome.where.lineno == 3 # assert rep.outcome.where.path.basename == "test_func.py" - # assert instanace(rep.failed.failurerepr, PythonFailureRepr) + # assert isinstance(rep.failed.failurerepr, PythonFailureRepr) def test_systemexit_does_not_bail_out(self, pytester: Pytester) -> None: try: @@ -486,7 +521,7 @@ class TestClass(object): assert res[1].name == "TestClass" -reporttypes: List[Type[reports.BaseReport]] = [ +reporttypes: list[type[reports.BaseReport]] = [ reports.BaseReport, reports.TestReport, reports.CollectReport, @@ -496,9 +531,9 @@ class TestClass(object): @pytest.mark.parametrize( "reporttype", reporttypes, ids=[x.__name__ for x in reporttypes] ) -def test_report_extra_parameters(reporttype: Type[reports.BaseReport]) -> None: +def test_report_extra_parameters(reporttype: type[reports.BaseReport]) -> None: args = list(inspect.signature(reporttype.__init__).parameters.keys())[1:] - basekw: Dict[str, List[object]] = dict.fromkeys(args, []) + basekw: dict[str, list[object]] = {arg: [] for arg in args} report = reporttype(newthing=1, **basekw) assert report.newthing == 1 @@ -995,7 +1030,7 @@ def runtest(self): assert sys.last_type is IndexError assert isinstance(sys.last_value, IndexError) if sys.version_info >= (3, 12, 0): - assert isinstance(sys.last_exc, IndexError) # type: ignore[attr-defined] + assert isinstance(sys.last_exc, IndexError) assert sys.last_value.args[0] == "TEST" assert sys.last_traceback @@ -1011,7 +1046,7 @@ def runtest(self): def test_current_test_env_var(pytester: Pytester, monkeypatch: MonkeyPatch) -> None: - pytest_current_test_vars: List[Tuple[str, str]] = [] + pytest_current_test_vars: list[tuple[str, str]] = [] monkeypatch.setattr( sys, "pytest_current_test_vars", pytest_current_test_vars, raising=False ) @@ -1179,3 +1214,53 @@ def test(): result = pytester.runpytest_inprocess() assert result.ret == ExitCode.OK assert os.environ["PYTEST_VERSION"] == "old version" + + +def test_teardown_session_failed(pytester: Pytester) -> None: + """Test that higher-scoped fixture teardowns run in the context of the last + item after the test session bails early due to --maxfail. + + Regression test for #11706. + """ + pytester.makepyfile( + """ + import pytest + + @pytest.fixture(scope="module") + def baz(): + yield + pytest.fail("This is a failing teardown") + + def test_foo(baz): + pytest.fail("This is a failing test") + + def test_bar(): pass + """ + ) + result = pytester.runpytest("--maxfail=1") + result.assert_outcomes(failed=1, errors=1) + + +def test_teardown_session_stopped(pytester: Pytester) -> None: + """Test that higher-scoped fixture teardowns run in the context of the last + item after the test session bails early due to --stepwise. + + Regression test for #11706. + """ + pytester.makepyfile( + """ + import pytest + + @pytest.fixture(scope="module") + def baz(): + yield + pytest.fail("This is a failing teardown") + + def test_foo(baz): + pytest.fail("This is a failing test") + + def test_bar(): pass + """ + ) + result = pytester.runpytest("--stepwise") + result.assert_outcomes(failed=1, errors=1) diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index 587c9eb9fef..75e838a49e8 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -1,7 +1,7 @@ # mypy: allow-untyped-defs """Test correct setup/teardowns at module, class, and instance level.""" -from typing import List +from __future__ import annotations from _pytest.pytester import Pytester import pytest @@ -251,7 +251,7 @@ def test_setup_teardown_function_level_with_optional_argument( """Parameter to setup/teardown xunit-style functions parameter is now optional (#1728).""" import sys - trace_setups_teardowns: List[str] = [] + trace_setups_teardowns: list[str] = [] monkeypatch.setattr( sys, "trace_setups_teardowns", trace_setups_teardowns, raising=False ) diff --git a/testing/test_scope.py b/testing/test_scope.py index 1727c2ee1bb..3cb811469a9 100644 --- a/testing/test_scope.py +++ b/testing/test_scope.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re from _pytest.scope import Scope diff --git a/testing/test_session.py b/testing/test_session.py index 8624af478b1..ba904916033 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch from _pytest.pytester import Pytester diff --git a/testing/test_setuponly.py b/testing/test_setuponly.py index 8638f5a6140..87123bd9a16 100644 --- a/testing/test_setuponly.py +++ b/testing/test_setuponly.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import sys from _pytest.config import ExitCode diff --git a/testing/test_setupplan.py b/testing/test_setupplan.py index d51a1873959..5a9211d7806 100644 --- a/testing/test_setupplan.py +++ b/testing/test_setupplan.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from _pytest.pytester import Pytester diff --git a/testing/test_skipping.py b/testing/test_skipping.py index a1511b26d1c..d1a63b1d920 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import sys import textwrap @@ -297,13 +299,12 @@ class TestXFail: @pytest.mark.parametrize("strict", [True, False]) def test_xfail_simple(self, pytester: Pytester, strict: bool) -> None: item = pytester.getitem( - """ + f""" import pytest - @pytest.mark.xfail(strict=%s) + @pytest.mark.xfail(strict={strict}) def test_func(): assert 0 """ - % strict ) reports = runtestprotocol(item, log=False) assert len(reports) == 3 @@ -630,15 +631,14 @@ def test_foo(): @pytest.mark.parametrize("strict", [True, False]) def test_strict_xfail(self, pytester: Pytester, strict: bool) -> None: p = pytester.makepyfile( - """ + f""" import pytest - @pytest.mark.xfail(reason='unsupported feature', strict=%s) + @pytest.mark.xfail(reason='unsupported feature', strict={strict}) def test_foo(): with open('foo_executed', 'w', encoding='utf-8'): pass # make sure test executes """ - % strict ) result = pytester.runpytest(p, "-rxX") if strict: @@ -658,14 +658,13 @@ def test_foo(): @pytest.mark.parametrize("strict", [True, False]) def test_strict_xfail_condition(self, pytester: Pytester, strict: bool) -> None: p = pytester.makepyfile( - """ + f""" import pytest - @pytest.mark.xfail(False, reason='unsupported feature', strict=%s) + @pytest.mark.xfail(False, reason='unsupported feature', strict={strict}) def test_foo(): pass """ - % strict ) result = pytester.runpytest(p, "-rxX") result.stdout.fnmatch_lines(["*1 passed*"]) @@ -674,14 +673,13 @@ def test_foo(): @pytest.mark.parametrize("strict", [True, False]) def test_xfail_condition_keyword(self, pytester: Pytester, strict: bool) -> None: p = pytester.makepyfile( - """ + f""" import pytest - @pytest.mark.xfail(condition=False, reason='unsupported feature', strict=%s) + @pytest.mark.xfail(condition=False, reason='unsupported feature', strict={strict}) def test_foo(): pass """ - % strict ) result = pytester.runpytest(p, "-rxX") result.stdout.fnmatch_lines(["*1 passed*"]) @@ -692,11 +690,10 @@ def test_strict_xfail_default_from_file( self, pytester: Pytester, strict_val ) -> None: pytester.makeini( - """ + f""" [pytest] - xfail_strict = %s + xfail_strict = {strict_val} """ - % strict_val ) p = pytester.makepyfile( """ @@ -1143,8 +1140,8 @@ def test_func(): result = pytester.runpytest() markline = " ^" pypy_version_info = getattr(sys, "pypy_version_info", None) - if pypy_version_info is not None and pypy_version_info < (6,): - markline = markline[1:] + if pypy_version_info is not None: + markline = markline[7:] if sys.version_info >= (3, 10): expected = [ diff --git a/testing/test_stash.py b/testing/test_stash.py index e523c4e6f2b..c7f6f4f95fe 100644 --- a/testing/test_stash.py +++ b/testing/test_stash.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from _pytest.stash import Stash from _pytest.stash import StashKey import pytest diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 472afea6620..affdb73375e 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from pathlib import Path from _pytest.cacheprovider import Cache diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 170f1efcf91..11ad623fb6b 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Terminal reporting of the full testing process.""" +from __future__ import annotations + from io import StringIO import os from pathlib import Path @@ -8,10 +10,7 @@ import textwrap from types import SimpleNamespace from typing import cast -from typing import Dict -from typing import List from typing import NamedTuple -from typing import Tuple import pluggy @@ -327,16 +326,17 @@ def test_rewrite(self, pytester: Pytester, monkeypatch) -> None: tr.rewrite("hey", erase=True) assert f.getvalue() == "hello" + "\r" + "hey" + (6 * " ") + @pytest.mark.parametrize("category", ["foo", "failed", "error", "passed"]) def test_report_teststatus_explicit_markup( - self, monkeypatch: MonkeyPatch, pytester: Pytester, color_mapping + self, monkeypatch: MonkeyPatch, pytester: Pytester, color_mapping, category: str ) -> None: """Test that TerminalReporter handles markup explicitly provided by a pytest_report_teststatus hook.""" monkeypatch.setenv("PY_COLORS", "1") pytester.makeconftest( - """ + f""" def pytest_report_teststatus(report): - return 'foo', 'F', ('FOO', {'red': True}) + return {category !r}, 'F', ('FOO', {{'red': True}}) """ ) pytester.makepyfile( @@ -345,7 +345,9 @@ def test_foobar(): pass """ ) + result = pytester.runpytest("-v") + assert not result.stderr.lines result.stdout.fnmatch_lines( color_mapping.format_for_fnmatch(["*{red}FOO{reset}*"]) ) @@ -926,7 +928,7 @@ def test_header(self, pytester: Pytester) -> None: def test_header_absolute_testpath( self, pytester: Pytester, monkeypatch: MonkeyPatch ) -> None: - """Regresstion test for #7814.""" + """Regression test for #7814.""" tests = pytester.path.joinpath("tests") tests.mkdir() pytester.makepyprojecttoml( @@ -1148,6 +1150,44 @@ def test(): result.stdout.fnmatch_lines([expected]) assert result.stdout.lines.count(expected) == 1 + def test_summary_s_folded(self, pytester: Pytester) -> None: + """Test that skipped tests are correctly folded""" + pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("param", [True, False]) + @pytest.mark.skip("Some reason") + def test(param): + pass + """ + ) + result = pytester.runpytest("-rs") + expected = "SKIPPED [2] test_summary_s_folded.py:3: Some reason" + result.stdout.fnmatch_lines([expected]) + assert result.stdout.lines.count(expected) == 1 + + def test_summary_s_unfolded(self, pytester: Pytester) -> None: + """Test that skipped tests are not folded if --no-fold-skipped is set""" + pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("param", [True, False]) + @pytest.mark.skip("Some reason") + def test(param): + pass + """ + ) + result = pytester.runpytest("-rs", "--no-fold-skipped") + expected = [ + "SKIPPED test_summary_s_unfolded.py::test[True] - Skipped: Some reason", + "SKIPPED test_summary_s_unfolded.py::test[False] - Skipped: Some reason", + ] + result.stdout.fnmatch_lines(expected) + assert result.stdout.lines.count(expected[0]) == 1 + assert result.stdout.lines.count(expected[1]) == 1 + @pytest.mark.parametrize( ("use_ci", "expected_message"), @@ -1421,7 +1461,7 @@ def test_opt(arg): s = result.stdout.str() assert "arg = 42" not in s assert "x = 0" not in s - result.stdout.fnmatch_lines(["*%s:8*" % p.name, " assert x", "E assert*"]) + result.stdout.fnmatch_lines([f"*{p.name}:8*", " assert x", "E assert*"]) result = pytester.runpytest() s = result.stdout.str() assert "x = 0" in s @@ -1497,8 +1537,8 @@ def test_func(): """ ) for tbopt in ["long", "short", "no"]: - print("testing --tb=%s..." % tbopt) - result = pytester.runpytest("-rN", "--tb=%s" % tbopt) + print(f"testing --tb={tbopt}...") + result = pytester.runpytest("-rN", f"--tb={tbopt}") s = result.stdout.str() if tbopt == "long": assert "print(6*7)" in s @@ -1528,7 +1568,7 @@ def test_func2(): result = pytester.runpytest("--tb=line") bn = p.name result.stdout.fnmatch_lines( - ["*%s:3: IndexError*" % bn, "*%s:8: AssertionError: hello*" % bn] + [f"*{bn}:3: IndexError*", f"*{bn}:8: AssertionError: hello*"] ) s = result.stdout.str() assert "def test_func2" not in s @@ -1544,7 +1584,7 @@ def test_func1(): result = pytester.runpytest("--tb=line") result.stdout.str() bn = p.name - result.stdout.fnmatch_lines(["*%s:3: Failed: test_func1" % bn]) + result.stdout.fnmatch_lines([f"*{bn}:3: Failed: test_func1"]) def test_pytest_report_header(self, pytester: Pytester, option) -> None: pytester.makeconftest( @@ -1929,9 +1969,9 @@ def tr() -> TerminalReporter: ) def test_summary_stats( tr: TerminalReporter, - exp_line: List[Tuple[str, Dict[str, bool]]], + exp_line: list[tuple[str, dict[str, bool]]], exp_color: str, - stats_arg: Dict[str, List[object]], + stats_arg: dict[str, list[object]], ) -> None: tr.stats = stats_arg @@ -1945,7 +1985,7 @@ class fake_session: # Reset cache. tr._main_color = None - print("Based on stats: %s" % stats_arg) + print(f"Based on stats: {stats_arg}") print(f'Expect summary: "{exp_line}"; with color "{exp_color}"') (line, color) = tr.build_summary_stats_line() print(f'Actually got: "{line}"; with color "{color}"') @@ -2386,8 +2426,8 @@ def __init__(self): self.option = Namespace(verbose=0) class rep: - def _get_verbose_word(self, *args): - return mocked_verbose_word + def _get_verbose_word_with_markup(self, *args): + return mocked_verbose_word, {} class longrepr: class reprcrash: @@ -2609,8 +2649,8 @@ def test_foo(): monkeypatch.setenv("PYTEST_THEME", "invalid") result = pytester.runpytest_subprocess("--color=yes") result.stderr.fnmatch_lines( - "ERROR: PYTEST_THEME environment variable had an invalid value: 'invalid'. " - "Only valid pygment styles are allowed." + "ERROR: PYTEST_THEME environment variable has an invalid value: 'invalid'. " + "Hint: See available pygments styles with `pygmentize -L styles`." ) def test_code_highlight_invalid_theme_mode( @@ -2625,8 +2665,8 @@ def test_foo(): monkeypatch.setenv("PYTEST_THEME_MODE", "invalid") result = pytester.runpytest_subprocess("--color=yes") result.stderr.fnmatch_lines( - "ERROR: PYTEST_THEME_MODE environment variable had an invalid value: 'invalid'. " - "The only allowed values are 'dark' and 'light'." + "ERROR: PYTEST_THEME_MODE environment variable has an invalid value: 'invalid'. " + "The allowed values are 'dark' (default) and 'light'." ) @@ -2909,54 +2949,77 @@ def test_xfail_reason(): assert result.stdout.lines.count(expect2) == 1 -def test_summary_xfail_tb(pytester: Pytester) -> None: - pytester.makepyfile( +@pytest.fixture() +def xfail_testfile(pytester: Pytester) -> Path: + return pytester.makepyfile( """ import pytest - @pytest.mark.xfail - def test_xfail(): + def test_fail(): a, b = 1, 2 assert a == b + + @pytest.mark.xfail + def test_xfail(): + c, d = 3, 4 + assert c == d """ ) - result = pytester.runpytest("-rx") + + +def test_xfail_tb_default(xfail_testfile, pytester: Pytester) -> None: + result = pytester.runpytest(xfail_testfile) + + # test_fail, show traceback + result.stdout.fnmatch_lines( + [ + "*= FAILURES =*", + "*_ test_fail _*", + "*def test_fail():*", + "* a, b = 1, 2*", + "*> assert a == b*", + "*E assert 1 == 2*", + ] + ) + + # test_xfail, don't show traceback + result.stdout.no_fnmatch_line("*= XFAILURES =*") + + +def test_xfail_tb_true(xfail_testfile, pytester: Pytester) -> None: + result = pytester.runpytest(xfail_testfile, "--xfail-tb") + + # both test_fail and test_xfail, show traceback result.stdout.fnmatch_lines( [ + "*= FAILURES =*", + "*_ test_fail _*", + "*def test_fail():*", + "* a, b = 1, 2*", + "*> assert a == b*", + "*E assert 1 == 2*", "*= XFAILURES =*", "*_ test_xfail _*", - "* @pytest.mark.xfail*", - "* def test_xfail():*", - "* a, b = 1, 2*", - "> *assert a == b*", - "E *assert 1 == 2*", - "test_summary_xfail_tb.py:6: AssertionError*", - "*= short test summary info =*", - "XFAIL test_summary_xfail_tb.py::test_xfail", - "*= 1 xfailed in * =*", + "*def test_xfail():*", + "* c, d = 3, 4*", + "*> assert c == d*", + "*E assert 3 == 4*", + "*short test summary info*", ] ) -def test_xfail_tb_line(pytester: Pytester) -> None: - pytester.makepyfile( - """ - import pytest +def test_xfail_tb_line(xfail_testfile, pytester: Pytester) -> None: + result = pytester.runpytest(xfail_testfile, "--xfail-tb", "--tb=line") - @pytest.mark.xfail - def test_xfail(): - a, b = 1, 2 - assert a == b - """ - ) - result = pytester.runpytest("-rx", "--tb=line") + # both test_fail and test_xfail, show line result.stdout.fnmatch_lines( [ + "*= FAILURES =*", + "*test_xfail_tb_line.py:5: assert 1 == 2", "*= XFAILURES =*", - "*test_xfail_tb_line.py:6: assert 1 == 2", - "*= short test summary info =*", - "XFAIL test_xfail_tb_line.py::test_xfail", - "*= 1 xfailed in * =*", + "*test_xfail_tb_line.py:10: assert 3 == 4", + "*short test summary info*", ] ) diff --git a/testing/test_threadexception.py b/testing/test_threadexception.py index 99837b94e8a..abd30144914 100644 --- a/testing/test_threadexception.py +++ b/testing/test_threadexception.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from _pytest.pytester import Pytester import pytest diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 331ee7da6c7..865d8e0b05c 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import dataclasses import os from pathlib import Path @@ -6,8 +8,6 @@ import sys from typing import Callable from typing import cast -from typing import List -from typing import Union import warnings from _pytest import pathlib @@ -34,7 +34,7 @@ def test_tmp_path_fixture(pytester: Pytester) -> None: @dataclasses.dataclass class FakeConfig: - basetemp: Union[str, Path] + basetemp: str | Path @property def trace(self): @@ -87,11 +87,11 @@ def test_1(tmp_path): pass """ ) - pytester.runpytest(p, "--basetemp=%s" % mytemp) + pytester.runpytest(p, f"--basetemp={mytemp}") assert mytemp.exists() mytemp.joinpath("hello").touch() - pytester.runpytest(p, "--basetemp=%s" % mytemp) + pytester.runpytest(p, f"--basetemp={mytemp}") assert mytemp.exists() assert not mytemp.joinpath("hello").exists() @@ -248,7 +248,7 @@ def test_abs_path(tmp_path_factory): """ ) - result = pytester.runpytest(p, "--basetemp=%s" % mytemp) + result = pytester.runpytest(p, f"--basetemp={mytemp}") if is_ok: assert result.ret == 0 assert mytemp.joinpath(basename).exists() @@ -394,7 +394,7 @@ def test_cleanup_lock_create(self, tmp_path): def test_lock_register_cleanup_removal(self, tmp_path: Path) -> None: lock = create_cleanup_lock(tmp_path) - registry: List[Callable[..., None]] = [] + registry: list[Callable[..., None]] = [] register_cleanup_lock_removal(lock, register=registry.append) (cleanup_func,) = registry diff --git a/testing/test_unittest.py b/testing/test_unittest.py index d726e74d603..56224c08228 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1,7 +1,7 @@ # mypy: allow-untyped-defs -import gc +from __future__ import annotations + import sys -from typing import List from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch @@ -192,30 +192,35 @@ def test_check(self): def test_teardown_issue1649(pytester: Pytester) -> None: """ Are TestCase objects cleaned up? Often unittest TestCase objects set - attributes that are large and expensive during setUp. + attributes that are large and expensive during test run or setUp. The TestCase will not be cleaned up if the test fails, because it would then exist in the stackframe. + + Regression test for #1649 (see also #12367). """ - testpath = pytester.makepyfile( + pytester.makepyfile( """ import unittest - class TestCaseObjectsShouldBeCleanedUp(unittest.TestCase): - def setUp(self): - self.an_expensive_object = 1 - def test_demo(self): - pass + import gc - """ + class TestCaseObjectsShouldBeCleanedUp(unittest.TestCase): + def test_expensive(self): + self.an_expensive_obj = object() + + def test_is_it_still_alive(self): + gc.collect() + for obj in gc.get_objects(): + if type(obj).__name__ == "TestCaseObjectsShouldBeCleanedUp": + assert not hasattr(obj, "an_expensive_obj") + break + else: + assert False, "Could not find TestCaseObjectsShouldBeCleanedUp instance" + """ ) - pytester.inline_run("-s", testpath) - gc.collect() - - # Either already destroyed, or didn't run setUp. - for obj in gc.get_objects(): - if type(obj).__name__ == "TestCaseObjectsShouldBeCleanedUp": - assert not hasattr(obj, "an_expensive_obj") + result = pytester.runpytest() + assert result.ret == ExitCode.OK def test_unittest_skip_issue148(pytester: Pytester) -> None: @@ -299,7 +304,7 @@ def test_func2(self): @classmethod def tearDownClass(cls): cls.x -= 1 - def test_teareddown(): + def test_torn_down(): assert MyTestCase.x == 0 """ ) @@ -346,7 +351,7 @@ def test_func2(self): assert self.x == 1 def teardown_class(cls): cls.x -= 1 - def test_teareddown(): + def test_torn_down(): assert MyTestCase.x == 0 """ ) @@ -380,7 +385,7 @@ def test_hello(self): @pytest.mark.parametrize("type", ["Error", "Failure"]) def test_testcase_custom_exception_info(pytester: Pytester, type: str) -> None: pytester.makepyfile( - """ + f""" from typing import Generic, TypeVar from unittest import TestCase import pytest, _pytest._code @@ -409,7 +414,7 @@ def from_exc_info(cls, *args, **kwargs): def test_hello(self): pass - """.format(**locals()) + """ ) result = pytester.runpytest() result.stdout.fnmatch_lines( @@ -881,7 +886,7 @@ def test_method1(self): def tearDownClass(cls): cls.x = 1 - def test_not_teareddown(): + def test_not_torn_down(): assert TestFoo.x == 0 """ @@ -1210,7 +1215,7 @@ def test_pdb_teardown_called(pytester: Pytester, monkeypatch: MonkeyPatch) -> No We delay the normal tearDown() calls when --pdb is given, so this ensures we are calling tearDown() eventually to avoid memory leaks when using --pdb. """ - teardowns: List[str] = [] + teardowns: list[str] = [] monkeypatch.setattr( pytest, "test_pdb_teardown_called_teardowns", teardowns, raising=False ) @@ -1247,7 +1252,7 @@ def test_pdb_teardown_skipped_for_functions( With --pdb, setUp and tearDown should not be called for tests skipped via a decorator (#7215). """ - tracked: List[str] = [] + tracked: list[str] = [] monkeypatch.setattr(pytest, "track_pdb_teardown_skipped", tracked, raising=False) pytester.makepyfile( @@ -1282,7 +1287,7 @@ def test_pdb_teardown_skipped_for_classes( With --pdb, setUp and tearDown should not be called for tests skipped via a decorator on the class (#10060). """ - tracked: List[str] = [] + tracked: list[str] = [] monkeypatch.setattr(pytest, "track_pdb_teardown_skipped", tracked, raising=False) pytester.makepyfile( @@ -1640,3 +1645,31 @@ def test_it2(self): pass assert skipped == 1 assert failed == 0 assert reprec.ret == ExitCode.NO_TESTS_COLLECTED + + +def test_abstract_testcase_is_not_collected(pytester: Pytester) -> None: + """Regression test for #12275.""" + pytester.makepyfile( + """ + import abc + import unittest + + class TestBase(unittest.TestCase, abc.ABC): + @abc.abstractmethod + def abstract1(self): pass + + @abc.abstractmethod + def abstract2(self): pass + + def test_it(self): pass + + class TestPartial(TestBase): + def abstract1(self): pass + + class TestConcrete(TestPartial): + def abstract2(self): pass + """ + ) + result = pytester.runpytest() + assert result.ret == ExitCode.OK + result.assert_outcomes(passed=1) diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index 1657cfe4a84..a15c754d067 100644 --- a/testing/test_unraisableexception.py +++ b/testing/test_unraisableexception.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys from _pytest.pytester import Pytester diff --git a/testing/test_warning_types.py b/testing/test_warning_types.py index a50d278bde2..19fe0f8a272 100644 --- a/testing/test_warning_types.py +++ b/testing/test_warning_types.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import inspect from _pytest import warning_types diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 3ef0cd3b546..d4d0e0b7f93 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -1,9 +1,8 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os import sys -from typing import List -from typing import Optional -from typing import Tuple import warnings from _pytest.fixtures import FixtureRequest @@ -44,7 +43,7 @@ def test_normal_flow(pytester: Pytester, pyfile_with_warnings) -> None: result = pytester.runpytest(pyfile_with_warnings) result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "test_normal_flow.py::test_func", "*normal_flow_module.py:3: UserWarning: user warning", '* warnings.warn(UserWarning("user warning"))', @@ -75,7 +74,7 @@ def test_func(fix): result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "*test_setup_teardown_warnings.py:6: UserWarning: warning during setup", '*warnings.warn(UserWarning("warning during setup"))', "*test_setup_teardown_warnings.py:8: UserWarning: warning during teardown", @@ -143,7 +142,7 @@ def test_func(fix): result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "*test_unicode.py:7: UserWarning: \u6d4b\u8bd5*", "* 1 passed, 1 warning*", ] @@ -280,10 +279,8 @@ def pytest_warning_recorded(self, warning_message, when, nodeid, location): ("call warning", "runtest", "test_warning_recorded_hook.py::test_func"), ("teardown warning", "runtest", "test_warning_recorded_hook.py::test_func"), ] - for index in range(len(expected)): - collected_result = collected[index] - expected_result = expected[index] - + assert len(collected) == len(expected) # python < 3.10 zip(strict=True) + for collected_result, expected_result in zip(collected, expected): assert collected_result[0] == expected_result[0], str(collected) assert collected_result[1] == expected_result[1], str(collected) assert collected_result[2] == expected_result[2], str(collected) @@ -315,7 +312,7 @@ def test_foo(): result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", " *collection_warnings.py:3: UserWarning: collection warning", ' warnings.warn(UserWarning("collection warning"))', "* 1 passed, 1 warning*", @@ -374,7 +371,7 @@ def test_bar(): else: result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "*test_hide_pytest_internal_warnings.py:4: PytestWarning: some internal warning", "* 1 passed, 1 warning *", ] @@ -461,7 +458,7 @@ def test_shown_by_default(self, pytester: Pytester, customize_filters) -> None: result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "*test_shown_by_default.py:3: DeprecationWarning: collection", "*test_shown_by_default.py:7: PendingDeprecationWarning: test run", "* 1 passed, 2 warnings*", @@ -492,7 +489,7 @@ def test_hidden_by_mark(self, pytester: Pytester) -> None: result = pytester.runpytest_subprocess() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "*test_hidden_by_mark.py:3: DeprecationWarning: collection", "* 1 passed, 1 warning*", ] @@ -555,7 +552,7 @@ def test(): class TestAssertionWarnings: @staticmethod def assert_result_warns(result, msg) -> None: - result.stdout.fnmatch_lines(["*PytestAssertRewriteWarning: %s*" % msg]) + result.stdout.fnmatch_lines([f"*PytestAssertRewriteWarning: {msg}*"]) def test_tuple_warning(self, pytester: Pytester) -> None: pytester.makepyfile( @@ -585,7 +582,7 @@ def test_group_warnings_by_message(pytester: Pytester) -> None: result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "test_group_warnings_by_message.py::test_foo[[]0[]]", "test_group_warnings_by_message.py::test_foo[[]1[]]", "test_group_warnings_by_message.py::test_foo[[]2[]]", @@ -617,14 +614,14 @@ def test_group_warnings_by_message_summary(pytester: Pytester) -> None: result = pytester.runpytest() result.stdout.fnmatch_lines( [ - "*== %s ==*" % WARNINGS_SUMMARY_HEADER, + f"*== {WARNINGS_SUMMARY_HEADER} ==*", "test_1.py: 21 warnings", "test_2.py: 1 warning", - " */test_1.py:8: UserWarning: foo", + " */test_1.py:10: UserWarning: foo", " warnings.warn(UserWarning(msg))", "", "test_1.py: 20 warnings", - " */test_1.py:8: UserWarning: bar", + " */test_1.py:10: UserWarning: bar", " warnings.warn(UserWarning(msg))", "", "-- Docs: *", @@ -656,8 +653,8 @@ class TestStackLevel: @pytest.fixture def capwarn(self, pytester: Pytester): class CapturedWarnings: - captured: List[ - Tuple[warnings.WarningMessage, Optional[Tuple[str, int, str]]] + captured: list[ + tuple[warnings.WarningMessage, tuple[str, int, str] | None] ] = [] @classmethod diff --git a/testing/typing_checks.py b/testing/typing_checks.py index 4b146a25110..d4d6a97aea6 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -5,6 +5,8 @@ none of the code triggers any mypy errors. """ +from __future__ import annotations + import contextlib from typing import Optional diff --git a/tox.ini b/tox.ini index cb3ca4b8366..61563ca2c5f 100644 --- a/tox.ini +++ b/tox.ini @@ -9,6 +9,7 @@ envlist = py310 py311 py312 + py313 pypy3 py38-{pexpect,xdist,unittestextras,numpy,pluggymain,pylib} doctesting @@ -25,6 +26,20 @@ envlist = [testenv] +description = + run the tests + coverage: collecting coverage + exceptiongroup: against `exceptiongroup` + nobyte: in no-bytecode mode + lsof: with `--lsof` pytest CLI option + numpy: against `numpy` + pexpect: against `pexpect` + pluggymain: against the bleeding edge `pluggy` from Git + pylib: against `py` lib + unittestextras: against the unit test extras + xdist: with pytest in parallel mode + under `{basepython}` + doctesting: including doctests commands = {env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:{env:_PYTEST_TOX_DEFAULT_POSARGS:}} doctesting: {env:_PYTEST_TOX_COVERAGE_RUN:} pytest --doctest-modules --pyargs _pytest @@ -71,6 +86,8 @@ deps = {env:_PYTEST_TOX_EXTRA_DEP:} [testenv:linting] +description = + run pre-commit-defined linters under `{basepython}` skip_install = True basepython = python3 deps = pre-commit>=2.9.3 @@ -80,23 +97,32 @@ setenv = PYTHONWARNDEFAULTENCODING= [testenv:docs] -basepython = python3 +description = + build the documentation site under \ + `{toxinidir}{/}doc{/}en{/}_build{/}html` with `{basepython}` +basepython = python3.12 # sync with rtd to get errors usedevelop = True deps = -r{toxinidir}/doc/en/requirements.txt - # https://github.com/twisted/towncrier/issues/340 - towncrier<21.3.0 +allowlist_externals = + git commands = - python scripts/towncrier-draft-to-file.py - # the '-t changelog_towncrier_draft' tags makes sphinx include the draft - # 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:} + # Retrieve possibly missing commits: + -git fetch --unshallow + -git fetch --tags + + sphinx-build \ + -j auto \ + -W --keep-going \ + -b html doc/en doc/en/_build/html \ + {posargs:} setenv = # Sphinx is not clean of this warning. PYTHONWARNDEFAULTENCODING= [testenv:docs-checklinks] +description = + check the links in the documentation with `{basepython}` basepython = python3 usedevelop = True changedir = doc/en @@ -108,6 +134,8 @@ setenv = PYTHONWARNDEFAULTENCODING= [testenv:regen] +description = + regenerate documentation examples under `{basepython}` changedir = doc/en basepython = python3 passenv = @@ -125,6 +153,8 @@ setenv = PYTHONWARNDEFAULTENCODING= [testenv:plugins] +description = + run reverse dependency testing against pytest plugins under `{basepython}` # use latest versions of all plugins, including pre-releases pip_pre=true # use latest pip to get new dependency resolver (#7783) @@ -134,15 +164,13 @@ changedir = testing/plugins_integration deps = -rtesting/plugins_integration/requirements.txt setenv = PYTHONPATH=. -# Command temporarily removed until pytest-bdd is fixed: -# https://github.com/pytest-dev/pytest/pull/11785 -# pytest bdd_wallet.py commands = pip check + pytest bdd_wallet.py pytest --cov=. simple_integration.py pytest --ds=django_settings simple_integration.py pytest --html=simple.html simple_integration.py - pytest --reruns 5 simple_integration.py + pytest --reruns 5 simple_integration.py pytest_rerunfailures_integration.py pytest pytest_anyio_integration.py pytest pytest_asyncio_integration.py pytest pytest_mock_integration.py @@ -151,6 +179,8 @@ commands = pytest simple_integration.py --force-sugar --flakes [testenv:py38-freeze] +description = + test pytest frozen with `pyinstaller` under `{basepython}` changedir = testing/freeze deps = pyinstaller