Compare commits

..

55 Commits

Author SHA1 Message Date
pytest bot
31afeeb0df Prepare release version 8.0.2 2024-02-24 22:02:40 +00:00
Ran Benita
1b00a2f4fb Merge pull request #12025 from pytest-dev/backport-12022-to-8.0.x
[8.0.x] Revert "Fix teardown error reporting when `--maxfail=1` (#11721)"
2024-02-23 15:52:42 +02:00
Ran Benita
ff2f66d84a [8.0.x] Revert "Fix teardown error reporting when --maxfail=1 (#11721)" 2024-02-23 13:35:15 +00:00
github-actions[bot]
8a8eed609c [8.0.x] Fix collection of short paths on Windows (#12024)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-02-23 11:20:19 +00:00
github-actions[bot]
74346f027c [8.0.x] Allow Sphinx 7.x (#12005)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-02-18 10:43:49 +00:00
github-actions[bot]
b7657b4d6b [8.0.x] Disallow Sphinx 6 and 7 (#12001)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-02-17 21:02:41 +00:00
Ran Benita
feb7c5e12e Merge pull request #11999 from pytest-dev/backport-11996-to-8.0.x
[8.0.x] code: fix `IndexError` crash in `getstatementrange_ast`
2024-02-17 20:28:13 +02:00
Ran Benita
090965574e [8.0.x] code: fix IndexError crash in getstatementrange_ast 2024-02-17 16:59:38 +00:00
Ran Benita
68524d4858 Merge pull request #11993 from pytest-dev/release-8.0.1
Prepare release 8.0.1
2024-02-17 00:09:47 +02:00
pytest bot
d7d320a15a Prepare release version 8.0.1 2024-02-16 13:20:17 +00:00
Ran Benita
93699166dc Merge pull request #11992 from bluetech/backport-11991
[8.0.x] recwarn: fix pytest.warns handling of Warnings with multiple arguments
2024-02-16 14:55:28 +02:00
Ran Benita
a232abd56c [8.0.x] recwarn: fix pytest.warns handling of Warnings with multiple arguments
(cherry picked from commit fbe18fc7a9)
2024-02-16 14:34:01 +02:00
Ran Benita
92203d2b78 Merge pull request #11990 from bluetech/backport-11920
[8.0.x] recwarn: let base exceptions propagate through `pytest.warns` again
2024-02-16 12:52:17 +02:00
Ran Benita
f1aa9226ac [8.0.x] recwarn: let base exceptions propagate through pytest.warns again
(cherry picked from commit 718cd40015)
2024-02-16 12:31:51 +02:00
github-actions[bot]
d86d081563 [8.0.x] Added logot to the plugin list (#11977)
Co-authored-by: Dave Hall <dave@etianen.com>
2024-02-14 20:08:38 +01:00
Ran Benita
c554c3d200 Merge pull request #11968 from pytest-dev/backport-11957-to-8.0.x
[8.0.x] main: fix reversed collection order in Session
2024-02-13 10:01:00 +02:00
Ran Benita
a6851e3459 [8.0.x] main: fix reversed collection order in Session 2024-02-13 07:44:36 +00:00
github-actions[bot]
e6f6be3bc9 [8.0.x] Improve error message when using @pytest.fixture twice (#11958)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-02-09 14:34:45 +00:00
Ran Benita
23b91d12de [8.0.x] Merge pull request #11941 from bluetech/doctest-parsefactories (#11948)
doctest: fix autouse fixtures possibly not getting picked up
(cherry picked from commit 6c0b6c2f92)
2024-02-07 22:09:19 -03:00
github-actions[bot]
b188f4d10d [8.0.x] doc: Remove sold out training (#11927)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-02-05 08:04:32 +00:00
github-actions[bot]
d60b6b0e28 [8.0.x] doc: Update training dates and add pytest sprint (#11926)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-02-05 07:12:10 +00:00
Pierre Sassoulas
c11cdfabd1 Migrate from autoflake, black, isort, pyupgrade, flake8 and pydocstyle, to ruff (#11911)
ruff is faster and handle everything we had prior.

isort configuration done based on the indication from
https://github.com/astral-sh/ruff/issues/4670, previousely based on
reorder-python-import (#11896)

flake8-docstrings was a wrapper around pydocstyle (now archived) that
explicitly asks to use ruff in https://github.com/PyCQA/pydocstyle/pull/658.

flake8-typing-import is useful mainly for project that support python 3.7
and the one useful check will be implemented in https://github.com/astral-sh/ruff/issues/2302

We need to keep blacken-doc because ruff does not handle detection
of python code inside .md and .rst. The direct link to the repo is
now used to avoid a redirection.

Manual fixes:
- Lines that became too long
- % formatting that was not done automatically
- type: ignore that were moved around
- noqa of hard to fix issues (UP031 generally)
- fmt: off and fmt: on that is not really identical
  between black and ruff
- autofix re-order in pre-commit from faster to slower

Co-authored-by: Ran Benita <ran@unusedvar.com>
2024-02-02 20:21:46 +01:00
Bruno Oliveira
368cc6225e [8.0.x] Upgrade blacken-doc to black's 2024 style (#11899) (#11900)
(cherry picked from commit 4546d5445a)

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
2024-01-31 10:46:50 -03:00
Bruno Oliveira
06e592370e [8.0.x] Replace reorder-python-imports by isort due to black incompatibility (#11898)
Backport of #11896
2024-01-31 09:08:36 -03:00
github-actions[bot]
a76aa6ff80 [8.0.x] fix incorrect examples for group_contains (#11897)
Co-authored-by: John Litborn <11260241+jakkdl@users.noreply.github.com>
2024-01-30 20:37:18 +00:00
Ran Benita
73371a03da Merge pull request #11894 from pytest-dev/backport-11879-to-8.0.x
[8.0.x] BUG: fix an edge case where ExceptionInfo._stringify_exception could crash pytest.raises
2024-01-30 17:38:43 +02:00
Clément Robert
eb698a64a0 [8.0.x] BUG: fix an edge case where ExceptionInfo._stringify_exception could crash pytest.raises 2024-01-30 15:20:56 +00:00
github-actions[bot]
3e48ef64cd [8.0.x] Add a changelog entry about FixtureManager.getfixtureclosure losing a default argument (#11891)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-01-30 09:55:36 -03:00
github-actions[bot]
8b70bb64d3 [8.0.x] Catch OSError from getpass.getuser() (#11876)
Co-authored-by: Russell Martin <russell@rjm.li>
2024-01-29 02:25:18 +00:00
Ran Benita
169d775eec Merge pull request #11866 from pytest-dev/backport-11718-to-8.0.x
[8.0.x] build(deps): Bump hynek/build-and-inspect-python-package from 1.5.4 to 2.0.0
2024-01-28 00:25:45 +02:00
Ran Benita
42f709ed44 [8.0.x] build(deps): Bump hynek/build-and-inspect-python-package from 1.5.4 to 2.0.0 2024-01-27 22:10:59 +00:00
Ran Benita
24c681d4ee Merge pull request #11864 from bluetech/release-8.0.0
Prepare release version 8.0.0
2024-01-27 23:55:26 +02:00
Ran Benita
478f8233bc Prepare release version 8.0.0 2024-01-27 23:23:56 +02:00
github-actions[bot]
608590097a [8.0.x] fix: avoid rounding microsecond to 1_000_000 (#11863)
Co-authored-by: Dương Quốc Khánh <dqkqdlot@gmail.com>
2024-01-27 16:48:01 +00:00
github-actions[bot]
3b41c65c81 [8.0.x] Escape skip reason in junitxml (#11845)
Co-authored-by: clee2000 <44682903+clee2000@users.noreply.github.com>
2024-01-19 01:26:06 +00:00
github-actions[bot]
747072ad26 [8.0.x] Update docstring of scripts/generate-gh-release-notes.py (#11768)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-01-18 15:45:25 -03:00
Bruno Oliveira
011a475baf Properly attach packages to the GH release notes (#11839) (#11840)
Follow up to https://github.com/pytest-dev/pytest/pull/11754, noticed that the latest GitHub release does not contain the attached files.

Output log from the action:

```
🤔 Pattern 'dist/*' does not match any files.
```
2024-01-17 20:56:12 -03:00
Ran Benita
97960bdd14 Merge pull request #11835 from pytest-dev/release-8.0.0rc2
Prepare release version 8.0.0rc2
2024-01-17 23:44:01 +02:00
Ran Benita
6be0a3cbf7 Prepare release version 8.0.0rc2 2024-01-17 23:10:03 +02:00
Ran Benita
44ffe07165 Merge pull request #11837 from pytest-dev/backport-11836-to-8.0.x
[8.0.x] testing: temporarily disable test due to hypothesis issue
2024-01-17 23:09:47 +02:00
Ran Benita
14ecb04973 [8.0.x] testing: temporarily disable test due to hypothesis issue 2024-01-17 20:53:29 +00:00
Ran Benita
41c8dabee3 Merge pull request #11831 from bluetech/backport-11825-to-8.0.x
[8.0.x] avoid using __file__ in pytest_plugin_registered as can be wrong on Windows
2024-01-17 21:34:56 +02:00
Ran Benita
6f4cbd7cd4 [8.0.x] avoid using __file__ in pytest_plugin_registered as can be wrong on Windows 2024-01-17 15:40:14 +02:00
Ran Benita
b0c7f923aa Merge pull request #11813 from pytest-dev/backport-11795-to-8.0.x
[8.0.x] Improve assert mod not in mods error message
2024-01-14 13:55:56 +02:00
Ran Benita
f15aff06dc [8.0.x] Improve assert mod not in mods error message 2024-01-14 11:39:34 +00:00
Ran Benita
10c8898845 Merge pull request #11810 from pytest-dev/backport-11708-to-8.0.x
[8.0.x] FIX key formating divergence when inspecting plugin dictionary.
2024-01-13 23:02:20 +02:00
Ran Benita
5b7ddedbf9 [8.0.x] FIX key formating divergence when inspecting plugin dictionary. 2024-01-13 19:46:07 +00:00
github-actions[bot]
3ae38baca6 [8.0.x] Add summary for xfails with -rxX option (#11778)
Co-authored-by: Fabian Sturm <fabio.sturm@gmail.com>
2024-01-05 22:35:47 +00:00
Ran Benita
6123b247d4 Merge pull request #11764 from pytest-dev/backport-11721-to-8.0.x
[8.0.x] Fix teardown error reporting when `--maxfail=1`
2024-01-04 11:23:03 +02:00
Bruno Oliveira
bb6f5d1b10 Merge pull request #11766 from pytest-dev/backport-11754-to-8.0.x
[8.0.x] Improve GitHub release workflow
2024-01-03 20:33:02 -03:00
Bruno Oliveira
72eb1b7ad1 [8.0.x] Improve GitHub release workflow 2024-01-03 23:15:18 +00:00
Ben Brown
620a454dba [8.0.x] Fix teardown error reporting when --maxfail=1 2024-01-03 17:39:50 +00:00
github-actions[bot]
838151638e [8.0.x] terminalwriter: fix crash trying to highlight empty source (#11763)
Co-authored-by: Ran Benita <ran@unusedvar.com>
2024-01-03 08:04:57 -03:00
Ran Benita
665e4e58d3 Merge pull request #11744 from pytest-dev/release-8.0.0rc1
Prepare release 8.0.0rc1
2024-01-02 10:58:20 +02:00
Ran Benita
e17d5ec871 Prepare release version 8.0.0rc1 2023-12-31 14:40:46 +02:00
230 changed files with 5301 additions and 8530 deletions

View File

@@ -23,11 +23,9 @@ afc607cfd81458d4e4f3b1f3cf8cc931b933907e
5f95dce95602921a70bfbc7d8de2f7712c5e4505
# ran pyupgrade-docs again
75d0b899bbb56d6849e9d69d83a9426ed3f43f8b
# move argument parser to own file
c9df77cbd6a365dcb73c39618e4842711817e871
# Replace reorder-python-imports by isort due to black incompatibility (#11896)
8b54596639f41dfac070030ef20394b9001fe63c
# Run blacken-docs with black's 2024's style
4546d5445aaefe6a03957db028c263521dfb5c4b
# Migration to ruff / ruff format
4588653b2497ed25976b7aaff225b889fb476756

View File

@@ -26,7 +26,7 @@ jobs:
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.4.0
uses: hynek/build-and-inspect-python-package@v2.0.0
deploy:
if: github.repository == 'pytest-dev/pytest'
@@ -47,7 +47,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.8.11
- name: Push tag
run: |
@@ -94,7 +94,7 @@ jobs:
tox -e generate-gh-release-notes -- ${{ github.event.inputs.version }} scripts/latest-release-notes.md
- name: Publish GitHub Release
uses: softprops/action-gh-release@v2
uses: softprops/action-gh-release@v1
with:
body_path: scripts/latest-release-notes.md
files: dist/*

View File

@@ -17,7 +17,7 @@ jobs:
days-before-issue-close: 7
only-labels: "status: needs information"
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has the `status: needs information` label and requested follow-up information was not provided for 14 days."
close-issue-message: "This issue was closed because it has the `status: needs information` label and follow-up information has not been provided for 7 days since being marked as stale."
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1

View File

@@ -35,7 +35,7 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.4.0
uses: hynek/build-and-inspect-python-package@v2.0.0
build:
needs: [package]
@@ -205,7 +205,7 @@ jobs:
- name: Upload coverage to Codecov
if: "matrix.use_coverage"
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v3
continue-on-error: true
with:
fail_ci_if_error: true

View File

@@ -30,7 +30,7 @@ jobs:
python-version: "3.11"
cache: pip
- name: requests-cache
uses: actions/cache@v4
uses: actions/cache@v3
with:
path: ~/.cache/pytest-plugin-list/
key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well
@@ -46,7 +46,7 @@ jobs:
run: python scripts/update-plugin-list.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
with:
commit-message: '[automated] Update plugin list'
author: 'pytest bot <pytestbot@users.noreply.github.com>'

1
.gitignore vendored
View File

@@ -51,7 +51,6 @@ coverage.xml
.settings
.vscode
__pycache__/
.python-version
# generated by pip
pip-wheel-metadata/

View File

@@ -1,12 +1,12 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.4.1"
rev: "v0.1.15"
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -21,12 +21,17 @@ repos:
hooks:
- id: blacken-docs
additional_dependencies: [black==24.1.1]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.5.0
hooks:
- id: setup-cfg-fmt
args: ["--max-py-version=3.12", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.9.0
rev: v1.8.0
hooks:
- id: mypy
files: ^(src/|testing/|scripts/)
@@ -34,7 +39,7 @@ repos:
additional_dependencies:
- iniconfig>=1.1.0
- attrs>=19.2.0
- pluggy>=1.5.0
- pluggy
- packaging
- tomli
- types-pkg_resources
@@ -42,21 +47,8 @@ repos:
# for mypy running on python>=3.11 since exceptiongroup is only a dependency
# on <3.11
- exceptiongroup>=1.0.0rc8
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "1.8.0"
hooks:
- id: pyproject-fmt
# https://pyproject-fmt.readthedocs.io/en/latest/#calculating-max-supported-python-version
additional_dependencies: ["tox>=4.9"]
- repo: local
hooks:
- id: pylint
name: pylint
entry: pylint
language: system
types: [python]
args: ["-rn", "-sn", "--fail-on=I"]
stages: [manual]
- id: rst
name: rst
entry: rst-lint --encoding utf-8

13
AUTHORS
View File

@@ -56,7 +56,6 @@ Babak Keyvani
Barney Gale
Ben Brown
Ben Gartner
Ben Leith
Ben Webb
Benjamin Peterson
Benjamin Schubert
@@ -101,7 +100,6 @@ Cyrus Maden
Damian Skrzypczak
Daniel Grana
Daniel Hahler
Daniel Miller
Daniel Nuri
Daniel Sánchez Castelló
Daniel Valenzuela Zenteno
@@ -119,7 +117,6 @@ Daw-Ran Liou
Debi Mishra
Denis Kirisov
Denivy Braiam Rück
Dheeraj C K
Dhiren Serai
Diego Russo
Dmitry Dygalo
@@ -130,8 +127,6 @@ Edison Gustavo Muenz
Edoardo Batini
Edson Tadeu M. Manoel
Eduardo Schettino
Edward Haigh
Eero Vaher
Eli Boyarski
Elizaveta Shashkova
Éloi Rivard
@@ -139,7 +134,6 @@ Endre Galaczi
Eric Hunsberger
Eric Liu
Eric Siegerman
Eric Yuan
Erik Aronesty
Erik Hasse
Erik M. Bray
@@ -148,7 +142,6 @@ Evgeny Seliverstov
Fabian Sturm
Fabien Zarifian
Fabio Zadrozny
faph
Felix Hofstätter
Felix Nieuwenhuizen
Feng Ma
@@ -238,7 +231,6 @@ Kyle Altendorf
Lawrence Mitchell
Lee Kamentsky
Lev Maximov
Levon Saldamli
Lewis Cowles
Llandy Riveron Del Risco
Loic Esteve
@@ -253,7 +245,6 @@ Marc Mueller
Marc Schlaich
Marcelo Duarte Trevisani
Marcin Bachry
Marc Bresson
Marco Gorelli
Mark Abramowitz
Mark Dickinson
@@ -287,7 +278,6 @@ Mike Hoyle (hoylemd)
Mike Lundy
Milan Lesnek
Miro Hrončok
mrbean-bremen
Nathaniel Compton
Nathaniel Waisbrot
Ned Batchelder
@@ -322,7 +312,6 @@ Pierre Sassoulas
Pieter Mulder
Piotr Banaszkiewicz
Piotr Helm
Poulami Sau
Prakhar Gurunani
Prashant Anand
Prashant Sharma
@@ -425,7 +414,6 @@ Vivaan Verma
Vlad Dragos
Vlad Radziuk
Vladyslav Rachek
Volodymyr Kochetkov
Volodymyr Piskun
Wei Lin
Wil Cooley
@@ -436,7 +424,6 @@ Xixi Zhao
Xuan Luong
Xuecong Liao
Yannick Péroux
Yao Xiao
Yoav Caspi
Yuliang Shao
Yusuke Kadowaki

View File

@@ -297,12 +297,12 @@ Here is a simple overview, with pytest-specific bits:
When committing, ``pre-commit`` will re-format the files if necessary.
#. If instead of using ``tox`` you prefer to run the tests directly, then we suggest to create a virtual environment and use
an editable install with the ``dev`` extra::
an editable install with the ``testing`` extra::
$ python3 -m venv .venv
$ source .venv/bin/activate # Linux
$ .venv/Scripts/activate.bat # Windows
$ pip install -e ".[dev]"
$ pip install -e ".[testing]"
Afterwards, you can edit the files and run pytest normally::

View File

@@ -94,12 +94,12 @@ Features
- `Modular fixtures <https://docs.pytest.org/en/stable/explanation/fixtures.html>`_ for
managing small or parametrized long-lived test resources
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial)
test suites out of the box
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
- Python 3.8+ or PyPy3
- Rich plugin architecture, with over 1300+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
Documentation

View File

@@ -23,9 +23,9 @@ members of the `contributors team`_ interested in receiving funding.
The current list of contributors receiving funding are:
* `@asottile`_
* `@nicoddemus`_
* `@The-Compiler`_
* `@RonnyPfannschmidt`_
Contributors interested in receiving a part of the funds just need to submit a PR adding their
name to the list. Contributors that want to stop receiving the funds should also submit a PR
@@ -55,6 +55,6 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the
.. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members
.. _`agreement`: https://tidelift.com/docs/lifting/agreement
.. _`@asottile`: https://github.com/asottile
.. _`@nicoddemus`: https://github.com/nicoddemus
.. _`@The-Compiler`: https://github.com/The-Compiler
.. _`@RonnyPfannschmidt`: https://github.com/RonnyPfannschmidt

View File

@@ -5,7 +5,7 @@ if __name__ == "__main__":
import cProfile
import pstats
import pytest # noqa: F401
import pytest # NOQA
script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"]
cProfile.run("pytest.cmdline.main(%r)" % script, "prof")

View File

@@ -44,7 +44,7 @@ Partner projects, sign up here! (by 22 March)
What does it mean to "adopt pytest"?
-----------------------------------------
There can be many different definitions of "success". Pytest can run many unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
There can be many different definitions of "success". Pytest can run many nose_ and unittest_ tests by default, so using pytest as your testrunner may be possible from day 1. Job done, right?
Progressive success might look like:
@@ -62,6 +62,7 @@ Progressive success might look like:
It may be after the month is up, the partner project decides that pytest is not right for it. That's okay - hopefully the pytest team will also learn something about its weaknesses or deficiencies.
.. _nose: nose.html
.. _unittest: unittest.html
.. _assert: assert.html
.. _pycmd: https://bitbucket.org/hpk42/pycmd/overview

View File

@@ -6,10 +6,6 @@ Release announcements
:maxdepth: 2
release-8.2.0
release-8.1.2
release-8.1.1
release-8.1.0
release-8.0.2
release-8.0.1
release-8.0.0

View File

@@ -1,54 +0,0 @@
pytest-8.1.0
=======================================
The pytest team is proud to announce the 8.1.0 release!
This release contains new features, improvements, and bug fixes,
the full list of changes is available in the changelog:
https://docs.pytest.org/en/stable/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/stable/
As usual, you can upgrade from PyPI via:
pip install -U pytest
Thanks to all of the contributors to this release:
* Ben Brown
* Ben Leith
* Bruno Oliveira
* Clément Robert
* Dave Hall
* Dương Quốc Khánh
* Eero Vaher
* Eric Larson
* Fabian Sturm
* Faisal Fawad
* Florian Bruhin
* Franck Charras
* Joachim B Haga
* John Litborn
* Loïc Estève
* Marc Bresson
* Patrick Lannigan
* Pierre Sassoulas
* Ran Benita
* Reagan Lee
* Ronny Pfannschmidt
* Russell Martin
* clee2000
* donghui
* faph
* jakkdl
* mrbean-bremen
* robotherapist
* whysage
* woutdenolf
Happy testing,
The pytest Development Team

View File

@@ -1,18 +0,0 @@
pytest-8.1.1
=======================================
pytest 8.1.1 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Ran Benita
Happy testing,
The pytest Development Team

View File

@@ -1,18 +0,0 @@
pytest-8.1.2
=======================================
pytest 8.1.2 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Bruno Oliveira
Happy testing,
The pytest Development Team

View File

@@ -1,43 +0,0 @@
pytest-8.2.0
=======================================
The pytest team is proud to announce the 8.2.0 release!
This release contains new features, improvements, and bug fixes,
the full list of changes is available in the changelog:
https://docs.pytest.org/en/stable/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/stable/
As usual, you can upgrade from PyPI via:
pip install -U pytest
Thanks to all of the contributors to this release:
* Bruno Oliveira
* Daniel Miller
* Florian Bruhin
* HolyMagician03-UMich
* John Litborn
* Levon Saldamli
* Linghao Zhang
* Manuel López-Ibáñez
* Pierre Sassoulas
* Ran Benita
* Ronny Pfannschmidt
* Sebastian Meyer
* Shekhar verma
* Tamir Duberstein
* Tobias Stoeckmann
* dj
* jakkdl
* poulami-sau
* tserg
Happy testing,
The pytest Development Team

View File

@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 0 items
cache -- .../_pytest/cacheprovider.py:542
cache -- .../_pytest/cacheprovider.py:527
Return a cache object that can persist state between testing sessions.
cache.get(key, default)
@@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Values can be any object handled by the json stdlib module.
capsysbinary -- .../_pytest/capture.py:1003
capsysbinary -- .../_pytest/capture.py:1007
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsysbinary.readouterr()``
@@ -50,7 +50,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsysbinary.readouterr()
assert captured.out == b"hello\n"
capfd -- .../_pytest/capture.py:1030
capfd -- .../_pytest/capture.py:1034
Enable text capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
@@ -67,7 +67,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capfd.readouterr()
assert captured.out == "hello\n"
capfdbinary -- .../_pytest/capture.py:1057
capfdbinary -- .../_pytest/capture.py:1061
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
@@ -84,7 +84,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capfdbinary.readouterr()
assert captured.out == b"hello\n"
capsys -- .../_pytest/capture.py:976
capsys -- .../_pytest/capture.py:980
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsys.readouterr()`` method
@@ -101,7 +101,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsys.readouterr()
assert captured.out == "hello\n"
doctest_namespace [session scope] -- .../_pytest/doctest.py:738
doctest_namespace [session scope] -- .../_pytest/doctest.py:745
Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.
@@ -115,7 +115,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1335
pytestconfig [session scope] -- .../_pytest/fixtures.py:1354
Session-scoped fixture that returns the session's :class:`pytest.Config`
object.
@@ -125,7 +125,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
if pytestconfig.getoption("verbose") > 0:
...
record_property -- .../_pytest/junitxml.py:284
record_property -- .../_pytest/junitxml.py:283
Add extra properties to the calling test.
User properties become part of the test report and are available to the
@@ -139,13 +139,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
def test_function(record_property):
record_property("example_key", 1)
record_xml_attribute -- .../_pytest/junitxml.py:307
record_xml_attribute -- .../_pytest/junitxml.py:306
Add extra xml attributes to the tag for the calling test.
The fixture is callable with ``name, value``. The value is
automatically XML-encoded.
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:345
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:344
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
This is suitable to writing global information regarding the entire test
@@ -170,18 +170,18 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
:issue:`7767` for details.
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:303
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
Return a :class:`pytest.TempdirFactory` instance for the test session.
tmpdir -- .../_pytest/legacypath.py:310
tmpdir -- .../_pytest/legacypath.py:309
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See
:ref:`temporary directory location and retention`.
``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
The returned object is a `legacy_path`_ object.
@@ -192,7 +192,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:602
caplog -- .../_pytest/logging.py:594
Access and control log capturing.
Captured logs are available through the following properties/methods::
@@ -203,7 +203,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
* caplog.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string
monkeypatch -- .../_pytest/monkeypatch.py:33
monkeypatch -- .../_pytest/monkeypatch.py:32
A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or
@@ -233,10 +233,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
on warning categories.
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:242
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:241
Return a :class:`pytest.TempPathFactory` instance for the test session.
tmp_path -- .../_pytest/tmpdir.py:257
tmp_path -- .../_pytest/tmpdir.py:256
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
@@ -245,8 +245,8 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
and old bases are removed after 3 sessions, to aid in debugging.
This behavior can be configured with :confval:`tmp_path_retention_count` and
:confval:`tmp_path_retention_policy`.
If ``--basetemp`` is used then it is cleared each session. See
:ref:`temporary directory location and retention`.
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
The returned object is a :class:`pathlib.Path` object.

View File

@@ -28,217 +28,6 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 8.2.0 (2024-04-27)
=========================
Deprecations
------------
- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: A deprecation warning is now raised when implementations of one of the following hooks request a deprecated ``py.path.local`` parameter instead of the ``pathlib.Path`` parameter which replaced it:
- :hook:`pytest_ignore_collect` - the ``path`` parameter - use ``collection_path`` instead.
- :hook:`pytest_collect_file` - the ``path`` parameter - use ``file_path`` instead.
- :hook:`pytest_pycollect_makemodule` - the ``path`` parameter - use ``module_path`` instead.
- :hook:`pytest_report_header` - the ``startdir`` parameter - use ``start_path`` instead.
- :hook:`pytest_report_collectionfinish` - the ``startdir`` parameter - use ``start_path`` instead.
The replacement parameters are available since pytest 7.0.0.
The old parameters will be removed in pytest 9.0.0.
See :ref:`legacy-path-hooks-deprecated` for more details.
Features
--------
- `#11871 <https://github.com/pytest-dev/pytest/issues/11871>`_: Added support for reading command line arguments from a file using the prefix character ``@``, like e.g.: ``pytest @tests.txt``. The file must have one argument per line.
See :ref:`Read arguments from file <args-from-file>` for details.
Improvements
------------
- `#11523 <https://github.com/pytest-dev/pytest/issues/11523>`_: :func:`pytest.importorskip` will now issue a warning if the module could be found, but raised :class:`ImportError` instead of :class:`ModuleNotFoundError`.
The warning can be suppressed by passing ``exc_type=ImportError`` to :func:`pytest.importorskip`.
See :ref:`import-or-skip-import-error` for details.
- `#11728 <https://github.com/pytest-dev/pytest/issues/11728>`_: For ``unittest``-based tests, exceptions during class cleanup (as raised by functions registered with :meth:`TestCase.addClassCleanup <unittest.TestCase.addClassCleanup>`) are now reported instead of silently failing.
- `#11777 <https://github.com/pytest-dev/pytest/issues/11777>`_: Text is no longer truncated in the ``short test summary info`` section when ``-vv`` is given.
- `#12112 <https://github.com/pytest-dev/pytest/issues/12112>`_: Improved namespace packages detection when :confval:`consider_namespace_packages` is enabled, covering more situations (like editable installs).
- `#9502 <https://github.com/pytest-dev/pytest/issues/9502>`_: Added :envvar:`PYTEST_VERSION` environment variable which is defined at the start of the pytest session and undefined afterwards. It contains the value of ``pytest.__version__``, and among other things can be used to easily check if code is running from within a pytest run.
Bug Fixes
---------
- `#12065 <https://github.com/pytest-dev/pytest/issues/12065>`_: Fixed a regression in pytest 8.0.0 where test classes containing ``setup_method`` and tests using ``@staticmethod`` or ``@classmethod`` would crash with ``AttributeError: 'NoneType' object has no attribute 'setup_method'``.
Now the :attr:`request.instance <pytest.FixtureRequest.instance>` attribute of tests using ``@staticmethod`` and ``@classmethod`` is no longer ``None``, but a fresh instance of the class, like in non-static methods.
Previously it was ``None``, and all fixtures of such tests would share a single ``self``.
- `#12135 <https://github.com/pytest-dev/pytest/issues/12135>`_: Fixed issue where fixtures adding their finalizer multiple times to fixtures they request would cause unreliable and non-intuitive teardown ordering in some instances.
- `#12194 <https://github.com/pytest-dev/pytest/issues/12194>`_: Fixed a bug with ``--importmode=importlib`` and ``--doctest-modules`` where child modules did not appear as attributes in parent modules.
- `#1489 <https://github.com/pytest-dev/pytest/issues/1489>`_: Fixed some instances where teardown of higher-scoped fixtures was not happening in the reverse order they were initialized in.
Trivial/Internal Changes
------------------------
- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: ``pluggy>=1.5.0`` is now required.
- `#12167 <https://github.com/pytest-dev/pytest/issues/12167>`_: :ref:`cache <cache>`: create supporting files (``CACHEDIR.TAG``, ``.gitignore``, etc.) in a temporary directory to provide atomic semantics.
pytest 8.1.2 (2024-04-26)
=========================
Bug Fixes
---------
- `#12114 <https://github.com/pytest-dev/pytest/issues/12114>`_: Fixed error in :func:`pytest.approx` when used with `numpy` arrays and comparing with other types.
pytest 8.1.1 (2024-03-08)
=========================
.. note::
This release is not a usual bug fix release -- it contains features and improvements, being a follow up
to ``8.1.0``, which has been yanked from PyPI.
Features
--------
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: Added the new :confval:`consider_namespace_packages` configuration option, defaulting to ``False``.
If set to ``True``, pytest will attempt to identify modules that are part of `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__ when importing modules.
- `#11653 <https://github.com/pytest-dev/pytest/issues/11653>`_: Added the new :confval:`verbosity_test_cases` configuration option for fine-grained control of test execution verbosity.
See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.
Improvements
------------
- `#10865 <https://github.com/pytest-dev/pytest/issues/10865>`_: :func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`.
Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion).
While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing.
- `#11311 <https://github.com/pytest-dev/pytest/issues/11311>`_: When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
as the relative directory.
Previoulsy this would raise an :class:`AssertionError`.
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: :ref:`--import-mode=importlib <import-mode-importlib>` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails.
This means that installed packages will be imported under their canonical name if possible first, for example ``app.core.models``, instead of having the module name always be derived from their path (for example ``.env310.lib.site_packages.app.core.models``).
- `#11801 <https://github.com/pytest-dev/pytest/issues/11801>`_: Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes.
It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list.
- `#11850 <https://github.com/pytest-dev/pytest/issues/11850>`_: Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12.
- `#11962 <https://github.com/pytest-dev/pytest/issues/11962>`_: In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``.
- `#11978 <https://github.com/pytest-dev/pytest/issues/11978>`_: Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``.
Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging.
- `#12047 <https://github.com/pytest-dev/pytest/issues/12047>`_: When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group.
Previously, only the first exception was reported.
Bug Fixes
---------
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: Fixed regression where ``--importmode=importlib`` would import non-test modules more than once.
- `#11904 <https://github.com/pytest-dev/pytest/issues/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.
- `#12011 <https://github.com/pytest-dev/pytest/issues/12011>`_: Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed.
- `#12014 <https://github.com/pytest-dev/pytest/issues/12014>`_: Fix the ``stacklevel`` used when warning about marks used on fixtures.
- `#12039 <https://github.com/pytest-dev/pytest/issues/12039>`_: Fixed a regression in ``8.0.2`` where tests created using :fixture:`tmp_path` have been collected multiple times in CI under Windows.
Improved Documentation
----------------------
- `#11790 <https://github.com/pytest-dev/pytest/issues/11790>`_: Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail.
Trivial/Internal Changes
------------------------
- `#11785 <https://github.com/pytest-dev/pytest/issues/11785>`_: Some changes were made to private functions which may affect plugins which access them:
- ``FixtureManager._getautousenames()`` now takes a ``Node`` itself instead of the nodeid.
- ``FixtureManager.getfixturedefs()`` now takes the ``Node`` itself instead of the nodeid.
- The ``_pytest.nodes.iterparentnodeids()`` function is removed without replacement.
Prefer to traverse the node hierarchy itself instead.
If you really need to, copy the function from the previous pytest release.
- `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`_: Delayed the deprecation of the following features to ``9.0.0``:
* :ref:`node-ctor-fspath-deprecation`.
* :ref:`legacy-path-hooks-deprecated`.
It was discovered after ``8.1.0`` was released that the warnings about the impeding removal were not being displayed, so the team decided to revert the removal.
This is the reason for ``8.1.0`` being yanked.
pytest 8.1.0 (YANKED)
=====================
.. note::
This release has been **yanked**: it broke some plugins without the proper warning period, due to
some warnings not showing up as expected.
See `#12069 <https://github.com/pytest-dev/pytest/issues/12069>`__.
pytest 8.0.2 (2024-02-24)
=========================
@@ -526,7 +315,7 @@ These are breaking changes where deprecation was not possible.
Deprecations
------------
- `#10465 <https://github.com/pytest-dev/pytest/issues/10465>`_: Test functions returning a value other than ``None`` will now issue a :class:`pytest.PytestWarning` instead of ``pytest.PytestRemovedIn8Warning``, meaning this will stay a warning instead of becoming an error in the future.
- `#10465 <https://github.com/pytest-dev/pytest/issues/10465>`_: Test functions returning a value other than ``None`` will now issue a :class:`pytest.PytestWarning` instead of :class:`pytest.PytestRemovedIn8Warning`, meaning this will stay a warning instead of becoming an error in the future.
- `#3664 <https://github.com/pytest-dev/pytest/issues/3664>`_: Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work.
@@ -1556,7 +1345,7 @@ Deprecations
See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details.
- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: ``pytest_cmdline_preparse`` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead.
- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: :hook:`pytest_cmdline_preparse` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead.
See :ref:`the deprecation note <cmdline-preparse-deprecated>` for full details.

View File

@@ -395,7 +395,7 @@ epub_copyright = "2013, holger krekel et alii"
# The format is a list of tuples containing the path and title.
# epub_pre_files = []
# HTML files that should be inserted after the pages created by sphinx.
# HTML files shat should be inserted after the pages created by sphinx.
# The format is a list of tuples containing the path and title.
# epub_post_files = []

View File

@@ -44,6 +44,7 @@ How-to guides
how-to/existingtestsuite
how-to/unittest
how-to/nose
how-to/xunit_setup
how-to/bash-completion

View File

@@ -19,302 +19,12 @@ Below is a complete list of all pytest features which are considered deprecated.
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
.. _import-or-skip-import-error:
``pytest.importorskip`` default behavior regarding :class:`ImportError`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 8.2
Traditionally :func:`pytest.importorskip` will capture :class:`ImportError`, with the original intent being to skip
tests where a dependent module is not installed, for example testing with different dependencies.
However some packages might be installed in the system, but are not importable due to
some other issue, for example, a compilation error or a broken installation. In those cases :func:`pytest.importorskip`
would still silently skip the test, but more often than not users would like to see the unexpected
error so the underlying issue can be fixed.
In ``8.2`` the ``exc_type`` parameter has been added, giving users the ability of passing :class:`ModuleNotFoundError`
to skip tests only if the module cannot really be found, and not because of some other error.
Catching only :class:`ModuleNotFoundError` by default (and letting other errors propagate) would be the best solution,
however for backward compatibility, pytest will keep the existing behavior but raise an warning if:
1. The captured exception is of type :class:`ImportError`, and:
2. The user does not pass ``exc_type`` explicitly.
If the import attempt raises :class:`ModuleNotFoundError` (the usual case), then the module is skipped and no
warning is emitted.
This way, the usual cases will keep working the same way, while unexpected errors will now issue a warning, with
users being able to supress the warning by passing ``exc_type=ImportError`` explicitly.
In ``9.0``, the warning will turn into an error, and in ``9.1`` :func:`pytest.importorskip` will only capture
:class:`ModuleNotFoundError` by default and no warnings will be issued anymore -- but users can still capture
:class:`ImportError` by passing it to ``exc_type``.
.. _node-ctor-fspath-deprecation:
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
is now deprecated.
Plugins which construct nodes should pass the ``path`` argument, of type
:class:`pathlib.Path`, instead of the ``fspath`` argument.
Plugins which implement custom items and collectors are encouraged to replace
``fspath`` parameters (``py.path.local``) with ``path`` parameters
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
If possible, plugins with custom items should use :ref:`cooperative
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
arguments they only pass on to the superclass.
.. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
new attribute being ``path``) is **the opposite** of the situation for
hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
argument being ``path``).
This is an unfortunate artifact due to historical reasons, which should be
resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion).
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
which still is expected to return a ``py.path.local`` object, nodes still have
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
no matter what argument was used in the constructor. We expect to deprecate the
``fspath`` attribute in a future release.
Configuring hook specs/impls using markers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before pluggy, pytest's plugin library, was its own package and had a clear API,
pytest just used ``pytest.mark`` to configure hooks.
The :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec` decorators
have been available since years and should be used instead.
.. code-block:: python
@pytest.mark.tryfirst
def pytest_runtest_call(): ...
# or
def pytest_runtest_call(): ...
pytest_runtest_call.tryfirst = True
should be changed to:
.. code-block:: python
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_call(): ...
Changed ``hookimpl`` attributes:
* ``tryfirst``
* ``trylast``
* ``optionalhook``
* ``hookwrapper``
Changed ``hookwrapper`` attributes:
* ``firstresult``
* ``historic``
.. _legacy-path-hooks-deprecated:
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
.. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
:ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
being ``path``) is **the opposite** of the situation for hooks (the old
argument being ``path``).
This is an unfortunate artifact due to historical reasons, which should be
resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion).
Directly constructing internal classes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
Directly constructing the following classes is now deprecated:
- ``_pytest.mark.structures.Mark``
- ``_pytest.mark.structures.MarkDecorator``
- ``_pytest.mark.structures.MarkGenerator``
- ``_pytest.python.Metafunc``
- ``_pytest.runner.CallInfo``
- ``_pytest._code.ExceptionInfo``
- ``_pytest.config.argparsing.Parser``
- ``_pytest.config.argparsing.OptionGroup``
- ``_pytest.pytester.HookRecorder``
These constructors have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.
.. _diamond-inheritance-deprecated:
Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning.
It was never sanely supported and triggers hard to debug errors.
Some plugins providing linting/code analysis have been using this as a hack.
Instead, a separate collector node should be used, which collects the item. See
:ref:`non-python tests` for an example, as well as an `example pr fixing inheritance`_.
.. _example pr fixing inheritance: https://github.com/asmeurer/pytest-flakes/pull/40/files
.. _uncooperative-constructors-deprecated:
Constructors of custom :class:`~_pytest.nodes.Node` subclasses should take ``**kwargs``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
If custom subclasses of nodes like :class:`pytest.Item` override the
``__init__`` method, they should take ``**kwargs``. Thus,
.. code-block:: python
class CustomItem(pytest.Item):
def __init__(self, name, parent, additional_arg):
super().__init__(name, parent)
self.additional_arg = additional_arg
should be turned into:
.. code-block:: python
class CustomItem(pytest.Item):
def __init__(self, *, additional_arg, **kwargs):
super().__init__(**kwargs)
self.additional_arg = additional_arg
to avoid hard-coding the arguments pytest can pass to the superclass.
See :ref:`non-python tests` for a full example.
For cases without conflicts, no deprecation warning is emitted. For cases with
conflicts (such as :class:`pytest.File` now taking ``path`` instead of
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
deprecation warning is now raised.
Applying a mark to a fixture function
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.4
Applying a mark to a fixture function never had any effect, but it is a common user error.
.. code-block:: python
@pytest.mark.usefixtures("clean_database")
@pytest.fixture
def user() -> User: ...
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
Returning non-None value in test functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.2
A :class:`pytest.PytestReturnNotNoneWarning` is now emitted if a test function returns something other than `None`.
This prevents a common mistake among beginners that expect that returning a `bool` would cause a test to pass or fail, for example:
.. code-block:: python
@pytest.mark.parametrize(
["a", "b", "result"],
[
[1, 2, 5],
[2, 3, 8],
[5, 3, 18],
],
)
def test_foo(a, b, result):
return foo(a, b) == result
Given that pytest ignores the return value, this might be surprising that it will never fail.
The proper fix is to change the `return` to an `assert`:
.. code-block:: python
@pytest.mark.parametrize(
["a", "b", "result"],
[
[1, 2, 5],
[2, 3, 8],
[5, 3, 18],
],
)
def test_foo(a, b, result):
assert foo(a, b) == result
The ``yield_fixture`` function/decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.2
``pytest.yield_fixture`` is a deprecated alias for :func:`pytest.fixture`.
It has been so for a very long time, so can be search/replaced safely.
Removed Features and Breaking Changes
-------------------------------------
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
an appropriate period of deprecation has passed.
Some breaking changes which could not be deprecated are also listed.
.. _nose-deprecation:
Support for tests written for nose
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.2
.. versionremoved:: 8.0
Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated.
@@ -405,20 +115,157 @@ Will also need to be ported to a supported pytest style. One way to do it is usi
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
.. _instance-collector-deprecation:
The ``compat_co_firstlineno`` attribute
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``pytest.Instance`` collector
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Nose inspects this attribute on function objects to allow overriding the function's inferred line number.
Pytest no longer respects this attribute.
.. versionremoved:: 7.0
The ``pytest.Instance`` collector type has been removed.
Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`.
Now :class:`~pytest.Class` collects the test methods directly.
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
using a check such as ``if isinstance(node, Instance): return``.
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
and importing it emits a deprecation warning. This will be removed in pytest 8.
.. _node-ctor-fspath-deprecation:
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
is now deprecated.
Plugins which construct nodes should pass the ``path`` argument, of type
:class:`pathlib.Path`, instead of the ``fspath`` argument.
Plugins which implement custom items and collectors are encouraged to replace
``fspath`` parameters (``py.path.local``) with ``path`` parameters
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
If possible, plugins with custom items should use :ref:`cooperative
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
arguments they only pass on to the superclass.
.. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
new attribute being ``path``) is **the opposite** of the situation for
hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
argument being ``path``).
This is an unfortunate artifact due to historical reasons, which should be
resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion).
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
which still is expected to return a ``py.path.local`` object, nodes still have
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
no matter what argument was used in the constructor. We expect to deprecate the
``fspath`` attribute in a future release.
.. _legacy-path-hooks-deprecated:
Configuring hook specs/impls using markers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Before pluggy, pytest's plugin library, was its own package and had a clear API,
pytest just used ``pytest.mark`` to configure hooks.
The :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec` decorators
have been available since years and should be used instead.
.. code-block:: python
@pytest.mark.tryfirst
def pytest_runtest_call(): ...
# or
def pytest_runtest_call(): ...
pytest_runtest_call.tryfirst = True
should be changed to:
.. code-block:: python
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_call(): ...
Changed ``hookimpl`` attributes:
* ``tryfirst``
* ``trylast``
* ``optionalhook``
* ``hookwrapper``
Changed ``hookwrapper`` attributes:
* ``firstresult``
* ``historic``
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
.. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
:ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
being ``path``) is **the opposite** of the situation for hooks (the old
argument being ``path``).
This is an unfortunate artifact due to historical reasons, which should be
resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion).
Directly constructing internal classes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
Directly constructing the following classes is now deprecated:
- ``_pytest.mark.structures.Mark``
- ``_pytest.mark.structures.MarkDecorator``
- ``_pytest.mark.structures.MarkGenerator``
- ``_pytest.python.Metafunc``
- ``_pytest.runner.CallInfo``
- ``_pytest._code.ExceptionInfo``
- ``_pytest.config.argparsing.Parser``
- ``_pytest.config.argparsing.OptionGroup``
- ``_pytest.pytester.HookRecorder``
These constructors have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.
.. _cmdline-preparse-deprecated:
Passing ``msg=`` to ``pytest.skip``, ``pytest.fail`` or ``pytest.exit``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
.. versionremoved:: 8.0
Passing the keyword argument ``msg`` to :func:`pytest.skip`, :func:`pytest.fail` or :func:`pytest.exit`
is now deprecated and ``reason`` should be used instead. This change is to bring consistency between these
@@ -447,74 +294,12 @@ functions and the ``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which
pytest.exit(reason="bar")
.. _instance-collector-deprecation:
The ``pytest.Instance`` collector
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 7.0
The ``pytest.Instance`` collector type has been removed.
Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`.
Now :class:`~pytest.Class` collects the test methods directly.
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
using a check such as ``if isinstance(node, Instance): return``.
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
and importing it emits a deprecation warning. This was removed in pytest 8.
Using ``pytest.warns(None)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
.. versionremoved:: 8.0
:func:`pytest.warns(None) <pytest.warns>` is now deprecated because it was frequently misused.
Its correct usage was checking that the code emits at least one warning of any type - like ``pytest.warns()``
or ``pytest.warns(Warning)``.
See :ref:`warns use cases` for examples.
Backward compatibilities in ``Parser.addoption``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 2.4
.. versionremoved:: 8.0
Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
removed in pytest 8 (deprecated since pytest 2.4.0):
- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead.
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
The ``--strict`` command-line option
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.2
.. versionremoved:: 8.0
The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
better conveys what the option does.
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
flag for all strictness related options (``--strict-markers`` and ``--strict-config``
at the moment, more might be introduced in the future).
.. _cmdline-preparse-deprecated:
Implementing the ``pytest_cmdline_preparse`` hook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
.. versionremoved:: 8.0
Implementing the ``pytest_cmdline_preparse`` hook has been officially deprecated.
Implementing the :hook:`pytest_cmdline_preparse` hook has been officially deprecated.
Implement the :hook:`pytest_load_initial_conftests` hook instead.
.. code-block:: python
@@ -529,6 +314,170 @@ Implement the :hook:`pytest_load_initial_conftests` hook instead.
early_config: Config, parser: Parser, args: List[str]
) -> None: ...
.. _diamond-inheritance-deprecated:
Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
Defining a custom pytest node type which is both an :class:`~pytest.Item` and a :class:`~pytest.Collector` (e.g. :class:`~pytest.File`) now issues a warning.
It was never sanely supported and triggers hard to debug errors.
Some plugins providing linting/code analysis have been using this as a hack.
Instead, a separate collector node should be used, which collects the item. See
:ref:`non-python tests` for an example, as well as an `example pr fixing inheritance`_.
.. _example pr fixing inheritance: https://github.com/asmeurer/pytest-flakes/pull/40/files
.. _uncooperative-constructors-deprecated:
Constructors of custom :class:`~_pytest.nodes.Node` subclasses should take ``**kwargs``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
If custom subclasses of nodes like :class:`pytest.Item` override the
``__init__`` method, they should take ``**kwargs``. Thus,
.. code-block:: python
class CustomItem(pytest.Item):
def __init__(self, name, parent, additional_arg):
super().__init__(name, parent)
self.additional_arg = additional_arg
should be turned into:
.. code-block:: python
class CustomItem(pytest.Item):
def __init__(self, *, additional_arg, **kwargs):
super().__init__(**kwargs)
self.additional_arg = additional_arg
to avoid hard-coding the arguments pytest can pass to the superclass.
See :ref:`non-python tests` for a full example.
For cases without conflicts, no deprecation warning is emitted. For cases with
conflicts (such as :class:`pytest.File` now taking ``path`` instead of
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
deprecation warning is now raised.
Applying a mark to a fixture function
-------------------------------------
.. deprecated:: 7.4
Applying a mark to a fixture function never had any effect, but it is a common user error.
.. code-block:: python
@pytest.mark.usefixtures("clean_database")
@pytest.fixture
def user() -> User: ...
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
Backward compatibilities in ``Parser.addoption``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 2.4
Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):
- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead.
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
Using ``pytest.warns(None)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
:func:`pytest.warns(None) <pytest.warns>` is now deprecated because it was frequently misused.
Its correct usage was checking that the code emits at least one warning of any type - like ``pytest.warns()``
or ``pytest.warns(Warning)``.
See :ref:`warns use cases` for examples.
Returning non-None value in test functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.2
A :class:`pytest.PytestReturnNotNoneWarning` is now emitted if a test function returns something other than `None`.
This prevents a common mistake among beginners that expect that returning a `bool` would cause a test to pass or fail, for example:
.. code-block:: python
@pytest.mark.parametrize(
["a", "b", "result"],
[
[1, 2, 5],
[2, 3, 8],
[5, 3, 18],
],
)
def test_foo(a, b, result):
return foo(a, b) == result
Given that pytest ignores the return value, this might be surprising that it will never fail.
The proper fix is to change the `return` to an `assert`:
.. code-block:: python
@pytest.mark.parametrize(
["a", "b", "result"],
[
[1, 2, 5],
[2, 3, 8],
[5, 3, 18],
],
)
def test_foo(a, b, result):
assert foo(a, b) == result
The ``--strict`` command-line option
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.2
The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
better conveys what the option does.
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
flag for all strictness related options (``--strict-markers`` and ``--strict-config``
at the moment, more might be introduced in the future).
The ``yield_fixture`` function/decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.2
``pytest.yield_fixture`` is a deprecated alias for :func:`pytest.fixture`.
It has been so for a very long time, so can be search/replaced safely.
Removed Features and Breaking Changes
-------------------------------------
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
an appropriate period of deprecation has passed.
Some breaking changes which could not be deprecated are also listed.
Collection changes in pytest 8
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -172,7 +172,7 @@ class TestRaises:
raise ValueError("demo error")
def test_tupleerror(self):
a, b = [1] # noqa: F841
a, b = [1] # NOQA
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
items = [1, 2, 3]
@@ -180,7 +180,7 @@ class TestRaises:
a, b = items.pop()
def test_some_error(self):
if namenotexi: # noqa: F821
if namenotexi: # NOQA
pass
def func1(self):

View File

@@ -18,6 +18,7 @@ For basic examples, see
- :ref:`Fixtures <fixtures>` for basic fixture/setup examples
- :ref:`parametrize` for basic test function parametrization
- :ref:`unittest` for basic unittest integration
- :ref:`noseintegration` for basic nosetests integration
The following examples aim at various use cases you might encounter.

View File

@@ -162,7 +162,7 @@ objects, they are still using the default pytest representation:
rootdir: /home/sweet/project
collected 8 items
<Dir parametrize.rst-198>
<Dir parametrize.rst-194>
<Module test_time.py>
<Function test_timedistance_v0[a0-b0-expected0]>
<Function test_timedistance_v0[a1-b1-expected1]>
@@ -239,7 +239,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia
rootdir: /home/sweet/project
collected 4 items
<Dir parametrize.rst-198>
<Dir parametrize.rst-194>
<Module test_scenarios.py>
<Class TestSampleWithScenarios>
<Function test_demo1[basic]>
@@ -318,7 +318,7 @@ Let's first see how it looks like at collection time:
rootdir: /home/sweet/project
collected 2 items
<Dir parametrize.rst-198>
<Dir parametrize.rst-194>
<Module test_backends.py>
<Function test_db_initialized[d1]>
<Function test_db_initialized[d2]>

View File

@@ -152,7 +152,7 @@ The test collection would look like this:
configfile: pytest.ini
collected 2 items
<Dir pythoncollection.rst-199>
<Dir pythoncollection.rst-195>
<Module check_myapp.py>
<Class CheckMyApp>
<Function simple_check>
@@ -215,7 +215,7 @@ You can always peek at the collection tree without running tests like this:
configfile: pytest.ini
collected 3 items
<Dir pythoncollection.rst-199>
<Dir pythoncollection.rst-195>
<Dir CWD>
<Module pythoncollection.py>
<Function test_function>

View File

@@ -445,7 +445,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
def test_tupleerror(self):
> a, b = [1] # noqa: F841
> a, b = [1] # NOQA
E ValueError: not enough values to unpack (expected 2, got 1)
failure_demo.py:175: ValueError
@@ -467,7 +467,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
self = <failure_demo.TestRaises object at 0xdeadbeef0022>
def test_some_error(self):
> if namenotexi: # noqa: F821
> if namenotexi: # NOQA
E NameError: name 'namenotexi' is not defined
failure_demo.py:183: NameError

View File

@@ -85,7 +85,7 @@ style of setup/teardown functions:
In addition, pytest continues to support :ref:`xunitsetup`. You can mix
both styles, moving incrementally from classic to new style, as you
prefer. You can also start out from existing :ref:`unittest.TestCase
style <unittest.TestCase>`.
style <unittest.TestCase>` or :ref:`nose based <nosestyle>` projects.

View File

@@ -52,9 +52,10 @@ Plugins
Rerunning any failed tests can mitigate the negative effects of flaky tests by giving them additional chances to pass, so that the overall build does not fail. Several pytest plugins support this:
* `flaky <https://github.com/box/flaky>`_
* `pytest-flakefinder <https://github.com/dropbox/pytest-flakefinder>`_ - `blog post <https://blogs.dropbox.com/tech/2016/03/open-sourcing-pytest-tools/>`_
* `pytest-rerunfailures <https://github.com/pytest-dev/pytest-rerunfailures>`_
* `pytest-replay <https://github.com/ESSS/pytest-replay>`_: This plugin helps to reproduce locally crashes or flaky tests observed during CI runs.
* `pytest-flakefinder <https://github.com/dropbox/pytest-flakefinder>`_ - `blog post <https://blogs.dropbox.com/tech/2016/03/open-sourcing-pytest-tools/>`_
Plugins to deliberately randomize tests can help expose tests with state problems:
@@ -105,7 +106,7 @@ This is a limited list, please submit an issue or pull request to expand it!
* Gao, Zebao, Yalan Liang, Myra B. Cohen, Atif M. Memon, and Zhen Wang. "Making system user interactive tests repeatable: When and what should we control?." In *Software Engineering (ICSE), 2015 IEEE/ACM 37th IEEE International Conference on*, vol. 1, pp. 55-65. IEEE, 2015. `PDF <http://www.cs.umd.edu/~atif/pubs/gao-icse15.pdf>`__
* Palomba, Fabio, and Andy Zaidman. "Does refactoring of test smells induce fixing flaky tests?." In *Software Maintenance and Evolution (ICSME), 2017 IEEE International Conference on*, pp. 1-12. IEEE, 2017. `PDF in Google Drive <https://drive.google.com/file/d/10HdcCQiuQVgW3yYUJD-TSTq1NbYEprl0/view>`__
* Bell, Jonathan, Owolabi Legunsen, Michael Hilton, Lamyaa Eloussi, Tifany Yung, and Darko Marinov. "DeFlaker: Automatically detecting flaky tests." In *Proceedings of the 2018 International Conference on Software Engineering*. 2018. `PDF <https://www.jonbell.net/icse18-deflaker.pdf>`__
* Dutta, Saikat and Shi, August and Choudhary, Rutvik and Zhang, Zhekun and Jain, Aryaman and Misailovic, Sasa. "Detecting flaky tests in probabilistic and machine learning applications." In *Proceedings of the 29th ACM SIGSOFT International Symposium on Software Testing and Analysis (ISSTA)*, pp. 211-224. ACM, 2020. `PDF <https://www.cs.cornell.edu/~saikatd/papers/flash-issta20.pdf>`__
Resources
^^^^^^^^^

View File

@@ -60,10 +60,8 @@ Within Python modules, ``pytest`` also discovers tests using the standard
:ref:`unittest.TestCase <unittest.TestCase>` subclassing technique.
.. _`test layout`:
Choosing a test layout
----------------------
Choosing a test layout / import rules
-------------------------------------
``pytest`` supports two common test layouts:

View File

@@ -10,27 +10,19 @@ Import modes
pytest as a testing framework 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 (at least until recently) is a non-trivial processes, often requiring
changing :data:`sys.path`. Some 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*
of :py:data:`sys.path` if not already there, and then imported with
the :func:`importlib.import_module <importlib.import_module>` function.
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
It is highly recommended to arrange your test modules as packages by adding ``__init__.py`` files to your directories
containing tests. This will make the tests part of a proper Python package, allowing pytest to resolve their full
name (for example ``tests.core.test_core`` for ``test_core.py`` inside the ``tests.core`` package).
If the test directory tree is not arranged as packages, then each test file needs to have a unique name
compared to the other test files, otherwise pytest will raise an error if it finds two tests with the same name.
This requires test module names to be unique when the test directory tree is not arranged in
packages, because the modules will put in :py:data:`sys.modules` after importing.
This is the classic mechanism, dating back from the time Python 2 was still supported.
.. _`import-mode-append`:
* ``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 <importlib.import_module>`.
@@ -46,78 +38,32 @@ 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
we advocate for using :ref:`src-layouts <src-layout>`.
we advocate for using :ref:`src <src-layout>` layouts.
Same as ``prepend``, requires test module names to be unique when the test directory tree is
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
.. _`import-mode-importlib`:
* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
* ``importlib``: this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules, without changing :py:data:`sys.path`.
For this reason this doesn't require test module names to be unique.
Advantages of this mode:
One drawback however is that test modules are non-importable by each other. Also, utility
modules in the tests directories are not automatically importable because the tests directory is no longer
added to :py:data:`sys.path`.
* pytest will not change :py:data:`sys.path` at all.
* Test module names do not need to be unique -- pytest will generate a unique name automatically based on the ``rootdir``.
Disadvantages:
* Test modules can't import each other.
* Testing utility modules in the tests directories (for example a ``tests.helpers`` module containing test-related functions/classes)
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
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.
It works like this:
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``.
This is happens when plugins import non-test modules (for example doctesting).
If this step succeeds, the module is returned.
For test modules, unless they are reachable from :py:data:`sys.path`, this step will fail.
2. If the previous step fails, we import the module directly using ``importlib`` facilities, which lets us import it without
changing :py:data:`sys.path`.
Because Python requires the module to also be available in :py:data:`sys.modules`, pytest derives a unique name for it based
on its relative location from the ``rootdir``, and adds the module to :py:data:`sys.modules`.
For example, ``tests/core/test_models.py`` will end up being imported as the module ``tests.core.test_models``.
.. versionadded:: 6.0
.. note::
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
.. note::
By default, pytest will not attempt to resolve namespace packages automatically, but that can
be changed via the :confval:`consider_namespace_packages` configuration variable.
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
.. seealso::
The :confval:`pythonpath` configuration variable.
The :confval:`consider_namespace_packages` configuration variable.
:ref:`test layout`.
``prepend`` and ``append`` import modes scenarios
-------------------------------------------------
Here's a list of scenarios when using ``prepend`` or ``append`` import modes where pytest needs to
change :py:data:`sys.path` in order to import test modules or ``conftest.py`` files, and the issues users
change ``sys.path`` in order to import test modules or ``conftest.py`` files, and the issues users
might encounter because of that.
Test modules / ``conftest.py`` files inside packages
@@ -146,7 +92,7 @@ pytest will find ``foo/bar/tests/test_foo.py`` and realize it is part of a packa
there's an ``__init__.py`` file in the same folder. It will then search upwards until it can find the
last folder which still contains an ``__init__.py`` file in order to find the package *root* (in
this case ``foo/``). To load the module, it will insert ``root/`` to the front of
:py:data:`sys.path` (if not there already) in order to load
``sys.path`` (if not there already) in order to load
``test_foo.py`` as the *module* ``foo.bar.tests.test_foo``.
The same logic applies to the ``conftest.py`` file: it will be imported as ``foo.conftest`` module.
@@ -176,8 +122,8 @@ When executing:
pytest will find ``foo/bar/tests/test_foo.py`` and realize it is NOT part of a package given that
there's no ``__init__.py`` file in the same folder. It will then add ``root/foo/bar/tests`` to
:py:data:`sys.path` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done
with the ``conftest.py`` file by adding ``root/foo`` to :py:data:`sys.path` to import it as ``conftest``.
``sys.path`` in order to import ``test_foo.py`` as the *module* ``test_foo``. The same is done
with the ``conftest.py`` file by adding ``root/foo`` to ``sys.path`` to import it as ``conftest``.
For this reason this layout cannot have test modules with the same name, as they all will be
imported in the global import namespace.
@@ -190,7 +136,7 @@ Invoking ``pytest`` versus ``python -m pytest``
-----------------------------------------------
Running pytest with ``pytest [...]`` instead of ``python -m pytest [...]`` yields nearly
equivalent behaviour, except that the latter will add the current directory to :py:data:`sys.path`, which
equivalent behaviour, except that the latter will add the current directory to ``sys.path``, which
is standard ``python`` behavior.
See also :ref:`invoke-python`.

View File

@@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 8.2.0
pytest 8.0.2
.. _`simpletest`:

View File

@@ -224,7 +224,6 @@ place the objects you want to appear in the doctest namespace:
.. code-block:: python
# content of conftest.py
import pytest
import numpy

View File

@@ -4,8 +4,8 @@ How to use pytest with an existing test suite
==============================================
Pytest can be used with most existing test suites, but its
behavior differs from other test runners such as Python's
default unittest framework.
behavior differs from other test runners such as :ref:`nose <noseintegration>` or
Python's default unittest framework.
Before using this section you will want to :ref:`install pytest <getstarted>`.

View File

@@ -494,7 +494,7 @@ Fixtures are created when first requested by a test, and are destroyed based on
* ``function``: the default scope, the fixture is destroyed at the end of the test.
* ``class``: the fixture is destroyed during teardown of the last test in the class.
* ``module``: the fixture is destroyed during teardown of the last test in the module.
* ``package``: the fixture is destroyed during teardown of the last test in the package where the fixture is defined, including sub-packages and sub-directories within it.
* ``package``: the fixture is destroyed during teardown of the last test in the package.
* ``session``: the fixture is destroyed at the end of the test session.
.. note::
@@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
rootdir: /home/sweet/project
collected 12 items
<Dir fixtures.rst-217>
<Dir fixtures.rst-213>
<Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]>

View File

@@ -52,6 +52,7 @@ pytest and other test systems
existingtestsuite
unittest
nose
xunit_setup
pytest development environment

View File

@@ -206,9 +206,8 @@ option names are:
* ``log_cli_date_format``
If you need to record the whole test suite logging calls to a file, you can pass
``--log-file=/path/to/log/file``. This log file is opened in write mode by default which
``--log-file=/path/to/log/file``. This log file is opened in write mode which
means that it will be overwritten at each run tests session.
If you'd like the file opened in append mode instead, then you can pass ``--log-file-mode=a``.
Note that relative paths for the log-file location, whether passed on the CLI or declared in a
config file, are always resolved relative to the current working directory.
@@ -224,13 +223,12 @@ All of the log file options can also be set in the configuration INI file. The
option names are:
* ``log_file``
* ``log_file_mode``
* ``log_file_level``
* ``log_file_format``
* ``log_file_date_format``
You can call ``set_log_path()`` to customize the log_file path dynamically. This functionality
is considered **experimental**. Note that ``set_log_path()`` respects the ``log_file_mode`` option.
is considered **experimental**.
.. _log_colors:

99
doc/en/how-to/nose.rst Normal file
View File

@@ -0,0 +1,99 @@
.. _`noseintegration`:
How to run tests written for nose
=======================================
``pytest`` has basic support for running tests written for nose_.
.. warning::
This functionality has been deprecated and is likely to be removed in ``pytest 8.x``.
.. _nosestyle:
Usage
-------------
After :ref:`installation` type:
.. code-block:: bash
python setup.py develop # make sure tests can import our package
pytest # instead of 'nosetests'
and you should be able to run your nose style tests and
make use of pytest's capabilities.
Supported nose Idioms
----------------------
* ``setup()`` and ``teardown()`` at module/class/method level: any function or method called ``setup`` will be called during the setup phase for each test, same for ``teardown``.
* ``SkipTest`` exceptions and markers
* setup/teardown decorators
* ``__test__`` attribute on modules/classes/functions
* general usage of nose utilities
Unsupported idioms / known issues
----------------------------------
- unittest-style ``setUp, tearDown, setUpClass, tearDownClass``
are recognized only on ``unittest.TestCase`` classes but not
on plain classes. ``nose`` supports these methods also on plain
classes but pytest deliberately does not. As nose and pytest already
both support ``setup_class, teardown_class, setup_method, teardown_method``
it doesn't seem useful to duplicate the unittest-API like nose does.
If you however rather think pytest should support the unittest-spelling on
plain classes please post to :issue:`377`.
- nose imports test modules with the same import path (e.g.
``tests.test_mode``) but different file system paths
(e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``)
by extending sys.path/import semantics. pytest does not do that. Note that
`nose2 choose to avoid this sys.path/import hackery <https://nose2.readthedocs.io/en/latest/differences.html#test-discovery-and-loading>`_.
If you place a conftest.py file in the root directory of your project
(as determined by pytest) pytest will run tests "nose style" against
the code below that directory by adding it to your ``sys.path`` instead of
running against your installed code.
You may find yourself wanting to do this if you ran ``python setup.py install``
to set up your project, as opposed to ``python setup.py develop`` or any of
the package manager equivalents. Installing with develop in a
virtual environment like tox is recommended over this pattern.
- nose-style doctests are not collected and executed correctly,
also doctest fixtures don't work.
- no nose-configuration is recognized.
- ``yield``-based methods are
fundamentally incompatible with pytest because they don't support fixtures
properly since collection and test execution are separated.
Here is a table comparing the default supported naming conventions for both
nose and pytest.
========= ========================== ======= =====
what default naming convention pytest nose
========= ========================== ======= =====
module ``test*.py``
module ``test_*.py`` ✅ ✅
module ``*_test.py``
module ``*_tests.py``
class ``*(unittest.TestCase)`` ✅ ✅
method ``test_*`` ✅ ✅
class ``Test*``
method ``test_*``
function ``test_*``
========= ========================== ======= =====
Migrating from nose to pytest
------------------------------
`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script
and pytest plugin to help convert Nose-based tests into pytest-based tests.
Specifically, the script transforms ``nose.tools.assert_*`` function calls into
raw assert statements, while preserving format of original arguments
as much as possible.
.. _nose: https://nose.readthedocs.io/en/latest/

View File

@@ -294,47 +294,9 @@ Now if we increase verbosity even more:
test_verbosity_example.py:19: AssertionError
========================= short test summary info ==========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi']
At index 2 diff: 'grapes' != 'orange'
Full diff:
[
'banana',
'apple',
- 'orange',
? ^ ^^
+ 'grapes',
? ^ ^ +
'melon',
'kiwi',
]
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
Common items:
{'0': 0}
Left contains 4 more items:
{'1': 1, '2': 2, '3': 3, '4': 4}
Right contains 4 more items:
{'10': 10, '20': 20, '30': 30, '40': 40}
Full diff:
{
'0': 0,
- '10': 10,
? - -
+ '1': 1,
- '20': 20,
? - -
+ '2': 2,
- '30': 30,
? - -
+ '3': 3,
- '40': 40,
? - -
+ '4': 4,
}
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
======================= 3 failed, 1 passed in 0.12s ========================
Notice now that:
@@ -363,9 +325,7 @@ This is done by setting a verbosity level in the configuration file for the spec
``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside
the file is shown by a single character in the output.
:confval:`verbosity_test_cases`: Controls how verbose the test execution output should be when pytest is executed.
Running ``pytest --no-header`` with a value of ``2`` would have the same output as the first verbosity example, but each
test inside the file gets its own line in the output.
(Note: currently this is the only option available, but more might be added in the future).
.. _`pytest.detailed_failed_tests_usage`:

View File

@@ -8,8 +8,9 @@ How to use temporary directories and files in tests
The ``tmp_path`` fixture
------------------------
You can use the ``tmp_path`` fixture which will provide a temporary directory
unique to each test function.
You can use the ``tmp_path`` fixture which will
provide a temporary directory unique to the test invocation,
created in the `base temporary directory`_.
``tmp_path`` is a :class:`pathlib.Path` object. Here is an example test usage:
@@ -61,11 +62,6 @@ Running this would result in a passed test except for the last
FAILED test_tmp_path.py::test_create_file - assert 0
============================ 1 failed in 0.12s =============================
By default, ``pytest`` retains the temporary directory for the last 3 ``pytest``
invocations. Concurrent invocations of the same test function are supported by
configuring the base temporary directory to be unique for each concurrent
run. See `temporary directory location and retention`_ for details.
.. _`tmp_path_factory example`:
The ``tmp_path_factory`` fixture
@@ -104,7 +100,7 @@ See :ref:`tmp_path_factory API <tmp_path_factory factory api>` for details.
.. _tmpdir:
The ``tmpdir`` and ``tmpdir_factory`` fixtures
----------------------------------------------
---------------------------------------------------
The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path``
and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects
@@ -128,10 +124,10 @@ See :fixture:`tmpdir <tmpdir>` :fixture:`tmpdir_factory <tmpdir_factory>`
API for details.
.. _`temporary directory location and retention`:
.. _`base temporary directory`:
Temporary directory location and retention
------------------------------------------
The default base temporary directory
-----------------------------------------------
Temporary directories are by default created as sub-directories of
the system temporary directory. The base name will be ``pytest-NUM`` where
@@ -156,7 +152,7 @@ You can override the default temporary directory setting like this:
for that purpose only.
When distributing tests on the local machine using ``pytest-xdist``, care is taken to
automatically configure a `basetemp` directory for the sub processes such that all temporary
data lands below a single per-test run temporary directory.
automatically configure a basetemp directory for the sub processes such that all temporary
data lands below a single per-test run basetemp directory.
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html

View File

@@ -17,8 +17,7 @@ in the current directory and its subdirectories. More generally, pytest follows
Specifying which tests to run
------------------------------
Pytest supports several ways to run and select tests from the command-line or from a file
(see below for :ref:`reading arguments from file <args-from-file>`).
Pytest supports several ways to run and select tests from the command-line.
**Run tests in a module**
@@ -92,28 +91,6 @@ For more information see :ref:`marks <mark>`.
This will import ``pkg.testing`` and use its filesystem location to find and run tests from.
.. _args-from-file:
**Read arguments from file**
.. versionadded:: 8.2
All of the above can be read from a file using the ``@`` prefix:
.. code-block:: bash
pytest @tests_to_run.txt
where ``tests_to_run.txt`` contains an entry per line, e.g.:
.. code-block:: text
tests/test_file.py
tests/test_mod.py::test_func[x1,y2]
tests/test_mod.py::TestClass
-m slow
This file can also be generated using ``pytest --collect-only -q`` and modified as needed.
Getting help on version, option names, environment variables
--------------------------------------------------------------

View File

@@ -100,7 +100,7 @@ object, the wrapper may modify that result, but it's probably better to avoid it
If the hook implementation failed with an exception, the wrapper can handle that
exception using a ``try-catch-finally`` around the ``yield``, by propagating it,
suppressing it, or raising a different exception entirely.
supressing it, or raising a different exception entirely.
For more information, consult the
:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.

View File

@@ -46,18 +46,24 @@ Plugin discovery order at tool startup
5. by loading all plugins specified through the :envvar:`PYTEST_PLUGINS` environment variable.
6. by loading all "initial ":file:`conftest.py` files:
6. by loading all :file:`conftest.py` files as inferred by the command line
invocation:
- determine the test paths: specified on the command line, otherwise in
:confval:`testpaths` if defined and running from the rootdir, otherwise the
current dir
- for each test path, load ``conftest.py`` and ``test*/conftest.py`` relative
to the directory part of the test path, if exist. Before a ``conftest.py``
file is loaded, load ``conftest.py`` files in all of its parent directories.
After a ``conftest.py`` file is loaded, recursively load all plugins specified
in its :globalvar:`pytest_plugins` variable if present.
- if no test paths are specified, use the current dir as a test path
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
to the directory part of the first test path. After the ``conftest.py``
file is loaded, load all plugins specified in its
:globalvar:`pytest_plugins` variable if present.
Note that pytest does not find ``conftest.py`` files in deeper nested
sub directories at tool startup. It is usually a good idea to keep
your ``conftest.py`` file in the top level test or project root directory.
7. by recursively loading all plugins specified by the
:globalvar:`pytest_plugins` variable in ``conftest.py`` files.
.. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/
.. _`conftest.py plugins`:
.. _`localplugin`:
.. _`local conftest plugins`:
@@ -102,9 +108,9 @@ Here is how you might run it::
See also: :ref:`pythonpath`.
.. note::
Some hooks cannot be implemented in conftest.py files which are not
:ref:`initial <pluginorder>` due to how pytest discovers plugins during
startup. See the documentation of each hook for details.
Some hooks should be implemented only in plugins or conftest.py files situated at the
tests root directory due to how pytest discovers plugins during startup,
see the documentation of each hook for details.
Writing your own plugin
-----------------------

View File

@@ -5,8 +5,7 @@
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_ (3 day in-depth training):
* **June 11th to 13th 2024**, Remote
* **March 4th to 6th 2025**, Leipzig, Germany / Remote
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, **June 17th -- 22nd 2024**
- pytest tips and tricks for a better testsuite, `Europython 2024 <https://ep2024.europython.eu/>`_, **July 8th -- 14th 2024** (3h), Prague
- `pytest development sprint <https://github.com/pytest-dev/pytest/discussions/11655>`_, June 2024 (`date poll <https://nuudel.digitalcourage.de/2tEsEpRcwMNcAXVO>`_)
Also see :doc:`previous talks and blogposts <talks>`.
@@ -78,17 +77,17 @@ Features
- :ref:`Modular fixtures <fixture>` for managing small or parametrized long-lived test resources
- Can run :ref:`unittest <unittest>` (including trial) test suites out of the box
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
- Python 3.8+ or PyPy 3
- Rich plugin architecture, with over 1300+ :ref:`external plugins <plugin-list>` and thriving community
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
Documentation
-------------
* :ref:`Get started <get-started>` - install pytest and grasp its basics in just twenty minutes
* :ref:`Get started <get-started>` - install pytest and grasp its basics just twenty minutes
* :ref:`How-to guides <how-to>` - step-by-step guides, covering a vast range of use-cases and needs
* :ref:`Reference guides <reference>` - includes the complete pytest API reference, lists of plugins and more
* :ref:`Explanation <explanation>` - background, discussion of key topics, answers to higher-level questions

View File

@@ -177,20 +177,13 @@ Files will only be matched for configuration if:
* ``tox.ini``: contains a ``[pytest]`` section.
* ``setup.cfg``: contains a ``[tool:pytest]`` section.
Finally, a ``pyproject.toml`` file will be considered the ``configfile`` if no other match was found, in this case
even if it does not contain a ``[tool.pytest.ini_options]`` table (this was added in ``8.1``).
The files are considered in the order above. Options from multiple ``configfiles`` candidates
are never merged - the first match wins.
The configuration file also determines the value of the ``rootpath``.
The :class:`Config <pytest.Config>` object (accessible via hooks or through the :fixture:`pytestconfig` fixture)
will subsequently carry these attributes:
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist. It is used as
a reference directory for constructing test addresses ("nodeids") and can be used also by plugins for storing
per-testrun information.
- :attr:`config.rootpath <pytest.Config.rootpath>`: the determined root directory, guaranteed to exist.
- :attr:`config.inipath <pytest.Config.inipath>`: the determined ``configfile``, may be ``None``
(it is named ``inipath`` for historical reasons).
@@ -200,7 +193,9 @@ will subsequently carry these attributes:
versions of the older ``config.rootdir`` and ``config.inifile``, which have type
``py.path.local``, and still exist for backward compatibility.
The ``rootdir`` is used as a reference directory for constructing test
addresses ("nodeids") and can be used also by plugins for storing
per-testrun information.
Example:

File diff suppressed because it is too large Load Diff

View File

@@ -136,7 +136,7 @@ pytest.freeze_includes
Marks
-----
Marks can be used to apply metadata to *test functions* (but not fixtures), which can then be accessed by
Marks can be used apply meta data to *test functions* (but not fixtures), which can then be accessed by
fixtures or plugins.
@@ -640,6 +640,8 @@ Bootstrapping hooks called for plugins registered early enough (internal and set
.. hook:: pytest_load_initial_conftests
.. autofunction:: pytest_load_initial_conftests
.. hook:: pytest_cmdline_preparse
.. autofunction:: pytest_cmdline_preparse
.. hook:: pytest_cmdline_parse
.. autofunction:: pytest_cmdline_parse
.. hook:: pytest_cmdline_main
@@ -1117,11 +1119,6 @@ When set (regardless of value), pytest acknowledges that is running in a CI proc
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given
by the user, see :ref:`adding default options` for more information.
.. envvar:: PYTEST_VERSION
This environment variable is defined at the start of the pytest session and is undefined afterwards.
It contains the value of ``pytest.__version__``, and among other things can be used to easily check if a code is running from within a pytest run.
.. envvar:: PYTEST_CURRENT_TEST
This is not meant to be set by users, but is set by pytest internally with the name of the current test so other
@@ -1209,6 +1206,9 @@ Custom warnings generated in some situations such as improper usage or deprecate
.. autoclass:: pytest.PytestReturnNotNoneWarning
:show-inheritance:
.. autoclass:: pytest.PytestRemovedIn8Warning
:show-inheritance:
.. autoclass:: pytest.PytestRemovedIn9Warning
:show-inheritance:
@@ -1279,18 +1279,6 @@ passed multiple times. The expected format is ``name=value``. For example::
variables, that will be expanded. For more information about cache plugin
please refer to :ref:`cache_provider`.
.. confval:: consider_namespace_packages
Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__
when collecting Python modules. Default is ``False``.
Set to ``True`` if the package you are testing is part of a namespace package.
Only `native namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages>`__
are supported, with no plans to support `legacy namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#legacy-namespace-packages>`__.
.. versionadded:: 8.1
.. confval:: console_output_style
Sets the console output style while running tests:
@@ -1882,19 +1870,6 @@ passed multiple times. The expected format is ``name=value``. For example::
"auto" can be used to explicitly use the global verbosity level.
.. confval:: verbosity_test_cases
Set a verbosity level specifically for test case execution related output, overriding the application wide level.
.. code-block:: ini
[pytest]
verbosity_test_cases = 2
Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of
"auto" can be used to explicitly use the global verbosity level.
.. confval:: xfail_strict
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
@@ -2107,8 +2082,6 @@ All the command-line flags can be obtained by running ``pytest --help``::
--log-cli-date-format=LOG_CLI_DATE_FORMAT
Log date format used by the logging module
--log-file=LOG_FILE Path to a file when logging will be written to
--log-file-mode={w,a}
Log file open mode
--log-file-level=LOG_FILE_LEVEL
Log file logging level
--log-file-format=LOG_FILE_FORMAT
@@ -2124,7 +2097,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
markers (linelist): Register new markers for test functions
markers (linelist): Markers for test functions
empty_parameter_set_mark (string):
Default marker for empty parametersets
norecursedirs (args): Directory patterns to avoid for recursion
@@ -2134,9 +2107,6 @@ All the command-line flags can be obtained by running ``pytest --help``::
Each line specifies a pattern for
warnings.filterwarnings. Processed after
-W/--pythonwarnings.
consider_namespace_packages (bool):
Consider namespace packages when resolving module
names during import
usefixtures (args): List of default fixtures to be used with this
project
python_files (args): Glob-style file patterns for Python test module
@@ -2155,11 +2125,6 @@ All the command-line flags can be obtained by running ``pytest --help``::
progress information ("progress" (percentage) |
"count" | "progress-even-when-capture-no" (forces
progress even when capture=no)
verbosity_test_cases (string):
Specify a verbosity level for test case execution,
overriding the main level. Higher levels will
provide more detailed information about each test
case executed.
xfail_strict (bool): Default for the strict parameter of xfail markers
when not given explicitly (default: False)
tmp_path_retention_count (string):
@@ -2207,8 +2172,6 @@ All the command-line flags can be obtained by running ``pytest --help``::
log_cli_date_format (string):
Default value for --log-cli-date-format
log_file (string): Default value for --log-file
log_file_mode (string):
Default value for --log-file-mode
log_file_level (string):
Default value for --log-file-level
log_file_format (string):

View File

@@ -1,5 +1,5 @@
pallets-sphinx-themes
pluggy>=1.5.0
pluggy>=1.2.0
pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0
sphinx>=7

View File

@@ -1,291 +1,13 @@
[project]
name = "pytest"
description = "pytest: simple powerful testing with Python"
readme = "README.rst"
keywords = [
"test",
"unittest",
]
license = {text = "MIT"}
authors = [
{name = "Holger Krekel"},
{name = "Bruno Oliveira"},
{name = "Ronny Pfannschmidt"},
{name = "Floris Bruynooghe"},
{name = "Brianna Laugher"},
{name = "Florian Bruhin"},
{name = "Others (See AUTHORS)"},
]
requires-python = ">=3.8"
classifiers = [
"Development Status :: 6 - Mature",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Operating System :: Unix",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Testing",
"Topic :: Utilities",
]
dynamic = [
"version",
]
dependencies = [
'colorama; sys_platform == "win32"',
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
"iniconfig",
"packaging",
"pluggy<2.0,>=1.5",
'tomli>=1; python_version < "3.11"',
]
[project.optional-dependencies]
dev = [
"argcomplete",
"attrs>=19.2",
"hypothesis>=3.56",
"mock",
"pygments>=2.7.2",
"requests",
"setuptools",
"xmlschema",
]
[project.urls]
Changelog = "https://docs.pytest.org/en/stable/changelog.html"
Homepage = "https://docs.pytest.org/en/latest/"
Source = "https://github.com/pytest-dev/pytest"
Tracker = "https://github.com/pytest-dev/pytest/issues"
Twitter = "https://twitter.com/pytestdotorg"
[project.scripts]
"py.test" = "pytest:console_main"
pytest = "pytest:console_main"
[build-system]
build-backend = "setuptools.build_meta"
requires = [
"setuptools>=61",
"setuptools-scm[toml]>=6.2.3",
"setuptools>=45.0",
"setuptools-scm[toml]>=6.2.3",
]
[tool.setuptools.package-data]
"_pytest" = ["py.typed"]
"pytest" = ["py.typed"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
write_to = "src/_pytest/_version.py"
[tool.black]
target-version = ['py38']
[tool.ruff]
src = ["src"]
line-length = 88
[tool.ruff.format]
docstring-code-format = true
[tool.ruff.lint]
select = [
"B", # bugbear
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"PYI", # flake8-pyi
"UP", # pyupgrade
"RUF", # ruff
"W", # pycodestyle
"PIE", # flake8-pie
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
"PLE", # pylint error
"PLW", # pylint warning
"PLR1714", # Consider merging multiple comparisons
]
ignore = [
# bugbear ignore
"B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
"B007", # Loop control variable `i` not used within loop body
"B009", # Do not call `getattr` with a constant attribute value
"B010", # [*] Do not call `setattr` with a constant attribute value.
"B011", # Do not `assert False` (`python -O` removes these calls)
"B028", # No explicit `stacklevel` keyword argument found
# pycodestyle ignore
# pytest can do weird low-level things, and we usually know
# what we're doing when we use type(..) is ...
"E721", # Do not compare types, use `isinstance()`
# pydocstyle ignore
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D102", # Missing docstring in public method
"D103", # Missing docstring in public function
"D104", # Missing docstring in public package
"D105", # Missing docstring in magic method
"D106", # Missing docstring in public nested class
"D107", # Missing docstring in `__init__`
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
"D205", # 1 blank line required between summary line and description
"D400", # First line should end with a period
"D401", # First line of docstring should be in imperative mood
"D402", # First line should not be the function's signature
"D404", # First word of the docstring should not be "This"
"D415", # First line should end with a period, question mark, or exclamation point
# ruff ignore
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
# pylint ignore
"PLW0603", # Using the global statement
"PLW0120", # remove the else and dedent its contents
"PLW2901", # for loop variable overwritten by assignment target
"PLR5501", # Use `elif` instead of `else` then `if`
]
[tool.ruff.lint.pycodestyle]
# In order to be able to format for 88 char in ruff format
max-line-length = 120
[tool.ruff.lint.pydocstyle]
convention = "pep257"
[tool.ruff.lint.isort]
force-single-line = true
combine-as-imports = true
force-sort-within-sections = true
order-by-type = false
known-local-folder = ["pytest", "_pytest"]
lines-after-imports = 2
[tool.ruff.lint.per-file-ignores]
"src/_pytest/_py/**/*.py" = ["B", "PYI"]
"src/_pytest/_version.py" = ["I001"]
"testing/python/approx.py" = ["B015"]
[tool.pylint.main]
# Maximum number of characters on a single line.
max-line-length = 120
disable= [
"abstract-method",
"arguments-differ",
"arguments-renamed",
"assigning-non-slot",
"attribute-defined-outside-init",
"bad-classmethod-argument",
"bad-mcs-method-argument",
"broad-exception-caught",
"broad-exception-raised",
"cell-var-from-loop",
"comparison-of-constants",
"comparison-with-callable",
"comparison-with-itself",
"condition-evals-to-constant",
"consider-using-dict-items",
"consider-using-enumerate",
"consider-using-from-import",
"consider-using-f-string",
"consider-using-in",
"consider-using-sys-exit",
"consider-using-ternary",
"consider-using-with",
"cyclic-import",
"disallowed-name",
"duplicate-code",
"eval-used",
"exec-used",
"expression-not-assigned",
"fixme",
"global-statement",
"implicit-str-concat",
"import-error",
"import-outside-toplevel",
"inconsistent-return-statements",
"invalid-bool-returned",
"invalid-name",
"invalid-repr-returned",
"invalid-str-returned",
"keyword-arg-before-vararg",
"line-too-long",
"method-hidden",
"misplaced-bare-raise",
"missing-docstring",
"missing-timeout",
"multiple-statements",
"no-else-break",
"no-else-continue",
"no-else-raise",
"no-else-return",
"no-member",
"no-name-in-module",
"no-self-argument",
"not-an-iterable",
"not-callable",
"pointless-exception-statement",
"pointless-statement",
"pointless-string-statement",
"protected-access",
"raise-missing-from",
"redefined-argument-from-local",
"redefined-builtin",
"redefined-outer-name",
"reimported",
"simplifiable-condition",
"simplifiable-if-expression",
"singleton-comparison",
"superfluous-parens",
"super-init-not-called",
"too-few-public-methods",
"too-many-ancestors",
"too-many-arguments",
"too-many-branches",
"too-many-function-args",
"too-many-instance-attributes",
"too-many-lines",
"too-many-locals",
"too-many-nested-blocks",
"too-many-public-methods",
"too-many-return-statements",
"too-many-statements",
"try-except-raise",
"typevar-name-incorrect-variance",
"unbalanced-tuple-unpacking",
"undefined-loop-variable",
"undefined-variable",
"unexpected-keyword-arg",
"unidiomatic-typecheck",
"unnecessary-comprehension",
"unnecessary-dunder-call",
"unnecessary-lambda",
"unnecessary-lambda-assignment",
"unpacking-non-sequence",
"unspecified-encoding",
"unsubscriptable-object",
"unused-argument",
"unused-import",
"unused-variable",
"used-before-assignment",
"use-dict-literal",
"use-implicit-booleaness-not-comparison",
"use-implicit-booleaness-not-len",
"useless-else-on-loop",
"useless-import-alias",
"useless-return",
"use-maxsplit-arg",
"using-constant-test",
"wrong-import-order",
]
[tool.check-wheel-contents]
# check-wheel-contents is executed by the build-and-inspect-python-package action.
# W009: Wheel contains multiple toplevel library entries
ignore = "W009"
[tool.pyproject-fmt]
indent = 4
[tool.pytest.ini_options]
minversion = "2.0"
addopts = "-rfEX -p pytester --strict-markers"
@@ -345,6 +67,7 @@ markers = [
"uses_pexpect",
]
[tool.towncrier]
package = "pytest"
package_dir = "src"
@@ -393,18 +116,65 @@ template = "changelog/_template.rst"
name = "Trivial/Internal Changes"
showcontent = true
[tool.mypy]
files = ["src", "testing", "scripts"]
mypy_path = ["src"]
check_untyped_defs = true
disallow_any_generics = true
disallow_untyped_defs = true
ignore_missing_imports = true
show_error_codes = true
strict_equality = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
no_implicit_reexport = true
warn_unused_ignores = true
[tool.black]
target-version = ['py38']
# check-wheel-contents is executed by the build-and-inspect-python-package action.
[tool.check-wheel-contents]
# W009: Wheel contains multiple toplevel library entries
ignore = "W009"
[tool.ruff]
src = ["src"]
line-length = 88
select = [
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"UP", # pyupgrade
"W", # pycodestyle
]
ignore = [
# pycodestyle ignore
# pytest can do weird low-level things, and we usually know
# what we're doing when we use type(..) is ...
"E721", # Do not compare types, use `isinstance()`
# pydocstyle ignore
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D102", # Missing docstring in public method
"D103", # Missing docstring in public function
"D104", # Missing docstring in public package
"D105", # Missing docstring in magic method
"D106", # Missing docstring in public nested class
"D107", # Missing docstring in `__init__`
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
"D205", # 1 blank line required between summary line and description
"D400", # First line should end with a period
"D401", # First line of docstring should be in imperative mood
"D402", # First line should not be the function's signature
"D404", # First word of the docstring should not be "This"
"D415", # First line should end with a period, question mark, or exclamation point
# Temp for backport 8.0.x
"E501",
"UP031",
]
[tool.ruff.format]
docstring-code-format = true
[tool.ruff.lint.pycodestyle]
# In order to be able to format for 88 char in ruff format
max-line-length = 120
[tool.ruff.lint.pydocstyle]
convention = "pep257"
[tool.ruff.lint.isort]
force-single-line = true
combine-as-imports = true
force-sort-within-sections = true
order-by-type = false
known-local-folder = ["pytest", "_pytest"]
lines-after-imports = 2

View File

@@ -8,7 +8,6 @@ our CHANGELOG) into Markdown (which is required by GitHub Releases).
Requires Python3.6+.
"""
from pathlib import Path
import re
import sys

View File

@@ -13,7 +13,6 @@ After that, it will create a release using the `release` tox environment, and pu
**Token**: currently the token from the GitHub Actions is used, pushed with
`pytest bot <pytestbot@gmail.com>` commit author.
"""
import argparse
from pathlib import Path
import re
@@ -80,7 +79,7 @@ def prepare_release_pr(
)
except InvalidFeatureRelease as e:
print(f"{Fore.RED}{e}")
raise SystemExit(1) from None
raise SystemExit(1)
print(f"Version: {Fore.CYAN}{version}")

View File

@@ -1,6 +1,5 @@
# mypy: disallow-untyped-defs
"""Invoke development tasks."""
import argparse
import os
from pathlib import Path
@@ -108,7 +107,7 @@ def pre_release(
def changelog(version: str, write_out: bool = False) -> None:
addopts = [] if write_out else ["--draft"]
check_call(["towncrier", "--yes", "--version", version, *addopts])
check_call(["towncrier", "--yes", "--version", version] + addopts)
def main() -> None:

View File

@@ -29,7 +29,7 @@ Pytest Plugin List
==================
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
It includes PyPI projects whose names begin with ``pytest-`` or ``pytest_`` and a handful of manually selected projects.
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
Packages classified as inactive are excluded.
For detailed insights into how this list is generated,
@@ -110,10 +110,7 @@ def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
return {
name: p["_last-serial"]
for p in response.json()["projects"]
if (
(name := p["name"]).startswith(("pytest-", "pytest_"))
or name in ADDITIONAL_PROJECTS
)
if (name := p["name"]).startswith("pytest-") or name in ADDITIONAL_PROJECTS
}
@@ -212,7 +209,7 @@ def main() -> None:
f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(".. only:: not latex\n\n")
_ = wcwidth # reference library that must exist for tabulate to work
wcwidth # reference library that must exist for tabulate to work
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
f.write(indent(plugin_table, " "))
f.write("\n\n")

105
setup.cfg Normal file
View File

@@ -0,0 +1,105 @@
[metadata]
name = pytest
description = pytest: simple powerful testing with Python
long_description = file: README.rst
long_description_content_type = text/x-rst
url = https://docs.pytest.org/en/latest/
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
license = MIT
license_files = LICENSE
platforms = unix, linux, osx, cygwin, win32
classifiers =
Development Status :: 6 - Mature
Intended Audience :: Developers
License :: OSI Approved :: MIT License
Operating System :: MacOS :: MacOS X
Operating System :: Microsoft :: Windows
Operating System :: POSIX
Programming Language :: Python :: 3
Programming Language :: Python :: 3 :: Only
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Software Development :: Libraries
Topic :: Software Development :: Testing
Topic :: Utilities
keywords = test, unittest
project_urls =
Changelog=https://docs.pytest.org/en/stable/changelog.html
Twitter=https://twitter.com/pytestdotorg
Source=https://github.com/pytest-dev/pytest
Tracker=https://github.com/pytest-dev/pytest/issues
[options]
packages =
_pytest
_pytest._code
_pytest._io
_pytest._py
_pytest.assertion
_pytest.config
_pytest.mark
pytest
py_modules = py
install_requires =
iniconfig
packaging
pluggy>=1.3.0,<2.0
colorama;sys_platform=="win32"
exceptiongroup>=1.0.0rc8;python_version<"3.11"
tomli>=1.0.0;python_version<"3.11"
python_requires = >=3.8
package_dir =
=src
setup_requires =
setuptools
setuptools-scm>=6.0
zip_safe = no
[options.entry_points]
console_scripts =
pytest=pytest:console_main
py.test=pytest:console_main
[options.extras_require]
testing =
argcomplete
attrs>=19.2.0
hypothesis>=3.56
mock
nose
pygments>=2.7.2
requests
setuptools
xmlschema
[options.package_data]
_pytest = py.typed
pytest = py.typed
[build_sphinx]
source_dir = doc/en/
build_dir = doc/build
all_files = 1
[check-manifest]
ignore =
src/_pytest/_version.py
[devpi:upload]
formats = sdist.tgz,bdist_wheel
[mypy]
mypy_path = src
check_untyped_defs = True
disallow_any_generics = True
ignore_missing_imports = True
show_error_codes = True
strict_equality = True
warn_redundant_casts = True
warn_return_any = True
warn_unreachable = True
warn_unused_configs = True
no_implicit_reexport = True

View File

@@ -7,4 +7,4 @@ except ImportError: # pragma: no cover
# broken installation, we don't even try
# unknown only works because we do poor mans version compare
__version__ = "unknown"
version_tuple = (0, 0, "unknown")
version_tuple = (0, 0, "unknown") # type:ignore[assignment]

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
import ast
import dataclasses
import inspect
@@ -52,7 +51,7 @@ from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
if sys.version_info < (3, 11):
if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
@@ -279,9 +278,9 @@ class TracebackEntry:
Mostly for internal use.
"""
tbh: Union[bool, Callable[[Optional[ExceptionInfo[BaseException]]], bool]] = (
False
)
tbh: Union[
bool, Callable[[Optional[ExceptionInfo[BaseException]]], 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
@@ -378,10 +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]
@@ -703,7 +704,7 @@ class ExceptionInfo(Generic[E]):
# Workaround for https://github.com/python/cpython/issues/98778 on
# Python <= 3.9, and some 3.10 and 3.11 patch versions.
HTTPError = getattr(sys.modules.get("urllib.error", None), "HTTPError", ())
if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
if sys.version_info[:2] <= (3, 11) and isinstance(exc, HTTPError):
notes = []
else:
raise
@@ -786,8 +787,6 @@ class ExceptionInfo(Generic[E]):
If `None`, will search for a matching exception at any nesting depth.
If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
the exceptions contained within the topmost exception group).
.. versionadded:: 8.0
"""
msg = "Captured exception is not an instance of `BaseExceptionGroup`"
assert isinstance(self.value, BaseExceptionGroup), msg
@@ -1018,7 +1017,7 @@ class FormattedExcinfo:
extraline: Optional[str] = (
"!!! 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"
f" {type(e).__name__}: {str(e)}\n"
f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
)
# Type ignored because adding two instances of a List subtype
@@ -1051,13 +1050,13 @@ 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] = (
ReprTracebackNative(
traceback.format_exception(
type(excinfo_.value),
excinfo_.value,
excinfo_.traceback[0]._rawentry,
)
reprtraceback: Union[
ReprTracebackNative, ReprTraceback
] = ReprTracebackNative(
traceback.format_exception(
type(excinfo_.value),
excinfo_.value,
excinfo_.traceback[0]._rawentry,
)
)
else:
@@ -1348,7 +1347,7 @@ def getfslineno(obj: object) -> Tuple[Union[str, Path], int]:
# in 6ec13a2b9. It ("place_as") appears to be something very custom.
obj = get_real_func(obj)
if hasattr(obj, "place_as"):
obj = obj.place_as
obj = obj.place_as # type: ignore[attr-defined]
try:
code = Code.from_function(obj)

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
import ast
from bisect import bisect_right
import inspect
@@ -47,10 +46,12 @@ class Source:
__hash__ = None # type: ignore
@overload
def __getitem__(self, key: int) -> str: ...
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"]:
if isinstance(key, int):

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
# This module was imported from the cpython standard library
# (https://github.com/python/cpython/) at commit
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).

View File

@@ -9,7 +9,6 @@ from typing import Optional
from typing import Sequence
from typing import TextIO
from ..compat import assert_never
from .wcwidth import wcswidth
@@ -210,8 +209,6 @@ class TerminalWriter:
from pygments.lexers.python import PythonLexer as Lexer
elif lexer == "diff":
from pygments.lexers.diff import DiffLexer as Lexer
else:
assert_never(lexer)
from pygments import highlight
import pygments.util
except ImportError:
@@ -235,17 +232,17 @@ class TerminalWriter:
# which may lead to the previous color being propagated to the
# start of the expression, so reset first.
return "\x1b[0m" + highlighted
except pygments.util.ClassNotFound as e:
except pygments.util.ClassNotFound:
raise UsageError(
"PYTEST_THEME environment variable had an invalid value: '{}'. "
"Only valid pygment styles are allowed.".format(
os.getenv("PYTEST_THEME")
)
) from e
except pygments.util.OptionError as e:
)
except pygments.util.OptionError:
raise UsageError(
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
"The only allowed values are 'dark' and 'light'.".format(
os.getenv("PYTEST_THEME_MODE")
)
) from e
)

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""local path implementation."""
from __future__ import annotations
@@ -206,10 +205,12 @@ class Stat:
if TYPE_CHECKING:
@property
def size(self) -> int: ...
def size(self) -> int:
...
@property
def mtime(self) -> float: ...
def mtime(self) -> float:
...
def __getattr__(self, name: str) -> Any:
return getattr(self._osstatresult, "st_" + name)
@@ -224,7 +225,7 @@ class Stat:
raise NotImplementedError("XXX win32")
import pwd
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore]
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined]
return entry[0]
@property
@@ -234,7 +235,7 @@ class Stat:
raise NotImplementedError("XXX win32")
import grp
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore]
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined]
return entry[0]
def isdir(self):
@@ -252,7 +253,7 @@ def getuserid(user):
import pwd
if not isinstance(user, int):
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore]
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
return user
@@ -260,7 +261,7 @@ def getgroupid(group):
import grp
if not isinstance(group, int):
group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore]
group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
return group
@@ -317,7 +318,7 @@ class LocalPath:
def readlink(self) -> str:
"""Return value of a symbolic link."""
# https://github.com/python/mypy/issues/12278
return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore]
return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value]
def mklinkto(self, oldname):
"""Posix style hard link to another name."""
@@ -451,7 +452,7 @@ class LocalPath:
def ensure_dir(self, *args):
"""Ensure the path joined with args is a directory."""
return self.ensure(*args, dir=True)
return self.ensure(*args, **{"dir": True})
def bestrelpath(self, dest):
"""Return a string which is a relative path from self
@@ -756,11 +757,15 @@ class LocalPath:
if ensure:
self.dirpath().ensure(dir=1)
if encoding:
# Using type ignore here because of this error:
# error: Argument 1 has incompatible type overloaded function;
# expected "Callable[[str, Any, Any], TextIOWrapper]" [arg-type]
# Which seems incorrect, given io.open supports the given argument types.
return error.checked_call(
io.open,
self.strpath,
mode,
encoding=encoding,
encoding=encoding, # type:ignore[arg-type]
)
return error.checked_call(open, self.strpath, mode)
@@ -961,10 +966,12 @@ class LocalPath:
return p
@overload
def stat(self, raising: Literal[True] = ...) -> Stat: ...
def stat(self, raising: Literal[True] = ...) -> Stat:
...
@overload
def stat(self, raising: Literal[False]) -> Stat | None: ...
def stat(self, raising: Literal[False]) -> Stat | None:
...
def stat(self, raising: bool = True) -> Stat | None:
"""Return an os.stat() tuple."""
@@ -1098,7 +1105,9 @@ class LocalPath:
modname = self.purebasename
spec = importlib.util.spec_from_file_location(modname, str(self))
if spec is None or spec.loader is None:
raise ImportError(f"Can't find module {modname} at location {self!s}")
raise ImportError(
f"Can't find module {modname} at location {str(self)}"
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
@@ -1270,7 +1279,13 @@ class LocalPath:
if rootdir is None:
rootdir = cls.get_temproot()
path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir))
# Using type ignore here because of this error:
# error: Argument 1 has incompatible type overloaded function; expected "Callable[[str], str]" [arg-type]
# Which seems incorrect, given tempfile.mkdtemp supports the given argument types.
path = error.checked_call(
tempfile.mkdtemp,
dir=str(rootdir), # type:ignore[arg-type]
)
return cls(path)
@classmethod

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Support for presenting detailed information in failing assertions."""
import sys

View File

@@ -289,13 +289,15 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
else:
from importlib.abc import TraversableResources
def get_resource_reader(self, name: str) -> TraversableResources:
def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore
if sys.version_info < (3, 11):
from importlib.readers import FileReader
else:
from importlib.resources.readers import FileReader
return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
return FileReader( # type:ignore[no-any-return]
types.SimpleNamespace(path=self._rewritten_names[name])
)
def _write_pyc_fp(
@@ -670,9 +672,9 @@ 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]] = (
defaultdict(dict)
)
self.variables_overwrite: defaultdict[
tuple[ast.AST, ...], Dict[str, str]
] = defaultdict(dict)
def run(self, mod: ast.Module) -> None:
"""Find all assert statements in *mod* and rewrite them."""
@@ -923,7 +925,7 @@ class AssertionRewriter(ast.NodeVisitor):
# If any hooks implement assert_pass hook
hook_impl_test = ast.If(
self.helper("_check_if_assertion_pass_impl"),
[*self.expl_stmts, hook_call_pass],
self.expl_stmts + [hook_call_pass],
[],
)
statements_pass = [hook_impl_test]
@@ -973,7 +975,7 @@ class AssertionRewriter(ast.NodeVisitor):
# name if it's a local variable or _should_repr_global_name()
# thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
target_id = name.target.id
target_id = name.target.id # type: ignore[attr-defined]
inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
@@ -1004,7 +1006,7 @@ class AssertionRewriter(ast.NodeVisitor):
if i:
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.append(ast.If(cond, fail_inner, [])) # noqa
self.expl_stmts = fail_inner
# Check if the left operand is a ast.NamedExpr and the value has already been visited
if (

View File

@@ -92,8 +92,7 @@ def _truncate_explanation(
else:
# Add proper ellipsis when we were able to fit a full line exactly
truncated_explanation[-1] = "..."
return [
*truncated_explanation,
return truncated_explanation + [
"",
f"...Full output truncated ({truncated_line_count} line"
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Utilities for assertion debugging."""
import collections.abc
@@ -223,9 +222,10 @@ def assertrepr_compare(
except outcomes.Exit:
raise
except Exception:
repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
explanation = [
f"(pytest_assertion plugin: representation of details failed: {repr_crash}.",
"(pytest_assertion plugin: representation of details failed: {}.".format(
_pytest._code.ExceptionInfo.from_current()._getreprcrash()
),
" Probably an object has a faulty __repr__.)",
]
@@ -233,8 +233,8 @@ def assertrepr_compare(
return None
if explanation[0] != "":
explanation = ["", *explanation]
return [summary, *explanation]
explanation = [""] + explanation
return [summary] + explanation
def _compare_eq_any(

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external
@@ -7,7 +6,6 @@ import dataclasses
import json
import os
from pathlib import Path
import tempfile
from typing import Dict
from typing import final
from typing import Generator
@@ -124,10 +122,6 @@ class Cache:
stacklevel=3,
)
def _mkdir(self, path: Path) -> None:
self._ensure_cache_dir_and_supporting_files()
path.mkdir(exist_ok=True, parents=True)
def mkdir(self, name: str) -> Path:
"""Return a directory path object with the given name.
@@ -146,7 +140,7 @@ class Cache:
if len(path.parts) > 1:
raise ValueError("name is not allowed to contain path separators")
res = self._cachedir.joinpath(self._CACHE_PREFIX_DIRS, path)
self._mkdir(res)
res.mkdir(exist_ok=True, parents=True)
return res
def _getvaluepath(self, key: str) -> Path:
@@ -183,13 +177,19 @@ class Cache:
"""
path = self._getvaluepath(key)
try:
self._mkdir(path.parent)
if path.parent.is_dir():
cache_dir_exists_already = True
else:
cache_dir_exists_already = self._cachedir.exists()
path.parent.mkdir(exist_ok=True, parents=True)
except OSError as exc:
self.warn(
f"could not create cache path {path}: {exc}",
_ispytest=True,
)
return
if not cache_dir_exists_already:
self._ensure_supporting_files()
data = json.dumps(value, ensure_ascii=False, indent=2)
try:
f = path.open("w", encoding="UTF-8")
@@ -202,32 +202,17 @@ class Cache:
with f:
f.write(data)
def _ensure_cache_dir_and_supporting_files(self) -> None:
"""Create the cache dir and its supporting files."""
if self._cachedir.is_dir():
return
def _ensure_supporting_files(self) -> None:
"""Create supporting files in the cache dir that are not really part of the cache."""
readme_path = self._cachedir / "README.md"
readme_path.write_text(README_CONTENT, encoding="UTF-8")
self._cachedir.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(
prefix="pytest-cache-files-",
dir=self._cachedir.parent,
) as newpath:
path = Path(newpath)
with open(path.joinpath("README.md"), "xt", encoding="UTF-8") as f:
f.write(README_CONTENT)
with open(path.joinpath(".gitignore"), "xt", encoding="UTF-8") as f:
f.write("# Created by pytest automatically.\n*\n")
with open(path.joinpath("CACHEDIR.TAG"), "xb") as f:
f.write(CACHEDIR_TAG_CONTENT)
gitignore_path = self._cachedir.joinpath(".gitignore")
msg = "# Created by pytest automatically.\n*\n"
gitignore_path.write_text(msg, encoding="UTF-8")
path.rename(self._cachedir)
# Create a directory in place of the one we just moved so that `TemporaryDirectory`'s
# cleanup doesn't complain.
#
# TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10. See
# https://github.com/python/cpython/issues/74168. Note that passing delete=False would
# do the wrong thing in case of errors and isn't supported until python 3.12.
path.mkdir()
cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG")
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
class LFPluginCollWrapper:
@@ -447,7 +432,7 @@ class NFPlugin:
return res
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True)
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]
def pytest_sessionfinish(self) -> None:
config = self.config

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Per-test stdout/stderr capturing mechanism."""
import abc
@@ -106,16 +105,17 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
return
# Bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666).
if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore]
if not hasattr(stream, "buffer"): # type: ignore[unreachable]
return
raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer
buffered = hasattr(stream.buffer, "raw")
raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore]
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined]
return
def _reopen_stdio(f, mode):
if not hasattr(stream.buffer, "raw") and mode[0] == "w":
if not buffered and mode[0] == "w":
buffering = 0
else:
buffering = -1
@@ -482,9 +482,12 @@ class FDCaptureBase(CaptureBase[AnyStr]):
self._state = "initialized"
def __repr__(self) -> str:
return (
f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} "
f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format(
self.__class__.__name__,
self.targetfd,
self.targetfd_save,
self._state,
self.tmpfile,
)
def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
@@ -595,8 +598,7 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
else:
class CaptureResult(
collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
Generic[AnyStr],
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
):
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
@@ -618,9 +620,12 @@ class MultiCapture(Generic[AnyStr]):
self.err: Optional[CaptureBase[AnyStr]] = err
def __repr__(self) -> str:
return (
f"<MultiCapture out={self.out!r} err={self.err!r} in_={self.in_!r} "
f"_state={self._state!r} _in_suspended={self._in_suspended!r}>"
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
self.out,
self.err,
self.in_,
self._state,
self._in_suspended,
)
def start_capturing(self) -> None:
@@ -729,9 +734,8 @@ class CaptureManager:
self._capture_fixture: Optional[CaptureFixture[Any]] = None
def __repr__(self) -> str:
return (
f"<CaptureManager _method={self._method!r} _global_capturing={self._global_capturing!r} "
f"_capture_fixture={self._capture_fixture!r}>"
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
self._method, self._global_capturing, self._capture_fixture
)
def is_capturing(self) -> Union[str, bool]:

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Python version compatibility code."""
from __future__ import annotations
@@ -16,10 +15,14 @@ from typing import Any
from typing import Callable
from typing import Final
from typing import NoReturn
from typing import TypeVar
import py
_T = TypeVar("_T")
_S = TypeVar("_S")
#: constant to prepare valuing pylib path replacements/lazy proxies later on
# intended for removal in pytest 8.0 or 9.0
@@ -39,7 +42,7 @@ def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum):
token = 0
NOTSET: Final = NotSetType.token
NOTSET: Final = NotSetType.token # noqa: E305
# fmt: on
@@ -103,6 +106,7 @@ def getfuncargnames(
function: Callable[..., object],
*,
name: str = "",
is_method: bool = False,
cls: type | None = None,
) -> tuple[str, ...]:
"""Return the names of a function's mandatory arguments.
@@ -113,8 +117,9 @@ def getfuncargnames(
* Aren't bound with functools.partial.
* Aren't replaced with mocks.
The cls arguments indicate that the function should be treated as a bound
method even though it's not unless the function is a static method.
The is_method and cls arguments indicate that the function should
be treated as a bound method even though it's not unless, only in
the case of cls, the function is a static method.
The name parameter should be the original name in which the function was collected.
"""
@@ -152,7 +157,7 @@ def getfuncargnames(
# If this function should be treated as a bound method even though
# it's passed as an unbound method or function, remove the first
# parameter name.
if (
if is_method or (
# Not using `getattr` because we don't want to resolve the staticmethod.
# Not using `cls.__dict__` because we want to check the entire MRO.
cls
@@ -187,13 +192,25 @@ _non_printable_ascii_translate_table.update(
)
def _translate_non_printable(s: str) -> str:
return s.translate(_non_printable_ascii_translate_table)
STRING_TYPES = bytes, str
def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace")
def ascii_escaped(val: bytes | str) -> str:
r"""If val is pure ASCII, return it as an str, otherwise, escape
bytes objects into a sequence of escaped bytes:
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
and escapes strings into a sequence of escaped unicode ids, e.g.:
and escapes unicode objects into a sequence of escaped unicode
ids, e.g.:
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
@@ -204,10 +221,10 @@ def ascii_escaped(val: bytes | str) -> str:
a UTF-8 string.
"""
if isinstance(val, bytes):
ret = val.decode("ascii", "backslashreplace")
ret = _bytes_to_ascii(val)
else:
ret = val.encode("unicode_escape").decode("ascii")
return ret.translate(_non_printable_ascii_translate_table)
return _translate_non_printable(ret)
@dataclasses.dataclass

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Command line options, ini-file and conftest.py processing."""
import argparse
@@ -18,6 +17,7 @@ import sys
from textwrap import dedent
import types
from types import FunctionType
from types import TracebackType
from typing import Any
from typing import Callable
from typing import cast
@@ -50,7 +50,6 @@ from .compat import PathAwareHookProxy
from .exceptions import PrintHelp as PrintHelp
from .exceptions import UsageError as UsageError
from .findpaths import determine_setup
from _pytest import __version__
import _pytest._code
from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
@@ -118,14 +117,14 @@ class ConftestImportFailure(Exception):
def __init__(
self,
path: Path,
*,
cause: Exception,
excinfo: Tuple[Type[Exception], Exception, TracebackType],
) -> None:
super().__init__(path, excinfo)
self.path = path
self.cause = cause
self.excinfo = excinfo
def __str__(self) -> str:
return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
return f"{self.excinfo[0].__name__}: {self.excinfo[1]} (from {self.path})"
def filter_traceback_for_conftest_import_failure(
@@ -152,13 +151,11 @@ def main(
:returns: An exit code.
"""
old_pytest_version = os.environ.get("PYTEST_VERSION")
try:
os.environ["PYTEST_VERSION"] = __version__
try:
config = _prepareconfig(args, plugins)
except ConftestImportFailure as e:
exc_info = ExceptionInfo.from_exception(e.cause)
exc_info = ExceptionInfo.from_exc_info(e.excinfo)
tw = TerminalWriter(sys.stderr)
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
exc_info.traceback = exc_info.traceback.filter(
@@ -189,11 +186,6 @@ def main(
for msg in e.args:
tw.line(f"ERROR: {msg}\n", red=True)
return ExitCode.USAGE_ERROR
finally:
if old_pytest_version is None:
os.environ.pop("PYTEST_VERSION", None)
else:
os.environ["PYTEST_VERSION"] = old_pytest_version
def console_main() -> int:
@@ -249,8 +241,7 @@ essential_plugins = (
"helpconfig", # Provides -p.
)
default_plugins = (
*essential_plugins,
default_plugins = essential_plugins + (
"python",
"terminal",
"debugging",
@@ -262,6 +253,7 @@ default_plugins = (
"monkeypatch",
"recwarn",
"pastebin",
"nose",
"assertion",
"junitxml",
"doctest",
@@ -556,10 +548,7 @@ class PytestPluginManager(PluginManager):
noconftest: bool,
rootpath: Path,
confcutdir: Optional[Path],
invocation_dir: Path,
importmode: Union[ImportMode, str],
*,
consider_namespace_packages: bool,
) -> None:
"""Load initial conftest files given a preparsed "namespace".
@@ -568,9 +557,8 @@ class PytestPluginManager(PluginManager):
All builtin and 3rd party plugins will have been loaded, however, so
common options will not confuse our logic here.
"""
self._confcutdir = (
absolutepath(invocation_dir / confcutdir) if confcutdir else None
)
current = Path.cwd()
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
self._noconftest = noconftest
self._using_pyargs = pyargs
foundanchor = False
@@ -580,73 +568,37 @@ class PytestPluginManager(PluginManager):
i = path.find("::")
if i != -1:
path = path[:i]
anchor = absolutepath(invocation_dir / path)
anchor = absolutepath(current / path)
# Ensure we do not break if what appears to be an anchor
# is in fact a very long option (#10169, #11394).
if safe_exists(anchor):
self._try_load_conftest(
anchor,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
self._try_load_conftest(anchor, importmode, rootpath)
foundanchor = True
if not foundanchor:
self._try_load_conftest(
invocation_dir,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
self._try_load_conftest(current, importmode, rootpath)
def _is_in_confcutdir(self, path: Path) -> bool:
"""Whether to consider the given path to load conftests from."""
"""Whether a path is within the confcutdir.
When false, should not load conftest.
"""
if self._confcutdir is None:
return True
# The semantics here are literally:
# Do not load a conftest if it is found upwards from confcut dir.
# But this is *not* the same as:
# Load only conftests from confcutdir or below.
# At first glance they might seem the same thing, however we do support use cases where
# we want to load conftests that are not found in confcutdir or below, but are found
# in completely different directory hierarchies like packages installed
# in out-of-source trees.
# (see #9767 for a regression where the logic was inverted).
return path not in self._confcutdir.parents
def _try_load_conftest(
self,
anchor: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
*,
consider_namespace_packages: bool,
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> None:
self._loadconftestmodules(
anchor,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
self._loadconftestmodules(anchor, importmode, rootpath)
# let's also consider test* subdirs
if anchor.is_dir():
for x in anchor.glob("test*"):
if x.is_dir():
self._loadconftestmodules(
x,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
self._loadconftestmodules(x, importmode, rootpath)
def _loadconftestmodules(
self,
path: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
*,
consider_namespace_packages: bool,
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> None:
if self._noconftest:
return
@@ -658,17 +610,15 @@ class PytestPluginManager(PluginManager):
if directory in self._dirpath2confmods:
return
# XXX these days we may rather want to use config.rootpath
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir.
clist = []
for parent in reversed((directory, *directory.parents)):
if self._is_in_confcutdir(parent):
conftestpath = parent / "conftest.py"
if conftestpath.is_file():
mod = self._importconftest(
conftestpath,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
mod = self._importconftest(conftestpath, importmode, rootpath)
clist.append(mod)
self._dirpath2confmods[directory] = clist
@@ -690,39 +640,23 @@ class PytestPluginManager(PluginManager):
raise KeyError(name)
def _importconftest(
self,
conftestpath: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
*,
consider_namespace_packages: bool,
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
) -> types.ModuleType:
conftestpath_plugin_name = str(conftestpath)
existing = self.get_plugin(conftestpath_plugin_name)
if existing is not None:
return cast(types.ModuleType, existing)
# conftest.py files there are not in a Python package all have module
# name "conftest", and thus conflict with each other. Clear the existing
# before loading the new one, otherwise the existing one will be
# returned from the module cache.
pkgpath = resolve_package_path(conftestpath)
if pkgpath is None:
try:
del sys.modules[conftestpath.stem]
except KeyError:
pass
_ensure_removed_sysmodule(conftestpath.stem)
try:
mod = import_path(
conftestpath,
mode=importmode,
root=rootpath,
consider_namespace_packages=consider_namespace_packages,
)
mod = import_path(conftestpath, mode=importmode, root=rootpath)
except Exception as e:
assert e.__traceback__ is not None
raise ConftestImportFailure(conftestpath, cause=e) from e
exc_info = (type(e), e, e.__traceback__)
raise ConftestImportFailure(conftestpath, exc_info) from e
self._check_non_top_pytest_plugins(mod, conftestpath)
@@ -733,7 +667,7 @@ class PytestPluginManager(PluginManager):
if dirpath in path.parents or path == dirpath:
if mod in mods:
raise AssertionError(
f"While trying to load conftest path {conftestpath!s}, "
f"While trying to load conftest path {str(conftestpath)}, "
f"found that the module {mod} is already loaded with path {mod.__file__}. "
"This is not supposed to happen. Please report this issue to pytest."
)
@@ -810,10 +744,13 @@ class PytestPluginManager(PluginManager):
self.set_blocked("pytest_" + name)
else:
name = arg
# Unblock the plugin.
self.unblock(name)
# Unblock the plugin. None indicates that it has been blocked.
# There is no interface with pluggy for this.
if self._name2plugin.get(name, -1) is None:
del self._name2plugin[name]
if not name.startswith("pytest_"):
self.unblock("pytest_" + name)
if self._name2plugin.get("pytest_" + name, -1) is None:
del self._name2plugin["pytest_" + name]
self.import_plugin(arg, consider_entry_points=True)
def consider_conftest(
@@ -897,6 +834,13 @@ def _get_plugin_specs_as_list(
)
def _ensure_removed_sysmodule(modname: str) -> None:
try:
del sys.modules[modname]
except KeyError:
pass
class Notset:
def __repr__(self):
return "<NOTSET>"
@@ -1233,11 +1177,7 @@ class Config:
noconftest=early_config.known_args_namespace.noconftest,
rootpath=early_config.rootpath,
confcutdir=early_config.known_args_namespace.confcutdir,
invocation_dir=early_config.invocation_params.dir,
importmode=early_config.known_args_namespace.importmode,
consider_namespace_packages=early_config.getini(
"consider_namespace_packages"
),
)
def _initini(self, args: Sequence[str]) -> None:
@@ -1245,8 +1185,8 @@ class Config:
args, namespace=copy.copy(self.option)
)
rootpath, inipath, inicfg = determine_setup(
inifile=ns.inifilename,
args=ns.file_or_dir + unknown_args,
ns.inifilename,
ns.file_or_dir + unknown_args,
rootdir_cmd_arg=ns.rootdir or None,
invocation_dir=self.invocation_params.dir,
)
@@ -1330,8 +1270,6 @@ class Config:
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
:param warn: Whether can issue warnings.
:returns: The args and the args source. Guaranteed to be non-empty.
"""
if args:
source = Config.ArgsSource.ARGS
@@ -1395,6 +1333,11 @@ class Config:
self._validate_plugins()
self._warn_about_skipped_plugins()
if self.known_args_namespace.strict:
self.issue_config_time_warning(
_pytest.deprecated.STRICT_OPTION, stacklevel=2
)
if self.known_args_namespace.confcutdir is None:
if self.inipath is not None:
confcutdir = str(self.inipath.parent)
@@ -1440,7 +1383,12 @@ class Config:
if Version(minver) > Version(pytest.__version__):
raise pytest.UsageError(
f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
"%s: 'minversion' requires pytest-%s, actual pytest-%s'"
% (
self.inipath,
minver,
pytest.__version__,
)
)
def _validate_config_options(self) -> None:
@@ -1499,6 +1447,8 @@ class Config:
kwargs=dict(pluginmanager=self.pluginmanager)
)
self._preparse(args, addopts=addopts)
# XXX deprecated hook:
self.hook.pytest_cmdline_preparse(config=self, args=args)
self._parser.after_preparse = True # type: ignore
try:
args = self._parser.parse_setoption(
@@ -1627,11 +1577,9 @@ class Config:
# in this case, we already have a list ready to use.
#
if type == "paths":
dp = (
self.inipath.parent
if self.inipath is not None
else self.invocation_params.dir
)
# TODO: This assert is probably not valid in all cases.
assert self.inipath is not None
dp = self.inipath.parent
input_values = shlex.split(value) if isinstance(value, str) else value
return [dp / x for x in input_values]
elif type == "args":
@@ -1718,8 +1666,6 @@ class Config:
#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
VERBOSITY_ASSERTIONS: Final = "assertions"
#: Verbosity type for test case execution (see :confval:`verbosity_test_cases`).
VERBOSITY_TEST_CASES: Final = "test_cases"
_VERBOSITY_INI_DEFAULT: Final = "auto"
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
@@ -1916,13 +1862,13 @@ def parse_warning_filter(
try:
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
except warnings._OptionError as e:
raise UsageError(error_template.format(error=str(e))) from None
raise UsageError(error_template.format(error=str(e)))
try:
category: Type[Warning] = _resolve_warning_category(category_)
except Exception:
exc_info = ExceptionInfo.from_current()
exception_text = exc_info.getrepr(style="native")
raise UsageError(error_template.format(error=exception_text)) from None
raise UsageError(error_template.format(error=exception_text))
if message and escape:
message = re.escape(message)
if module and escape:
@@ -1935,7 +1881,7 @@ def parse_warning_filter(
except ValueError as e:
raise UsageError(
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
) from None
)
else:
lineno = 0
return action, message, category, module, lineno

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
import argparse
from gettext import gettext
import os
@@ -16,9 +15,13 @@ from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union
import warnings
import _pytest._io
from _pytest.config.exceptions import UsageError
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
from _pytest.deprecated import ARGUMENT_TYPE_STR
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
from _pytest.deprecated import check_ispytest
@@ -122,7 +125,7 @@ class Parser:
from _pytest._argcomplete import filescompleter
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
groups = [*self._groups, self._anonymous]
groups = self._groups + [self._anonymous]
for group in groups:
if group.options:
desc = group.description or group.name
@@ -198,16 +201,9 @@ class Parser:
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
* ``pathlist``: a list of ``py.path``, separated as in a shell
For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file.
In case the execution is happening without an ini-file defined,
they will be considered relative to the current working directory (for example with ``--override-ini``).
.. versionadded:: 7.0
The ``paths`` variable type.
.. versionadded:: 8.1
Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of an ini-file.
Defaults to ``string`` if ``None`` or not passed.
:param default:
Default value if no ini-file option exists but is queried.
@@ -264,15 +260,39 @@ class Argument:
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
"""
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
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] = []
if "%default" in (attrs.get("help") or ""):
warnings.warn(ARGUMENT_PERCENT_DEFAULT, stacklevel=3)
try:
self.type = attrs["type"]
typ = attrs["type"]
except KeyError:
pass
else:
# This might raise a keyerror as well, don't want to catch that.
if isinstance(typ, str):
if typ == "choice":
warnings.warn(
ARGUMENT_TYPE_STR_CHOICE.format(typ=typ, names=names),
stacklevel=4,
)
# argparse expects a type here take it from
# the type of the first element
attrs["type"] = type(attrs["choices"][0])
else:
warnings.warn(
ARGUMENT_TYPE_STR.format(typ=typ, names=names), stacklevel=4
)
attrs["type"] = Argument._typ_map[typ]
# Used in test_parseopt -> test_parse_defaultgetter.
self.type = attrs["type"]
else:
self.type = typ
try:
# Attribute existence is tested in Config._processopt.
self.default = attrs["default"]
@@ -303,6 +323,11 @@ class Argument:
self._attrs[attr] = getattr(self, attr)
except AttributeError:
pass
if self._attrs.get("help"):
a = self._attrs["help"]
a = a.replace("%default", "%(default)s")
# a = a.replace('%prog', '%(prog)s')
self._attrs["help"] = a
return self._attrs
def _set_opt_strings(self, opts: Sequence[str]) -> None:
@@ -415,7 +440,6 @@ class MyOptionParser(argparse.ArgumentParser):
add_help=False,
formatter_class=DropShorterLongHelpFormatter,
allow_abbrev=False,
fromfile_prefix_chars="@",
)
# extra_info is a dict of (param -> value) to display if there's
# an usage error to provide more contextual information to the user.
@@ -426,7 +450,8 @@ class MyOptionParser(argparse.ArgumentParser):
msg = f"{self.prog}: error: {message}"
if hasattr(self._parser, "_config_source_hint"):
msg = f"{msg} ({self._parser._config_source_hint})"
# Type ignored because the attribute is set dynamically.
msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore
raise UsageError(self.format_usage() + msg)
@@ -448,7 +473,7 @@ class MyOptionParser(argparse.ArgumentParser):
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
return parsed
if sys.version_info < (3, 9): # pragma: no cover
if sys.version_info[:2] < (3, 9): # pragma: no cover
# Backport of https://github.com/python/cpython/pull/14316 so we can
# disable long --argument abbreviations without breaking short flags.
def _parse_optional(

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import functools
from pathlib import Path
from typing import Any
from typing import Mapping
import warnings
@@ -55,7 +54,7 @@ class PathAwareHookProxy:
path_var, fspath_var = imply_paths_hooks[key]
@functools.wraps(hook)
def fixed_hook(**kw: Any) -> Any:
def fixed_hook(**kw):
path_value: Path | None = kw.pop(path_var, None)
fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
if fspath_value is not None:

View File

@@ -86,7 +86,6 @@ 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]]]]:
"""Search in the list of arguments for a valid ini-file for pytest,
@@ -100,28 +99,20 @@ 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
args = [Path.cwd()]
for arg in args:
argpath = absolutepath(arg)
for base in (argpath, *argpath.parents):
for config_name in config_names:
p = base / config_name
if p.is_file():
if p.name == "pyproject.toml" and found_pyproject_toml is None:
found_pyproject_toml = p
ini_config = load_config_dict_from_file(p)
if ini_config is not None:
return base, p, ini_config
if found_pyproject_toml is not None:
return found_pyproject_toml.parent, found_pyproject_toml, {}
return None, None, {}
def get_common_ancestor(
invocation_dir: Path,
paths: Iterable[Path],
) -> Path:
def get_common_ancestor(paths: Iterable[Path]) -> Path:
common_ancestor: Optional[Path] = None
for path in paths:
if not path.exists():
@@ -138,7 +129,7 @@ def get_common_ancestor(
if shared is not None:
common_ancestor = shared
if common_ancestor is None:
common_ancestor = invocation_dir
common_ancestor = Path.cwd()
elif common_ancestor.is_file():
common_ancestor = common_ancestor.parent
return common_ancestor
@@ -170,11 +161,10 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte
def determine_setup(
*,
inifile: Optional[str],
args: Sequence[str],
rootdir_cmd_arg: Optional[str],
invocation_dir: Path,
rootdir_cmd_arg: Optional[str] = None,
invocation_dir: Optional[Path] = None,
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
"""Determine the rootdir, inifile and ini configuration values from the
command line arguments.
@@ -186,7 +176,8 @@ def determine_setup(
:param rootdir_cmd_arg:
The `--rootdir` command line argument, if given.
:param invocation_dir:
The working directory when pytest was invoked.
The working directory when pytest was invoked, if known.
If not known, the current working directory is used.
"""
rootdir = None
dirs = get_dirs_from_args(args)
@@ -197,8 +188,8 @@ def determine_setup(
if rootdir_cmd_arg is None:
rootdir = inipath_.parent
else:
ancestor = get_common_ancestor(invocation_dir, dirs)
rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor])
ancestor = get_common_ancestor(dirs)
rootdir, inipath, inicfg = locate_config([ancestor])
if rootdir is None and rootdir_cmd_arg is None:
for possible_rootdir in (ancestor, *ancestor.parents):
if (possible_rootdir / "setup.py").is_file():
@@ -206,11 +197,13 @@ def determine_setup(
break
else:
if dirs != [ancestor]:
rootdir, inipath, inicfg = locate_config(invocation_dir, dirs)
rootdir, inipath, inicfg = locate_config(dirs)
if rootdir is None:
rootdir = get_common_ancestor(
invocation_dir, [invocation_dir, ancestor]
)
if invocation_dir is not None:
cwd = invocation_dir
else:
cwd = Path.cwd()
rootdir = get_common_ancestor([cwd, ancestor])
if is_fs_root(rootdir):
rootdir = ancestor
if rootdir_cmd_arg:

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Interactive debugging with PDB, the Python Debugger."""
import argparse
@@ -155,7 +154,9 @@ class pytestPDB:
def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
import _pytest.config
class PytestPdbWrapper(pdb_cls):
# Type ignored because mypy doesn't support "dynamic"
# inheritance like this.
class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc]
_pytest_capman = capman
_continued = False
@@ -377,8 +378,7 @@ def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.Traceb
elif isinstance(excinfo.value, ConftestImportFailure):
# A config.ConftestImportFailure is not useful for post_mortem.
# Use the underlying exception instead:
assert excinfo.value.cause.__traceback__ is not None
return excinfo.value.cause.__traceback__
return excinfo.value.excinfo[2]
else:
assert excinfo._excinfo is not None
return excinfo._excinfo[2]

View File

@@ -12,6 +12,7 @@ in case of warnings which need to format their messages.
from warnings import warn
from _pytest.warning_types import PytestDeprecationWarning
from _pytest.warning_types import PytestRemovedIn8Warning
from _pytest.warning_types import PytestRemovedIn9Warning
from _pytest.warning_types import UnformattedWarning
@@ -24,6 +25,21 @@ DEPRECATED_EXTERNAL_PLUGINS = {
"pytest_faulthandler",
}
NOSE_SUPPORT = UnformattedWarning(
PytestRemovedIn8Warning,
"Support for nose tests is deprecated and will be removed in a future release.\n"
"{nodeid} is using nose method: `{method}` ({stage})\n"
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
)
NOSE_SUPPORT_METHOD = UnformattedWarning(
PytestRemovedIn8Warning,
"Support for nose tests is deprecated and will be removed in a future release.\n"
"{nodeid} is using nose-specific method: `{method}(self)`\n"
"To remove this warning, rename it to `{method}_method(self)`\n"
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
)
# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
# * If you're in the future: "could have been".
@@ -32,25 +48,74 @@ YIELD_FIXTURE = PytestDeprecationWarning(
"Use @pytest.fixture instead; they are the same."
)
WARNING_CMDLINE_PREPARSE_HOOK = PytestRemovedIn8Warning(
"The pytest_cmdline_preparse hook is deprecated and will be removed in a future release. \n"
"Please use pytest_load_initial_conftests hook instead."
)
FSCOLLECTOR_GETHOOKPROXY_ISINITPATH = PytestRemovedIn8Warning(
"The gethookproxy() and isinitpath() methods of FSCollector and Package are deprecated; "
"use self.session.gethookproxy() and self.session.isinitpath() instead. "
)
STRICT_OPTION = PytestRemovedIn8Warning(
"The --strict option is deprecated, use --strict-markers instead."
)
# This deprecation is never really meant to be removed.
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
ARGUMENT_PERCENT_DEFAULT = PytestRemovedIn8Warning(
'pytest now uses argparse. "%default" should be changed to "%(default)s"',
)
ARGUMENT_TYPE_STR_CHOICE = UnformattedWarning(
PytestRemovedIn8Warning,
"`type` argument to addoption() is the string {typ!r}."
" For choices this is optional and can be omitted, "
" but when supplied should be a type (for example `str` or `int`)."
" (options: {names})",
)
ARGUMENT_TYPE_STR = UnformattedWarning(
PytestRemovedIn8Warning,
"`type` argument to addoption() is the string {typ!r}, "
" but when supplied should be a type (for example `str` or `int`)."
" (options: {names})",
)
HOOK_LEGACY_PATH_ARG = UnformattedWarning(
PytestRemovedIn9Warning,
PytestRemovedIn8Warning,
"The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n"
"see https://docs.pytest.org/en/latest/deprecations.html"
"#py-path-local-arguments-for-hooks-replaced-with-pathlib-path",
)
NODE_CTOR_FSPATH_ARG = UnformattedWarning(
PytestRemovedIn9Warning,
PytestRemovedIn8Warning,
"The (fspath: py.path.local) argument to {node_type_name} is deprecated. "
"Please use the (path: pathlib.Path) argument instead.\n"
"See https://docs.pytest.org/en/latest/deprecations.html"
"#fspath-argument-for-node-constructors-replaced-with-pathlib-path",
)
WARNS_NONE_ARG = PytestRemovedIn8Warning(
"Passing None has been deprecated.\n"
"See https://docs.pytest.org/en/latest/how-to/capture-warnings.html"
"#additional-use-cases-of-warnings-in-tests"
" for alternatives in common use cases."
)
KEYWORD_MSG_ARG = UnformattedWarning(
PytestRemovedIn8Warning,
"pytest.{func}(msg=...) is now deprecated, use pytest.{func}(reason=...) instead",
)
INSTANCE_COLLECTOR = PytestRemovedIn8Warning(
"The pytest.Instance collector type is deprecated and is no longer used. "
"See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector",
)
HOOK_LEGACY_MARKING = UnformattedWarning(
PytestDeprecationWarning,
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Discover and run doctests in modules and test files."""
import bdb
@@ -48,7 +47,6 @@ from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
import doctest
from typing import Self
DOCTEST_REPORT_CHOICE_NONE = "none"
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
@@ -135,9 +133,11 @@ def pytest_collect_file(
if config.option.doctestmodules and not any(
(_is_setup_py(file_path), _is_main_py(file_path))
):
return DoctestModule.from_parent(parent, path=file_path)
mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)
return mod
elif _is_doctest(config, file_path, parent):
return DoctestTextfile.from_parent(parent, path=file_path)
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
return txt
return None
@@ -272,14 +272,14 @@ class DoctestItem(Item):
self._initrequest()
@classmethod
def from_parent( # type: ignore[override]
def from_parent( # type: ignore
cls,
parent: "Union[DoctestTextfile, DoctestModule]",
*,
name: str,
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)
@@ -559,17 +559,24 @@ class DoctestModule(Module):
else: # pragma: no cover
pass
try:
module = self.obj
except Collector.CollectError:
if self.config.getvalue("doctest_ignore_import_errors"):
skip("unable to import module %r" % self.path)
else:
raise
if self.path.name == "conftest.py":
module = self.config.pluginmanager._importconftest(
self.path,
self.config.getoption("importmode"),
rootpath=self.config.rootpath,
)
else:
try:
module = self.obj
except Collector.CollectError:
if self.config.getvalue("doctest_ignore_import_errors"):
skip("unable to import module %r" % self.path)
else:
raise
# While doctests currently don't support fixtures directly, we still
# need to pick up autouse fixtures.
self.session._fixturemanager.parsefactories(self)
# While doctests currently don't support fixtures directly, we still
# need to pick up autouse fixtures.
self.session._fixturemanager.parsefactories(self)
# Uses internal doctest module parsing mechanism.
finder = MockAwareDocTestFinder()

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@ def _iter_all_modules(
else:
# Type ignored because typeshed doesn't define ModuleType.__path__
# (only defined on packages).
package_path = package.__path__
package_path = package.__path__ # type: ignore[attr-defined]
path, prefix = package_path[0], package.__name__ + "."
for _, name, is_package in pkgutil.iter_modules([path]):
if is_package:

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Version info, help messages, tracing configuration."""
from argparse import Action
@@ -110,11 +109,11 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
path = config.option.debug
debugfile = open(path, "w", encoding="utf-8")
debugfile.write(
"versions pytest-{}, "
"python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format(
"versions pytest-%s, "
"python-%s\ncwd=%s\nargs=%s\n\n"
% (
pytest.__version__,
".".join(map(str, sys.version_info)),
config.invocation_params.dir,
os.getcwd(),
config.invocation_params.args,
)

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Hook specifications for pytest plugins which are invoked by pytest itself
and by builtin plugins."""
@@ -15,7 +14,7 @@ from typing import Union
from pluggy import HookspecMarker
from .deprecated import HOOK_LEGACY_PATH_ARG
from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
if TYPE_CHECKING:
@@ -60,16 +59,10 @@ 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) <pytest.PytestPluginManager.add_hookspecs>`.
:param pluginmanager: The pytest plugin manager.
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
.. note::
This hook is incompatible with hook wrappers.
Use in conftest plugins
=======================
If a conftest plugin implements this hook, it will be called immediately
when the conftest is registered.
"""
@@ -87,14 +80,6 @@ def pytest_plugin_registered(
.. note::
This hook is incompatible with hook wrappers.
Use in conftest plugins
=======================
If a conftest plugin implements this hook, it will be called immediately
when the conftest is registered, once for each plugin registered thus far
(including itself!), and for all plugins thereafter when they are
registered.
"""
@@ -103,13 +88,19 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
"""Register argparse-style options and ini-style config values,
called once at the beginning of a test run.
:param parser:
.. note::
This function should be implemented only in plugins or ``conftest.py``
files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`.
:param pytest.Parser parser:
To add command line options, call
:py:func:`parser.addoption(...) <pytest.Parser.addoption>`.
To add ini-file values call :py:func:`parser.addini(...)
<pytest.Parser.addini>`.
:param pluginmanager:
:param pytest.PytestPluginManager pluginmanager:
The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks
to change how command line options are added.
@@ -128,14 +119,6 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
.. note::
This hook is incompatible with hook wrappers.
Use in conftest plugins
=======================
If a conftest plugin implements this hook, it will be called immediately
when the conftest is registered.
This hook is only called for :ref:`initial conftests <pluginorder>`.
"""
@@ -143,17 +126,16 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
def pytest_configure(config: "Config") -> None:
"""Allow plugins and conftest files to perform initial configuration.
This hook is called for every plugin and initial conftest file
after command line options have been parsed.
After that, the hook is called for other conftest files as they are
imported.
.. note::
This hook is incompatible with hook wrappers.
:param config: The pytest config object.
Use in conftest plugins
=======================
This hook is called for every :ref:`initial conftest <pluginorder>` file
after command line options have been parsed. After that, the hook is called
for other conftest files as they are registered.
:param pytest.Config config: The pytest config object.
"""
@@ -172,54 +154,55 @@ def pytest_cmdline_parse(
Stops at first non-None result, see :ref:`firstresult`.
.. note::
This hook is only called for plugin classes passed to the
This hook will only be called for plugin classes passed to the
``plugins`` arg when using `pytest.main`_ to perform an in-process
test run.
:param pluginmanager: The pytest plugin manager.
:param args: List of arguments passed on the command line.
:returns: A pytest config object.
"""
Use in conftest plugins
=======================
This hook is not called for conftest files.
@hookspec(warn_on_impl=WARNING_CMDLINE_PREPARSE_HOOK)
def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None:
"""(**Deprecated**) modify command line arguments before option parsing.
This hook is considered deprecated and will be removed in a future pytest version. Consider
using :hook:`pytest_load_initial_conftests` instead.
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
:param config: The pytest config object.
:param args: Arguments passed on the command line.
"""
@hookspec(firstresult=True)
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
"""Called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop.
Stops at first non-None result, see :ref:`firstresult`.
:param config: The pytest config object.
:returns: The exit code.
"""
def pytest_load_initial_conftests(
early_config: "Config", parser: "Parser", args: List[str]
) -> None:
"""Called to implement the loading of :ref:`initial conftest files
<pluginorder>` ahead of command line option parsing.
"""Called to implement the loading of initial conftest files ahead
of command line option parsing.
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
:param early_config: The pytest config object.
:param args: Arguments passed on the command line.
:param parser: To add command line options.
Use in conftest plugins
=======================
This hook is not called for conftest files.
"""
@hookspec(firstresult=True)
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
"""Called for performing the main command line action.
The default implementation will invoke the configure hooks and
:hook:`pytest_runtestloop`.
Stops at first non-None result, see :ref:`firstresult`.
:param config: The pytest config object.
:returns: The exit code.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
"""
@@ -262,11 +245,6 @@ def pytest_collection(session: "Session") -> Optional[object]:
counter (and returns `None`).
:param session: The pytest session object.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
"""
@@ -279,11 +257,6 @@ def pytest_collection_modifyitems(
:param session: The pytest session object.
:param config: The pytest config object.
:param items: List of item objects.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -291,22 +264,10 @@ def pytest_collection_finish(session: "Session") -> None:
"""Called after collection has been performed and modified.
:param session: The pytest session object.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@hookspec(
firstresult=True,
warn_on_impl_args={
"path": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="path", pathlib_path_arg="collection_path"
),
},
)
@hookspec(firstresult=True)
def pytest_ignore_collect(
collection_path: Path, path: "LEGACY_PATH", config: "Config"
) -> Optional[bool]:
@@ -325,14 +286,6 @@ def pytest_ignore_collect(
The ``collection_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collection path, only
conftest files in parent directories of the collection path are consulted
(if the path is a directory, its own conftest file is *not* consulted - a
directory cannot ignore itself!).
"""
@@ -354,24 +307,9 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
See :ref:`custom directory collectors` for a simple example of use of this
hook.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collection path, only
conftest files in parent directories of the collection path are consulted
(if the path is a directory, its own conftest file is *not* consulted - a
directory cannot collect itself!).
"""
@hookspec(
warn_on_impl_args={
"path": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="path", pathlib_path_arg="file_path"
),
},
)
def pytest_collect_file(
file_path: Path, path: "LEGACY_PATH", parent: "Collector"
) -> "Optional[Collector]":
@@ -389,12 +327,6 @@ def pytest_collect_file(
The ``file_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given file path, only
conftest files in parent directories of the file path are consulted.
"""
@@ -406,13 +338,6 @@ def pytest_collectstart(collector: "Collector") -> None:
:param collector:
The collector.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories are
consulted.
"""
@@ -421,12 +346,6 @@ def pytest_itemcollected(item: "Item") -> None:
:param item:
The item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -435,13 +354,6 @@ def pytest_collectreport(report: "CollectReport") -> None:
:param report:
The collect report.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories are
consulted.
"""
@@ -452,11 +364,6 @@ def pytest_deselected(items: Sequence["Item"]) -> None:
:param items:
The items.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -469,13 +376,6 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
:param collector:
The collector.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories are
consulted.
"""
@@ -484,14 +384,7 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
# -------------------------------------------------------------------------
@hookspec(
firstresult=True,
warn_on_impl_args={
"path": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="path", pathlib_path_arg="module_path"
),
},
)
@hookspec(firstresult=True)
def pytest_pycollect_makemodule(
module_path: Path, path: "LEGACY_PATH", parent
) -> Optional["Module"]:
@@ -511,13 +404,6 @@ def pytest_pycollect_makemodule(
equivalent of the ``path`` parameter.
The ``path`` parameter has been deprecated in favor of ``fspath``.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given parent collector,
only conftest files in the collector's directory and its parent directories
are consulted.
"""
@@ -537,13 +423,6 @@ def pytest_pycollect_makeitem(
The object.
:returns:
The created items/collectors.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories
are consulted.
"""
@@ -555,13 +434,6 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
:param pyfuncitem:
The function item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only
conftest files in the item's directory and its parent directories
are consulted.
"""
@@ -570,13 +442,6 @@ def pytest_generate_tests(metafunc: "Metafunc") -> None:
:param metafunc:
The :class:`~pytest.Metafunc` helper for the test function.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given function definition,
only conftest files in the functions's directory and its parent directories
are consulted.
"""
@@ -594,12 +459,7 @@ def pytest_make_parametrize_id(
:param config: The pytest config object.
:param val: The parametrized value.
:param argname: The automatic parameter name produced by pytest.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
:param str argname: The automatic parameter name produced by pytest.
"""
@@ -626,11 +486,6 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
Stops at first non-None result, see :ref:`firstresult`.
The return value is not used, but only stops further processing.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -669,11 +524,6 @@ def pytest_runtest_protocol(
Stops at first non-None result, see :ref:`firstresult`.
The return value is not used, but only stops further processing.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -688,12 +538,6 @@ def pytest_runtest_logstart(
:param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -708,12 +552,6 @@ def pytest_runtest_logfinish(
:param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -727,12 +565,6 @@ def pytest_runtest_setup(item: "Item") -> None:
:param item:
The item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -743,12 +575,6 @@ def pytest_runtest_call(item: "Item") -> None:
:param item:
The item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -767,12 +593,6 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
scheduled). This argument is used to perform exact teardowns, i.e.
calling just enough finalizers so that nextitem only needs to call
setup functions.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -789,12 +609,6 @@ def pytest_runtest_makereport(
:param call: The :class:`~pytest.CallInfo` for the phase.
Stops at first non-None result, see :ref:`firstresult`.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -803,12 +617,6 @@ def pytest_runtest_logreport(report: "TestReport") -> None:
of the setup, call and teardown runtest phases of an item.
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -822,12 +630,6 @@ def pytest_report_to_serializable(
:param config: The pytest config object.
:param report: The report.
Use in conftest plugins
=======================
Any conftest file can implement this hook. The exact details may depend
on the plugin which calls the hook.
"""
@@ -840,12 +642,6 @@ def pytest_report_from_serializable(
:hook:`pytest_report_to_serializable`.
:param config: The pytest config object.
Use in conftest plugins
=======================
Any conftest file can implement this hook. The exact details may depend
on the plugin which calls the hook.
"""
@@ -873,13 +669,6 @@ def pytest_fixture_setup(
If the fixture function returns None, other implementations of
this hook function will continue to be called, according to the
behavior of the :ref:`firstresult` option.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given fixture, only
conftest files in the fixture scope's directory and its parent directories
are consulted.
"""
@@ -894,13 +683,6 @@ def pytest_fixture_post_finalizer(
The fixture definition object.
:param request:
The fixture request object.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given fixture, only
conftest files in the fixture scope's directory and its parent directories
are consulted.
"""
@@ -914,11 +696,6 @@ def pytest_sessionstart(session: "Session") -> None:
and entering the run test loop.
:param session: The pytest session object.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
"""
@@ -930,11 +707,6 @@ def pytest_sessionfinish(
:param session: The pytest session object.
:param exitstatus: The status which pytest will return to the system.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -942,11 +714,6 @@ def pytest_unconfigure(config: "Config") -> None:
"""Called before test process is exited.
:param config: The pytest config object.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -969,12 +736,6 @@ def pytest_assertrepr_compare(
:param op: The operator, e.g. `"=="`, `"!="`, `"not in"`.
:param left: The left operand.
:param right: The right operand.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -1003,12 +764,6 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
:param lineno: Line number of the assert statement.
:param orig: String with the original assertion.
:param expl: String with the assert explanation.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -1017,13 +772,6 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
# -------------------------------------------------------------------------
@hookspec(
warn_on_impl_args={
"startdir": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="startdir", pathlib_path_arg="start_path"
),
},
)
def pytest_report_header( # type:ignore[empty-body]
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
) -> Union[str, List[str]]:
@@ -1040,25 +788,19 @@ def pytest_report_header( # type:ignore[empty-body]
If you want to have your line(s) displayed first, use
:ref:`trylast=True <plugin-hookorder>`.
.. note::
This function should be implemented only in plugins or ``conftest.py``
files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`.
.. versionchanged:: 7.0.0
The ``start_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
"""
@hookspec(
warn_on_impl_args={
"startdir": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="startdir", pathlib_path_arg="start_path"
),
},
)
def pytest_report_collectionfinish( # type:ignore[empty-body]
config: "Config",
start_path: Path,
@@ -1088,11 +830,6 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
The ``start_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -1121,11 +858,6 @@ def pytest_report_teststatus( # type:ignore[empty-body]
:returns: The test status.
Stops at first non-None result, see :ref:`firstresult`.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -1142,11 +874,6 @@ def pytest_terminal_summary(
.. versionadded:: 4.2
The ``config`` parameter.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -1171,8 +898,7 @@ def pytest_warning_recorded(
* ``"runtest"``: during test execution.
:param nodeid:
Full id of the item. Empty string for warnings that are not specific to
a particular node.
Full id of the item.
:param location:
When available, holds information about the execution context of the captured
@@ -1180,13 +906,6 @@ def pytest_warning_recorded(
when the execution context is at the module level.
.. versionadded:: 6.0
Use in conftest plugins
=======================
Any conftest file can implement this hook. If the warning is specific to a
particular node, only conftest files in parent directories of the node are
consulted.
"""
@@ -1210,12 +929,6 @@ def pytest_markeval_namespace( # type:ignore[empty-body]
:param config: The pytest config object.
:returns: A dictionary of additional globals to add.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in parent directories of the item are consulted.
"""
@@ -1235,11 +948,6 @@ def pytest_internalerror(
:param excrepr: The exception repr object.
:param excinfo: The exception info.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -1249,11 +957,6 @@ def pytest_keyboard_interrupt(
"""Called for keyboard interrupt.
:param excinfo: The exception info.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -1280,12 +983,6 @@ def pytest_exception_interact(
The call information. Contains the exception.
:param report:
The collection or test report.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given node, only conftest
files in parent directories of the node are consulted.
"""
@@ -1297,11 +994,6 @@ def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
:param config: The pytest config object.
:param pdb: The Pdb instance.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -1313,9 +1005,4 @@ def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
:param config: The pytest config object.
:param pdb: The Pdb instance.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Report test results in JUnit-XML format, for use with Jenkins and build
integration servers.
@@ -61,7 +60,7 @@ def bin_xml_escape(arg: object) -> str:
# Char ::= #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
# For an unknown(?) reason, we disallow #x7F (DEL) as well.
illegal_xml_re = (
"[^\u0009\u000a\u000d\u0020-\u007e\u0080-\ud7ff\ue000-\ufffd\u10000-\u10ffff]"
"[^\u0009\u000A\u000D\u0020-\u007E\u0080-\uD7FF\uE000-\uFFFD\u10000-\u10FFFF]"
)
return re.sub(illegal_xml_re, repl, str(arg))
@@ -143,7 +142,7 @@ class _NodeReporter:
# Filter out attributes not permitted by this test family.
# Including custom attributes because they are not valid here.
temp_attrs = {}
for key in self.attrs:
for key in self.attrs.keys():
if key in families[self.family]["testcase"]:
temp_attrs[key] = self.attrs[key]
self.attrs = temp_attrs
@@ -262,7 +261,7 @@ class _NodeReporter:
self.__dict__.clear()
# Type ignored because mypy doesn't like overriding a method.
# Also the return value doesn't match...
self.to_xml = lambda: data # type: ignore[method-assign]
self.to_xml = lambda: data # type: ignore[assignment]
def _warn_incompatibility_with_xunit2(
@@ -376,7 +375,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
xml = request.config.stash.get(xml_key, None)
if xml is not None:
record_func = xml.add_global_property
record_func = xml.add_global_property # noqa
return record_func
@@ -625,7 +624,7 @@ class LogXML:
def update_testcase_duration(self, report: TestReport) -> None:
"""Accumulate total duration for nodeid from given report and update
the Junit.testcase with the new total if already created."""
if self.report_duration in {"total", report.when}:
if self.report_duration == "total" or report.when == self.report_duration:
reporter = self.node_reporter(report)
reporter.duration += getattr(report, "duration", 0.0)

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Add backward compatibility support for the legacy py path type."""
import dataclasses
@@ -315,8 +314,8 @@ class LegacyTmpdirPlugin:
By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See
:ref:`temporary directory location and retention`.
``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
The returned object is a `legacy_path`_ object.

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Access and control log capturing."""
from contextlib import contextmanager
@@ -210,7 +209,7 @@ class PercentStyleMultiline(logging.PercentStyle):
if "\n" in record.message:
if hasattr(record, "auto_indent"):
# Passed in from the "extra={}" kwarg on the call to logging.log().
auto_indent = self._get_auto_indent(record.auto_indent)
auto_indent = self._get_auto_indent(record.auto_indent) # type: ignore[attr-defined]
else:
auto_indent = self._auto_indent
@@ -299,13 +298,6 @@ def pytest_addoption(parser: Parser) -> None:
default=None,
help="Path to a file when logging will be written to",
)
add_option_ini(
"--log-file-mode",
dest="log_file_mode",
default="w",
choices=["w", "a"],
help="Log file open mode",
)
add_option_ini(
"--log-file-level",
dest="log_file_level",
@@ -513,7 +505,7 @@ class LogCaptureFixture:
:return: The original disabled logging level.
"""
original_disable_level: int = logger_obj.manager.disable
original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
if isinstance(level, str):
# Try to translate the level string to an int for `logging.disable()`
@@ -677,10 +669,7 @@ class LoggingPlugin:
if not os.path.isdir(directory):
os.makedirs(directory)
self.log_file_mode = get_option_ini(config, "log_file_mode") or "w"
self.log_file_handler = _FileHandler(
log_file, mode=self.log_file_mode, encoding="UTF-8"
)
self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
log_file_format = get_option_ini(config, "log_file_format", "log_format")
log_file_date_format = get_option_ini(
config, "log_file_date_format", "log_date_format"
@@ -757,7 +746,7 @@ class LoggingPlugin:
fpath.parent.mkdir(exist_ok=True, parents=True)
# https://github.com/python/mypy/issues/11193
stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment]
stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment]
old_stream = self.log_file_handler.setStream(stream)
if old_stream:
old_stream.close()

View File

@@ -5,7 +5,6 @@ import dataclasses
import fnmatch
import functools
import importlib
import importlib.util
import os
from pathlib import Path
import sys
@@ -22,7 +21,6 @@ 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
@@ -52,10 +50,6 @@ from _pytest.runner import SetupState
from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
from typing import Self
def pytest_addoption(parser: Parser) -> None:
parser.addini(
"norecursedirs",
@@ -223,12 +217,6 @@ def pytest_addoption(parser: Parser) -> None:
help="Prepend/append to sys.path when importing test modules and conftest "
"files. Default: prepend.",
)
parser.addini(
"consider_namespace_packages",
type="bool",
default=False,
help="Consider namespace packages when resolving module names during import",
)
group = parser.getgroup("debugconfig", "test session debugging and configuration")
group.addoption(
@@ -390,9 +378,6 @@ def _in_venv(path: Path) -> bool:
def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[bool]:
if collection_path.name == "__pycache__":
return True
ignore_paths = config._getconftest_pathlist(
"collect_ignore", path=collection_path.parent
)
@@ -504,16 +489,16 @@ class Dir(nodes.Directory):
@classmethod
def from_parent( # type: ignore[override]
cls,
parent: nodes.Collector,
parent: nodes.Collector, # type: ignore[override]
*,
path: Path,
) -> "Self":
) -> "Dir":
"""The public constructor.
:param parent: The parent collector of this Dir.
:param path: The directory's path.
"""
return super().from_parent(parent=parent, path=path)
return super().from_parent(parent=parent, path=path) # type: ignore[no-any-return]
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
config = self.config
@@ -522,6 +507,8 @@ class Dir(nodes.Directory):
ihook = self.ihook
for direntry in scandir(self.path):
if direntry.is_dir():
if direntry.name == "__pycache__":
continue
path = Path(direntry.path)
if not self.session.isinitpath(path, with_parents=True):
if ihook.pytest_ignore_collect(collection_path=path, config=config):
@@ -572,7 +559,7 @@ class Session(nodes.Collector):
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._initial_parts: List[Tuple[Path, List[str]]] = []
self._collection_cache: Dict[nodes.Collector, CollectReport] = {}
self.items: List[nodes.Item] = []
@@ -736,14 +723,16 @@ class Session(nodes.Collector):
@overload
def perform_collect(
self, args: Optional[Sequence[str]] = ..., genitems: "Literal[True]" = ...
) -> Sequence[nodes.Item]: ...
) -> Sequence[nodes.Item]:
...
@overload
def perform_collect(
def perform_collect( # noqa: F811
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
) -> Sequence[Union[nodes.Item, nodes.Collector]]: ...
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
...
def perform_collect(
def perform_collect( # noqa: F811
self, args: Optional[Sequence[str]] = None, genitems: bool = True
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
"""Perform the collection phase for this session.
@@ -776,15 +765,15 @@ class Session(nodes.Collector):
initialpaths: List[Path] = []
initialpaths_with_parents: List[Path] = []
for arg in args:
collection_argument = resolve_collection_argument(
fspath, parts = resolve_collection_argument(
self.config.invocation_params.dir,
arg,
as_pypath=self.config.option.pyargs,
)
self._initial_parts.append(collection_argument)
initialpaths.append(collection_argument.path)
initialpaths_with_parents.append(collection_argument.path)
initialpaths_with_parents.extend(collection_argument.path.parents)
self._initial_parts.append((fspath, parts))
initialpaths.append(fspath)
initialpaths_with_parents.append(fspath)
initialpaths_with_parents.extend(fspath.parents)
self._initialpaths = frozenset(initialpaths)
self._initialpaths_with_parents = frozenset(initialpaths_with_parents)
@@ -846,35 +835,21 @@ class Session(nodes.Collector):
pm = self.config.pluginmanager
for collection_argument in self._initial_parts:
self.trace("processing argument", collection_argument)
for argpath, names in self._initial_parts:
self.trace("processing argument", (argpath, names))
self.trace.root.indent += 1
argpath = collection_argument.path
names = collection_argument.parts
module_name = collection_argument.module_name
# resolve_collection_argument() ensures this.
if argpath.is_dir():
assert not names, f"invalid arg {(argpath, names)!r}"
paths = [argpath]
# Add relevant parents of the path, from the root, e.g.
# Match the argpath from the root, e.g.
# /a/b/c.py -> [/, /a, /a/b, /a/b/c.py]
if module_name is None:
# Paths outside of the confcutdir should not be considered.
for path in argpath.parents:
if not pm._is_in_confcutdir(path):
break
paths.insert(0, path)
else:
# For --pyargs arguments, only consider paths matching the module
# name. Paths beyond the package hierarchy are not included.
module_name_parts = module_name.split(".")
for i, path in enumerate(argpath.parents, 2):
if i > len(module_name_parts) or path.stem != module_name_parts[-i]:
break
paths.insert(0, path)
paths = [*reversed(argpath.parents), argpath]
# Paths outside of the confcutdir should not be considered, unless
# it's the argpath itself.
while len(paths) > 1 and not pm._is_in_confcutdir(paths[0]):
paths = paths[1:]
# Start going over the parts from the root, collecting each level
# and discarding all nodes which don't match the level's part.
@@ -882,7 +857,7 @@ class Session(nodes.Collector):
notfound_collectors = []
work: List[
Tuple[Union[nodes.Collector, nodes.Item], List[Union[Path, str]]]
] = [(self, [*paths, *names])]
] = [(self, paths + names)]
while work:
matchnode, matchparts = work.pop()
@@ -930,14 +905,7 @@ class Session(nodes.Collector):
if sys.platform == "win32" and not is_match:
# In case the file paths do not match, fallback to samefile() to
# account for short-paths on Windows (#11895).
same_file = os.path.samefile(node.path, matchparts[0])
# We don't want to match links to the current node,
# otherwise we would match the same file more than once (#12039).
is_match = same_file and (
os.path.islink(node.path)
== os.path.islink(matchparts[0])
)
is_match = os.path.samefile(node.path, matchparts[0])
# Name part e.g. `TestIt` in `/a/b/test_file.py::TestIt::test_it`.
else:
# TODO: Remove parametrized workaround once collection structure contains
@@ -981,36 +949,26 @@ class Session(nodes.Collector):
node.ihook.pytest_collectreport(report=rep)
def search_pypath(module_name: str) -> Optional[str]:
"""Search sys.path for the given a dotted module name, and return its file
system path if found."""
def search_pypath(module_name: str) -> str:
"""Search sys.path for the given a dotted module name, and return its file system path."""
try:
spec = importlib.util.find_spec(module_name)
# AttributeError: looks like package module, but actually filename
# ImportError: module does not exist
# ValueError: not a module name
except (AttributeError, ImportError, ValueError):
return None
return module_name
if spec is None or spec.origin is None or spec.origin == "namespace":
return None
return module_name
elif spec.submodule_search_locations:
return os.path.dirname(spec.origin)
else:
return spec.origin
@dataclasses.dataclass(frozen=True)
class CollectionArgument:
"""A resolved collection argument."""
path: Path
parts: Sequence[str]
module_name: Optional[str]
def resolve_collection_argument(
invocation_path: Path, arg: str, *, as_pypath: bool = False
) -> CollectionArgument:
) -> Tuple[Path, List[str]]:
"""Parse path arguments optionally containing selection parts and return (fspath, names).
Command-line arguments can point to files and/or directories, and optionally contain
@@ -1018,13 +976,9 @@ def resolve_collection_argument(
"pkg/tests/test_foo.py::TestClass::test_foo"
This function ensures the path exists, and returns a resolved `CollectionArgument`:
This function ensures the path exists, and returns a tuple:
CollectionArgument(
path=Path("/full/path/to/pkg/tests/test_foo.py"),
parts=["TestClass", "test_foo"],
module_name=None,
)
(Path("/full/path/to/pkg/tests/test_foo.py"), ["TestClass", "test_foo"])
When as_pypath is True, expects that the command-line argument actually contains
module paths instead of file-system paths:
@@ -1032,13 +986,7 @@ def resolve_collection_argument(
"pkg.tests.test_foo::TestClass::test_foo"
In which case we search sys.path for a matching module, and then return the *path* to the
found module, which may look like this:
CollectionArgument(
path=Path("/home/u/myvenv/lib/site-packages/pkg/tests/test_foo.py"),
parts=["TestClass", "test_foo"],
module_name="pkg.tests.test_foo",
)
found module.
If the path doesn't exist, raise UsageError.
If the path is a directory and selection parts are present, raise UsageError.
@@ -1047,12 +995,8 @@ def resolve_collection_argument(
strpath, *parts = base.split("::")
if parts:
parts[-1] = f"{parts[-1]}{squacket}{rest}"
module_name = None
if as_pypath:
pyarg_strpath = search_pypath(strpath)
if pyarg_strpath is not None:
module_name = strpath
strpath = pyarg_strpath
strpath = search_pypath(strpath)
fspath = invocation_path / strpath
fspath = absolutepath(fspath)
if not safe_exists(fspath):
@@ -1069,8 +1013,4 @@ def resolve_collection_argument(
else "directory argument cannot contain :: selection parts: {arg}"
)
raise UsageError(msg.format(arg=arg))
return CollectionArgument(
path=fspath,
parts=parts,
module_name=module_name,
)
return fspath, parts

View File

@@ -107,7 +107,7 @@ def pytest_addoption(parser: Parser) -> None:
help="show markers (builtin, plugin and per-project ones).",
)
parser.addini("markers", "Register new markers for test functions", "linelist")
parser.addini("markers", "Markers for test functions", "linelist")
parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets")

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
import collections.abc
import dataclasses
import inspect
@@ -342,7 +341,7 @@ class MarkDecorator:
# return type. Not much we can do about that. Thankfully mypy picks
# the first match so it works out even if we break the rules.
@overload
def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap]
def __call__(self, arg: Markable) -> Markable: # type: ignore[misc]
pass
@overload
@@ -355,7 +354,7 @@ class MarkDecorator:
func = args[0]
is_class = inspect.isclass(func)
if len(args) == 1 and (istestfunc(func) or is_class):
store_mark(func, self.mark, stacklevel=3)
store_mark(func, self.mark)
return func
return self.with_args(*args, **kwargs)
@@ -406,11 +405,11 @@ def normalize_mark_list(
for mark in mark_list:
mark_obj = getattr(mark, "mark", mark)
if not isinstance(mark_obj, Mark):
raise TypeError(f"got {mark_obj!r} instead of Mark")
raise TypeError(f"got {repr(mark_obj)} instead of Mark")
yield mark_obj
def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
def store_mark(obj, mark: Mark) -> None:
"""Store a Mark on an object.
This is used to implement the Mark declarations/decorators correctly.
@@ -420,7 +419,7 @@ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
from ..fixtures import getfixturemarker
if getfixturemarker(obj) is not None:
warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel)
warnings.warn(MARKED_FIXTURE, stacklevel=2)
# Always reassign name to avoid updating pytestmark in a reference that
# was only borrowed.
@@ -433,11 +432,13 @@ if TYPE_CHECKING:
from _pytest.scope import _ScopeName
class _SkipMarkDecorator(MarkDecorator):
@overload # type: ignore[override,no-overload-impl]
def __call__(self, arg: Markable) -> Markable: ...
@overload # type: ignore[override,misc,no-overload-impl]
def __call__(self, arg: Markable) -> Markable:
...
@overload
def __call__(self, reason: str = ...) -> "MarkDecorator": ...
def __call__(self, reason: str = ...) -> "MarkDecorator":
...
class _SkipifMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
@@ -445,11 +446,13 @@ if TYPE_CHECKING:
condition: Union[str, bool] = ...,
*conditions: Union[str, bool],
reason: str = ...,
) -> MarkDecorator: ...
) -> MarkDecorator:
...
class _XfailMarkDecorator(MarkDecorator):
@overload # type: ignore[override,no-overload-impl]
def __call__(self, arg: Markable) -> Markable: ...
@overload # type: ignore[override,misc,no-overload-impl]
def __call__(self, arg: Markable) -> Markable:
...
@overload
def __call__(
@@ -462,7 +465,8 @@ if TYPE_CHECKING:
None, Type[BaseException], Tuple[Type[BaseException], ...]
] = ...,
strict: bool = ...,
) -> MarkDecorator: ...
) -> MarkDecorator:
...
class _ParametrizeMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
@@ -478,7 +482,8 @@ if TYPE_CHECKING:
]
] = ...,
scope: Optional[_ScopeName] = ...,
) -> MarkDecorator: ...
) -> MarkDecorator:
...
class _UsefixturesMarkDecorator(MarkDecorator):
def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Monkeypatching and mocking functionality."""
from contextlib import contextmanager
@@ -168,7 +167,8 @@ class MonkeyPatch:
name: object,
value: Notset = ...,
raising: bool = ...,
) -> None: ...
) -> None:
...
@overload
def setattr(
@@ -177,7 +177,8 @@ class MonkeyPatch:
name: str,
value: object,
raising: bool = ...,
) -> None: ...
) -> None:
...
def setattr(
self,

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
import abc
from functools import cached_property
from inspect import signature
@@ -12,7 +11,6 @@ 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
@@ -34,6 +32,7 @@ from _pytest.compat import LEGACY_PATH
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.config.compat import _check_path
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator
@@ -46,8 +45,6 @@ from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
from typing import Self
# Imported here due to circular import.
from _pytest._code.code import _TracebackStyle
from _pytest.main import Session
@@ -58,7 +55,49 @@ SEP = "/"
tracebackcutdir = Path(_pytest.__file__).parent
_T = TypeVar("_T")
def iterparentnodeids(nodeid: str) -> Iterator[str]:
"""Return the parent node IDs of a given node ID, inclusive.
For the node ID
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
the result would be
""
"testing"
"testing/code"
"testing/code/test_excinfo.py"
"testing/code/test_excinfo.py::TestFormattedExcinfo"
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
Note that / components are only considered until the first ::.
"""
pos = 0
first_colons: Optional[int] = nodeid.find("::")
if first_colons == -1:
first_colons = None
# The root Session node - always present.
yield ""
# Eagerly consume SEP parts until first colons.
while True:
at = nodeid.find(SEP, pos, first_colons)
if at == -1:
break
if at > 0:
yield nodeid[:at]
pos = at + len(SEP)
# Eagerly consume :: parts.
while True:
at = nodeid.find("::", pos)
if at == -1:
break
if at > 0:
yield nodeid[:at]
pos = at + len("::")
# The node ID itself.
if nodeid:
yield nodeid
def _imply_path(
@@ -100,33 +139,33 @@ class NodeMeta(abc.ABCMeta):
progress on detangling the :class:`Node` classes.
"""
def __call__(cls, *k, **kw) -> NoReturn:
def __call__(self, *k, **kw):
msg = (
"Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
"See "
"https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
" for more details."
).format(name=f"{cls.__module__}.{cls.__name__}")
).format(name=f"{self.__module__}.{self.__name__}")
fail(msg, pytrace=False)
def _create(cls: Type[_T], *k, **kw) -> _T:
def _create(self, *k, **kw):
try:
return super().__call__(*k, **kw) # type: ignore[no-any-return,misc]
return super().__call__(*k, **kw)
except TypeError:
sig = signature(getattr(cls, "__init__"))
sig = signature(getattr(self, "__init__"))
known_kw = {k: v for k, v in kw.items() if k in sig.parameters}
from .warning_types import PytestDeprecationWarning
warnings.warn(
PytestDeprecationWarning(
f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
f"{self} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
"See https://docs.pytest.org/en/stable/deprecations.html"
"#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
"for more details."
)
)
return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc]
return super().__call__(*k, **known_kw)
class Node(abc.ABC, metaclass=NodeMeta):
@@ -141,8 +180,8 @@ class Node(abc.ABC, metaclass=NodeMeta):
#: A ``LEGACY_PATH`` copy of the :attr:`path` attribute. Intended for usage
#: for methods not migrated to ``pathlib.Path`` yet, such as
#: :meth:`Item.reportinfo <pytest.Item.reportinfo>`. Will be deprecated in
#: a future release, prefer using :attr:`path` instead.
fspath: LEGACY_PATH
#: a future release, prefer using :attr:`path` instead.
# Use __slots__ to make attribute access faster.
# Note that __dict__ is still available.
@@ -219,7 +258,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):
"""Public constructor for Nodes.
This indirection got introduced in order to enable removing
@@ -295,20 +334,12 @@ class Node(abc.ABC, metaclass=NodeMeta):
def teardown(self) -> None:
pass
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
while parent is not None:
yield parent
parent = parent.parent
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."""
"""Return list of all parent collectors up to self, starting from
the root of collection tree.
:returns: The nodes.
"""
chain = []
item: Optional[Node] = self
while item is not None:
@@ -357,16 +388,18 @@ class Node(abc.ABC, metaclass=NodeMeta):
:param name: If given, filter the results by the name attribute.
:returns: An iterator of (node, mark) tuples.
"""
for node in self.iter_parents():
for node in reversed(self.listchain()):
for mark in node.own_markers:
if name is None or getattr(mark, "name", None) == name:
yield node, mark
@overload
def get_closest_marker(self, name: str) -> Optional[Mark]: ...
def get_closest_marker(self, name: str) -> Optional[Mark]:
...
@overload
def get_closest_marker(self, name: str, default: Mark) -> Mark: ...
def get_closest_marker(self, name: str, default: Mark) -> Mark:
...
def get_closest_marker(
self, name: str, default: Optional[Mark] = None
@@ -399,16 +432,17 @@ class Node(abc.ABC, metaclass=NodeMeta):
self.session._setupstate.addfinalizer(fin, self)
def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]:
"""Get the closest parent node (including self) which is an instance of
"""Get the next parent node (including self) which is an instance of
the given class.
:param cls: The node class to search for.
:returns: The node, if found.
"""
for node in self.iter_parents():
if isinstance(node, cls):
return node
return None
current: Optional[Node] = self
while current and not isinstance(current, cls):
current = current.parent
assert current is None or isinstance(current, cls)
return current
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
return excinfo.traceback
@@ -421,7 +455,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
from _pytest.fixtures import FixtureLookupError
if isinstance(excinfo.value, ConftestImportFailure):
excinfo = ExceptionInfo.from_exception(excinfo.value.cause)
excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo)
if isinstance(excinfo.value, fail.Exception):
if not excinfo.value.pytrace:
style = "value"
@@ -487,7 +521,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
* "location": a pair (path, lineno)
* "obj": a Python object that the node wraps.
* "path": just a path
* "fspath": just a path
:rtype: A tuple of (str|Path, int) with filename and 0-based line number.
"""
@@ -498,7 +532,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
obj = getattr(node, "obj", None)
if obj is not None:
return getfslineno(obj)
return getattr(node, "path", "unknown location"), -1
return getattr(node, "fspath", "unknown location"), -1
class Collector(Node, abc.ABC):
@@ -621,10 +655,18 @@ class FSCollector(Collector, abc.ABC):
fspath: Optional[LEGACY_PATH] = None,
path: Optional[Path] = None,
**kw,
) -> "Self":
):
"""The public constructor."""
return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
def gethookproxy(self, fspath: "os.PathLike[str]"):
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.gethookproxy(fspath)
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.isinitpath(path)
class File(FSCollector, abc.ABC):
"""Base class for collecting tests from a file.
@@ -765,7 +807,7 @@ class Item(Node, abc.ABC):
and lineno is a 0-based line number.
"""
location = self.reportinfo()
path = absolutepath(location[0])
path = absolutepath(os.fspath(location[0]))
relfspath = self.session._node_location_to_relpath(path)
assert type(location[2]) is str
return (relfspath, location[1], location[2])

51
src/_pytest/nose.py Normal file
View File

@@ -0,0 +1,51 @@
"""Run testsuites written for nose."""
import warnings
from _pytest.config import hookimpl
from _pytest.deprecated import NOSE_SUPPORT
from _pytest.fixtures import getfixturemarker
from _pytest.nodes import Item
from _pytest.python import Function
from _pytest.unittest import TestCaseFunction
@hookimpl(trylast=True)
def pytest_runtest_setup(item: Item) -> None:
if not isinstance(item, Function):
return
# Don't do nose style setup/teardown on direct unittest style classes.
if isinstance(item, TestCaseFunction):
return
# Capture the narrowed type of item for the teardown closure,
# see https://github.com/python/mypy/issues/2608
func = item
call_optional(func.obj, "setup", func.nodeid)
func.addfinalizer(lambda: call_optional(func.obj, "teardown", func.nodeid))
# NOTE: Module- and class-level fixtures are handled in python.py
# with `pluginmanager.has_plugin("nose")` checks.
# It would have been nicer to implement them outside of core, but
# it's not straightforward.
def call_optional(obj: object, name: str, nodeid: str) -> bool:
method = getattr(obj, name, None)
if method is None:
return False
is_fixture = getfixturemarker(method) is not None
if is_fixture:
return False
if not callable(method):
return False
# Warn about deprecation of this plugin.
method_name = getattr(method, "__name__", str(method))
warnings.warn(
NOSE_SUPPORT.format(nodeid=nodeid, method=method_name, stage=name), stacklevel=2
)
# If there are any problems allow the exception to raise rather than
# silently ignoring it.
method()
return True

View File

@@ -10,8 +10,9 @@ from typing import Optional
from typing import Protocol
from typing import Type
from typing import TypeVar
import warnings
from .warning_types import PytestDeprecationWarning
from _pytest.deprecated import KEYWORD_MSG_ARG
class OutcomeException(BaseException):
@@ -103,8 +104,7 @@ def _with_exception(exception_type: _ET) -> Callable[[_F], _WithException[_F, _E
@_with_exception(Exit)
def exit(
reason: str = "",
returncode: Optional[int] = None,
reason: str = "", returncode: Optional[int] = None, *, msg: Optional[str] = None
) -> NoReturn:
"""Exit testing process.
@@ -114,16 +114,28 @@ def exit(
:param returncode:
Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
"""
__tracebackhide__ = True
from _pytest.config import UsageError
if reason and msg:
raise UsageError(
"cannot pass reason and msg to exit(), `msg` is deprecated, use `reason`."
)
if not reason:
if msg is None:
raise UsageError("exit() requires a reason argument")
warnings.warn(KEYWORD_MSG_ARG.format(func="exit"), stacklevel=2)
reason = msg
raise Exit(reason, returncode)
@_with_exception(Skipped)
def skip(
reason: str = "",
*,
allow_module_level: bool = False,
reason: str = "", *, allow_module_level: bool = False, msg: Optional[str] = None
) -> NoReturn:
"""Skip an executing test with the given message.
@@ -142,6 +154,9 @@ def skip(
Defaults to False.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
.. note::
It is better to use the :ref:`pytest.mark.skipif ref` marker when
possible to declare a test to be skipped under certain conditions
@@ -150,11 +165,12 @@ def skip(
to skip a doctest statically.
"""
__tracebackhide__ = True
reason = _resolve_msg_to_reason("skip", reason, msg)
raise Skipped(msg=reason, allow_module_level=allow_module_level)
@_with_exception(Failed)
def fail(reason: str = "", pytrace: bool = True) -> NoReturn:
def fail(reason: str = "", pytrace: bool = True, msg: Optional[str] = None) -> NoReturn:
"""Explicitly fail an executing test with the given message.
:param reason:
@@ -163,11 +179,51 @@ def fail(reason: str = "", pytrace: bool = True) -> NoReturn:
:param pytrace:
If False, msg represents the full failure information and no
python traceback will be reported.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
"""
__tracebackhide__ = True
reason = _resolve_msg_to_reason("fail", reason, msg)
raise Failed(msg=reason, pytrace=pytrace)
def _resolve_msg_to_reason(
func_name: str, reason: str, msg: Optional[str] = None
) -> str:
"""
Handles converting the deprecated msg parameter if provided into
reason, raising a deprecation warning. This function will be removed
when the optional msg argument is removed from here in future.
:param str func_name:
The name of the offending function, this is formatted into the deprecation message.
:param str reason:
The reason= passed into either pytest.fail() or pytest.skip()
:param str msg:
The msg= passed into either pytest.fail() or pytest.skip(). This will
be converted into reason if it is provided to allow pytest.skip(msg=) or
pytest.fail(msg=) to continue working in the interim period.
:returns:
The value to use as reason.
"""
__tracebackhide__ = True
if msg is not None:
if reason:
from pytest import UsageError
raise UsageError(
f"Passing both ``reason`` and ``msg`` to pytest.{func_name}(...) is not permitted."
)
warnings.warn(KEYWORD_MSG_ARG.format(func=func_name), stacklevel=3)
reason = msg
return reason
class XFailed(Failed):
"""Raised from an explicit call to pytest.xfail()."""
@@ -194,11 +250,7 @@ def xfail(reason: str = "") -> NoReturn:
def importorskip(
modname: str,
minversion: Optional[str] = None,
reason: Optional[str] = None,
*,
exc_type: Optional[Type[ImportError]] = None,
modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
) -> Any:
"""Import and return the requested module ``modname``, or skip the
current test if the module cannot be imported.
@@ -211,18 +263,6 @@ def importorskip(
:param reason:
If given, this reason is shown as the message when the module cannot
be imported.
:param exc_type:
The exception that should be captured in order to skip modules.
Must be :py:class:`ImportError` or a subclass.
If the module can be imported but raises :class:`ImportError`, pytest will
issue a warning to the user, as often users expect the module not to be
found (which would raise :class:`ModuleNotFoundError` instead).
This warning can be suppressed by passing ``exc_type=ImportError`` explicitly.
See :ref:`import-or-skip-import-error` for details.
:returns:
The imported module. This should be assigned to its canonical name.
@@ -230,62 +270,23 @@ def importorskip(
Example::
docutils = pytest.importorskip("docutils")
.. versionadded:: 8.2
The ``exc_type`` parameter.
"""
import warnings
__tracebackhide__ = True
compile(modname, "", "eval") # to catch syntaxerrors
# Until pytest 9.1, we will warn the user if we catch ImportError (instead of ModuleNotFoundError),
# as this might be hiding an installation/environment problem, which is not usually what is intended
# when using importorskip() (#11523).
# In 9.1, to keep the function signature compatible, we just change the code below to:
# 1. Use `exc_type = ModuleNotFoundError` if `exc_type` is not given.
# 2. Remove `warn_on_import` and the warning handling.
if exc_type is None:
exc_type = ImportError
warn_on_import_error = True
else:
warn_on_import_error = False
skipped: Optional[Skipped] = None
warning: Optional[Warning] = None
with warnings.catch_warnings():
# Make sure to ignore ImportWarnings that might happen because
# of existing directories with the same name we're trying to
# import but without a __init__.py file.
warnings.simplefilter("ignore")
try:
__import__(modname)
except exc_type as exc:
# Do not raise or issue warnings inside the catch_warnings() block.
except ImportError as exc:
if reason is None:
reason = f"could not import {modname!r}: {exc}"
skipped = Skipped(reason, allow_module_level=True)
if warn_on_import_error and not isinstance(exc, ModuleNotFoundError):
lines = [
"",
f"Module '{modname}' was found, but when imported by pytest it raised:",
f" {exc!r}",
"In pytest 9.1 this warning will become an error by default.",
"You can fix the underlying problem, or alternatively overwrite this behavior and silence this "
"warning by passing exc_type=ImportError explicitly.",
"See https://docs.pytest.org/en/stable/deprecations.html#pytest-importorskip-default-behavior-regarding-importerror",
]
warning = PytestDeprecationWarning("\n".join(lines))
if warning:
warnings.warn(warning, stacklevel=2)
if skipped:
raise skipped
raise Skipped(reason, allow_module_level=True) from None
mod = sys.modules[modname]
if minversion is None:
return mod

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Submit failure or test session information to a pastebin service."""
from io import StringIO

View File

@@ -7,7 +7,6 @@ from errno import ENOENT
from errno import ENOTDIR
import fnmatch
from functools import partial
from importlib.machinery import ModuleSpec
import importlib.util
import itertools
import os
@@ -22,7 +21,6 @@ import shutil
import sys
import types
from types import ModuleType
from typing import Any
from typing import Callable
from typing import Dict
from typing import Iterable
@@ -59,7 +57,7 @@ _IGNORED_WINERRORS = (
)
def _ignore_error(exception: Exception) -> bool:
def _ignore_error(exception):
return (
getattr(exception, "errno", None) in _IGNORED_ERRORS
or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
@@ -71,7 +69,7 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
def on_rm_rf_error(
func: Optional[Callable[..., Any]],
func,
path: str,
excinfo: Union[
BaseException,
@@ -172,23 +170,23 @@ 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[Path]:
"""Find all elements in root that begin with the prefix, case insensitive."""
l_prefix = prefix.lower()
for x in os.scandir(root):
for x in root.iterdir():
if x.name.lower().startswith(l_prefix):
yield x
def extract_suffixes(iter: Iterable["os.DirEntry[str]"], prefix: str) -> Iterator[str]:
def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]:
"""Return the parts of the paths following the prefix.
:param iter: Iterator over path names.
:param prefix: Expected prefix of the path names.
"""
p_len = len(prefix)
for entry in iter:
yield entry.name[p_len:]
for p in iter:
yield p.name[p_len:]
def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
@@ -196,7 +194,7 @@ def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
return extract_suffixes(find_prefixed(root, prefix), prefix)
def parse_num(maybe_num: str) -> int:
def parse_num(maybe_num) -> int:
"""Parse number path suffixes, returns -1 on error."""
try:
return int(maybe_num)
@@ -264,9 +262,7 @@ def create_cleanup_lock(p: Path) -> Path:
return lock_path
def register_cleanup_lock_removal(
lock_path: Path, register: Any = atexit.register
) -> Any:
def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
"""Register a cleanup function for removing a lock, by default on atexit."""
pid = os.getpid()
@@ -349,15 +345,15 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
"""List candidates for numbered directories to be removed - follows py.path."""
max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1)
max_delete = max_existing - keep
entries = find_prefixed(root, prefix)
entries, entries2 = itertools.tee(entries)
numbers = map(parse_num, extract_suffixes(entries2, prefix))
for entry, number in zip(entries, numbers):
paths = find_prefixed(root, prefix)
paths, paths2 = itertools.tee(paths)
numbers = map(parse_num, extract_suffixes(paths2, prefix))
for path, number in zip(paths, numbers):
if number <= max_delete:
yield Path(entry)
yield path
def cleanup_dead_symlinks(root: Path) -> None:
def cleanup_dead_symlinks(root: Path):
for left_dir in root.iterdir():
if left_dir.is_symlink():
if not left_dir.resolve().exists():
@@ -461,14 +457,10 @@ def parts(s: str) -> Set[str]:
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],
**kwargs: Any,
) -> None:
def symlink_or_skip(src, dst, **kwargs):
"""Make a symlink, or skip the test in case symlinks are not supported."""
try:
os.symlink(src, dst, **kwargs)
os.symlink(str(src), str(dst), **kwargs)
except OSError as e:
skip(f"symlinks not supported: {e}")
@@ -491,90 +483,73 @@ class ImportPathMismatchError(ImportError):
def import_path(
path: Union[str, "os.PathLike[str]"],
p: Union[str, "os.PathLike[str]"],
*,
mode: Union[str, ImportMode] = ImportMode.prepend,
root: Path,
consider_namespace_packages: bool,
) -> ModuleType:
"""
Import and return a module from the given path, which can be a file (a module) or
"""Import and return a module from the given path, which can be a file (a module) or
a directory (a package).
:param path:
Path to the file to import.
The import mechanism used is controlled by the `mode` parameter:
:param mode:
Controls the underlying import mechanism that will be used:
* `mode == ImportMode.prepend`: the directory containing the module (or package, taking
`__init__.py` files into account) will be put at the *start* of `sys.path` before
being imported with `importlib.import_module`.
* ImportMode.prepend: the directory containing the module (or package, taking
`__init__.py` files into account) will be put at the *start* of `sys.path` before
being imported with `importlib.import_module`.
* `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
to the end of `sys.path`, if not already in `sys.path`.
* ImportMode.append: same as `prepend`, but the directory will be appended
to the end of `sys.path`, if not already in `sys.path`.
* ImportMode.importlib: uses more fine control mechanisms provided by `importlib`
to import the module, which avoids having to muck with `sys.path` at all. It effectively
allows having same-named test modules in different places.
* `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib`
to import the module, which avoids having to muck with `sys.path` at all. It effectively
allows having same-named test modules in different places.
:param root:
Used as an anchor when mode == ImportMode.importlib to obtain
a unique name for the module being imported so it can safely be stored
into ``sys.modules``.
:param consider_namespace_packages:
If True, consider namespace packages when resolving module names.
:raises ImportPathMismatchError:
If after importing the given `path` and the module `__file__`
are different. Only raised in `prepend` and `append` modes.
"""
path = Path(path)
mode = ImportMode(mode)
path = Path(p)
if not path.exists():
raise ImportError(path)
if mode is ImportMode.importlib:
# Try to import this module using the standard import mechanisms, but
# without touching sys.path.
try:
pkg_root, module_name = resolve_pkg_root_and_module_name(
path, consider_namespace_packages=consider_namespace_packages
)
except CouldNotResolvePathError:
pass
else:
# If the given module name is already in sys.modules, do not import it again.
with contextlib.suppress(KeyError):
return sys.modules[module_name]
mod = _import_module_using_spec(
module_name, path, pkg_root, insert_modules=False
)
if mod is not None:
return mod
# Could not import the module with the current sys.path, so we fall back
# to importing the file as a single module, not being a part of a package.
module_name = module_name_from_path(path, root)
with contextlib.suppress(KeyError):
return sys.modules[module_name]
mod = _import_module_using_spec(
module_name, path, path.parent, insert_modules=True
)
if mod is None:
for meta_importer in sys.meta_path:
spec = meta_importer.find_spec(module_name, [str(path.parent)])
if spec is not None:
break
else:
spec = importlib.util.spec_from_file_location(module_name, str(path))
if spec is None:
raise ImportError(f"Can't find module {module_name} at location {path}")
mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod
spec.loader.exec_module(mod) # type: ignore[union-attr]
insert_missing_modules(sys.modules, module_name)
return mod
try:
pkg_root, module_name = resolve_pkg_root_and_module_name(
path, consider_namespace_packages=consider_namespace_packages
)
except CouldNotResolvePathError:
pkg_root, module_name = path.parent, path.stem
pkg_path = resolve_package_path(path)
if pkg_path is not None:
pkg_root = pkg_path.parent
names = list(path.with_suffix("").relative_to(pkg_root).parts)
if names[-1] == "__init__":
names.pop()
module_name = ".".join(names)
else:
pkg_root = path.parent
module_name = path.stem
# Change sys.path permanently: restoring it at the end of this function would cause surprising
# problems because of delayed imports: for example, a conftest.py file imported by this function
@@ -616,80 +591,6 @@ def import_path(
return mod
def _import_module_using_spec(
module_name: str, module_path: Path, module_location: Path, *, insert_modules: bool
) -> Optional[ModuleType]:
"""
Tries to import a module by its canonical name, path to the .py file, and its
parent location.
:param insert_modules:
If True, will call insert_missing_modules to create empty intermediate modules
for made-up module names (when importing test files not reachable from sys.path).
"""
# Checking with sys.meta_path first in case one of its hooks can import this module,
# such as our own assertion-rewrite hook.
for meta_importer in sys.meta_path:
spec = meta_importer.find_spec(module_name, [str(module_location)])
if spec_matches_module_path(spec, module_path):
break
else:
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
if spec_matches_module_path(spec, module_path):
assert spec is not None
# Attempt to import the parent module, seems is our responsibility:
# https://github.com/python/cpython/blob/73906d5c908c1e0b73c5436faeff7d93698fc074/Lib/importlib/_bootstrap.py#L1308-L1311
parent_module_name, _, name = module_name.rpartition(".")
parent_module: Optional[ModuleType] = None
if parent_module_name:
parent_module = sys.modules.get(parent_module_name)
if parent_module is None:
# Find the directory of this module's parent.
parent_dir = (
module_path.parent.parent
if module_path.name == "__init__.py"
else module_path.parent
)
# Consider the parent module path as its __init__.py file, if it has one.
parent_module_path = (
parent_dir / "__init__.py"
if (parent_dir / "__init__.py").is_file()
else parent_dir
)
parent_module = _import_module_using_spec(
parent_module_name,
parent_module_path,
parent_dir,
insert_modules=insert_modules,
)
# Find spec and import this module.
mod = importlib.util.module_from_spec(spec)
sys.modules[module_name] = mod
spec.loader.exec_module(mod) # type: ignore[union-attr]
# Set this module as an attribute of the parent module (#12194).
if parent_module is not None:
setattr(parent_module, name, mod)
if insert_modules:
insert_missing_modules(sys.modules, module_name)
return mod
return None
def spec_matches_module_path(
module_spec: Optional[ModuleSpec], 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
return Path(module_spec.origin) == module_path
# Implement a special _is_same function on Windows which returns True if the two filenames
# compare equal, to circumvent os.path.samefile returning False for mounts in UNC (#7678).
if sys.platform.startswith("win"):
@@ -726,11 +627,6 @@ def module_name_from_path(path: Path, root: Path) -> str:
if len(path_parts) >= 2 and path_parts[-1] == "__init__":
path_parts = path_parts[:-1]
# Module names cannot contain ".", normalize them to "_". This prevents
# a directory having a "." in the name (".env.310" for example) causing extra intermediate modules.
# Also, important to replace "." at the start of paths, as those are considered relative imports.
path_parts = tuple(x.replace(".", "_") for x in path_parts)
return ".".join(path_parts)
@@ -743,31 +639,34 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
otherwise "src.tests.test_foo" is not importable by ``__import__``.
"""
module_parts = module_name.split(".")
child_module: Union[ModuleType, None] = None
module: Union[ModuleType, None] = None
child_name: str = ""
while module_name:
parent_module_name, _, child_name = module_name.rpartition(".")
if parent_module_name:
parent_module = modules.get(parent_module_name)
if parent_module is None:
try:
# If sys.meta_path is empty, calling import_module will issue
# a warning and raise ModuleNotFoundError. To avoid the
# warning, we check sys.meta_path explicitly and raise the error
# ourselves to fall back to creating a dummy module.
if not sys.meta_path:
raise ModuleNotFoundError
parent_module = importlib.import_module(parent_module_name)
except ModuleNotFoundError:
parent_module = ModuleType(
module_name,
doc="Empty module created by pytest's importmode=importlib.",
)
modules[parent_module_name] = parent_module
if module_name not in modules:
try:
# If sys.meta_path is empty, calling import_module will issue
# a warning and raise ModuleNotFoundError. To avoid the
# warning, we check sys.meta_path explicitly and raise the error
# ourselves to fall back to creating a dummy module.
if not sys.meta_path:
raise ModuleNotFoundError
module = importlib.import_module(module_name)
except ModuleNotFoundError:
module = ModuleType(
module_name,
doc="Empty module created by pytest's importmode=importlib.",
)
else:
module = modules[module_name]
if child_module:
# Add child attribute to the parent that can reference the child
# modules.
if not hasattr(parent_module, child_name):
setattr(parent_module, child_name, modules[module_name])
if not hasattr(module, child_name):
setattr(module, child_name, child_module)
modules[module_name] = module
# Keep track of the child module while moving up the tree.
child_module, child_name = module, module_name.rpartition(".")[-1]
module_parts.pop(-1)
module_name = ".".join(module_parts)
@@ -789,100 +688,6 @@ def resolve_package_path(path: Path) -> Optional[Path]:
return result
def resolve_pkg_root_and_module_name(
path: Path, *, consider_namespace_packages: bool = False
) -> Tuple[Path, str]:
"""
Return the path to the directory of the root package that contains the
given Python file, and its module name:
src/
app/
__init__.py
core/
__init__.py
models.py
Passing the full path to `models.py` will yield Path("src") and "app.core.models".
If consider_namespace_packages is True, then we additionally check upwards in the hierarchy
for namespace packages:
https://packaging.python.org/en/latest/guides/packaging-namespace-packages
Raises CouldNotResolvePathError if the given path does not belong to a package (missing any __init__.py files).
"""
pkg_root: Optional[Path] = None
pkg_path = resolve_package_path(path)
if pkg_path is not None:
pkg_root = pkg_path.parent
if consider_namespace_packages:
start = pkg_root if pkg_root is not None else path.parent
for candidate in (start, *start.parents):
module_name = compute_module_name(candidate, path)
if module_name and is_importable(module_name, path):
# Point the pkg_root to the root of the namespace package.
pkg_root = candidate
break
if pkg_root is not None:
module_name = compute_module_name(pkg_root, path)
if module_name:
return pkg_root, module_name
raise CouldNotResolvePathError(f"Could not resolve for {path}")
def is_importable(module_name: str, module_path: Path) -> bool:
"""
Return if the given module path could be imported normally by Python, akin to the user
entering the REPL and importing the corresponding module name directly, and corresponds
to the module_path specified.
:param module_name:
Full module name that we want to check if is importable.
For example, "app.models".
:param module_path:
Full path to the python module/package we want to check if is importable.
For example, "/projects/src/app/models.py".
"""
try:
# Note this is different from what we do in ``_import_module_using_spec``, where we explicitly search through
# sys.meta_path to be able to pass the path of the module that we want to import (``meta_importer.find_spec``).
# Using importlib.util.find_spec() is different, it gives the same results as trying to import
# the module normally in the REPL.
spec = importlib.util.find_spec(module_name)
except (ImportError, ValueError, ImportWarning):
return False
else:
return spec_matches_module_path(spec, module_path)
def compute_module_name(root: Path, module_path: Path) -> Optional[str]:
"""Compute a module name based on a path and a root anchor."""
try:
path_without_suffix = module_path.with_suffix("")
except ValueError:
# Empty paths (such as Path.cwd()) might break meta_path hooks (like our own assertion rewriter).
return None
try:
relative = path_without_suffix.relative_to(root)
except ValueError: # pragma: no cover
return None
names = list(relative.parts)
if not names:
return None
if names[-1] == "__init__":
names.pop()
return ".".join(names)
class CouldNotResolvePathError(Exception):
"""Custom exception raised by resolve_pkg_root_and_module_name."""
def scandir(
path: Union[str, "os.PathLike[str]"],
sort_key: Callable[["os.DirEntry[str]"], object] = lambda entry: entry.name,
@@ -924,13 +729,13 @@ def visit(
yield from visit(entry.path, recurse)
def absolutepath(path: "Union[str, os.PathLike[str]]") -> Path:
def absolutepath(path: Union[Path, str]) -> Path:
"""Convert a path to an absolute path using os.path.abspath.
Prefer this over Path.resolve() (see #6523).
Prefer this over Path.absolute() (not public, doesn't normalize).
"""
return Path(os.path.abspath(path))
return Path(os.path.abspath(str(path)))
def commonpath(path1: Path, path2: Path) -> Optional[Path]:

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""(Disabled by default) support for testing pytest and pytest plugins.
PYTEST_DONT_REWRITE
@@ -246,7 +245,8 @@ class RecordedHookCall:
if TYPE_CHECKING:
# The class has undetermined attributes, this tells mypy about it.
def __getattr__(self, key: str): ...
def __getattr__(self, key: str):
...
@final
@@ -327,13 +327,15 @@ class HookRecorder:
def getreports(
self,
names: "Literal['pytest_collectreport']",
) -> Sequence[CollectReport]: ...
) -> Sequence[CollectReport]:
...
@overload
def getreports(
self,
names: "Literal['pytest_runtest_logreport']",
) -> Sequence[TestReport]: ...
) -> Sequence[TestReport]:
...
@overload
def getreports(
@@ -342,7 +344,8 @@ class HookRecorder:
"pytest_collectreport",
"pytest_runtest_logreport",
),
) -> Sequence[Union[CollectReport, TestReport]]: ...
) -> Sequence[Union[CollectReport, TestReport]]:
...
def getreports(
self,
@@ -387,13 +390,15 @@ class HookRecorder:
def getfailures(
self,
names: "Literal['pytest_collectreport']",
) -> Sequence[CollectReport]: ...
) -> Sequence[CollectReport]:
...
@overload
def getfailures(
self,
names: "Literal['pytest_runtest_logreport']",
) -> Sequence[TestReport]: ...
) -> Sequence[TestReport]:
...
@overload
def getfailures(
@@ -402,7 +407,8 @@ class HookRecorder:
"pytest_collectreport",
"pytest_runtest_logreport",
),
) -> Sequence[Union[CollectReport, TestReport]]: ...
) -> Sequence[Union[CollectReport, TestReport]]:
...
def getfailures(
self,
@@ -1055,7 +1061,7 @@ class Pytester:
:param cmdlineargs: Any extra command line arguments to use.
"""
p = self.makepyfile(source)
values = [*list(cmdlineargs), p]
values = list(cmdlineargs) + [p]
return self.inline_run(*values)
def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]:
@@ -1485,10 +1491,10 @@ class Pytester:
"""
__tracebackhide__ = True
p = make_numbered_dir(root=self.path, prefix="runpytest-", mode=0o700)
args = ("--basetemp=%s" % p, *args)
args = ("--basetemp=%s" % p,) + args
plugins = [x for x in self.plugins if isinstance(x, str)]
if plugins:
args = ("-p", plugins[0], *args)
args = ("-p", plugins[0]) + args
args = self._getpytestargs() + args
return self.run(*args, timeout=timeout)

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Python test discovery, setup and run of test functions."""
import abc
@@ -12,6 +11,7 @@ import inspect
import itertools
import os
from pathlib import Path
import sys
import types
from typing import Any
from typing import Callable
@@ -28,7 +28,6 @@ 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
@@ -40,21 +39,27 @@ from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback
from _pytest._io import TerminalWriter
from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped
from _pytest.compat import get_default_arg_names
from _pytest.compat import get_real_func
from _pytest.compat import getimfunc
from _pytest.compat import getlocation
from _pytest.compat import is_async_function
from _pytest.compat import is_generator
from _pytest.compat import LEGACY_PATH
from _pytest.compat import NOTSET
from _pytest.compat import safe_getattr
from _pytest.compat import safe_isclass
from _pytest.compat import STRING_TYPES
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import INSTANCE_COLLECTOR
from _pytest.deprecated import NOSE_SUPPORT_METHOD
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import FixtureRequest
from _pytest.fixtures import FuncFixtureInfo
@@ -68,6 +73,7 @@ from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import normalize_mark_list
from _pytest.outcomes import fail
from _pytest.outcomes import skip
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportPathMismatchError
@@ -80,11 +86,27 @@ from _pytest.warning_types import PytestReturnNotNoneWarning
from _pytest.warning_types import PytestUnhandledCoroutineWarning
if TYPE_CHECKING:
from typing import Self
_PYTEST_DIR = Path(_pytest.__file__).parent
def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("general")
group.addoption(
"--fixtures",
"--funcargs",
action="store_true",
dest="showfixtures",
default=False,
help="Show available fixtures, sorted by plugin appearance "
"(fixtures with leading '_' are only shown with '-v')",
)
group.addoption(
"--fixtures-per-test",
action="store_true",
dest="show_fixtures_per_test",
default=False,
help="Show fixtures per test",
)
parser.addini(
"python_files",
type="args",
@@ -113,6 +135,16 @@ def pytest_addoption(parser: Parser) -> None:
)
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
if config.option.showfixtures:
showfixtures(config)
return 0
if config.option.show_fixtures_per_test:
show_fixtures_per_test(config)
return 0
return 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)
@@ -177,7 +209,8 @@ def pytest_collect_directory(
) -> Optional[nodes.Collector]:
pkginit = path / "__init__.py"
if pkginit.is_file():
return Package.from_parent(parent, path=path)
pkg: Package = Package.from_parent(parent, path=path)
return pkg
return None
@@ -202,7 +235,8 @@ def path_matches_patterns(path: Path, patterns: Iterable[str]) -> bool:
def pytest_pycollect_makemodule(module_path: Path, parent) -> "Module":
return Module.from_parent(parent, path=module_path)
mod: Module = Module.from_parent(parent, path=module_path)
return mod
@hookimpl(trylast=True)
@@ -213,7 +247,8 @@ def pytest_pycollect_makeitem(
# Nothing was collected elsewhere, let's do it here.
if safe_isclass(obj):
if collector.istestclass(obj, name):
return Class.from_parent(collector, name=name, obj=obj)
klass: Class = Class.from_parent(collector, name=name, obj=obj)
return klass
elif collector.istestfunction(obj, name):
# mock seems to store unbound methods (issue473), normalize it.
obj = getattr(obj, "__func__", obj)
@@ -232,7 +267,7 @@ def pytest_pycollect_makeitem(
)
elif getattr(obj, "__test__", True):
if is_generator(obj):
res = Function.from_parent(collector, name=name)
res: Function = Function.from_parent(collector, name=name)
reason = (
f"yield tests were removed in pytest 4.0 - {name} will be ignored"
)
@@ -269,10 +304,10 @@ class PyobjMixin(nodes.Node):
"""Python instance object the function is bound to.
Returns None if not a test method, e.g. for a standalone test function,
a class or a module.
a staticmethod, a class or a module.
"""
# Overridden by Function.
return None
node = self.getparent(Function)
return getattr(node.obj, "__self__", None) if node is not None else None
@property
def obj(self):
@@ -302,8 +337,10 @@ class PyobjMixin(nodes.Node):
def getmodpath(self, stopatmodule: bool = True, includemodule: bool = False) -> str:
"""Return Python path relative to the containing module."""
chain = self.listchain()
chain.reverse()
parts = []
for node in self.iter_parents():
for node in chain:
name = node.name
if isinstance(node, Module):
name = os.path.splitext(name)[0]
@@ -317,8 +354,20 @@ class PyobjMixin(nodes.Node):
def reportinfo(self) -> Tuple[Union["os.PathLike[str]", str], Optional[int], str]:
# XXX caching?
path, lineno = getfslineno(self.obj)
obj = self.obj
compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None)
if isinstance(compat_co_firstlineno, int):
# nose compatibility
file_path = sys.modules[obj.__module__].__file__
assert file_path is not None
if file_path.endswith(".pyc"):
file_path = file_path[:-1]
path: Union["os.PathLike[str]", str] = file_path
lineno = compat_co_firstlineno
else:
path, lineno = getfslineno(obj)
modpath = self.getmodpath()
assert isinstance(lineno, int)
return path, lineno, modpath
@@ -327,7 +376,7 @@ class PyobjMixin(nodes.Node):
# hook is not called for them.
# fmt: off
class _EmptyClass: pass # noqa: E701
IGNORED_ATTRIBUTES = frozenset.union(
IGNORED_ATTRIBUTES = frozenset.union( # noqa: E305
frozenset(),
# Module.
dir(types.ModuleType("empty_module")),
@@ -435,7 +484,9 @@ class PyCollector(PyobjMixin, nodes.Collector, abc.ABC):
clscol = self.getparent(Class)
cls = clscol and clscol.obj or None
definition = FunctionDefinition.from_parent(self, name=name, callobj=funcobj)
definition: FunctionDefinition = FunctionDefinition.from_parent(
self, name=name, callobj=funcobj
)
fixtureinfo = definition._fixtureinfo
# pytest_generate_tests impls call metafunc.parametrize() which fills
@@ -484,12 +535,7 @@ def importtestmodule(
# We assume we are only called once per module.
importmode = config.getoption("--import-mode")
try:
mod = import_path(
path,
mode=importmode,
root=config.rootpath,
consider_namespace_packages=config.getini("consider_namespace_packages"),
)
mod = import_path(path, mode=importmode, root=config.rootpath)
except SyntaxError as e:
raise nodes.Collector.CollectError(
ExceptionInfo.from_current().getrepr(style="short")
@@ -497,12 +543,12 @@ def importtestmodule(
except ImportPathMismatchError as e:
raise nodes.Collector.CollectError(
"import file mismatch:\n"
"imported module {!r} has this __file__ attribute:\n"
" {}\n"
"imported module %r has this __file__ attribute:\n"
" %s\n"
"which is not the same as the test file we want to collect:\n"
" {}\n"
" %s\n"
"HINT: remove __pycache__ / .pyc files and/or use a "
"unique basename for your test file modules".format(*e.args)
"unique basename for your test file modules" % e.args
) from e
except ImportError as e:
exc_info = ExceptionInfo.from_current()
@@ -540,47 +586,56 @@ class Module(nodes.File, PyCollector):
return importtestmodule(self.path, self.config)
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
self._register_setup_module_fixture()
self._register_setup_function_fixture()
self._inject_setup_module_fixture()
self._inject_setup_function_fixture()
self.session._fixturemanager.parsefactories(self)
return super().collect()
def _register_setup_module_fixture(self) -> None:
"""Register an autouse, module-scoped fixture for the collected module object
def _inject_setup_module_fixture(self) -> None:
"""Inject a hidden autouse, module scoped fixture into the collected module object
that invokes setUpModule/tearDownModule if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
has_nose = self.config.pluginmanager.has_plugin("nose")
setup_module = _get_first_non_fixture_func(
self.obj, ("setUpModule", "setup_module")
)
if setup_module is None and has_nose:
# The name "setup" is too common - only treat as fixture if callable.
setup_module = _get_first_non_fixture_func(self.obj, ("setup",))
if not callable(setup_module):
setup_module = None
teardown_module = _get_first_non_fixture_func(
self.obj, ("tearDownModule", "teardown_module")
)
if teardown_module is None and has_nose:
teardown_module = _get_first_non_fixture_func(self.obj, ("teardown",))
# Same as "setup" above - only treat as fixture if callable.
if not callable(teardown_module):
teardown_module = None
if setup_module is None and teardown_module is None:
return
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
module = request.module
if setup_module is not None:
_call_with_optional_argument(setup_module, module)
yield
if teardown_module is not None:
_call_with_optional_argument(teardown_module, module)
self.session._fixturemanager._register_fixture(
@fixtures.fixture(
autouse=True,
scope="module",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_module_fixture_{self.obj.__name__}",
func=xunit_setup_module_fixture,
nodeid=self.nodeid,
scope="module",
autouse=True,
)
def xunit_setup_module_fixture(request) -> Generator[None, None, None]:
if setup_module is not None:
_call_with_optional_argument(setup_module, request.module)
yield
if teardown_module is not None:
_call_with_optional_argument(teardown_module, request.module)
def _register_setup_function_fixture(self) -> None:
"""Register an autouse, function-scoped fixture for the collected module object
self.obj.__pytest_setup_module = xunit_setup_module_fixture
def _inject_setup_function_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected module object
that invokes setup_function/teardown_function if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@@ -593,27 +648,25 @@ class Module(nodes.File, PyCollector):
if setup_function is None and teardown_function is None:
return
@fixtures.fixture(
autouse=True,
scope="function",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
)
def xunit_setup_function_fixture(request) -> Generator[None, None, None]:
if request.instance is not None:
# in this case we are bound to an instance, so we need to let
# setup_method handle this
yield
return
function = request.function
if setup_function is not None:
_call_with_optional_argument(setup_function, function)
_call_with_optional_argument(setup_function, request.function)
yield
if teardown_function is not None:
_call_with_optional_argument(teardown_function, function)
_call_with_optional_argument(teardown_function, request.function)
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_xunit_setup_function_fixture_{self.obj.__name__}",
func=xunit_setup_function_fixture,
nodeid=self.nodeid,
scope="function",
autouse=True,
)
self.obj.__pytest_setup_function = xunit_setup_function_fixture
class Package(nodes.Directory):
@@ -682,6 +735,8 @@ class Package(nodes.Directory):
ihook = self.ihook
for direntry in scandir(self.path, sort_key):
if direntry.is_dir():
if direntry.name == "__pycache__":
continue
path = Path(direntry.path)
if not self.session.isinitpath(path, with_parents=True):
if ihook.pytest_ignore_collect(collection_path=path, config=config):
@@ -726,7 +781,7 @@ 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):
"""The public constructor."""
return super().from_parent(name=name, parent=parent, **kw)
@@ -740,8 +795,9 @@ class Class(PyCollector):
assert self.parent is not None
self.warn(
PytestCollectionWarning(
f"cannot collect test class {self.obj.__name__!r} because it has a "
f"__init__ constructor (from: {self.parent.nodeid})"
"cannot collect test class %r because it has a "
"__init__ constructor (from: %s)"
% (self.obj.__name__, self.parent.nodeid)
)
)
return []
@@ -749,21 +805,22 @@ class Class(PyCollector):
assert self.parent is not None
self.warn(
PytestCollectionWarning(
f"cannot collect test class {self.obj.__name__!r} because it has a "
f"__new__ constructor (from: {self.parent.nodeid})"
"cannot collect test class %r because it has a "
"__new__ constructor (from: %s)"
% (self.obj.__name__, self.parent.nodeid)
)
)
return []
self._register_setup_class_fixture()
self._register_setup_method_fixture()
self._inject_setup_class_fixture()
self._inject_setup_method_fixture()
self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid)
return super().collect()
def _register_setup_class_fixture(self) -> None:
"""Register an autouse, class scoped fixture into the collected class object
def _inject_setup_class_fixture(self) -> None:
"""Inject a hidden autouse, class scoped fixture into the collected class object
that invokes setup_class/teardown_class if either or both are available.
Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with
@@ -774,58 +831,93 @@ class Class(PyCollector):
if setup_class is None and teardown_class is None:
return
def xunit_setup_class_fixture(request) -> Generator[None, None, None]:
cls = request.cls
@fixtures.fixture(
autouse=True,
scope="class",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
)
def xunit_setup_class_fixture(cls) -> Generator[None, None, None]:
if setup_class is not None:
func = getimfunc(setup_class)
_call_with_optional_argument(func, cls)
_call_with_optional_argument(func, self.obj)
yield
if teardown_class is not None:
func = getimfunc(teardown_class)
_call_with_optional_argument(func, cls)
_call_with_optional_argument(func, self.obj)
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
name=f"_xunit_setup_class_fixture_{self.obj.__qualname__}",
func=xunit_setup_class_fixture,
nodeid=self.nodeid,
scope="class",
autouse=True,
)
self.obj.__pytest_setup_class = xunit_setup_class_fixture
def _register_setup_method_fixture(self) -> None:
"""Register an autouse, function scoped fixture into the collected class object
def _inject_setup_method_fixture(self) -> None:
"""Inject a hidden autouse, function scoped fixture into the collected class object
that invokes setup_method/teardown_method if either or both are available.
Using a fixture to invoke these methods ensures we play nicely and unsurprisingly with
other fixtures (#517).
"""
has_nose = self.config.pluginmanager.has_plugin("nose")
setup_name = "setup_method"
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
emit_nose_setup_warning = False
if setup_method is None and has_nose:
setup_name = "setup"
emit_nose_setup_warning = True
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
teardown_name = "teardown_method"
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
emit_nose_teardown_warning = False
if teardown_method is None and has_nose:
teardown_name = "teardown"
emit_nose_teardown_warning = True
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
if setup_method is None and teardown_method is None:
return
def xunit_setup_method_fixture(request) -> Generator[None, None, None]:
instance = request.instance
method = request.function
if setup_method is not None:
func = getattr(instance, setup_name)
_call_with_optional_argument(func, method)
yield
if teardown_method is not None:
func = getattr(instance, teardown_name)
_call_with_optional_argument(func, method)
self.session._fixturemanager._register_fixture(
@fixtures.fixture(
autouse=True,
scope="function",
# Use a unique name to speed up lookup.
name=f"_xunit_setup_method_fixture_{self.obj.__qualname__}",
func=xunit_setup_method_fixture,
nodeid=self.nodeid,
scope="function",
autouse=True,
)
def xunit_setup_method_fixture(self, request) -> Generator[None, None, None]:
method = request.function
if setup_method is not None:
func = getattr(self, setup_name)
_call_with_optional_argument(func, method)
if emit_nose_setup_warning:
warnings.warn(
NOSE_SUPPORT_METHOD.format(
nodeid=request.node.nodeid, method="setup"
),
stacklevel=2,
)
yield
if teardown_method is not None:
func = getattr(self, teardown_name)
_call_with_optional_argument(func, method)
if emit_nose_teardown_warning:
warnings.warn(
NOSE_SUPPORT_METHOD.format(
nodeid=request.node.nodeid, method="teardown"
),
stacklevel=2,
)
self.obj.__pytest_setup_method = xunit_setup_method_fixture
class InstanceDummy:
"""Instance used to be a node type between Class and Function. It has been
removed in pytest 7.0. Some plugins exist which reference `pytest.Instance`
only to ignore it; this dummy class keeps them working. This will be removed
in pytest 8."""
def __getattr__(name: str) -> object:
if name == "Instance":
warnings.warn(INSTANCE_COLLECTOR, 2)
return InstanceDummy
raise AttributeError(f"module {__name__} has no attribute {name}")
def hasinit(obj: object) -> bool:
@@ -972,7 +1064,7 @@ class IdMaker:
def _idval_from_value(self, val: object) -> Optional[str]:
"""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)):
if isinstance(val, STRING_TYPES):
return _ascii_escaped_by_config(val, self.config)
elif val is None or isinstance(val, (float, int, bool, complex)):
return str(val)
@@ -1242,6 +1334,7 @@ class Metafunc:
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
# artificial "pseudo" FixtureDef's so that later at test execution time we can
# rely on a proper FixtureDef to exist for fixture setup.
arg2fixturedefs = self._arg2fixturedefs
node = None
# If we have a scope that is higher than function, we need
# to make sure we only ever create an according fixturedef on
@@ -1255,7 +1348,7 @@ class Metafunc:
# If used class scope and there is no class, use module-level
# collector (for now).
if scope_ is Scope.Class:
assert isinstance(collector, Module)
assert isinstance(collector, _pytest.python.Module)
node = collector
# If used package scope and there is no package, use session
# (for now).
@@ -1278,18 +1371,19 @@ class Metafunc:
fixturedef = name2pseudofixturedef[argname]
else:
fixturedef = FixtureDef(
config=self.config,
fixturemanager=self.definition.session._fixturemanager,
baseid="",
argname=argname,
func=get_direct_param_fixture_func,
scope=scope_,
params=None,
unittest=False,
ids=None,
_ispytest=True,
)
if name2pseudofixturedef is not None:
name2pseudofixturedef[argname] = fixturedef
self._arg2fixturedefs[argname] = [fixturedef]
arg2fixturedefs[argname] = [fixturedef]
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
# more than once) then we accumulate those calls generating the cartesian product
@@ -1409,8 +1503,7 @@ class Metafunc:
arg_directness[arg] = "indirect"
else:
fail(
f"In {self.function.__name__}: expected Sequence or boolean"
f" for indirect, got {type(indirect).__name__}",
f"In {self.function.__name__}: expected Sequence or boolean for indirect, got {type(indirect).__name__}",
pytrace=False,
)
return arg_directness
@@ -1491,6 +1584,138 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -
return val if escape_option else ascii_escaped(val) # type: ignore
def _pretty_fixture_path(func) -> str:
cwd = Path.cwd()
loc = Path(getlocation(func, str(cwd)))
prefix = Path("...", "_pytest")
try:
return str(prefix / loc.relative_to(_PYTEST_DIR))
except ValueError:
return bestrelpath(cwd, loc)
def show_fixtures_per_test(config):
from _pytest.main import wrap_session
return wrap_session(config, _show_fixtures_per_test)
def _show_fixtures_per_test(config: Config, session: Session) -> None:
import _pytest.config
session.perform_collect()
curdir = Path.cwd()
tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose")
def get_best_relpath(func) -> str:
loc = getlocation(func, str(curdir))
return bestrelpath(curdir, Path(loc))
def write_fixture(fixture_def: fixtures.FixtureDef[object]) -> None:
argname = fixture_def.argname
if verbose <= 0 and argname.startswith("_"):
return
prettypath = _pretty_fixture_path(fixture_def.func)
tw.write(f"{argname}", green=True)
tw.write(f" -- {prettypath}", yellow=True)
tw.write("\n")
fixture_doc = inspect.getdoc(fixture_def.func)
if fixture_doc:
write_docstring(
tw, fixture_doc.split("\n\n")[0] if verbose <= 0 else fixture_doc
)
else:
tw.line(" no docstring available", red=True)
def write_item(item: nodes.Item) -> None:
# Not all items have _fixtureinfo attribute.
info: Optional[FuncFixtureInfo] = getattr(item, "_fixtureinfo", None)
if info is None or not info.name2fixturedefs:
# This test item does not use any fixtures.
return
tw.line()
tw.sep("-", f"fixtures used by {item.name}")
# TODO: Fix this type ignore.
tw.sep("-", f"({get_best_relpath(item.function)})") # type: ignore[attr-defined]
# dict key not used in loop but needed for sorting.
for _, fixturedefs in sorted(info.name2fixturedefs.items()):
assert fixturedefs is not None
if not fixturedefs:
continue
# Last item is expected to be the one used by the test item.
write_fixture(fixturedefs[-1])
for session_item in session.items:
write_item(session_item)
def showfixtures(config: Config) -> Union[int, ExitCode]:
from _pytest.main import wrap_session
return wrap_session(config, _showfixtures_main)
def _showfixtures_main(config: Config, session: Session) -> None:
import _pytest.config
session.perform_collect()
curdir = Path.cwd()
tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose")
fm = session._fixturemanager
available = []
seen: Set[Tuple[str, str]] = set()
for argname, fixturedefs in fm._arg2fixturedefs.items():
assert fixturedefs is not None
if not fixturedefs:
continue
for fixturedef in fixturedefs:
loc = getlocation(fixturedef.func, str(curdir))
if (fixturedef.argname, loc) in seen:
continue
seen.add((fixturedef.argname, loc))
available.append(
(
len(fixturedef.baseid),
fixturedef.func.__module__,
_pretty_fixture_path(fixturedef.func),
fixturedef.argname,
fixturedef,
)
)
available.sort()
currentmodule = None
for baseid, module, prettypath, argname, fixturedef in available:
if currentmodule != module:
if not module.startswith("_pytest."):
tw.line()
tw.sep("-", f"fixtures defined from {module}")
currentmodule = module
if verbose <= 0 and argname.startswith("_"):
continue
tw.write(f"{argname}", green=True)
if fixturedef.scope != "function":
tw.write(" [%s scope]" % fixturedef.scope, cyan=True)
tw.write(f" -- {prettypath}", yellow=True)
tw.write("\n")
doc = inspect.getdoc(fixturedef.func)
if doc:
write_docstring(tw, doc.split("\n\n")[0] if verbose <= 0 else doc)
else:
tw.line(" no docstring available", red=True)
tw.line()
def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
for line in doc.split("\n"):
tw.line(indent + line)
class Function(PyobjMixin, nodes.Item):
"""Item responsible for setting up and executing a Python test function.
@@ -1538,8 +1763,7 @@ class Function(PyobjMixin, nodes.Item):
super().__init__(name, parent, config=config, session=session)
if callobj is not NOTSET:
self._obj = callobj
self._instance = getattr(callobj, "__self__", None)
self.obj = callobj
#: Original function name, without any decorations (for example
#: parametrization adds a ``"[...]"`` suffix to function names), used to access
@@ -1574,9 +1798,8 @@ class Function(PyobjMixin, nodes.Item):
self.fixturenames = fixtureinfo.names_closure
self._initrequest()
# todo: determine sound type limitations
@classmethod
def from_parent(cls, parent, **kw) -> "Self":
def from_parent(cls, parent, **kw): # todo: determine sound type limitations
"""The public constructor."""
return super().from_parent(parent=parent, **kw)
@@ -1589,31 +1812,12 @@ class Function(PyobjMixin, nodes.Item):
"""Underlying python 'function' object."""
return getimfunc(self.obj)
@property
def instance(self):
try:
return self._instance
except AttributeError:
if isinstance(self.parent, Class):
# Each Function gets a fresh class instance.
self._instance = self._getinstance()
else:
self._instance = None
return self._instance
def _getinstance(self):
def _getobj(self):
assert self.parent is not None
if isinstance(self.parent, Class):
# Each Function gets a fresh class instance.
return self.parent.newinstance()
parent_obj = self.parent.newinstance()
else:
return None
def _getobj(self):
instance = self.instance
if instance is not None:
parent_obj = instance
else:
assert self.parent is not None
parent_obj = self.parent.obj # type: ignore[attr-defined]
return getattr(parent_obj, self.originalname)
@@ -1649,10 +1853,11 @@ class Function(PyobjMixin, nodes.Item):
if len(ntraceback) > 2:
ntraceback = Traceback(
(
ntraceback[0],
*(t.with_repr_style("short") for t in ntraceback[1:-1]),
ntraceback[-1],
entry
if i == 0 or i == len(ntraceback) - 1
else entry.with_repr_style("short")
)
for i, entry in enumerate(ntraceback)
)
return ntraceback

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
from collections.abc import Collection
from collections.abc import Sized
from decimal import Decimal
@@ -24,6 +23,7 @@ from typing import TypeVar
from typing import Union
import _pytest._code
from _pytest.compat import STRING_TYPES
from _pytest.outcomes import fail
@@ -142,7 +142,7 @@ 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[str]:
import itertools
import math
@@ -163,14 +163,10 @@ class ApproxNumpy(ApproxBase):
self._approx_scalar, self.expected.tolist()
)
# convert other_side to numpy array to ensure shape attribute is available
other_side_as_array = _as_numpy_array(other_side)
assert other_side_as_array is not None
if np_array_shape != other_side_as_array.shape:
if np_array_shape != other_side.shape:
return [
"Impossible to compare arrays with different shapes.",
f"Shapes: {np_array_shape} and {other_side_as_array.shape}",
f"Shapes: {np_array_shape} and {other_side.shape}",
]
number_of_elements = self.expected.size
@@ -179,7 +175,7 @@ class ApproxNumpy(ApproxBase):
different_ids = []
for index in itertools.product(*(range(i) for i in np_array_shape)):
approx_value = get_value_from_nested_list(approx_side_as_seq, index)
other_value = get_value_from_nested_list(other_side_as_array, index)
other_value = get_value_from_nested_list(other_side, index)
if approx_value != other_value:
abs_diff = abs(approx_value.expected - other_value)
max_abs_diff = max(max_abs_diff, abs_diff)
@@ -192,7 +188,7 @@ class ApproxNumpy(ApproxBase):
message_data = [
(
str(index),
str(get_value_from_nested_list(other_side_as_array, index)),
str(get_value_from_nested_list(other_side, index)),
str(get_value_from_nested_list(approx_side_as_seq, index)),
)
for index in different_ids
@@ -397,7 +393,7 @@ class ApproxScalar(ApproxBase):
# tolerances, i.e. non-numerics and infinities. Need to call abs to
# handle complex numbers, e.g. (inf + 1j).
if (not isinstance(self.expected, (Complex, Decimal))) or math.isinf(
abs(self.expected)
abs(self.expected) # type: ignore[arg-type]
):
return str(self.expected)
@@ -441,8 +437,8 @@ class ApproxScalar(ApproxBase):
# Allow the user to control whether NaNs are considered equal to each
# other or not. The abs() calls are for compatibility with complex
# numbers.
if math.isnan(abs(self.expected)):
return self.nan_ok and math.isnan(abs(actual))
if math.isnan(abs(self.expected)): # type: ignore[arg-type]
return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type]
# Infinity shouldn't be approximately equal to anything but itself, but
# if there's a relative tolerance, it will be infinite and infinity
@@ -450,11 +446,11 @@ class ApproxScalar(ApproxBase):
# case would have been short circuited above, so here we can just
# return false if the expected value is infinite. The abs() call is
# for compatibility with complex numbers.
if math.isinf(abs(self.expected)):
if math.isinf(abs(self.expected)): # type: ignore[arg-type]
return False
# Return true if the two numbers are within the tolerance.
result: bool = abs(self.expected - actual) <= self.tolerance # type: ignore[arg-type]
result: bool = abs(self.expected - actual) <= self.tolerance
return result
# Ignore type because of https://github.com/python/mypy/issues/4266.
@@ -724,11 +720,16 @@ def approx(expected, rel=None, abs=None, nan_ok: bool = False) -> ApproxBase:
elif (
hasattr(expected, "__getitem__")
and isinstance(expected, Sized)
and not isinstance(expected, (str, bytes))
# Type ignored because the error is wrong -- not unreachable.
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
):
cls = ApproxSequenceLike
elif isinstance(expected, Collection) and not isinstance(expected, (str, bytes)):
msg = f"pytest.approx() only supports ordered sequences, but got: {expected!r}"
elif (
isinstance(expected, Collection)
# Type ignored because the error is wrong -- not unreachable.
and not isinstance(expected, STRING_TYPES) # type: ignore[unreachable]
):
msg = f"pytest.approx() only supports ordered sequences, but got: {repr(expected)}"
raise TypeError(msg)
else:
cls = ApproxScalar
@@ -773,19 +774,21 @@ def raises(
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
*,
match: Optional[Union[str, Pattern[str]]] = ...,
) -> "RaisesContext[E]": ...
) -> "RaisesContext[E]":
...
@overload
def raises(
def raises( # noqa: F811
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
func: Callable[..., Any],
*args: Any,
**kwargs: Any,
) -> _pytest._code.ExceptionInfo[E]: ...
) -> _pytest._code.ExceptionInfo[E]:
...
def raises(
def raises( # noqa: F811
expected_exception: Union[Type[E], Tuple[Type[E], ...]], *args: Any, **kwargs: Any
) -> Union["RaisesContext[E]", _pytest._code.ExceptionInfo[E]]:
r"""Assert that a code block/function call raises an exception type, or one of its subclasses.

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Record warnings during test function execution."""
from pprint import pformat
@@ -20,6 +19,7 @@ from typing import Union
import warnings
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import WARNS_NONE_ARG
from _pytest.fixtures import fixture
from _pytest.outcomes import Exit
from _pytest.outcomes import fail
@@ -44,14 +44,18 @@ def recwarn() -> Generator["WarningsRecorder", None, None]:
@overload
def deprecated_call(
*, match: Optional[Union[str, Pattern[str]]] = ...
) -> "WarningsRecorder": ...
) -> "WarningsRecorder":
...
@overload
def deprecated_call(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ...
def deprecated_call( # noqa: F811
func: Callable[..., T], *args: Any, **kwargs: Any
) -> T:
...
def deprecated_call(
def deprecated_call( # noqa: F811
func: Optional[Callable[..., Any]] = None, *args: Any, **kwargs: Any
) -> Union["WarningsRecorder", Any]:
"""Assert that code produces a ``DeprecationWarning`` or ``PendingDeprecationWarning`` or ``FutureWarning``.
@@ -79,7 +83,7 @@ def deprecated_call(
"""
__tracebackhide__ = True
if func is not None:
args = (func, *args)
args = (func,) + args
return warns(
(DeprecationWarning, PendingDeprecationWarning, FutureWarning), *args, **kwargs
)
@@ -90,19 +94,21 @@ def warns(
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ...,
*,
match: Optional[Union[str, Pattern[str]]] = ...,
) -> "WarningsChecker": ...
) -> "WarningsChecker":
...
@overload
def warns(
def warns( # noqa: F811
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]],
func: Callable[..., T],
*args: Any,
**kwargs: Any,
) -> T: ...
) -> T:
...
def warns(
def warns( # noqa: F811
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning,
*args: Any,
match: Optional[Union[str, Pattern[str]]] = None,
@@ -110,7 +116,7 @@ def warns(
) -> Union["WarningsChecker", Any]:
r"""Assert that code raises a particular class of warning.
Specifically, the parameter ``expected_warning`` can be a warning class or tuple
Specifically, the parameter ``expected_warning`` can be a warning class or sequence
of warning classes, and the code inside the ``with`` block must issue at least one
warning of that class or classes.
@@ -181,7 +187,8 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
def __init__(self, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
super().__init__(record=True)
# Type ignored due to the way typeshed handles warnings.catch_warnings.
super().__init__(record=True) # type: ignore[call-arg]
self._entered = False
self._list: List[warnings.WarningMessage] = []
@@ -259,7 +266,9 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
class WarningsChecker(WarningsRecorder):
def __init__(
self,
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = Warning,
expected_warning: Optional[
Union[Type[Warning], Tuple[Type[Warning], ...]]
] = Warning,
match_expr: Optional[Union[str, Pattern[str]]] = None,
*,
_ispytest: bool = False,
@@ -268,14 +277,15 @@ class WarningsChecker(WarningsRecorder):
super().__init__(_ispytest=True)
msg = "exceptions must be derived from Warning, not %s"
if isinstance(expected_warning, tuple):
if expected_warning is None:
warnings.warn(WARNS_NONE_ARG, stacklevel=4)
expected_warning_tup = None
elif isinstance(expected_warning, tuple):
for exc in expected_warning:
if not issubclass(exc, Warning):
raise TypeError(msg % type(exc))
expected_warning_tup = expected_warning
elif isinstance(expected_warning, type) and issubclass(
expected_warning, Warning
):
elif issubclass(expected_warning, Warning):
expected_warning_tup = (expected_warning,)
else:
raise TypeError(msg % type(expected_warning))
@@ -299,6 +309,10 @@ class WarningsChecker(WarningsRecorder):
__tracebackhide__ = True
if self.expected_warning is None:
# nothing to do in this deprecated case, see WARNS_NONE_ARG above
return
# BaseExceptions like pytest.{skip,fail,xfail,exit} or Ctrl-C within
# pytest.warns should *not* trigger "DID NOT WARN" and get suppressed
# when the warning doesn't happen. Control-flow exceptions should always
@@ -310,7 +324,7 @@ class WarningsChecker(WarningsRecorder):
):
return
def found_str() -> str:
def found_str():
return pformat([record.message for record in self], indent=2)
try:
@@ -337,30 +351,3 @@ class WarningsChecker(WarningsRecorder):
module=w.__module__,
source=w.source,
)
# Currently in Python it is possible to pass other types than an
# `str` message when creating `Warning` instances, however this
# causes an exception when :func:`warnings.filterwarnings` is used
# to filter those warnings. See
# https://github.com/python/cpython/issues/103577 for a discussion.
# While this can be considered a bug in CPython, we put guards in
# pytest as the error message produced without this check in place
# is confusing (#10865).
for w in self:
if type(w.message) is not UserWarning:
# If the warning was of an incorrect type then `warnings.warn()`
# creates a UserWarning. Any other warning must have been specified
# explicitly.
continue
if not w.message.args:
# UserWarning() without arguments must have been specified explicitly.
continue
msg = w.message.args[0]
if isinstance(msg, str):
continue
# It's possible that UserWarning was explicitly specified, and
# its first argument was not a string. But that case can't be
# distinguished from an invalid type.
raise TypeError(
f"Warning must be str or Warning, got {msg!r} (type {type(msg).__name__})"
)

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
import dataclasses
from io import StringIO
import os
@@ -72,7 +71,8 @@ class BaseReport:
if TYPE_CHECKING:
# Can have arbitrary fields given to __init__().
def __getattr__(self, key: str) -> Any: ...
def __getattr__(self, key: str) -> Any:
...
def toterminal(self, out: TerminalWriter) -> None:
if hasattr(self, "node"):
@@ -605,9 +605,9 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
description,
)
)
exception_info: Union[ExceptionChainRepr, ReprExceptionInfo] = (
ExceptionChainRepr(chain)
)
exception_info: Union[
ExceptionChainRepr, ReprExceptionInfo
] = ExceptionChainRepr(chain)
else:
exception_info = ReprExceptionInfo(
reprtraceback=reprtraceback,

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Basic collect and runtest protocol implementations."""
import bdb
@@ -39,7 +38,7 @@ from _pytest.outcomes import Skipped
from _pytest.outcomes import TEST_OUTCOME
if sys.version_info < (3, 11):
if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup
if TYPE_CHECKING:
@@ -85,7 +84,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
dlist.append(rep)
if not dlist:
return
dlist.sort(key=lambda x: x.duration, reverse=True)
dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return]
if not durations:
tr.write_sep("=", "slowest durations")
else:
@@ -165,8 +164,6 @@ def pytest_runtest_call(item: Item) -> None:
del sys.last_type
del sys.last_value
del sys.last_traceback
if sys.version_info >= (3, 12, 0):
del sys.last_exc # type: ignore[attr-defined]
except AttributeError:
pass
try:
@@ -175,8 +172,6 @@ def pytest_runtest_call(item: Item) -> None:
# Store trace info to allow postmortem debugging
sys.last_type = type(e)
sys.last_value = e
if sys.version_info >= (3, 12, 0):
sys.last_exc = e # type: ignore[attr-defined]
assert e.__traceback__ is not None
# Skip *this* frame
sys.last_traceback = e.__traceback__.tb_next
@@ -225,26 +220,13 @@ def pytest_report_teststatus(report: BaseReport) -> Optional[Tuple[str, str, str
def call_and_report(
item: Item, when: Literal["setup", "call", "teardown"], log: bool = True, **kwds
) -> TestReport:
ihook = item.ihook
if when == "setup":
runtest_hook: Callable[..., None] = ihook.pytest_runtest_setup
elif when == "call":
runtest_hook = ihook.pytest_runtest_call
elif when == "teardown":
runtest_hook = ihook.pytest_runtest_teardown
else:
assert False, f"Unhandled runtest hook case: {when}"
reraise: Tuple[Type[BaseException], ...] = (Exit,)
if not item.config.getoption("usepdb", False):
reraise += (KeyboardInterrupt,)
call = CallInfo.from_call(
lambda: runtest_hook(item=item, **kwds), when=when, reraise=reraise
)
report: TestReport = ihook.pytest_runtest_makereport(item=item, call=call)
call = call_runtest_hook(item, when, **kwds)
hook = item.ihook
report: TestReport = hook.pytest_runtest_makereport(item=item, call=call)
if log:
ihook.pytest_runtest_logreport(report=report)
hook.pytest_runtest_logreport(report=report)
if check_interactive_exception(call, report):
ihook.pytest_exception_interact(node=item, call=call, report=report)
hook.pytest_exception_interact(node=item, call=call, report=report)
return report
@@ -263,6 +245,25 @@ def check_interactive_exception(call: "CallInfo[object]", report: BaseReport) ->
return True
def call_runtest_hook(
item: Item, when: Literal["setup", "call", "teardown"], **kwds
) -> "CallInfo[None]":
if when == "setup":
ihook: Callable[..., None] = item.ihook.pytest_runtest_setup
elif when == "call":
ihook = item.ihook.pytest_runtest_call
elif when == "teardown":
ihook = item.ihook.pytest_runtest_teardown
else:
assert False, f"Unhandled runtest hook case: {when}"
reraise: Tuple[Type[BaseException], ...] = (Exit,)
if not item.config.getoption("usepdb", False):
reraise += (KeyboardInterrupt,)
return CallInfo.from_call(
lambda: ihook(item=item, **kwds), when=when, reraise=reraise
)
TResult = TypeVar("TResult", covariant=True)
@@ -381,9 +382,6 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
collector.path,
collector.config.getoption("importmode"),
rootpath=collector.config.rootpath,
consider_namespace_packages=collector.config.getini(
"consider_namespace_packages"
),
)
return list(collector.collect())
@@ -396,7 +394,8 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
skip_exceptions = [Skipped]
unittest = sys.modules.get("unittest")
if unittest is not None:
skip_exceptions.append(unittest.SkipTest)
# Type ignored because unittest is loaded dynamically.
skip_exceptions.append(unittest.SkipTest) # type: ignore
if isinstance(call.excinfo.value, tuple(skip_exceptions)):
outcome = "skipped"
r_ = collector._repr_failure_py(call.excinfo, "line")

View File

@@ -47,23 +47,20 @@ def pytest_fixture_setup(
else:
param = request.param
fixturedef.cached_param = param # type: ignore[attr-defined]
_show_fixture_action(fixturedef, request.config, "SETUP")
_show_fixture_action(fixturedef, "SETUP")
def pytest_fixture_post_finalizer(
fixturedef: FixtureDef[object], request: SubRequest
) -> None:
def pytest_fixture_post_finalizer(fixturedef: FixtureDef[object]) -> None:
if fixturedef.cached_result is not None:
config = request.config
config = fixturedef._fixturemanager.config
if config.option.setupshow:
_show_fixture_action(fixturedef, request.config, "TEARDOWN")
_show_fixture_action(fixturedef, "TEARDOWN")
if hasattr(fixturedef, "cached_param"):
del fixturedef.cached_param
del fixturedef.cached_param # type: ignore[attr-defined]
def _show_fixture_action(
fixturedef: FixtureDef[object], config: Config, msg: str
) -> None:
def _show_fixture_action(fixturedef: FixtureDef[object], msg: str) -> None:
config = fixturedef._fixturemanager.config
capman = config.pluginmanager.getplugin("capturemanager")
if capman:
capman.suspend_global_capture()
@@ -87,7 +84,7 @@ def _show_fixture_action(
tw.write(" (fixtures used: {})".format(", ".join(deps)))
if hasattr(fixturedef, "cached_param"):
tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]")
tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]") # type: ignore[attr-defined]
tw.flush()

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
"""Support for skip/xfail functions and markers."""
from collections.abc import Mapping
@@ -110,7 +109,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
)
globals_.update(dictionary)
if hasattr(item, "obj"):
globals_.update(item.obj.__globals__)
globals_.update(item.obj.__globals__) # type: ignore[attr-defined]
try:
filename = f"<{mark.name} condition>"
condition_code = compile(condition, filename, "eval")

View File

@@ -19,8 +19,6 @@ class StashKey(Generic[T]):
A ``StashKey`` is associated with the type ``T`` of the value of the key.
A ``StashKey`` is unique and cannot conflict with another key.
.. versionadded:: 7.0
"""
__slots__ = ()
@@ -63,8 +61,6 @@ class Stash:
some_str = stash[some_str_key]
# The static type of some_bool is bool.
some_bool = stash[some_bool_key]
.. versionadded:: 7.0
"""
__slots__ = ("_storage",)

Some files were not shown because too many files have changed in this diff Show More