diff --git a/.github/chronographer.yml b/.github/chronographer.yml new file mode 100644 index 000000000..803db1e34 --- /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 000000000..5d62fca12 --- /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 38ce72602..000000000 --- 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 640a0f1c2..f5ea4d397 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -54,7 +54,7 @@ jobs: path: dist - name: Publish package to PyPI - uses: pypa/gh-action-pypi-publish@v1.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 45e9c918a..64b87eee2 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" @@ -182,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: @@ -222,8 +247,22 @@ 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 + token: 1eca3b1f-31a2-4fb8-a8c3-138b441b50a7 #repo token; cfg read fails 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 1015d01c9..ade8452af 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@6d6857d36972b65feb161a90e484f2984215f83e + 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 9fccf93f7..c4557b33a 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 42fbc31ea..56d9a0c86 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.8" + rev: "v0.4.10" hooks: - id: ruff args: ["--fix"] @@ -66,8 +66,41 @@ 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: py-deprecated name: py library is deprecated diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 76% rename from .readthedocs.yml rename to .readthedocs.yaml index 266d4e07a..f7370f1bb 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 2916a7d7d..fb3244a03 100644 --- a/AUTHORS +++ b/AUTHORS @@ -149,6 +149,7 @@ Evgeny Seliverstov Fabian Sturm Fabien Zarifian Fabio Zadrozny +Farbod Ahmadian faph Felix Hofstätter Felix Nieuwenhuizen @@ -212,6 +213,7 @@ Jordan Guymon Jordan Moldow Jordan Speicher Joseph Hunkeler +Joseph Sawaya Josh Karpel Joshua Bronson Jurko Gospodnetić @@ -244,6 +246,7 @@ Levon Saldamli Lewis Cowles Llandy Riveron Del Risco Loic Esteve +lovetheguitar Lukas Bednar Luke Murphy Maciek Fijalkowski @@ -259,6 +262,7 @@ Marc Bresson Marco Gorelli Mark Abramowitz Mark Dickinson +Mark Vong Marko Pacak Markus Unterwaditzer Martijn Faassen @@ -302,6 +306,7 @@ Nicholas Devenish Nicholas Murphy Niclas Olofsson Nicolas Delaby +Nico Vidal Nikolay Kondratyev Nipunn Koorapati Oleg Pidsadnyi @@ -391,6 +396,7 @@ Stefano Taschini Steffen Allner Stephan Obermann Sven-Hendrik Haase +Sviatoslav Sydorenko Sylvain Marié Tadek Teleżyński Takafumi Arakaki @@ -450,6 +456,7 @@ 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 0bf440da2..12e2b18bb 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,5 +1,5 @@ ============================ -Contribution getting started +Contributing ============================ Contributions are highly welcomed and appreciated. Every little bit of help counts, @@ -124,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 0bb13c75a..139c292ec 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys diff --git a/bench/bench_argcomplete.py b/bench/bench_argcomplete.py index 459a12f93..468c59217 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 4e7371b6f..35abeef41 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 1226c73bd..579f7b248 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 fd5c292d9..9145cc0ce 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 d3db111e1..0f106e16b 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 3a77dcdce..31ab43244 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 000000000..3b34da34b --- /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/11797.bugfix.rst b/changelog/11797.bugfix.rst new file mode 100644 index 000000000..94b72da00 --- /dev/null +++ b/changelog/11797.bugfix.rst @@ -0,0 +1 @@ +:func:`pytest.approx` now correctly handles :class:`Sequence `-like objects. diff --git a/changelog/12281.feature.rst b/changelog/12281.feature.rst new file mode 100644 index 000000000..c6e8e3b30 --- /dev/null +++ b/changelog/12281.feature.rst @@ -0,0 +1,8 @@ +Added support for keyword matching in marker expressions. + +Now tests can be selected by marker keyword arguments. +Supported values are :class:`int`, (unescaped) :class:`str`, :class:`bool` & :data:`None`. + +See :ref:`marker examples ` for more information. + +-- by :user:`lovetheguitar` diff --git a/changelog/12467.improvement.rst b/changelog/12467.improvement.rst new file mode 100644 index 000000000..b1e0581ed --- /dev/null +++ b/changelog/12467.improvement.rst @@ -0,0 +1,3 @@ +Migrated all internal type-annotations to the python3.10+ style by using the `annotations` future import. + +-- by :user:`RonnyPfannschmidt` diff --git a/changelog/12469.doc.rst b/changelog/12469.doc.rst new file mode 100644 index 000000000..234031535 --- /dev/null +++ b/changelog/12469.doc.rst @@ -0,0 +1,6 @@ +The external plugin mentions in the documentation now avoid mentioning +:std:doc:`setuptools entry-points ` as the concept is +much more generic nowadays. Instead, the terminology of "external", +"installed", or "third-party" plugins (or packages) replaces that. + +-- by :user:`webknjaz` diff --git a/changelog/12469.improvement.rst b/changelog/12469.improvement.rst new file mode 100644 index 000000000..a90fb1e66 --- /dev/null +++ b/changelog/12469.improvement.rst @@ -0,0 +1,4 @@ +The console output now uses the "third-party plugins" terminology, +replacing the previously established but confusing and outdated +reference to :std:doc:`setuptools ` +-- by :user:`webknjaz`. diff --git a/changelog/12493.contrib.rst b/changelog/12493.contrib.rst new file mode 100644 index 000000000..db3d04569 --- /dev/null +++ b/changelog/12493.contrib.rst @@ -0,0 +1,13 @@ +The change log draft preview integration has been refactored to use a +third party extension ``sphinxcontib-towncrier``. The previous in-repo +script was putting the change log preview file at +:file:`doc/en/_changelog_towncrier_draft.rst`. Said file is no longer +ignored in Git and might show up among untracked files in the +development environments of the contributors. To address that, the +contributors can run the following command that will clean it up: + +.. code-block:: console + + $ git clean -x -i -- doc/en/_changelog_towncrier_draft.rst + +-- by :user:`webknjaz` diff --git a/changelog/12498.contrib.rst b/changelog/12498.contrib.rst new file mode 100644 index 000000000..436c6f0e9 --- /dev/null +++ b/changelog/12498.contrib.rst @@ -0,0 +1,5 @@ +All the undocumented ``tox`` environments now have descriptions. +They can be listed in one's development environment by invoking +``tox -av`` in a terminal. + +-- by :user:`webknjaz` diff --git a/changelog/12501.contrib.rst b/changelog/12501.contrib.rst new file mode 100644 index 000000000..6f434c287 --- /dev/null +++ b/changelog/12501.contrib.rst @@ -0,0 +1,11 @@ +The changelog configuration has been updated to introduce more accurate +audience-tailored categories. Previously, there was a ``trivial`` +change log fragment type with an unclear and broad meaning. It was +removed and we now have ``contrib``, ``misc`` and ``packaging`` in +place of it. + +The new change note types target the readers who are downstream +packagers and project contributors. Additionally, the miscellaneous +section is kept for unspecified updates that do not fit anywhere else. + +-- by :user:`webknjaz` diff --git a/changelog/12502.contrib.rst b/changelog/12502.contrib.rst new file mode 100644 index 000000000..a7db42252 --- /dev/null +++ b/changelog/12502.contrib.rst @@ -0,0 +1,7 @@ +The UX of the GitHub automation making pull requests to update the +plugin list has been updated. Previously, the maintainers had to close +the automatically created pull requests and re-open them to trigger the +CI runs. From now on, they only need to click the `Ready for review` +button instead. + +-- by :user:`webknjaz`. diff --git a/changelog/12522.contrib.rst b/changelog/12522.contrib.rst new file mode 100644 index 000000000..dd9943171 --- /dev/null +++ b/changelog/12522.contrib.rst @@ -0,0 +1,4 @@ +The ``:pull:`` RST role has been replaced with a shorter +``:pr:`` due to starting to use the implementation from +the third-party :pypi:`sphinx-issues` Sphinx extension +-- by :user:`webknjaz`. diff --git a/changelog/12533.contrib.rst b/changelog/12533.contrib.rst new file mode 100644 index 000000000..3da7007a0 --- /dev/null +++ b/changelog/12533.contrib.rst @@ -0,0 +1,7 @@ +The ``extlinks`` Sphinx extension is no longer enabled. The ``:bpo:`` +role it used to declare has been removed with that. BPO itself has +migrated to GitHub some years ago and it is possible to link the +respective issues by using their GitHub issue numbers and the +``:issue:`` role that the ``sphinx-issues`` extension implements. + +-- by :user:`webknjaz` diff --git a/changelog/12544.improvement.rst b/changelog/12544.improvement.rst new file mode 100644 index 000000000..9edbf7c4f --- /dev/null +++ b/changelog/12544.improvement.rst @@ -0,0 +1,4 @@ +The _in_venv function now detects Python virtual environments by checking +for a pyvenv.cfg file, ensuring reliable detection on various platforms. + +-- by :user:`zachsnickers`. diff --git a/changelog/389.improvement.rst b/changelog/389.improvement.rst new file mode 100644 index 000000000..f8e2c19fd --- /dev/null +++ b/changelog/389.improvement.rst @@ -0,0 +1,38 @@ +The readability of assertion introspection of bound methods has been enhanced +-- by :user:`farbodahm`, :user:`webknjaz`, :user:`obestwalter`, :user:`flub` +and :user:`glyphack`. + +Earlier, it was like: + +.. code-block:: console + + =================================== FAILURES =================================== + _____________________________________ test _____________________________________ + + def test(): + > assert Help().fun() == 2 + E assert 1 == 2 + E + where 1 = >() + E + where > = .fun + E + where = Help() + + example.py:7: AssertionError + =========================== 1 failed in 0.03 seconds =========================== + + +And now it's like: + +.. code-block:: console + + =================================== FAILURES =================================== + _____________________________________ test _____________________________________ + + def test(): + > assert Help().fun() == 2 + E assert 1 == 2 + E + where 1 = fun() + E + where fun = .fun + E + where = Help() + + test_local.py:13: AssertionError + =========================== 1 failed in 0.03 seconds =========================== diff --git a/changelog/7662.improvement.rst b/changelog/7662.improvement.rst new file mode 100644 index 000000000..b6ae1ba7e --- /dev/null +++ b/changelog/7662.improvement.rst @@ -0,0 +1 @@ +Added timezone information to the testsuite timestamp in the JUnit XML report. diff --git a/changelog/README.rst b/changelog/README.rst index 88956ef28..fdaa573d4 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 f1cc86973..0841ab049 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/announce/release-2.9.0.rst b/doc/en/announce/release-2.9.0.rst index 3aea08cb2..753bb7bf6 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 6a627ad3c..7a46d2ae6 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 2dc82a111..3e75af7fe 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/backwards-compatibility.rst b/doc/en/backwards-compatibility.rst index e04e64a76..c0feb833c 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/changelog.rst b/doc/en/changelog.rst index 3ab2307ee..8e3efd047 100644 --- a/doc/en/changelog.rst +++ b/doc/en/changelog.rst @@ -19,12 +19,15 @@ 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 @@ -265,7 +268,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. @@ -1419,7 +1422,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. @@ -2588,7 +2591,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. @@ -2871,7 +2874,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. @@ -3041,7 +3044,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. @@ -3357,7 +3360,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). @@ -4064,7 +4069,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. @@ -4265,7 +4270,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. @@ -7226,10 +7231,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. @@ -7257,7 +7262,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 @@ -7266,7 +7271,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. @@ -7520,7 +7525,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`` @@ -7575,12 +7580,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 @@ -7591,19 +7596,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`` @@ -7621,7 +7626,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`). @@ -7629,23 +7634,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. @@ -7663,7 +7668,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: @@ -7707,19 +7712,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`). @@ -7730,11 +7735,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. @@ -7742,32 +7747,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. @@ -7783,20 +7788,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`). @@ -7854,11 +7859,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. @@ -7866,7 +7871,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``. @@ -7883,14 +7888,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 @@ -7917,7 +7922,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 @@ -7938,7 +7943,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. @@ -7949,14 +7954,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`. @@ -7988,7 +7993,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. diff --git a/doc/en/conf.py b/doc/en/conf.py index 738d07dc2..0d440ec44 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -15,17 +15,33 @@ # # 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 from textwrap import dedent from typing import TYPE_CHECKING -from _pytest import __version__ as version +from _pytest import __version__ as full_version +version = full_version.split("+")[0] + if TYPE_CHECKING: import sphinx.application +PROJECT_ROOT_DIR = Path(__file__).parents[2].resolve() +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 + release = ".".join(version.split(".")[:2]) # If extensions (or modules to document with autodoc) are in another directory, @@ -66,12 +82,13 @@ extensions = [ "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 @@ -153,16 +170,6 @@ linkcheck_ignore = [ 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?) @@ -176,6 +183,7 @@ nitpick_ignore = [ ("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"), @@ -197,13 +205,16 @@ nitpick_ignore = [ ("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"), ] @@ -422,6 +433,18 @@ texinfo_documents = [ ) ] +# -- Options for towncrier_draft extension ----------------------------------- + +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 ----------------------------------- + +issues_github_path = "pytest-dev/pytest" + intersphinx_mapping = { "pluggy": ("https://pluggy.readthedocs.io/en/stable", None), @@ -435,31 +458,7 @@ intersphinx_mapping = { } -def configure_logging(app: "sphinx.application.Sphinx") -> None: - """Configure Sphinx's WarningHandler to handle (expected) missing include.""" - import logging - - import sphinx.util.logging - - class WarnLogFilter(logging.Filter): - def filter(self, record: logging.LogRecord) -> bool: - """Ignore warnings about missing include with "only" directive. - - 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 - - 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", @@ -488,8 +487,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 1a62e1b5d..50e43a0b5 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 68efef522..44957a0d4 100644 --- a/doc/en/contact.rst +++ b/doc/en/contact.rst @@ -22,9 +22,9 @@ Contact channels requests to GitHub. - ``#pytest`` `on irc.libera.chat `_ IRC - channel for random questions (using an IRC client, `via webchat - `_, or `via Matrix - `_). + channel for random questions (using an IRC client, or `via webchat + `) +- ``#pytest`` `on Matrix https://matrix.to/#/#pytest:matrix.org>`. .. _`pytest issue tracker`: https://github.com/pytest-dev/pytest/issues diff --git a/doc/en/example/assertion/failure_demo.py b/doc/en/example/assertion/failure_demo.py index f7a9c2794..dd1485b0b 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 4aa7ec23b..835726473 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 a31a601a1..e3c927316 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 19d862f60..17373f622 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 0e7eded06..fe11c2bf3 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 66e70f14d..21c9a4899 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 b2f68dba4..ea922e047 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 diff --git a/doc/en/example/customdirectory/tests/test_first.py b/doc/en/example/customdirectory/tests/test_first.py index 0a78de599..9953dd377 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 eed724a7d..df264f48b 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 61cf59dc1..b8b072dd7 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 ec282ab4b..04cbc268b 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 de0c26427..828fa4cf6 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 ba01ad32f..ebd5d10f5 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 e76e3f93c..1c59f0103 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 5d9487cab..4b4260fbd 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 00e2e46d8..dee61f8c4 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 c04d2a078..babcd9e2f 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 861ae9e52..f54524213 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 e969e3e25..b7bdc77a0 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/pythoncollection.py b/doc/en/example/pythoncollection.py index 8742526a1..7595ee02c 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/simple.rst b/doc/en/example/simple.rst index dec1bed5f..d4ace3f04 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 @@ -1073,8 +1073,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 1040c8929..4999e15f2 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/pythonpath.rst b/doc/en/explanation/pythonpath.rst index 33eba86b5..d0314a6db 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/historical-notes.rst b/doc/en/historical-notes.rst index 5eb527c58..be67036d6 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 35c3238de..ecd297867 100644 --- a/doc/en/how-to/fixtures.rst +++ b/doc/en/how-to/fixtures.rst @@ -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/usage.rst b/doc/en/how-to/usage.rst index fe46fad2d..0e0a0310f 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 4bb6d1833..14e5194ce 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,8 @@ 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 `. 5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable. @@ -142,7 +143,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 +267,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 58527ea73..8de3b3993 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -4,8 +4,7 @@ .. sidebar:: **Next Open Trainings and Events** - - `pytest development sprint `_, **June 17th -- 22nd 2024**, Klaus (AT) / Remote - - `pytest tips and tricks for a better testsuite `_, at `Europython 2024 `_, **July 8th -- 14th 2024** (3h), Prague (CZ) + - `pytest tips and tricks for a better testsuite `_, at `Europython 2024 `_, **July 9th 2024** (3h), Prague (CZ) - `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 diff --git a/doc/en/reference/customize.rst b/doc/en/reference/customize.rst index cab111726..373223ec9 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/plugin_list.rst b/doc/en/reference/plugin_list.rst index ab7d28edf..c8591ece5 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.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) @@ -100,6 +101,7 @@ This list contains 1476 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 22, 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 @@ -125,7 +127,7 @@ This list contains 1476 plugins. :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 May 31, 2024 N/A N/A + :pypi:`pytest-aux` templates/examples and aux for pytest Jun 18, 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 @@ -150,7 +152,7 @@ This list contains 1476 plugins. :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 Jun 07, 2024 3 - Alpha pytest + :pypi:`pytest-bec-e2e` BEC pytest plugin for end-to-end tests Jun 21, 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 @@ -160,7 +162,7 @@ This list contains 1476 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) @@ -222,7 +224,7 @@ This list contains 1476 plugins. :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 @@ -236,7 +238,7 @@ This list contains 1476 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 May 30, 2024 N/A pytest + :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 @@ -261,12 +263,11 @@ This list contains 1476 plugins. :pypi:`pytest-collect-formatter` Formatter for pytest collect output Mar 29, 2021 5 - Production/Stable N/A :pypi:`pytest-collect-formatter2` Formatter for pytest collect output May 31, 2021 5 - Production/Stable N/A :pypi:`pytest-collect-interface-info-plugin` Get executed interface information in pytest interface automation framework Sep 25, 2023 4 - Beta N/A - :pypi:`pytest-collect-jmeter-report-tests` A simple plugin to use with pytest May 20, 2024 4 - Beta pytest>=7.2.1 :pypi:`pytest-collector` Python package for collecting pytest. Aug 02, 2022 N/A pytest (>=7.0,<8.0) :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 Jun 03, 2024 N/A pytest<9,>=3.6 + :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 @@ -305,7 +306,7 @@ This list contains 1476 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 May 30, 2024 4 - Beta pytest>=6.2.0 + :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. Jun 17, 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 @@ -314,7 +315,7 @@ This list contains 1476 plugins. :pypi:`pytest-dash` pytest fixtures to run dash applications. Mar 18, 2019 N/A N/A :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. May 25, 2024 4 - Beta pytest + :pypi:`pytest-databases` Reusable database fixtures for any and all databases. Jun 11, 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) @@ -366,15 +367,16 @@ This list contains 1476 plugins. :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. 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. May 29, 2024 4 - Beta pytest>=3.5.0 + :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` May 21, 2024 5 - Production/Stable pytest<9.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 @@ -443,7 +445,7 @@ This list contains 1476 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 07, 2024 N/A pytest + :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) @@ -487,7 +489,7 @@ This list contains 1476 plugins. :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-exasol-saas` Jun 07, 2024 N/A pytest<9,>=7 - :pypi:`pytest-excel` pytest plugin for generating excel reports Sep 14, 2023 5 - Production/Stable N/A + :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 @@ -636,7 +638,7 @@ This list contains 1476 plugins. :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 Jun 08, 2024 3 - Alpha pytest==8.2.0 + :pypi:`pytest-homeassistant-custom-component` Experimental package to automatically extract test plugins for Home Assistant custom components Jun 22, 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 @@ -678,6 +680,7 @@ This list contains 1476 plugins. :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-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 @@ -723,13 +726,12 @@ This list contains 1476 plugins. :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) May 21, 2024 N/A pytest>6.0.0 + :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-jtl-collector` A simple plugin to use with pytest May 20, 2024 4 - Beta pytest>=7.2.1 :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 @@ -824,7 +826,7 @@ This list contains 1476 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 May 28, 2024 N/A pytest + :pypi:`pytest-mh` Pytest multihost plugin Jun 13, 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) @@ -839,7 +841,7 @@ This list contains 1476 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 @@ -877,7 +879,7 @@ This list contains 1476 plugins. :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 May 01, 2024 1 - Planning N/A + :pypi:`pytest-neos` Pytest plugin for neos Jun 11, 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-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 @@ -886,7 +888,7 @@ This list contains 1476 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 May 20, 2024 N/A pytest<9.0.0,>=8.2.0 + :pypi:`pytest-nhsd-apim` Pytest plugin accessing NHSDigital's APIM proxies Jun 18, 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 @@ -950,7 +952,7 @@ This list contains 1476 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 @@ -1016,7 +1018,7 @@ This list contains 1476 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 @@ -1037,7 +1039,7 @@ This list contains 1476 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 Apr 30, 2024 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) @@ -1059,7 +1061,7 @@ This list contains 1476 plugins. :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 @@ -1086,7 +1088,7 @@ This list contains 1476 plugins. :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-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. Jun 05, 2024 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) @@ -1198,7 +1200,7 @@ This list contains 1476 plugins. :pypi:`pytest-sequence-markers` Pytest plugin for sequencing markers for execution of tests May 23, 2023 5 - Production/Stable N/A :pypi:`pytest-server-fixtures` Extensible server fixures for py.test 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 May 09, 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 @@ -1261,13 +1263,13 @@ This list contains 1476 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 Jun 03, 2024 N/A pytest<8,>5.4.0 - :pypi:`pytest-splunk-addon-ui-smartx` Library to support testing Splunk Add-on UX Jun 05, 2024 N/A N/A + :pypi:`pytest-splunk-addon-ui-smartx` Library to support testing Splunk Add-on UX Jun 14, 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 @@ -1280,7 +1282,7 @@ This list contains 1476 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-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 @@ -1289,7 +1291,7 @@ This list contains 1476 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 Jun 08, 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) @@ -1461,7 +1463,7 @@ This list contains 1476 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 May 28, 2024 N/A pytest>=7.3.1 :pypi:`pytest-whirlwind` Testing Tornado. Jun 12, 2020 N/A N/A @@ -1499,6 +1501,7 @@ This list contains 1476 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 @@ -1879,6 +1882,13 @@ This list contains 1476 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, @@ -1977,6 +1987,13 @@ This list contains 1476 plugins. Useful assertion utilities for use with pytest + :pypi:`pytest-assist` + *last release*: Jun 22, 2024, + *status*: N/A, + *requires*: pytest + + load testing library + :pypi:`pytest-assume` *last release*: Jun 24, 2021, *status*: N/A, @@ -2153,7 +2170,7 @@ This list contains 1476 plugins. This fixture provides a configured "driver" for Android Automated Testing, using uiautomator2. :pypi:`pytest-aux` - *last release*: May 31, 2024, + *last release*: Jun 18, 2024, *status*: N/A, *requires*: N/A @@ -2328,7 +2345,7 @@ This list contains 1476 plugins. Pytest plugin to run your tests with beartype checking enabled. :pypi:`pytest-bec-e2e` - *last release*: Jun 07, 2024, + *last release*: Jun 21, 2024, *status*: 3 - Alpha, *requires*: pytest @@ -2398,7 +2415,7 @@ This list contains 1476 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 @@ -2832,7 +2849,7 @@ This list contains 1476 plugins. Check links in files :pypi:`pytest-checklist` - *last release*: Mar 12, 2024, + *last release*: Jun 10, 2024, *status*: N/A, *requires*: N/A @@ -2930,7 +2947,7 @@ This list contains 1476 plugins. Easy quality control for CLDF datasets using pytest :pypi:`pytest-cleanslate` - *last release*: May 30, 2024, + *last release*: Jun 17, 2024, *status*: N/A, *requires*: pytest @@ -3104,13 +3121,6 @@ This list contains 1476 plugins. Get executed interface information in pytest interface automation framework - :pypi:`pytest-collect-jmeter-report-tests` - *last release*: May 20, 2024, - *status*: 4 - Beta, - *requires*: pytest>=7.2.1 - - A simple plugin to use with pytest - :pypi:`pytest-collector` *last release*: Aug 02, 2022, *status*: N/A, @@ -3140,7 +3150,7 @@ This list contains 1476 plugins. An interactive GUI test runner for PyTest :pypi:`pytest-common-subject` - *last release*: Jun 03, 2024, + *last release*: Jun 12, 2024, *status*: N/A, *requires*: pytest<9,>=3.6 @@ -3413,11 +3423,11 @@ This list contains 1476 plugins. Custom grouping for pytest-xdist, rename test cases name and test cases nodeid, support allure report :pypi:`pytest-custom-outputs` - *last release*: May 30, 2024, + *last release*: Jun 17, 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 + 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, @@ -3476,7 +3486,7 @@ This list contains 1476 plugins. Useful functions for managing data for pytest fixtures :pypi:`pytest-databases` - *last release*: May 25, 2024, + *last release*: Jun 11, 2024, *status*: 4 - Beta, *requires*: pytest @@ -3840,7 +3850,7 @@ This list contains 1476 plugins. Pytest plugin to record discovered tests in a file :pypi:`pytest-ditto` - *last release*: May 29, 2024, + *last release*: Jun 09, 2024, *status*: 4 - Beta, *requires*: pytest>=3.5.0 @@ -3853,6 +3863,13 @@ This list contains 1476 plugins. 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, @@ -3896,7 +3913,7 @@ This list contains 1476 plugins. A pytest plugin for running django in class-scoped fixtures :pypi:`pytest-django-docker-pg` - *last release*: May 21, 2024, + *last release*: Jun 13, 2024, *status*: 5 - Production/Stable, *requires*: pytest<9.0.0,>=7.0.0 @@ -4379,7 +4396,7 @@ This list contains 1476 plugins. pytest plugin with mechanisms for echoing environment variables, package version and generic attributes :pypi:`pytest-edit` - *last release*: Jun 07, 2024, + *last release*: Jun 09, 2024, *status*: N/A, *requires*: pytest @@ -4687,9 +4704,9 @@ This list contains 1476 plugins. :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 @@ -5730,7 +5747,7 @@ This list contains 1476 plugins. A pytest plugin for use with homeassistant custom components. :pypi:`pytest-homeassistant-custom-component` - *last release*: Jun 08, 2024, + *last release*: Jun 22, 2024, *status*: 3 - Alpha, *requires*: pytest==8.2.0 @@ -6023,6 +6040,13 @@ This list contains 1476 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, @@ -6339,7 +6363,7 @@ This list contains 1476 plugins. Generate JSON test reports :pypi:`pytest-json-ctrf` - *last release*: May 21, 2024, + *last release*: Jun 15, 2024, *status*: N/A, *requires*: pytest>6.0.0 @@ -6380,13 +6404,6 @@ This list contains 1476 plugins. A pytest plugin to perform JSONSchema validations - :pypi:`pytest-jtl-collector` - *last release*: May 20, 2024, - *status*: 4 - Beta, - *requires*: pytest>=7.2.1 - - A simple plugin to use with pytest - :pypi:`pytest-jtr` *last release*: Jun 04, 2024, *status*: N/A, @@ -7046,7 +7063,7 @@ This list contains 1476 plugins. Custom metrics report for pytest :pypi:`pytest-mh` - *last release*: May 28, 2024, + *last release*: Jun 13, 2024, *status*: N/A, *requires*: pytest @@ -7151,7 +7168,7 @@ This list contains 1476 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 @@ -7417,7 +7434,7 @@ This list contains 1476 plugins. pytest-neo is a plugin for pytest that shows tests like screen of Matrix. :pypi:`pytest-neos` - *last release*: May 01, 2024, + *last release*: Jun 11, 2024, *status*: 1 - Planning, *requires*: N/A @@ -7480,7 +7497,7 @@ This list contains 1476 plugins. pytest ngs fixtures :pypi:`pytest-nhsd-apim` - *last release*: May 20, 2024, + *last release*: Jun 18, 2024, *status*: N/A, *requires*: pytest<9.0.0,>=8.2.0 @@ -7928,7 +7945,7 @@ This list contains 1476 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 @@ -8390,9 +8407,9 @@ This list contains 1476 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 @@ -8537,7 +8554,7 @@ This list contains 1476 plugins. Record PyMySQL queries and mock with the stored data. :pypi:`pytest-pyodide` - *last release*: Apr 30, 2024, + *last release*: Jun 12, 2024, *status*: N/A, *requires*: pytest @@ -8691,9 +8708,9 @@ This list contains 1476 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 @@ -8880,7 +8897,7 @@ This list contains 1476 plugins. Provides pytest plugins for reporting request/response traffic, screenshots, and more to ReportPortal :pypi:`pytest-redis` - *last release*: Jun 05, 2024, + *last release*: Jun 19, 2024, *status*: 5 - Production/Stable, *requires*: pytest>=6.2 @@ -9664,7 +9681,7 @@ This list contains 1476 plugins. Automatically mocks resources from serverless.yml in pytest using moto. :pypi:`pytest-servers` - *last release*: May 09, 2024, + *last release*: Jun 17, 2024, *status*: 3 - Alpha, *requires*: pytest>=6.2 @@ -10105,9 +10122,9 @@ This list contains 1476 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. @@ -10147,7 +10164,7 @@ This list contains 1476 plugins. A Dynamic test tool for Splunk Apps and Add-ons :pypi:`pytest-splunk-addon-ui-smartx` - *last release*: Jun 05, 2024, + *last release*: Jun 14, 2024, *status*: N/A, *requires*: N/A @@ -10238,9 +10255,9 @@ This list contains 1476 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 @@ -10301,7 +10318,7 @@ This list contains 1476 plugins. A Pytest plugin that allows you to loop tests for a user defined amount of time. :pypi:`pytest-structlog` - *last release*: Jun 08, 2024, + *last release*: Jun 09, 2024, *status*: N/A, *requires*: pytest @@ -11505,9 +11522,9 @@ This list contains 1476 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. @@ -11770,6 +11787,13 @@ This list contains 1476 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, diff --git a/doc/en/reference/reference.rst b/doc/en/reference/reference.rst index 3675c7cb2..6926cd61b 100644 --- a/doc/en/reference/reference.rst +++ b/doc/en/reference/reference.rst @@ -650,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 @@ -1701,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. diff --git a/doc/en/requirements.txt b/doc/en/requirements.txt index 6e7221d64..0637c967b 100644 --- a/doc/en/requirements.txt +++ b/doc/en/requirements.txt @@ -9,3 +9,5 @@ sphinxcontrib-svg2pdfconverter # See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045. packaging furo +sphinxcontrib-towncrier +sphinx-issues diff --git a/extra/get_issues.py b/extra/get_issues.py index 64e859e0c..851d2f6d7 100644 --- a/extra/get_issues.py +++ b/extra/get_issues.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json from pathlib import Path import sys diff --git a/pyproject.toml b/pyproject.toml index 0627c94c7..e0ed2b900 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,7 @@ lint.select = [ "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 @@ -155,6 +156,10 @@ lint.per-file-ignores."src/_pytest/_version.py" = [ 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 @@ -164,9 +169,13 @@ lint.isort.known-local-folder = [ ] 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 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. @@ -349,6 +358,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 @@ -363,44 +375,74 @@ directory = "changelog/" title_format = "pytest {version} ({project_date})" template = "changelog/_template.rst" +# 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 = "Breaking Changes" +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" +name = "Deprecations (removal in next major release)" showcontent = true [[tool.towncrier.type]] +# New behaviors, public APIs. That sort of stuff. directory = "feature" -name = "Features" +name = "New features" showcontent = true [[tool.towncrier.type]] +# New behaviors in existing features. directory = "improvement" -name = "Improvements" +name = "Improvements in existing functionality" 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" +name = "Bug fixes" showcontent = true [[tool.towncrier.type]] +# Updates regarding bundling dependencies. directory = "vendor" -name = "Vendored Libraries" +name = "Vendored libraries" showcontent = true [[tool.towncrier.type]] +# Notable updates to the documentation structure or build process. directory = "doc" -name = "Improved Documentation" +name = "Improved documentation" showcontent = true [[tool.towncrier.type]] -directory = "trivial" -name = "Trivial/Internal Changes" +# 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]] +# 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] diff --git a/scripts/generate-gh-release-notes.py b/scripts/generate-gh-release-notes.py index 4222702d5..7f195ba1e 100644 --- a/scripts/generate-gh-release-notes.py +++ b/scripts/generate-gh-release-notes.py @@ -9,6 +9,8 @@ our CHANGELOG) into Markdown (which is required by GitHub Releases). 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 7dabbd3b3..49cb21106 100644 --- a/scripts/prepare-release-pr.py +++ b/scripts/prepare-release-pr.py @@ -14,6 +14,8 @@ After that, it will create a release using the `release` tox environment, and pu `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 bcbc4262d..545919cd6 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 f771295a0..000000000 --- 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 6831fc984..75df0ddba 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 b694a5f24..8eb8ec960 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 c24f92520..59426ef94 100644 --- a/src/_pytest/_argcomplete.py +++ b/src/_pytest/_argcomplete.py @@ -62,13 +62,13 @@ If things do not work right away: 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 @@ if os.environ.get("_ARGCOMPLETE"): 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 b0a418e95..0bfde4260 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 b6e06340d..e74528257 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 types import TracebackType 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 Iterable from typing import List from typing import Literal from typing import Mapping -from typing import Optional from typing import overload from typing import Pattern from typing import Sequence -from typing import Set from typing import SupportsIndex from typing import Tuple from typing import Type @@ -57,6 +56,8 @@ if sys.version_info < (3, 11): TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] +EXCEPTION_OR_MORE = Union[Type[Exception], Tuple[Type[Exception], ...]] + class Code: """Wrapper around Python code objects.""" @@ -67,7 +68,7 @@ class Code: 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 @@ class Code: 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 @@ class Code: 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 @@ class Frame: 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 @@ class Frame: 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 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 @@ class TracebackEntry: 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 @@ class TracebackEntry: 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 @@ class TracebackEntry: 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 @@ class TracebackEntry: 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 @@ class Traceback(List[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 @@ class Traceback(List[TracebackEntry]): 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 @@ class Traceback(List[TracebackEntry]): 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,10 +408,10 @@ class Traceback(List[TracebackEntry]): 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 @@ -445,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: @@ -469,8 +463,8 @@ class ExceptionInfo(Generic[E]): # 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, @@ -495,9 +489,9 @@ class ExceptionInfo(Generic[E]): @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): @@ -510,9 +504,7 @@ class ExceptionInfo(Generic[E]): 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:: @@ -532,17 +524,17 @@ class ExceptionInfo(Generic[E]): 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 @@ -605,16 +597,14 @@ class ExceptionInfo(Generic[E]): 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): @@ -630,14 +620,13 @@ class ExceptionInfo(Generic[E]): showlocals: bool = False, 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: @@ -719,7 +708,7 @@ class ExceptionInfo(Generic[E]): ] ) - 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`. @@ -737,9 +726,9 @@ class ExceptionInfo(Generic[E]): 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.""" @@ -766,10 +755,10 @@ class ExceptionInfo(Generic[E]): 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. @@ -811,16 +800,16 @@ class FormattedExcinfo: showlocals: bool = False 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)) @@ -835,13 +824,13 @@ class FormattedExcinfo: 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): @@ -855,11 +844,11 @@ class FormattedExcinfo: 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: @@ -888,7 +877,7 @@ class FormattedExcinfo: excinfo: ExceptionInfo[BaseException], indent: int = 4, markall: bool = False, - ) -> List[str]: + ) -> list[str]: lines = [] indentstr = " " * indent # Get the real exception information out. @@ -900,7 +889,7 @@ class FormattedExcinfo: 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] != "@"] @@ -928,10 +917,10 @@ class FormattedExcinfo: 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 @@ -966,7 +955,7 @@ class FormattedExcinfo: 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) @@ -976,7 +965,7 @@ class FormattedExcinfo: 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) @@ -1007,7 +996,7 @@ class FormattedExcinfo: 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. @@ -1024,7 +1013,7 @@ class FormattedExcinfo: 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" @@ -1042,16 +1031,12 @@ class FormattedExcinfo: 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)) @@ -1060,7 +1045,7 @@ class FormattedExcinfo: # 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), @@ -1118,9 +1103,9 @@ class TerminalRepr: @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 ) @@ -1135,13 +1120,11 @@ class ExceptionRepr(TerminalRepr): @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. @@ -1162,8 +1145,8 @@ class ExceptionChainRepr(ExceptionRepr): @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) @@ -1172,8 +1155,8 @@ class ReprExceptionInfo(ExceptionRepr): @dataclasses.dataclass(eq=False) class ReprTraceback(TerminalRepr): - reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] - extraline: Optional[str] + reprentries: Sequence[ReprEntry | ReprEntryNative] + extraline: str | None style: TracebackStyle entrysep: ClassVar = "_ " @@ -1217,9 +1200,9 @@ class ReprEntryNative(TerminalRepr): @dataclasses.dataclass(eq=False) class ReprEntry(TerminalRepr): lines: Sequence[str] - reprfuncargs: Optional["ReprFuncArgs"] - reprlocals: Optional["ReprLocals"] - reprfileloc: Optional["ReprFileLocation"] + reprfuncargs: ReprFuncArgs | None + reprlocals: ReprLocals | None + reprfileloc: ReprFileLocation | None style: TracebackStyle def _write_entry_lines(self, tw: TerminalWriter) -> None: @@ -1243,9 +1226,9 @@ class ReprEntry(TerminalRepr): # 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: @@ -1324,7 +1307,7 @@ class ReprLocals(TerminalRepr): @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: @@ -1345,7 +1328,7 @@ class ReprFuncArgs(TerminalRepr): 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 7fa577e03..604aff8ba 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 tokenize 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 @@ class Source: 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 @@ class Source: 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 @@ class 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 @@ class Source: 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 @@ class Source: # -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 db001e918..b0155b18b 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 e637eec59..7213be7ba 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 re 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: objid = id(object) @@ -136,7 +133,7 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: write = stream.write @@ -177,7 +174,7 @@ class PrettyPrinter: stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: if not len(object): @@ -196,7 +193,7 @@ class PrettyPrinter: stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: stream.write("[") @@ -211,7 +208,7 @@ class PrettyPrinter: stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: stream.write("(") @@ -226,7 +223,7 @@ class PrettyPrinter: stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: if not len(object): @@ -252,7 +249,7 @@ class PrettyPrinter: stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: write = stream.write @@ -311,7 +308,7 @@ class PrettyPrinter: stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: write = stream.write @@ -340,7 +337,7 @@ class PrettyPrinter: stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: write = stream.write @@ -358,7 +355,7 @@ class PrettyPrinter: stream: IO[str], indent: int, allowance: int, - context: Set[int], + context: set[int], level: int, ) -> None: stream.write("mappingproxy(") @@ -373,7 +370,7 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: 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 @@ class PrettyPrinter: _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 @@ class PrettyPrinter: 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): diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 9f33fced6..13b793f0a 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: @@ -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 @@ class SafeRepr(reprlib.Repr): s = ascii(x) else: s = super().repr(x) - except (KeyboardInterrupt, SystemExit): raise except BaseException as exc: @@ -97,7 +97,7 @@ DEFAULT_REPR_MAX_SIZE = 240 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 083c18232..70ebd3d06 100644 --- a/src/_pytest/_io/terminalwriter.py +++ b/src/_pytest/_io/terminalwriter.py @@ -1,11 +1,12 @@ """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 @@ -71,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": @@ -85,7 +86,7 @@ class TerminalWriter: 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 @@ -116,8 +117,8 @@ class TerminalWriter: 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: @@ -200,9 +201,7 @@ class TerminalWriter: for indent, new_line in zip(indents, new_lines): self.line(indent + new_line) - def _get_pygments_lexer( - self, lexer: Literal["python", "diff"] - ) -> Optional["Lexer"]: + def _get_pygments_lexer(self, lexer: Literal["python", "diff"]) -> Lexer | None: try: if lexer == "python": from pygments.lexers.python import PythonLexer @@ -217,7 +216,7 @@ class TerminalWriter: except ModuleNotFoundError: return None - def _get_pygments_formatter(self) -> Optional["Formatter"]: + def _get_pygments_formatter(self) -> Formatter | None: try: import pygments.util except ModuleNotFoundError: diff --git a/src/_pytest/_io/wcwidth.py b/src/_pytest/_io/wcwidth.py index 538031335..23886ff15 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/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 21dd4a4a4..f2f1d029b 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 install_importhook(config: Config) -> rewrite.AssertionRewritingHook: 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 pytest_runtest_protocol(item: Item) -> Generator[None, object, object]: 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 b29a254f5..bfcbcbd3f 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 sys 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 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) 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,9 +89,9 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) 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] @@ -132,7 +128,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) 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 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) 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 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) 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 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) 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: @@ -278,7 +274,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader) 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. @@ -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 @@ -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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): # 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): current = self.stack.pop() if self.stack: self.explanation_specifiers = self.stack[-1] - keys: List[Optional[ast.expr]] = [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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): [*self.expl_stmts, hook_call_pass], [], ) - statements_pass: List[ast.stmt] = [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: List[ast.expr] = [ + 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): # 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): 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 @@ class AssertionRewriter(ast.NodeVisitor): 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: List[ast.expr] = [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: List[ast.expr] = [] - syms: List[ast.expr] = [] + expls: list[ast.expr] = [] + syms: list[ast.expr] = [] results = [left_res] for i, op, next_operand in it: if ( diff --git a/src/_pytest/assertion/truncate.py b/src/_pytest/assertion/truncate.py index 4fdfd86a5..b67f02cca 100644 --- a/src/_pytest/assertion/truncate.py +++ b/src/_pytest/assertion/truncate.py @@ -4,8 +4,7 @@ Current default behaviour is to truncate assertion explanations at 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 @@ USAGE_MSG = "use '-vv' to show" 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 a118befcc..4dc1af4af 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 AbstractSet 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 @@ from _pytest.config import Config # 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 @@ -327,7 +327,7 @@ def _compare_eq_iterable( right: Iterable[Any], 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 @@ -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,8 +486,8 @@ 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) @@ -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): @@ -584,7 +584,7 @@ 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) :] diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 8ad36f9b9..51778c456 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -3,20 +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 @@ -77,7 +74,7 @@ class Cache: 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: @@ -249,7 +246,7 @@ class Cache: class LFPluginCollWrapper: - def __init__(self, lfplugin: "LFPlugin") -> None: + def __init__(self, lfplugin: LFPlugin) -> None: self.lfplugin = lfplugin self._collected_at_least_one_failure = False @@ -263,7 +260,7 @@ class LFPluginCollWrapper: lf_paths = self.lfplugin._last_failed_paths # Use stable sort to prioritize last failed. - def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool: + def sort_key(node: nodes.Item | nodes.Collector) -> bool: return node.path in lf_paths res.result = sorted( @@ -301,13 +298,13 @@ class LFPluginCollWrapper: 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 @@ -326,9 +323,9 @@ class LFPlugin: 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"): @@ -337,7 +334,7 @@ class LFPlugin: 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 @@ -348,7 +345,7 @@ class LFPlugin: 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 f"run-last-failure: {self._report_status}" return None @@ -370,7 +367,7 @@ class LFPlugin: @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 @@ -442,13 +439,13 @@ class NFPlugin: @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 @@ -464,7 +461,7 @@ class NFPlugin: 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: @@ -541,7 +538,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 @@ -572,7 +569,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 diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 89a938d54..c4dfcc275 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 Generator from typing import Generic from typing import Iterable from typing import Iterator -from typing import List from typing import Literal from typing import NamedTuple -from typing import Optional from typing import TextIO -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 @@ class DontReadFromInput(TextIO): 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 @@ class DontReadFromInput(TextIO): 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 @@ class DontReadFromInput(TextIO): 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 @@ class NoCapture(CaptureBase[str]): 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 @@ class SysCaptureBase(CaptureBase[AnyStr]): 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 @@ class FDCaptureBase(CaptureBase[AnyStr]): # 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 @@ class FDCaptureBase(CaptureBase[AnyStr]): 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 @@ class MultiCapture(Generic[AnyStr]): 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,7 +735,7 @@ class CaptureManager: 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: @@ -782,7 +783,7 @@ class CaptureManager: # 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 diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 058aaa1ff..0c1850df5 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 glob import importlib.metadata import inspect import os -from pathlib import Path +import pathlib import re import shlex import sys @@ -21,22 +23,16 @@ from types import FunctionType 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 @@ -118,7 +114,7 @@ class ExitCode(enum.IntEnum): class ConftestImportFailure(Exception): def __init__( self, - path: Path, + path: pathlib.Path, *, cause: Exception, ) -> None: @@ -141,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: @@ -176,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: @@ -286,9 +280,9 @@ builtin_plugins.add("pytester_assertions") 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( @@ -296,7 +290,7 @@ def get_config( invocation_params=Config.InvocationParams( args=args or (), plugins=plugins, - dir=Path.cwd(), + dir=pathlib.Path.cwd(), ), ) @@ -310,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. @@ -322,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): @@ -353,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 @@ -364,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: @@ -410,13 +404,13 @@ class PytestPluginManager(PluginManager): # -- 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 @@ -430,7 +424,7 @@ class PytestPluginManager(PluginManager): # 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) @@ -456,7 +450,7 @@ class PytestPluginManager(PluginManager): 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 @@ -480,7 +474,7 @@ class PytestPluginManager(PluginManager): 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: @@ -493,9 +487,7 @@ class PytestPluginManager(PluginManager): ) 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( @@ -522,14 +514,14 @@ class PytestPluginManager(PluginManager): 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. @@ -552,13 +544,13 @@ class PytestPluginManager(PluginManager): # 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: @@ -601,7 +593,7 @@ class PytestPluginManager(PluginManager): 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 @@ -618,9 +610,9 @@ class PytestPluginManager(PluginManager): 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: @@ -643,9 +635,9 @@ class PytestPluginManager(PluginManager): 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: @@ -673,15 +665,15 @@ class PytestPluginManager(PluginManager): 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: @@ -692,9 +684,9 @@ class PytestPluginManager(PluginManager): 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: @@ -746,7 +738,7 @@ class PytestPluginManager(PluginManager): def _check_non_top_pytest_plugins( self, mod: types.ModuleType, - conftestpath: Path, + conftestpath: pathlib.Path, ) -> None: if ( hasattr(mod, "pytest_plugins") @@ -832,7 +824,7 @@ class PytestPluginManager(PluginManager): 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: @@ -877,8 +869,8 @@ class PytestPluginManager(PluginManager): 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: @@ -999,19 +991,19 @@ class Config: 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) @@ -1032,20 +1024,20 @@ class Config: TESTPATHS = enum.auto() # Set by cacheprovider plugin. - cache: Optional["Cache"] + 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() @@ -1083,20 +1075,20 @@ class Config: 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] = [] + self.args: list[str] = [] @property - def rootpath(self) -> Path: + def rootpath(self) -> pathlib.Path: """The path to the :ref:`rootdir `. :type: pathlib.Path @@ -1106,11 +1098,9 @@ class Config: 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 @@ -1137,15 +1127,15 @@ class Config: 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: @@ -1171,7 +1161,7 @@ class Config: 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" @@ -1194,7 +1184,7 @@ class Config: 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) @@ -1203,7 +1193,7 @@ class 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 @@ -1212,7 +1202,7 @@ class Config: 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. @@ -1290,7 +1280,8 @@ class Config: 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 = ( @@ -1303,7 +1294,7 @@ class Config: 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: @@ -1318,13 +1309,13 @@ class Config: 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. @@ -1360,7 +1351,7 @@ class Config: 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): @@ -1381,8 +1372,8 @@ class Config: 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() @@ -1484,11 +1475,11 @@ class Config: 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 == [] @@ -1592,7 +1583,7 @@ class Config: # 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 @@ -1648,24 +1639,26 @@ class Config: 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, @@ -1720,7 +1713,7 @@ class Config: 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 @@ -1771,7 +1764,7 @@ class Config: 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. @@ -1827,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. @@ -1871,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: @@ -1917,7 +1910,7 @@ def parse_warning_filter( 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") @@ -1940,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 f270b864c..85aa46327 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 @@ import sys 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 @@ class Parser: 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 @@ class Parser: 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 @@ class Parser: 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 @@ class Parser: 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 @@ class Parser: 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 @@ class Parser: 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 @@ class Parser: 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 @@ class Argument: 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 @@ class Argument: 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]: @@ -335,7 +332,7 @@ class Argument: 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 @@ class OptionGroup: 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: @@ -391,7 +388,7 @@ class OptionGroup: 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,8 +430,8 @@ class MyOptionParser(argparse.ArgumentParser): # 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) @@ -455,7 +452,7 @@ class MyOptionParser(argparse.ArgumentParser): # 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: @@ -507,7 +504,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): 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(", ") @@ -516,7 +513,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter): 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 diff --git a/src/_pytest/config/exceptions.py b/src/_pytest/config/exceptions.py index 4031ea732..90108eca9 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 9909376de..ce4c990b8 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 load_config_dict_from_file( 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 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte 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 eacb2836d..3e1463fff 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -2,6 +2,8 @@ # ruff: noqa: T100 """Interactive debugging with PDB, the Python Debugger.""" +from __future__ import annotations + import argparse import functools import sys @@ -9,11 +11,6 @@ import types 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 Union import unittest from _pytest import outcomes @@ -30,7 +27,7 @@ from _pytest.reports import BaseReport 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(":") @@ -95,22 +92,22 @@ def pytest_configure(config: Config) -> 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 @@ -149,7 +146,7 @@ class pytestPDB: 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): @@ -238,7 +235,7 @@ class pytestPDB: import _pytest.config if cls._pluginmanager is None: - capman: Optional[CaptureManager] = None + capman: CaptureManager | None = None else: capman = cls._pluginmanager.getplugin("capturemanager") if capman: @@ -281,7 +278,7 @@ class pytestPDB: 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: diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 10811d158..a605c24e5 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -9,6 +9,8 @@ All constants defined in this module should be either instances of 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 23ad7a7a9..cb46d9a3b 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 traceback 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 @@ DOCTEST_REPORT_CHOICES = ( # 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 @@ class ReprFailDoctest(TerminalRepr): 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 @@ def _init_runner_class() -> Type["doctest.DocTestRunner"]: 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_runner_class() -> Type["doctest.DocTestRunner"]: 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 _init_runner_class() -> Type["doctest.DocTestRunner"]: 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 _init_runner_class() -> Type["doctest.DocTestRunner"]: 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 @@ class DoctestItem(Item): @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 @@ class DoctestItem(Item): 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 @@ class DoctestItem(Item): 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) ): @@ -381,11 +377,11 @@ class DoctestItem(Item): reprlocation_lines.append((reprlocation, lines)) return ReprFailDoctest(reprlocation_lines) - def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]: + 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 @@ class DoctestTextfile(Module): ) -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,7 +501,13 @@ class DoctestModule(Module): import doctest class MockAwareDocTestFinder(doctest.DocTestFinder): - if sys.version_info < (3, 11): + 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 @@ -588,7 +590,7 @@ class DoctestModule(Module): ) -def _init_checker_class() -> Type["doctest.OutputChecker"]: +def _init_checker_class() -> type[doctest.OutputChecker]: import doctest import re @@ -656,8 +658,8 @@ def _init_checker_class() -> Type["doctest.OutputChecker"]: 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 _init_checker_class() -> Type["doctest.OutputChecker"]: 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 083bcb837..07e60f03f 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 383084e07..0151a4d9c 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 @@ -20,7 +22,6 @@ from typing import Generator 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 @@ -28,7 +29,6 @@ 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 @@ -113,18 +113,18 @@ _FixtureCachedResult = Union[ @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(): @@ -133,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: @@ -152,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( @@ -171,8 +171,8 @@ def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]: 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") @@ -212,10 +212,10 @@ def get_parametrized_fixture_argkeys( yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls) -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]] +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_by_item = argkeys_by_item[scope] = {} @@ -249,7 +249,7 @@ def reorder_items_atscope( scoped_items_by_argkey = items_by_argkey[scope] scoped_argkeys_by_item = argkeys_by_item[scope] - ignore: Set[FixtureArgKey] = set() + ignore: set[FixtureArgKey] = set() items_deque = deque(items) items_done: OrderedSet[nodes.Item] = {} while items_deque: @@ -309,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. @@ -334,7 +334,7 @@ class FuncFixtureInfo: 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() @@ -360,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: @@ -390,7 +390,7 @@ class FixtureRequest(abc.ABC): self.param: Any @property - def _fixturemanager(self) -> "FixtureManager": + def _fixturemanager(self) -> FixtureManager: return self._pyfuncitem.session._fixturemanager @property @@ -406,13 +406,13 @@ class FixtureRequest(abc.ABC): @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)) @@ -477,7 +477,7 @@ class FixtureRequest(abc.ABC): return node.keywords @property - def session(self) -> "Session": + def session(self) -> Session: """Pytest session object.""" return self._pyfuncitem.session @@ -487,7 +487,7 @@ class FixtureRequest(abc.ABC): 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 @@ -498,7 +498,7 @@ class FixtureRequest(abc.ABC): """ 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: @@ -535,7 +535,7 @@ class FixtureRequest(abc.ABC): ) 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. @@ -547,7 +547,7 @@ class FixtureRequest(abc.ABC): 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) @@ -618,7 +618,7 @@ class FixtureRequest(abc.ABC): 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 @@ -651,7 +651,7 @@ class FixtureRequest(abc.ABC): ) 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 @@ -661,7 +661,7 @@ class FixtureRequest(abc.ABC): 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, @@ -676,7 +676,7 @@ class TopRequest(FixtureRequest): 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. @@ -710,7 +710,7 @@ class SubRequest(FixtureRequest): scope: Scope, param: Any, param_index: int, - fixturedef: "FixtureDef[object]", + fixturedef: FixtureDef[object], *, _ispytest: bool = False, ) -> None: @@ -740,7 +740,7 @@ class SubRequest(FixtureRequest): 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: @@ -753,7 +753,7 @@ class SubRequest(FixtureRequest): 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): @@ -774,7 +774,7 @@ class SubRequest(FixtureRequest): 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): @@ -791,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)) @@ -847,11 +847,11 @@ class FixtureLookupError(LookupError): 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 @@ -879,7 +879,7 @@ class FixtureLookupErrorRepr(TerminalRepr): def call_fixture_func( - fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs + fixturefunc: _FixtureFunc[FixtureValue], request: FixtureRequest, kwargs ) -> FixtureValue: if is_generator(fixturefunc): fixturefunc = cast( @@ -950,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: @@ -1003,8 +1001,8 @@ class FixtureDef(Generic[FixtureValue]): 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: @@ -1015,7 +1013,7 @@ class FixtureDef(Generic[FixtureValue]): self._finalizers.append(finalizer) def finish(self, request: SubRequest) -> None: - exceptions: List[BaseException] = [] + exceptions: list[BaseException] = [] while self._finalizers: fin = self._finalizers.pop() try: @@ -1099,7 +1097,7 @@ class FixtureDef(Generic[FixtureValue]): 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 @@ -1147,7 +1145,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.""" @@ -1173,13 +1171,11 @@ def wrap_function_to_error_out_if_called_directly( @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 @@ -1217,13 +1213,11 @@ class FixtureFunctionMarker: 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: ... @@ -1231,27 +1225,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 @@ -1385,7 +1375,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 @@ -1395,7 +1385,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. @@ -1404,7 +1394,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( @@ -1414,7 +1404,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)) @@ -1451,17 +1441,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") @@ -1469,8 +1459,8 @@ class FixtureManager: 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. @@ -1541,9 +1531,9 @@ class FixtureManager: 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 @@ -1553,7 +1543,7 @@ class FixtureManager: 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) @@ -1580,7 +1570,7 @@ class FixtureManager: 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]: @@ -1625,7 +1615,7 @@ class FixtureManager: # 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) @@ -1633,15 +1623,11 @@ class FixtureManager: self, *, name: str, - func: "_FixtureFunc[object]", - nodeid: Optional[str], - scope: Union[ - Scope, _ScopeName, Callable[[str, Config], _ScopeName] - ] = "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 @@ -1699,14 +1685,14 @@ class FixtureManager: 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. @@ -1764,7 +1750,7 @@ class FixtureManager: 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. @@ -1791,7 +1777,7 @@ class FixtureManager: 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) @@ -1809,7 +1795,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() @@ -1842,7 +1828,7 @@ def _show_fixtures_per_test(config: Config, session: "Session") -> None: 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 @@ -1862,13 +1848,13 @@ def _show_fixtures_per_test(config: Config, session: "Session") -> 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() @@ -1879,7 +1865,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 diff --git a/src/_pytest/freeze_support.py b/src/_pytest/freeze_support.py index e03a6d175..2ba6f9b8b 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 68e0bd881..f23b83994 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 @@ -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 @@ -239,11 +238,11 @@ def showhelp(config: Config) -> None: 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 +250,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 c7f9d036c..996148999 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -3,16 +3,13 @@ """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 @@ -57,7 +54,7 @@ hookspec = HookspecMarker("pytest") @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) `. @@ -76,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. @@ -100,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. @@ -141,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:: @@ -166,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`. @@ -189,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. @@ -206,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 @@ -230,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`. @@ -272,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. @@ -288,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. @@ -309,8 +306,8 @@ def pytest_collection_finish(session: "Session") -> None: }, ) def pytest_ignore_collect( - collection_path: Path, path: "LEGACY_PATH", config: "Config" -) -> Optional[bool]: + 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. @@ -324,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. @@ -343,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. @@ -357,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. @@ -379,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 @@ -389,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 @@ -407,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: @@ -422,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: @@ -436,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: @@ -451,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. @@ -467,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`. @@ -499,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. @@ -510,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 @@ -529,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`. @@ -554,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`. @@ -571,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: @@ -587,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``. @@ -615,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 @@ -641,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): @@ -683,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. @@ -704,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. @@ -723,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 @@ -742,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()``. @@ -758,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()`` @@ -783,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. @@ -804,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. @@ -820,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. @@ -839,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`. @@ -862,8 +855,8 @@ 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 fixturedef: @@ -890,7 +883,7 @@ 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 @@ -915,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. @@ -929,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. @@ -944,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. @@ -962,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 @@ -984,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 @@ -1031,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:: @@ -1066,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. @@ -1080,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. @@ -1104,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. @@ -1136,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. @@ -1158,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. @@ -1202,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. @@ -1231,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 @@ -1250,7 +1245,7 @@ def pytest_internalerror( def pytest_keyboard_interrupt( - excinfo: "ExceptionInfo[Union[KeyboardInterrupt, Exit]]", + excinfo: ExceptionInfo[KeyboardInterrupt | Exit], ) -> None: """Called for keyboard interrupt. @@ -1264,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. @@ -1295,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 @@ -1311,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 011af6100..3a2cb59a6 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -8,18 +8,16 @@ Output conforms to 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 @@ -89,15 +87,15 @@ families["xunit2"] = families["_base"] 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 @@ class _NodeReporter: 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 @@ class _NodeReporter: 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], @@ -156,7 +154,7 @@ class _NodeReporter: 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 @@ class _NodeReporter: 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 @@ class _NodeReporter: 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 @@ class LogXML: 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 @@ class LogXML: 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) @@ -671,7 +665,9 @@ class LogXML: skipped=str(self.stats["skipped"]), tests=str(numtests), time=f"{suite_time_delta:.3f}", - timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(), + 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 @@ class LogXML: _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 d9de65b1a..61476d689 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 @@ class Testdir: """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 @@ class Testdir: """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 @@ class Testdir: 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 @@ class Testdir: """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`. @@ -394,9 +389,7 @@ def Session_startdir(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 diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index c9139d369..fe3be060f 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 Generic 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 @@ class ColoredLevelFormatter(DatetimeFormatter): 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 @@ class catching_logs(Generic[_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.""" @@ -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 @@ class LogCaptureFixture: 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 @@ class LogCaptureFixture: 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 @@ class LogCaptureFixture: 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 @@ class LogCaptureFixture: 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 @@ class LogCaptureFixture: 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 @@ class LogCaptureFixture: @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 @@ class LoggingPlugin: 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 @@ class LoggingPlugin: 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 @@ class LoggingPlugin: 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 @@ class _LiveLoggingStreamHandler(logging_StreamHandler): """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 d200a6877..8ec269060 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 AbstractSet 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 @@ -271,8 +268,8 @@ def validate_basetemp(path: str) -> str: 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 @@ -291,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 @@ -329,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) @@ -346,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" @@ -372,25 +369,15 @@ 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/]""" try: - if not bindir.is_dir(): - return False + return path.joinpath("pyvenv.cfg").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 @@ -430,11 +417,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 @@ -508,17 +495,18 @@ class Dir(nodes.Directory): 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,8 +540,8 @@ class Session(nodes.Collector): # Set on the session by runner.pytest_sessionstart. _setupstate: SetupState # Set on the session by fixtures.pytest_sessionstart. - _fixturemanager: "FixtureManager" - exitstatus: Union[int, ExitCode] + _fixturemanager: FixtureManager + exitstatus: int | ExitCode def __init__(self, config: Config) -> None: super().__init__( @@ -567,22 +555,22 @@ class Session(nodes.Collector): ) 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 @@ -596,11 +584,11 @@ class Session(nodes.Collector): ) @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: @@ -614,11 +602,11 @@ class Session(nodes.Collector): 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: @@ -651,9 +639,7 @@ class Session(nodes.Collector): 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") @@ -664,7 +650,7 @@ class Session(nodes.Collector): def isinitpath( self, - path: Union[str, "os.PathLike[str]"], + path: str | os.PathLike[str], *, with_parents: bool = False, ) -> bool: @@ -686,7 +672,7 @@ class Session(nodes.Collector): 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 @@ -706,7 +692,7 @@ class Session(nodes.Collector): 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. @@ -718,7 +704,7 @@ class Session(nodes.Collector): 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 () @@ -736,17 +722,17 @@ class Session(nodes.Collector): @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 @@ -772,10 +758,10 @@ class Session(nodes.Collector): 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, @@ -830,7 +816,7 @@ class Session(nodes.Collector): 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 @@ -839,11 +825,11 @@ class Session(nodes.Collector): 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 @@ -881,9 +867,9 @@ class Session(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() @@ -900,7 +886,7 @@ class Session(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) @@ -960,9 +946,7 @@ class Session(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) @@ -982,7 +966,7 @@ class Session(nodes.Collector): 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: @@ -1006,7 +990,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 950a6959e..a4f942c5a 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 Config 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 @@ old_mark_config_key = StashKey[Optional[Config]]() 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 `. @@ -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: @@ -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 @@ class 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 @@ class KeywordMatcher: 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) -> bool: - return name in self.own_mark_names + 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 + + 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 78b7fda69..89cc0e94d 100644 --- a/src/_pytest/mark/expression.py +++ b/src/_pytest/mark/expression.py @@ -5,26 +5,35 @@ The grammar is: 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 @@ class Scanner: 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 @@ class Scanner: ) 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 @@ class Expression: 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 @@ class 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 @@ class Expression: :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 456808063..92ade55f7 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 Collection 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 @@ -48,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) @@ -76,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: @@ -101,9 +98,9 @@ class ParameterSet(NamedTuple): @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: @@ -128,11 +125,11 @@ class ParameterSet(NamedTuple): @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 @@ -142,9 +139,9 @@ class ParameterSet(NamedTuple): @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 ] @@ -152,12 +149,12 @@ class ParameterSet(NamedTuple): @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 @@ -200,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: @@ -233,7 +230,7 @@ class Mark: 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. @@ -245,7 +242,7 @@ class 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 @@ -316,7 +313,7 @@ class MarkDecorator: return self.mark.name @property - def args(self) -> Tuple[Any, ...]: + def args(self) -> tuple[Any, ...]: """Alias for mark.args.""" return self.mark.args @@ -330,7 +327,7 @@ class MarkDecorator: """: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 @@ -347,7 +344,7 @@ class MarkDecorator: 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): @@ -362,10 +359,10 @@ class MarkDecorator: 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 @@ -395,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 @@ -437,13 +434,13 @@ if TYPE_CHECKING: 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 @@ if TYPE_CHECKING: @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 @@ class MarkGenerator: 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.""" @@ -569,7 +561,7 @@ MARK_GEN = MarkGenerator(_ispytest=True) 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 @@ class NodeKeywords(MutableMapping[str, Any]): 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 f498d60df..75b019a3b 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 @@ import sys 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 @@ V = TypeVar("V") @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,14 +128,14 @@ 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. @@ -182,8 +180,8 @@ class MonkeyPatch: def setattr( self, - target: Union[str, object], - name: Union[object, str], + target: str | object, + name: object | str, value: object = notset, raising: bool = True, ) -> None: @@ -254,8 +252,8 @@ class MonkeyPatch: 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``. @@ -310,7 +308,7 @@ class MonkeyPatch: # 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 @@ -363,7 +361,7 @@ class MonkeyPatch: 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 aad54f8b3..bbde2664b 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 Callable 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 @@ -62,9 +58,9 @@ _T = TypeVar("_T") 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 @@ class NodeMeta(abc.ABCMeta): ).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 @@ class Node(abc.ABC, metaclass=NodeMeta): 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 @@ class Node(abc.ABC, metaclass=NodeMeta): 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 @@ class Node(abc.ABC, metaclass=NodeMeta): 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 @@ class Node(abc.ABC, metaclass=NodeMeta): 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 @@ class Node(abc.ABC, metaclass=NodeMeta): 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 @@ class Node(abc.ABC, metaclass=NodeMeta): 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 @@ class Node(abc.ABC, metaclass=NodeMeta): """ 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 @@ class Node(abc.ABC, metaclass=NodeMeta): """ 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 @@ class Node(abc.ABC, metaclass=NodeMeta): 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 @@ class Node(abc.ABC, metaclass=NodeMeta): 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 @@ -474,8 +466,8 @@ class Node(abc.ABC, metaclass=NodeMeta): 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` @@ -485,7 +477,7 @@ class Node(abc.ABC, metaclass=NodeMeta): 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) @@ -495,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) @@ -515,14 +507,14 @@ class Collector(Node, abc.ABC): """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. @@ -551,7 +543,7 @@ class Collector(Node, abc.ABC): 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)) @@ -564,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): @@ -621,10 +613,10 @@ class FSCollector(Collector, abc.ABC): 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) @@ -666,9 +658,9 @@ class Item(Node, abc.ABC): 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, @@ -683,11 +675,11 @@ class Item(Node, abc.ABC): 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() @@ -747,7 +739,7 @@ class Item(Node, abc.ABC): 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: @@ -761,7 +753,7 @@ class Item(Node, abc.ABC): 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 f953dabe0..5b20803e5 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 _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _E @_with_exception(Exit) def exit( reason: str = "", - returncode: Optional[int] = None, + returncode: int | None = None, ) -> NoReturn: """Exit testing process. @@ -207,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. @@ -267,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 20cb8ca83..69c011ed2 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 @@ -68,7 +69,7 @@ def pytest_unconfigure(config: Config) -> None: 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. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index b11eea4e7..e4dc4eddc 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 @@ import types 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,7 +165,7 @@ def rm_rf(path: Path) -> None: shutil.rmtree(str(path), onerror=onerror) -def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]: +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): @@ -180,7 +173,7 @@ def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]: 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,7 +761,7 @@ 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. @@ -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 42f50900a..5c6ce5e88 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 sys 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() @@ -199,7 +195,7 @@ class LsofFdLeakChecker: @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 @@ class HookRecorder: 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,13 +275,13 @@ class HookRecorder: 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) @@ -327,42 +323,42 @@ class HookRecorder: @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): @@ -387,31 +383,31 @@ class HookRecorder: @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]: @@ -419,10 +415,10 @@ class HookRecorder: def listoutcomes( self, - ) -> Tuple[ + ) -> tuple[ Sequence[TestReport], - Sequence[Union[CollectReport, TestReport]], - Sequence[Union[CollectReport, TestReport]], + Sequence[CollectReport | TestReport], + Sequence[CollectReport | TestReport], ]: passed = [] skipped = [] @@ -441,7 +437,7 @@ class HookRecorder: 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: @@ -461,14 +457,14 @@ class HookRecorder: @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). @@ -480,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. @@ -524,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 @@ -555,7 +551,7 @@ class RunResult: % (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. @@ -568,7 +564,7 @@ class RunResult: 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:: @@ -599,8 +595,8 @@ class RunResult: 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 @@ -626,7 +622,7 @@ class RunResult: 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) @@ -659,7 +655,7 @@ class Pytester: __test__ = False - CLOSE_STDIN: "Final" = NOTSET + CLOSE_STDIN: Final = NOTSET class TimeoutExpired(Exception): pass @@ -674,9 +670,9 @@ class Pytester: ) -> 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: @@ -687,7 +683,7 @@ class Pytester: #: :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) @@ -755,8 +751,8 @@ class Pytester: 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()) @@ -769,7 +765,7 @@ class Pytester: 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: @@ -892,9 +888,7 @@ class 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 @@ -908,19 +902,20 @@ class Pytester: 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 @@ -931,13 +926,14 @@ class Pytester: 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: @@ -976,9 +972,7 @@ class Pytester: 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: @@ -997,9 +991,7 @@ class Pytester: 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 @@ -1019,7 +1011,7 @@ class Pytester: 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 @@ -1031,7 +1023,7 @@ class Pytester: The collected items. """ session = colitems[0].session - result: List[Item] = [] + result: list[Item] = [] for colitem in colitems: result.extend(session.genitems(colitem)) return result @@ -1065,7 +1057,7 @@ class Pytester: 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 @@ -1078,7 +1070,7 @@ class Pytester: def inline_run( self, - *args: Union[str, "os.PathLike[str]"], + *args: str | os.PathLike[str], plugins=(), no_reraise_ctrlc: bool = False, ) -> HookRecorder: @@ -1148,7 +1140,7 @@ class Pytester: 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.""" @@ -1191,9 +1183,7 @@ class Pytester: 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) @@ -1204,8 +1194,8 @@ class Pytester: 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"): @@ -1216,7 +1206,7 @@ class Pytester: ) 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. @@ -1240,7 +1230,7 @@ class Pytester: 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 @@ -1252,7 +1242,7 @@ class Pytester: 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. @@ -1273,7 +1263,7 @@ class Pytester: 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 @@ -1284,7 +1274,7 @@ class Pytester: def getmodulecol( self, - source: Union[str, "os.PathLike[str]"], + source: str | os.PathLike[str], configargs=(), *, withinit: bool = False, @@ -1316,9 +1306,7 @@ class Pytester: 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 @@ -1336,10 +1324,10 @@ class Pytester: 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`. @@ -1374,9 +1362,9 @@ class Pytester: 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. @@ -1404,8 +1392,10 @@ class Pytester: - 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 @@ -1462,10 +1452,10 @@ class Pytester: 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) @@ -1474,7 +1464,7 @@ class Pytester: 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. @@ -1501,9 +1491,7 @@ class Pytester: 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 @@ -1517,7 +1505,7 @@ class Pytester: 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. @@ -1562,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. @@ -1574,7 +1562,7 @@ class LineMatcher: """ 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): diff --git a/src/_pytest/pytester_assertions.py b/src/_pytest/pytester_assertions.py index d20c2bb59..d543798f7 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 4887614de..9182ce7df 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 final from typing import Generator from typing import Iterable from typing import Iterator -from typing import List from typing import Literal from typing import Mapping -from typing import Optional from typing import Pattern from typing import Sequence -from typing import Set -from typing import Tuple from typing import TYPE_CHECKING -from typing import Union import 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,7 +171,7 @@ 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" try: has_pkginit = pkginit.is_file() @@ -186,7 +183,7 @@ def pytest_collect_directory( 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( @@ -206,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): @@ -320,7 +317,7 @@ class PyobjMixin(nodes.Node): 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() @@ -394,7 +391,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): 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 [] @@ -406,11 +403,11 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): # 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()): @@ -437,7 +434,7 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC): 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 @@ -548,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) @@ -642,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) @@ -680,13 +677,13 @@ class Package(nodes.Directory): 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): @@ -720,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 @@ -735,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): @@ -872,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. @@ -903,7 +900,7 @@ class IdMaker: # 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: @@ -950,9 +947,7 @@ class IdMaker: 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: @@ -968,17 +963,17 @@ class IdMaker: 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)): @@ -1036,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, @@ -1052,10 +1047,10 @@ class CallSpec2: 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) @@ -1103,7 +1098,7 @@ class Metafunc: def __init__( self, - definition: "FunctionDefinition", + definition: FunctionDefinition, fixtureinfo: fixtures.FuncFixtureInfo, config: Config, cls=None, @@ -1134,19 +1129,17 @@ class Metafunc: 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 @@ -1175,7 +1168,7 @@ class Metafunc: 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 @@ -1275,7 +1268,7 @@ class Metafunc: if node is None: name2pseudofixturedef = None else: - default: Dict[str, FixtureDef[Any]] = {} + default: dict[str, FixtureDef[Any]] = {} name2pseudofixturedef = node.stash.setdefault( name2pseudofixturedef_key, default ) @@ -1322,12 +1315,10 @@ class Metafunc: 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: @@ -1365,10 +1356,10 @@ class Metafunc: 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: @@ -1388,8 +1379,8 @@ class Metafunc: 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 @@ -1402,7 +1393,7 @@ class Metafunc: :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" @@ -1427,7 +1418,7 @@ class Metafunc: 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. @@ -1458,7 +1449,7 @@ class Metafunc: 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. @@ -1487,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: @@ -1536,13 +1527,13 @@ class Function(PyobjMixin, nodes.Item): 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) @@ -1585,12 +1576,12 @@ class Function(PyobjMixin, nodes.Item): # 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 @@ -1671,7 +1662,7 @@ class Function(PyobjMixin, nodes.Item): 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 dd0209cb0..cbdd84bf3 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + from collections.abc import Collection from collections.abc import Sized from decimal import Decimal @@ -11,9 +13,7 @@ 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 +21,6 @@ from typing import Tuple 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 +32,12 @@ if TYPE_CHECKING: 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 +78,7 @@ class ApproxBase: 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 +102,7 @@ class ApproxBase: 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 +128,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 +143,12 @@ class ApproxNumpy(ApproxBase): ) 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 +245,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 = { @@ -318,7 +319,7 @@ class ApproxSequenceLike(ApproxBase): 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): @@ -383,8 +384,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 @@ -714,17 +715,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}" @@ -735,6 +732,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, @@ -743,7 +748,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. @@ -769,15 +774,15 @@ E = TypeVar("E", bound=BaseException) @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, @@ -785,8 +790,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: @@ -934,7 +939,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: @@ -946,7 +951,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)) @@ -972,14 +977,14 @@ raises.Exception = fail.Exception # type: ignore 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 def __enter__(self) -> _pytest._code.ExceptionInfo[E]: self.excinfo = _pytest._code.ExceptionInfo.for_later() @@ -987,9 +992,9 @@ class RaisesContext(ContextManager[_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 cceabbca1..6e33c8a39 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 63e7a4bd6..3fc00d947 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 Callable from typing import final from typing import Generator from typing import Iterator -from typing import List -from typing import Optional from typing import overload from typing import Pattern -from typing import Tuple -from typing import Type +from typing import 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 @@ T = TypeVar("T") @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 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] 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 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] """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 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] """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 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] 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 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg] 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 @@ class WarningsChecker(WarningsRecorder): 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 2064183d0..2f39adbfa 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,24 +1,19 @@ # 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 TYPE_CHECKING -from typing import TypeVar -from typing import Union from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo @@ -39,6 +34,8 @@ from _pytest.outcomes import skip if TYPE_CHECKING: + from typing_extensions import Self + from _pytest.runner import CallInfo @@ -54,16 +51,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 +88,7 @@ class BaseReport: 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 +170,7 @@ class BaseReport: 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:: @@ -202,7 +196,7 @@ class BaseReport: ) return verbose - def _to_json(self) -> Dict[str, Any]: + def _to_json(self) -> dict[str, Any]: """Return the contents of this report as a dict of builtin entries, suitable for serialization. @@ -213,7 +207,7 @@ class BaseReport: 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,7 +221,7 @@ class BaseReport: 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() @@ -256,18 +250,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 +274,7 @@ class TestReport(BaseReport): #: 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 +313,7 @@ class TestReport(BaseReport): 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 +330,13 @@ class TestReport(BaseReport): 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 +390,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 +423,7 @@ class CollectReport(BaseReport): @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 +439,8 @@ class CollectErrorRepr(TerminalRepr): 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 +450,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 +463,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 +471,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 +480,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]: 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 +488,18 @@ def _report_to_json(report: BaseReport) -> 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 +536,7 @@ def _report_to_json(report: 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 +557,7 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]: 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 +576,7 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]: ] 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 +603,8 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> 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 e5af60e38..716c4948f 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Basic collect and runtest protocol implementations.""" +from __future__ import annotations + import bdb import dataclasses import os @@ -8,17 +10,11 @@ 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 @@ -72,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") @@ -103,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) @@ -120,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 @@ -188,14 +184,14 @@ def pytest_runtest_call(item: Item) -> None: 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. @@ -211,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 @@ -239,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( @@ -253,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: @@ -276,9 +272,9 @@ TResult = TypeVar("TResult", covariant=True) 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. @@ -290,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, @@ -325,14 +321,13 @@ class CallInfo(Generic[TResult]): 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: @@ -343,7 +338,7 @@ class CallInfo(Generic[TResult]): 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): @@ -374,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 @@ -396,7 +391,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport: call = CallInfo.from_call( collect, "collect", reraise=(KeyboardInterrupt, SystemExit) ) - longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None + longrepr: None | tuple[str, int, str] | str | TerminalRepr = None if not call.excinfo: outcome: Literal["passed", "skipped", "failed"] = "passed" else: @@ -490,18 +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]], + list[Callable[[], object]], # Node's exception and original traceback, if its setup raised. - Optional[ - Tuple[ - Union[OutcomeException, Exception], - Optional[types.TracebackType], - ] - ], + tuple[OutcomeException | Exception, types.TracebackType | None] | None, ], ] = {} @@ -536,7 +526,7 @@ class SetupState: 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. @@ -544,7 +534,7 @@ class SetupState: 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 2c6e23208..976a3ba24 100644 --- a/src/_pytest/scope.py +++ b/src/_pytest/scope.py @@ -8,10 +8,11 @@ would cause circular references. 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 39ab28b46..de297f408 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 13c0df84e..4e124cce2 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 54500b285..08fcb283e 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 sys 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 pytest_configure(config: Config) -> None: ) -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 @@ -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 a4b829fc6..6a9ff884e 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 @@ class Stash: """ 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 1e3a09d96..bd906ce63 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,5 +1,4 @@ -from typing import List -from typing import Optional +from __future__ import annotations from _pytest import nodes from _pytest.cacheprovider import Cache @@ -55,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." @@ -113,7 +112,7 @@ class StepwisePlugin: 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 2eef8d1f2..26c573f58 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 @@ import textwrap 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 @@ class MoreQuietAction(argparse.Action): 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 @@ class MoreQuietAction(argparse.Action): 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: @@ -311,7 +307,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 = "." @@ -339,12 +335,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 @@ -357,31 +353,31 @@ class WarningReport: @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.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.""" @@ -428,7 +424,7 @@ class TerminalReporter: return self._showfspath @showfspath.setter - def showfspath(self, value: Optional[bool]) -> None: + def showfspath(self, value: bool | None) -> None: self._showfspath = value @property @@ -492,7 +488,7 @@ class TerminalReporter: 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() @@ -519,8 +515,8 @@ class TerminalReporter: 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() @@ -570,7 +566,7 @@ class TerminalReporter: 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 @@ -777,7 +773,7 @@ class TerminalReporter: 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: @@ -804,7 +800,7 @@ class TerminalReporter: 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): @@ -813,14 +809,14 @@ class TerminalReporter: 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() @@ -830,7 +826,7 @@ class TerminalReporter: ) 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( @@ -863,7 +859,7 @@ class TerminalReporter: 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 @@ -884,7 +880,7 @@ class TerminalReporter: @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("") @@ -948,7 +944,7 @@ class TerminalReporter: ) 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) @@ -993,7 +989,7 @@ class TerminalReporter: 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 @@ -1006,11 +1002,11 @@ class TerminalReporter: 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) @@ -1056,7 +1052,7 @@ class TerminalReporter: ) -> 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) @@ -1067,7 +1063,7 @@ class TerminalReporter: 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 @@ -1107,11 +1103,11 @@ class TerminalReporter: sep_title: str, *, style: str, - needed_opt: Optional[str] = None, + needed_opt: str | None = None, ) -> None: 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) @@ -1128,7 +1124,7 @@ class TerminalReporter: 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") @@ -1195,7 +1191,7 @@ class TerminalReporter: 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 @@ -1207,7 +1203,7 @@ class TerminalReporter: ) 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) @@ -1222,7 +1218,7 @@ class TerminalReporter: 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) @@ -1236,8 +1232,8 @@ class TerminalReporter: line += " - " + str(reason) lines.append(line) - def show_skipped(lines: List[str]) -> None: - skipped: List[CollectReport] = self.stats.get("skipped", []) + def show_skipped(lines: list[str]) -> None: + skipped: list[CollectReport] = self.stats.get("skipped", []) fskips = _folded_skips(self.startpath, skipped) if skipped else [] if not fskips: return @@ -1256,7 +1252,7 @@ class TerminalReporter: else: lines.append("%s [%d] %s: %s" % (markup_word, num, fspath, reason)) - REPORTCHAR_ACTIONS: Mapping[str, Callable[[List[str]], None]] = { + REPORTCHAR_ACTIONS: Mapping[str, Callable[[list[str]], None]] = { "x": show_xfailed, "X": show_xpassed, "f": partial(show_simple, stat="failed"), @@ -1265,7 +1261,7 @@ class TerminalReporter: "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. @@ -1276,7 +1272,7 @@ class TerminalReporter: 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 @@ -1296,7 +1292,7 @@ class TerminalReporter: 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: @@ -1304,7 +1300,7 @@ class TerminalReporter: 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. @@ -1329,14 +1325,14 @@ class TerminalReporter: 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 = [] @@ -1355,7 +1351,7 @@ class TerminalReporter: 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")) @@ -1396,7 +1392,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. @@ -1422,7 +1418,7 @@ 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) @@ -1452,8 +1448,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) @@ -1470,11 +1466,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 @@ -1489,7 +1485,7 @@ _color_for_type = { _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 @@ -1502,8 +1498,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 603a1777c..d78c32c85 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 0541dc8e0..b23c7f69e 100644 --- a/src/_pytest/timing.py +++ b/src/_pytest/timing.py @@ -6,6 +6,8 @@ pytest runtime information (issue #185). 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 72efed3e8..91109ea69 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 Dict 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 @@ class TempPathFactory: config: Config, *, _ispytest: bool = False, - ) -> "TempPathFactory": + ) -> TempPathFactory: """Create a factory according to pytest configuration. :meta private: @@ -198,7 +198,7 @@ class TempPathFactory: 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 0f201d0f3..aefea1333 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -1,6 +1,8 @@ # mypy: allow-untyped-defs """Discover and run std-library "unittest" style tests.""" +from __future__ import annotations + import inspect import sys import traceback @@ -9,8 +11,6 @@ from typing import Any 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 @@ -49,8 +49,8 @@ _SysExcInfoType = Union[ def pytest_pycollect_makeitem( - collector: Union[Module, Class], name: str, obj: object -) -> Optional["UnitTestCase"]: + collector: Module | Class, name: str, obj: object +) -> UnitTestCase | None: try: # Has unittest been imported? ut = sys.modules["unittest"] @@ -81,7 +81,7 @@ class UnitTestCase(Class): # 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 @@ -201,7 +201,7 @@ class UnitTestCase(Class): 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) @@ -215,7 +215,7 @@ class TestCaseFunction(Function): 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: @@ -226,7 +226,7 @@ class TestCaseFunction(Function): 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: @@ -265,7 +265,7 @@ class TestCaseFunction(Function): 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): @@ -275,11 +275,11 @@ class TestCaseFunction(Function): 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: @@ -287,7 +287,7 @@ class TestCaseFunction(Function): def addExpectedFailure( self, - testcase: "unittest.TestCase", + testcase: unittest.TestCase, rawexcinfo: _SysExcInfoType, reason: str = "", ) -> None: @@ -298,8 +298,8 @@ class TestCaseFunction(Function): 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: @@ -310,13 +310,13 @@ class TestCaseFunction(Function): 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 50b121e88..c191703a3 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 a5884f295..4ab14e48c 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 22590892f..5c59e55c5 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 d1c39d203..5c661e66c 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 c6b6de827..90abcdab0 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 e4cb67d5d..cccab5d57 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 0215aba96..4a95e2d0c 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 diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ac7fab3d2..01d911e8c 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 @@ -1464,14 +1466,21 @@ def test_issue_9765(pytester: Pytester) -> None: } ) - 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 57ab4cdfd..7ae5ad461 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/conftest.py b/testing/conftest.py index b7e2d6111..24e5d1830 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 @@ def color_mapping(): 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 9e83a49d5..5d0e69c58 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 d802a7f87..e612ae01e 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 58c41942d..5e30bb158 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 d88c001c2..3cb8f1be0 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 bba5db8b2..c2d2b9188 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 2809d0cc6..38c51e586 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 58c41942d..5e30bb158 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 d88c001c2..3cb8f1be0 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 2da4ffe2f..5e0ab5441 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 d88c001c2..3cb8f1be0 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 64bbeefac..a6690bdc3 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 fe1c743a6..4718d7d5b 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 890ca3dea..06f40ca47 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 42108d5da..79bcc099e 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 ede0f3e60..5af476ad4 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 d96c90a91..18180b99f 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 7479c66c1..0dcc7ab28 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 4737ef904..4985c69ff 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 e026fe3d1..b787cb39e 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 d687fc225..fc589e1fd 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 801aa0a73..885edd7d9 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 0a4820c69..b45a6772c 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 c8a124f54..3a0f6bed1 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 26a4d90bc..8c150da5c 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 fe1ae620a..274ab97d0 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 2809d0cc6..38c51e586 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 3a5d3ac33..94eaa3e07 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 d0c4bdbdf..cb3f9fbf4 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 a1f3b2d58..112d1e05f 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 45e974478..3dea97f54 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 84e5256f0..d90961ae3 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 7f1769beb..b4fcc17bf 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 ad26fdd8c..b933b70ed 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 9ee74a471..d31ab971f 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 7f1769beb..b4fcc17bf 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 fa688f0a8..2d6d7faef 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 f78a57c32..45e5deaaf 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 12e0e3e91..1c7a710cd 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 8b6e8697e..96f0cacfa 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 40587cf2b..b78ca04b3 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 0cc8446d8..0dd782e42 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 a2ab7ee33..db88bcdab 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 0f316f0e4..0559905ce 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 bde5c0711..2e88c5ad5 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 dd18e1741..b10f874e7 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 397661644..138c07e95 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 d95ad0a83..c98e58316 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 17085e50b..3b580aa34 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 f50eb6552..d9449485d 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 4aa35faa0..503ead473 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 d66b66df5..733202915 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 7550a0975..52ff96ea8 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 48f7e476f..fe431d8e7 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 eee4263d2..07fd96c9c 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 a82ddaebc..8792492b3 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 e9b10171e..8a93366b9 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 2a4a66509..ea1ae3715 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 be64a1ff2..ee3bc2bbe 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 95fa795ef..cc514bafb 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 5204fde8a..33d5ce8ce 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 7b9c10988..80f78d843 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 fbfda2e5d..2015d22c7 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 ef63a2d15..286c98ac5 100644 --- a/testing/freeze/runtests_script.py +++ b/testing/freeze/runtests_script.py @@ -3,6 +3,9 @@ This is the script that is actually frozen into an executable: simply executes 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 425f29a64..000ca9731 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 1230fcce1..38c1e75cf 100644 --- a/testing/freeze/tox_run.py +++ b/testing/freeze/tox_run.py @@ -3,6 +3,9 @@ Called by tox.ini: uses the generated executable to run the tests in ./tests/ directory. """ +from __future__ import annotations + + if __name__ == "__main__": import os import sys diff --git a/testing/io/test_pprint.py b/testing/io/test_pprint.py index 15fe66112..1326ef34b 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 f627434c4..075d40cdf 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 diff --git a/testing/io/test_terminalwriter.py b/testing/io/test_terminalwriter.py index afa8d5cae..043c2d1d9 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 re 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 82503b830..9ff1ad06e 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 c1cfff632..0603eaba2 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 diff --git a/testing/logging/test_formatter.py b/testing/logging/test_formatter.py index 379712937..cfe3bee68 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 7e592febf..cf54788e2 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 2bdb15454..d74802884 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 0715f4765..e36e554db 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 383d7a0b5..41ffad18a 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 b216c4bee..cef67f83e 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 5494c4427..a49129cf0 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 index 9a13a3279..449661f72 100644 --- a/testing/plugins_integration/pytest_rerunfailures_integration.py +++ b/testing/plugins_integration/pytest_rerunfailures_integration.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import unittest diff --git a/testing/plugins_integration/pytest_trio_integration.py b/testing/plugins_integration/pytest_trio_integration.py index 60f48ec60..eceac5076 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 0dbf5faeb..4f386bf1b 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/simple_integration.py b/testing/plugins_integration/simple_integration.py index 48089afcc..ed504ae4b 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 968e88285..69743cdbe 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 @@ from math import sqrt 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 @@ class TestApprox: (-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 @@ class TestApprox: 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 @@ class TestRecursiveSequenceMap: (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 843fa3c0e..063866112 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 @@ -1129,7 +1130,7 @@ class TestTracebackCutting: 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 d3cff38f9..bc091bb1f 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 @@ -4514,7 +4516,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 c20aaeed8..c52a683a3 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 diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 3d0058fa0..2dd85607e 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 Any 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 @@ class TestMetafunc: # 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 TestMetafunc: 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 @@ class TestMetafunc: 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 @@ class TestMetafunc: 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 @@ class TestMetafunc: 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 @@ class TestMetafunc: 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 @@ class TestMetafuncFunctionalAuto: 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 929865e31..271dd3e5a 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 diff --git a/testing/python/show_fixtures_per_test.py b/testing/python/show_fixtures_per_test.py index f756dca41..c860b61e2 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 0c41c0286..5d1513b62 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 726235999..69ca0f73f 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 @@ from _pytest.pytester import Pytester 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 @@ def mock_config(verbose: int = 0, assertion_override: Optional[int] = None): 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: @@ -223,7 +223,7 @@ class TestImportHookInstallation: ) -> 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": """\ @@ -369,12 +369,12 @@ class TestBinReprIntegration: 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 diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 8db9dbbe5..5ee40ee65 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 marshal 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._io.saferepr import DEFAULT_REPR_MAX_SIZE 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) @@ -1638,8 +1638,8 @@ class TestEarlyRewriteBailout: """ 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 @@ -2038,7 +2038,9 @@ class TestPyCacheDir: 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() @@ -2059,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() @@ -2105,3 +2107,26 @@ class TestIssue11140: ) 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 08158f619..72b4265cf 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 @@ from pathlib import Path 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 @@ -579,7 +579,7 @@ class TestLastFailed: 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)) @@ -693,7 +693,7 @@ class TestLastFailed: 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", {})) diff --git a/testing/test_capture.py b/testing/test_capture.py index b6c206ec4..fe6bd7d14 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 diff --git a/testing/test_collection.py b/testing/test_collection.py index 8ff38a334..f58222403 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,4 +1,6 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import os from pathlib import Path import pprint @@ -6,8 +8,6 @@ import shutil import sys import tempfile import textwrap -from typing import List -from typing import Type from _pytest.assertion.util import running_on_ci from _pytest.config import ExitCode @@ -152,20 +152,8 @@ class TestCollectFS: 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", - ), - ) - 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) + def test_ignored_virtualenvs(self, pytester: Pytester) -> None: + ensure_file(pytester.path / "virtual" / "pyvenv.cfg") testfile = ensure_file(pytester.path / "virtual" / "test_invenv.py") testfile.write_text("def test_hello(): pass", encoding="utf-8") @@ -179,23 +167,11 @@ class TestCollectFS: 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", - ), - ) def test_ignored_virtualenvs_norecursedirs_precedence( - self, pytester: Pytester, fname: str + self, pytester: Pytester ) -> 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" / "pyvenv.cfg") 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") @@ -204,27 +180,13 @@ class TestCollectFS: 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: + def test__in_venv(self, pytester: Pytester) -> None: """Directly test the virtual env detection function""" - bindir = "Scripts" if sys.platform.startswith("win") else "bin" - # no bin/activate, not a virtualenv + # no pyvenv.cfg, not a virtualenv 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 pyvenv.cfg, totally a virtualenv + base_path.joinpath("pyvenv.cfg").touch() assert _in_venv(base_path) is True def test_custom_norecursedirs(self, pytester: Pytester) -> None: @@ -536,7 +498,7 @@ class TestSession: 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 [ @@ -1885,7 +1847,7 @@ def test_do_not_collect_symlink_siblings( ) def test_respect_system_exceptions( pytester: Pytester, - exception_class: Type[BaseException], + exception_class: type[BaseException], msg: str, ): head = "Before exception" diff --git a/testing/test_compat.py b/testing/test_compat.py index 73ac1bad8..2c6b0269c 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 @@ -216,7 +217,7 @@ def test_cached_property() -> None: 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 1cb31fed0..232839399 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 re 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 @@ -633,7 +630,7 @@ class TestConfigCmdlineParsing: 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 @@ -996,7 +993,7 @@ class TestConfigFromdictargs: 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) @@ -1210,7 +1207,7 @@ def test_plugin_preparse_prevents_setuptools_loading( 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: @@ -1304,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 @@ -2241,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 eb3ebecdc..d51846f2f 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, @@ -536,7 +535,7 @@ def test_conftest_found_with_double_dash(pytester: Pytester) -> None: 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") diff --git a/testing/test_debugging.py b/testing/test_debugging.py index 1f3422947..37032f923 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 @@ -854,7 +855,7 @@ class TestPDB: 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 @@ class TestPDB: 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) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 9b33d641a..4aa4876c7 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 @@ -1595,7 +1596,7 @@ class Broken: "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 68e3a8a92..543f3252b 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 f290eb167..741a6ca82 100644 --- a/testing/test_error_diffs.py +++ b/testing/test_error_diffs.py @@ -5,6 +5,8 @@ See https://github.com/pytest-dev/pytest/issues/3333 for details. """ +from __future__ import annotations + from _pytest.pytester import Pytester import pytest diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index e50169761..c416e81d2 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 diff --git a/testing/test_findpaths.py b/testing/test_findpaths.py index 260b9d07c..9532f1eef 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 4906ef5c8..7fcf5804a 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 1ebc3ed3a..fd1fecb54 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,8 +38,8 @@ class RunAndParse: 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") @@ -220,11 +219,11 @@ class TestPython: 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 @@ -940,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: @@ -1183,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" @@ -1495,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() @@ -1531,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 diff --git a/testing/test_legacypath.py b/testing/test_legacypath.py index d2b33b4fb..72854e4e5 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 diff --git a/testing/test_link_resolve.py b/testing/test_link_resolve.py index 0461cd755..0557dae66 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 6294f66b3..94eac02ce 100644 --- a/testing/test_main.py +++ b/testing/test_main.py @@ -1,9 +1,10 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import argparse import os from pathlib import Path import re -from typing import Optional from _pytest.config import ExitCode from _pytest.config import UsageError @@ -66,7 +67,7 @@ def test_wrap_session_notify_exception(ret_exc, pytester: Pytester) -> None: @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 2896afa45..89eef7920 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_strict_prohibits_unregistered_markers( ], ) 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_mark_option( 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_mark_option_custom( ], ) 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_parametrize_with_module(pytester: Pytester) -> None: "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 @@ class TestKeywordSelection: ) 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 @@ class TestMarkDecorator: @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 07c89f908..f8f5f9221 100644 --- a/testing/test_mark_expression.py +++ b/testing/test_mark_expression.py @@ -1,12 +1,17 @@ -from typing import Callable +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: @@ -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( + 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 40ed95d6b..e7d836f7a 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -4,16 +4,17 @@ This ensures all internal packages can be imported without needing the pytest 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 2ad3ccc4d..079d8ff60 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) diff --git a/testing/test_nodes.py b/testing/test_nodes.py index a3caf471f..f039acf24 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 test_subclassing_both_item_and_collector_deprecated( "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 e959dfd63..14e2b5f69 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 9ca0da8f6..8fdd60bac 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 @@ import pytest 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 diff --git a/testing/test_pathlib.py b/testing/test_pathlib.py index 688d13f2f..81aba25f7 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 types import ModuleType 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 @@ class TestImportLibMode: 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 @@ class TestNamespacePackages: 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 TestNamespacePackages: 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 99b003b66..db85124bf 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 @@ class TestPytestPluginInteractions: 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: diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 9c6081a56..87714b470 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 @@ class TestInlineRunModulesCleanup: 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 @@ class TestSysPathsSnapshot: 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 73a872568..1db02252d 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 27ee9aa72..384f2b66a 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 TestSubclassWarningPop: 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 @@ class TestWarningsRecorderChecker: 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 7987b4017..3e314d2aa 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 @@ -294,8 +295,8 @@ class TestReportSerialization: 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 3ec567827..79f7a3fd4 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 @@ -523,7 +521,7 @@ class TestSessionReports: assert res[1].name == "TestClass" -reporttypes: List[Type[reports.BaseReport]] = [ +reporttypes: list[type[reports.BaseReport]] = [ reports.BaseReport, reports.TestReport, reports.CollectReport, @@ -533,9 +531,9 @@ reporttypes: List[Type[reports.BaseReport]] = [ @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]] = dict.fromkeys(args, []) report = reporttype(newthing=1, **basekw) assert report.newthing == 1 @@ -1048,7 +1046,7 @@ def test_store_except_info_on_error() -> None: 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 ) diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index 587c9eb9f..75e838a49 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 1727c2ee1..3cb811469 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 8624af478..ba9049160 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 8638f5a61..87123bd9a 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 d51a18739..5a9211d78 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 459216a6d..558e3d35c 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 diff --git a/testing/test_stash.py b/testing/test_stash.py index e523c4e6f..c7f6f4f95 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 472afea66..affdb7337 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 ce9fdc50c..01a84fd8d 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 sys 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 @@ -1929,9 +1928,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 diff --git a/testing/test_threadexception.py b/testing/test_threadexception.py index 99837b94e..abd301449 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 f424998e5..865d8e0b0 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 stat 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): @@ -394,7 +394,7 @@ class TestNumberedDir: 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 9561cad5e..56224c082 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1,6 +1,7 @@ # mypy: allow-untyped-defs +from __future__ import annotations + import sys -from typing import List from _pytest.config import ExitCode from _pytest.monkeypatch import MonkeyPatch @@ -1214,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 ) @@ -1251,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( @@ -1286,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( diff --git a/testing/test_unraisableexception.py b/testing/test_unraisableexception.py index 1657cfe4a..a15c754d0 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 a50d278bd..19fe0f8a2 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 73c8c1b32..d4d0e0b7f 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 @@ -618,11 +617,11 @@ def test_group_warnings_by_message_summary(pytester: Pytester) -> None: 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: *", @@ -654,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 4b146a251..d4d6a97ae 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -5,6 +5,8 @@ This file is not executed, it is only checked by mypy to ensure that 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 35b335a01..61563ca2c 100644 --- a/tox.ini +++ b/tox.ini @@ -26,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 @@ -72,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 @@ -81,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 @@ -109,6 +134,8 @@ setenv = PYTHONWARNDEFAULTENCODING= [testenv:regen] +description = + regenerate documentation examples under `{basepython}` changedir = doc/en basepython = python3 passenv = @@ -126,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) @@ -150,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