Compare commits

..

147 Commits

Author SHA1 Message Date
pytest bot
329d371214 Prepare release version 8.2.2 2024-06-04 12:54:55 +00:00
Ran Benita
214d098fcc Merge pull request #12414 from bluetech/backport-12409
[8.2.x] fixtures: fix catastrophic performance problem in `reorder_items`
2024-06-04 12:47:12 +03:00
Ran Benita
153a436bc4 [8.2.x] fixtures: fix catastrophic performance problem in reorder_items
Manual minimal backport from commit e89d23b247.

Fix #12355.

In the issue, it was reported that the `reorder_items` has quadratic (or
worse...) behavior with certain simple parametrizations. After some
debugging I found that the problem happens because the "Fix
items_by_argkey order" loop keeps adding the same item to the deque,
and it reaches epic sizes which causes the slowdown.

I don't claim to understand how the `reorder_items` algorithm works, but
if as far as I understand, if an item already exists in the deque, the
correct thing to do is to move it to the front. Since a deque doesn't
have such an (efficient) operation, this switches to `OrderedDict` which
can efficiently append from both sides, deduplicate and move to front.
2024-06-04 10:33:50 +03:00
Ran Benita
b41d5a52bb Merge pull request #12412 from pytest-dev/backport-12408-to-8.2.x
[8.2.x] cacheprovider: fix "Directory not empty" crash from cache directory creation
2024-06-03 13:16:29 +03:00
Ran Benita
9bb73d734f [8.2.x] cacheprovider: fix "Directory not empty" crash from cache directory creation 2024-06-03 09:44:21 +00:00
github-actions[bot]
4569a01e3d [8.2.x] doc: Update trainings/events (#12402)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-05-30 17:05:32 +02:00
github-actions[bot]
1d103e5cdc [8.2.x] Clarify pytest_ignore_collect docs (#12386)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-05-28 17:41:35 +00:00
github-actions[bot]
240a252d34 [8.2.x] Add html_baseurl to sphinx conf.py (#12372)
Co-authored-by: James Frost <james.frost@metoffice.gov.uk>
2024-05-26 10:38:25 +00:00
Ran Benita
a5ee3c4126 Merge pull request #12370 from pytest-dev/backport-12368-to-8.2.x
[8.2.x] unittest: fix class instances no longer released on test teardown since pytest 8.2.0
2024-05-26 11:04:09 +03:00
Ran Benita
f7358aec28 [8.2.x] unittest: fix class instances no longer released on test teardown since pytest 8.2.0 2024-05-26 07:34:33 +00:00
github-actions[bot]
558e4fa71a [8.2.x] Add thread safety section to flaky test docs (#12362)
Co-authored-by: Nathan Goldbaum <nathan.goldbaum@gmail.com>
2024-05-24 08:22:20 -03:00
github-actions[bot]
f0e7a07667 [8.2.x] Issue #12290 - Docs using Furo Theme W/ Dark Mode (#12348)
Co-authored-by: Sam Jirovec <47160720+samjirovec@users.noreply.github.com>
2024-05-21 16:11:45 +00:00
Bruno Oliveira
22e885f109 Fix link in changelog (#12343) (#12344)
Co-authored-by: Hynek Schlawack <hs@ox.cx>
2024-05-19 18:17:40 -03:00
Ran Benita
bddbeba74b Merge pull request #12341 from pytest-dev/release-8.2.1
Prepare release 8.2.1
2024-05-19 22:09:37 +03:00
pytest bot
66ff8dffdf Prepare release version 8.2.1 2024-05-19 16:43:40 +00:00
Ran Benita
3ffcfd122c Merge pull request #12340 from pytest-dev/backport-12334-to-8.2.x
[8.2.x] Add Python 3.13 (beta) support
2024-05-19 12:45:56 +03:00
Ran Benita
0b28313b46 [8.2.x] Add Python 3.13 (beta) support 2024-05-19 06:45:36 +00:00
github-actions[bot]
f3dd93ad8d [8.2.x] Attest package provenance (#12335)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-05-17 08:20:32 -03:00
github-actions[bot]
bb5a1257b0 [8.2.x] Spelling (#12331)
Co-authored-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2024-05-15 18:25:57 +00:00
Ran Benita
f179bf252f Merge pull request #12327 from pytest-dev/backport-12325-to-8.2.x
[8.2.x] cacheprovider: fix `.pytest_cache` not being world-readable
2024-05-15 14:15:10 +03:00
Ran Benita
2b671b5f92 [8.2.x] cacheprovider: fix .pytest_cache not being world-readable 2024-05-15 07:57:34 +00:00
Ran Benita
65ab7cb96c Merge pull request #12324 from pytest-dev/backport-12320-to-8.2.x
[8.2.x] changelog: document unittest 8.2 change as breaking
2024-05-14 23:49:40 +03:00
Ran Benita
4d5fb7d71c Merge pull request #12319 from pytest-dev/backport-12311-to-8.2.x
[8.2.x] python: add workaround for permission error crashes from non-selected directories
2024-05-14 23:07:16 +03:00
Ran Benita
cbe5996cc6 [8.2.x] changelog: document unittest 8.2 change as breaking 2024-05-14 20:07:07 +00:00
Ran Benita
c9e9315725 [8.2.x] python: add workaround for permission error crashes from non-selected directories 2024-05-13 17:33:00 +00:00
github-actions[bot]
328001eab1 [8.2.x] Fixes crashing under a squashfuse_ll read-only mount (#12302)
Co-authored-by: Yutian Li <hotpxless@gmail.com>
2024-05-09 18:20:57 +00:00
github-actions[bot]
8fdb72947e [8.2.x] doc: update sprint repo link (#12297)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-05-08 10:16:22 -03:00
github-actions[bot]
5c1c73b961 [8.2.x] Document exceptions raised by exit, skip, xfail, fail, and importorskip (#12288)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-05-06 01:32:21 +00:00
github-actions[bot]
a152c2cee4 [8.2.x] Consider KeyboardInterrupt/SystemExit at collection time (#12282)
Co-authored-by: Anita Hammer <166057949+anitahammer@users.noreply.github.com>
2024-05-02 12:18:05 +00:00
Bruno Oliveira
69c3bcea36 Merge pull request #12257 from pytest-dev/release-8.2.0
Prepare release 8.2.0
2024-04-27 20:35:29 -03:00
Bruno Oliveira
6bd3f31344 Tweak changelog for 8.2.0 2024-04-27 10:20:51 -03:00
pytest bot
9b6219b5e8 Prepare release version 8.2.0 2024-04-27 13:07:39 +00:00
Bruno Oliveira
835765c9d3 Merge pull request #12130 from bluetech/fixtures-inline
fixtures: inline some functions to streamline the code
2024-04-27 10:01:36 -03:00
Daniel Miller
7e7503c0b0 unittest: report class cleanup exceptions (#12250)
Fixes #11728

---------

Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
2024-04-27 09:49:05 -03:00
Ran Benita
882c4da2f3 fixtures: inline fail_fixturefunc
Doesn't add much.
2024-04-27 09:48:14 -03:00
Ran Benita
2e8fb9f140 fixtures: extract a _check_fixturedef method
This stuff is less interesting when reading `_get_active_fixturedef`.
2024-04-27 09:48:14 -03:00
Ran Benita
acf2971f46 fixtures: inline _getnextfixturedef into _get_active_fixturedef 2024-04-27 09:48:14 -03:00
Ran Benita
3c77aec1da fixtures: move "request" check early 2024-04-27 09:48:14 -03:00
Ran Benita
d217d68cde fixtures: inline _compute_fixture_value 2024-04-27 09:48:14 -03:00
Ran Benita
530be28575 fixtures: use early return in _get_active_fixturedef 2024-04-27 09:48:14 -03:00
Bruno Oliveira
d208c1d4a5 Merge pull request #12256 from bluetech/misc2
2 small cleanups
2024-04-27 09:39:16 -03:00
Ran Benita
80ca255d42 pathlib: make absolutepath support os.PathLike[str]
This slightly simplifies a bit of path.
2024-04-27 11:53:01 +03:00
Ran Benita
1a332802ff Avoid slicing sys.version_info in version conditionals
It is unnecessary, and some static analyzers don't handle it.
2024-04-27 01:14:21 +03:00
Bruno Oliveira
93c2cdf6d6 cherry-pick release-8.1.2 (#12252)
(cherry picked from commit 7df3dbc545)
2024-04-26 15:25:44 -03:00
Shekhar verma
4eb8b6d525 Changed importError to ModuleNotFoundError (#12220)
* Changed importError to ModuleNotFoundError

* added testing for importorskip

* added exc_types parameter in importorskip

* Added warning and Test Cases

* Improve tests and docs

* Improve deprecation docs

* Change exc_type to kw only

* Apply suggestions from code review

Co-authored-by: Florian Bruhin <me@the-compiler.org>

* Fix check

---------

Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2024-04-26 09:48:57 +02:00
pre-commit-ci[bot]
fafab1dbfd [pre-commit.ci] pre-commit autoupdate (#12238)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.7 → v0.4.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.7...v0.4.1)
- [github.com/tox-dev/pyproject-fmt: 1.7.0 → 1.8.0](https://github.com/tox-dev/pyproject-fmt/compare/1.7.0...1.8.0)

* noqa a test

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
2024-04-23 11:59:12 -03:00
poulami-sau
5cffef7f07 Fixed Bug Regarding Attribute Error in pytest.approx For Types Implicitly Convertible to Numpy Arrays (#12232)
* added test case in testing/python/approx.py based on test case provided by reporter in issue #12114
* test cases pass for pytest testing/python/approx.py
* expanded the type annotation to include objects which may cast to a array and renamed other_side to other_side_as_array and asserted that it is not none
2024-04-23 10:45:33 +02:00
Florian Bruhin
a830a3e98d Update events/trainings (#12236) 2024-04-22 19:52:25 +02:00
dependabot[bot]
1d322ffb48 build(deps): Bump peter-evans/create-pull-request from 6.0.3 to 6.0.4 (#12233)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6.0.3 to 6.0.4.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](c55203cfde...9153d834b6)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 11:54:48 +02:00
github-actions[bot]
63d985c6d3 [automated] Update plugin list (#12230)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-04-21 10:57:36 -03:00
Ran Benita
3617ef1c79 Merge pull request #12227 from bluetech/deprecate-legacy-path-impls
hookspec: deprecate hookimpls requesting py.path parameters
2024-04-21 11:52:16 +03:00
Ran Benita
58136c5376 hookspec: deprecate hookimpls requesting py.path parameters 2024-04-21 11:37:56 +03:00
Bruno Oliveira
042625957a Fully type annotate pathlib.py (#12229)
Add full type annotations to `pathlib.py` and remove the `allow-untyped-defs` directive.
2024-04-20 16:32:36 -03:00
Bruno Oliveira
6fb474a3eb Refactor insert_missing_modules function (#12210)
Makes the logic more straightforward IMO.
2024-04-20 11:58:14 +00:00
Bruno Oliveira
ff806b239e importlib: set children as attribute of parent modules (#12208)
Now `importlib` mode will correctly set the imported modules as an attribute of their parent modules.

As helpfully posted on #12194, that's how the Python import module works so we should follow suit.

In addition, we also try to import the parent modules as part of the process of importing a child module, again mirroring how Python importing works.

Fix #12194
2024-04-20 11:31:33 +00:00
dj
ad95d59d61 Use monkeypatch in test_pytest_version_env_var
Follow up to #12190
2024-04-18 23:05:29 -03:00
dj
48b6d18834 Add PYTEST_VERSION environment variable (#12190)
Among other things, it can be used to check if a code is running from within a pytest session.

Fixes #9502
2024-04-18 07:45:47 -03:00
pre-commit-ci[bot]
58844247f7 [pre-commit.ci] pre-commit autoupdate (#12218)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.5 → v0.3.7](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.5...v0.3.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-15 23:04:19 +00:00
dependabot[bot]
b56b294f54 build(deps): Bump peter-evans/create-pull-request from 6.0.2 to 6.0.3 (#12215)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](70a41aba78...c55203cfde)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 13:08:38 -03:00
Ran Benita
6e248818b9 Merge pull request #12212 from pytest-dev/update-plugin-list/patch-089116bdf
[automated] Update plugin list
2024-04-15 17:59:30 +03:00
dependabot[bot]
0644e467fb build(deps): Bump hynek/build-and-inspect-python-package (#12214)
Bumps [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) from 2.2.1 to 2.4.0.
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.2.1...v2.4.0)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 10:33:41 +02:00
pytest bot
5f6e825a26 [automated] Update plugin list 2024-04-14 00:21:52 +00:00
HolyMagician03-UMich
089116bdff short test summary: do not truncate text when -vv is given
Fix #11777

---------

Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
2024-04-10 08:07:18 -03:00
Ran Benita
1f001cd105 Merge pull request #12199 from tamird/mkdir-test-init
Add test for Cache.mkdir
2024-04-09 22:26:36 +03:00
Bruno Oliveira
99890636bf Refine how we detect namespace packages (#12169)
Previously we used a hand crafted approach to detect namespace packages, however we should rely on ``importlib`` to detect them for us.

Fix #12112

---------

Co-authored-by: Ran Benita <ran@unusedvar.com>
2024-04-09 13:21:51 -03:00
dependabot[bot]
17fc20af78 build(deps): Bump hynek/build-and-inspect-python-package (#12195)
Bumps [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) from 2.0.2 to 2.2.1.
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.0.2...v2.2.1)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
2024-04-09 10:04:42 -03:00
pre-commit-ci[bot]
1ca59dfd6b [pre-commit.ci] pre-commit autoupdate (#12200)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.5.0 → v4.6.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.5.0...v4.6.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-04-09 09:55:05 -03:00
Tamir Duberstein
e06c337bd8 Add test for Cache.mkdir 2024-04-08 15:08:18 +01:00
Tamir Duberstein
a02cc0ad1b Add types 2024-04-08 15:08:17 +01:00
dependabot[bot]
74f05648d5 build(deps): Bump django in /testing/plugins_integration (#12196)
Bumps [django](https://github.com/django/django) from 5.0.3 to 5.0.4.
- [Commits](https://github.com/django/django/compare/5.0.3...5.0.4)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-08 12:15:01 +02:00
github-actions[bot]
f75dd87eb7 [automated] Update plugin list (#12193)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-04-07 08:57:04 -03:00
Ran Benita
5acc3f86ac Merge pull request #12168 from tamird/fix-gitignore-missing
Initialize cache directory in isolation
2024-04-06 23:18:19 +03:00
Florian Bruhin
4e3dd21506 Improve stale bot messages (#12185)
To avoid confusion, see e.g. https://github.com/pytest-dev/pytest/issues/12123#issuecomment-2035283472

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-04-04 00:24:34 +02:00
Tamir Duberstein
2e65f4e3ac Initialize cache directory in isolation
Creating and initializing the cache directory is interruptible; this
avoids a pathological case where interrupting a cache write can cause
the cache directory to never be properly initialized with its supporting
files.

Unify `Cache.mkdir` with `Cache.set` while I'm here so the former also
properly initializes the cache directory.

Closes #12167.
2024-04-03 16:26:43 +01:00
Manuel López-Ibáñez
cc588d1a1a Doc: import pytest in conftest.py example in doctest.rst (#12181) 2024-04-03 09:02:09 -03:00
Ran Benita
b63bea9523 Merge pull request #11910 from bluetech/move-show-fixtures
Move show-fixtures code from python.py to fixtures.py
2024-04-02 23:28:32 +03:00
Ran Benita
a10ddad774 Merge pull request #12171 from pytest-dev/update-plugin-list/patch-12e061e2e
[automated] Update plugin list
2024-04-02 23:15:16 +03:00
Ran Benita
1188ea2608 Move show-fixtures code from python.py to fixtures.py
It makes more sense, also, we have a long term idea of generalizing
fixture support to items defined by other plugins, not just python, in
which case `--fixtures` would definitely not be python-plugin
specific.
2024-04-02 23:02:10 +03:00
Ran Benita
aa5324560b Merge pull request #12159 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-cov-5.0.0
build(deps): Bump pytest-cov from 4.1.0 to 5.0.0 in /testing/plugins_integration
2024-04-02 22:59:20 +03:00
Ronny Pfannschmidt
381593ccf0 Merge pull request #12176 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-04-02 08:41:02 +02:00
pre-commit-ci[bot]
9d6b5f8772 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.4 → v0.3.5](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.4...v0.3.5)
2024-04-01 22:13:25 +00:00
Pierre Sassoulas
44895289c7 Merge pull request #12170 from Pierre-Sassoulas/small-performance-improvments
Small performance/readability improvments when iterating dictionnary with ``keys()``
2024-03-31 15:15:03 +02:00
Pierre Sassoulas
1125296b53 Small performance/readability improvments when iterating dictionnary with `keys()`
Based on pylint's message ``consider-iterating-dictionary`` suggestion.
Surprisingly using a dict or set comprehension instead of a new temp var is
actually consistently slower here, which was not intuitive for me.

```python
from timeit import timeit

families = {1: {"testcase": [1, 2, 3, 5, 8]}}
attrs = {1: "a", 2: "b", 3: "c", 4: "d", 5: "e", 6: "f", 7: "g", 8: "h"}

class Old:
    def old(self):
        self.attrs = attrs
        temp_attrs = {}
        for key in self.attrs.keys():
            if key in families[1]["testcase"]:
                temp_attrs[key] = self.attrs[key]
        self.attrs = temp_attrs

class OldBis:
    def old(self):
        self.attrs = attrs
        temp_attrs = {}
        for key in self.attrs:
            if key in families[1]["testcase"]:
                temp_attrs[key] = self.attrs[key]
        self.attrs = temp_attrs

class New:
    def new(self):
        self.attrs = attrs
        self.attrs = { # Even worse with k: v for k in self.attrs.items()
            k: self.attrs[k] for k in self.attrs if k in families[1]["testcase"]
        }

if __name__ == "__main__":
    n = 1000000
    print(f"Old: {timeit(Old().old, number=n)}")
    print(f"Just removing the keys(): {timeit(OldBis().old, number=n)}")
    print(f"List comp, no temp var: {timeit(New().new, number=n)}")
```

Result:
Old: 0.9493889989680611
Just removing the keys(): 0.9042672360083088
List comp, no temp var: 0.9916125109884888

It's also true for the other example with similar benchmark, but the exact
code probably does not need to be in the commit message.
2024-03-31 14:43:07 +02:00
Pierre Sassoulas
bd9b62161a [tooling] Add a manual step to run pylint in pre-commit 2024-03-31 14:43:07 +02:00
John Litborn
e64efd8653 Don't reregister subfixture finalizer in requested fixture if value is cached (#12136) 2024-03-31 15:02:09 +03:00
pytest bot
2dd58e827e [automated] Update plugin list 2024-03-31 00:20:45 +00:00
pre-commit-ci[bot]
12e061e2e8 [pre-commit.ci] pre-commit autoupdate (#12162)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.3 → v0.3.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.3...v0.3.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-03-25 23:37:36 +01:00
dependabot[bot]
5d345f6d7f build(deps): Bump pytest-cov in /testing/plugins_integration
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.1.0 to 5.0.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.1.0...v5.0.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-25 06:32:55 +00:00
dependabot[bot]
2cba2237cd build(deps): Bump pytest-twisted in /testing/plugins_integration (#12158)
Bumps [pytest-twisted](https://github.com/pytest-dev/pytest-twisted) from 1.14.0 to 1.14.1.
- [Release notes](https://github.com/pytest-dev/pytest-twisted/releases)
- [Commits](https://github.com/pytest-dev/pytest-twisted/compare/v1.14.0...v1.14.1)

---
updated-dependencies:
- dependency-name: pytest-twisted
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 07:32:29 +01:00
dependabot[bot]
e7b048e92d build(deps): Bump pytest-mock in /testing/plugins_integration (#12160)
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.12.0 to 3.14.0.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.12.0...v3.14.0)

---
updated-dependencies:
- dependency-name: pytest-mock
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 07:30:45 +01:00
dependabot[bot]
4a8cefbf64 build(deps): Bump pytest-asyncio in /testing/plugins_integration (#12161)
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.23.5 to 0.23.6.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.23.5...v0.23.6)

---
updated-dependencies:
- dependency-name: pytest-asyncio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-25 07:30:08 +01:00
tserg
909759de2d Improve sentence in marks documentation (#12157) 2024-03-24 15:59:07 +01:00
Ran Benita
85ff5d1b54 Merge pull request #12156 from pytest-dev/update-plugin-list/patch-c650e3a94
[automated] Update plugin list
2024-03-24 16:15:46 +02:00
Ran Benita
cfc4cf074e Merge pull request #12154 from bluetech/fixtures-cache-key-idx
fixtures: stop using `request.param_index` in fixture cache key
2024-03-24 15:40:52 +02:00
pytest bot
aeae91e27d [automated] Update plugin list 2024-03-24 00:20:37 +00:00
Ran Benita
3eb16b34be fixtures: stop using request.param_index in fixture cache key
When `param` is not defined, `param_index` is always 0 (see
`_compute_fixture_value`), so no point in using it besides adding some
confusion.
2024-03-23 12:07:24 +02:00
Linghao Zhang
c650e3a94f Add research item in doc (#12147) 2024-03-21 11:21:43 -03:00
Ran Benita
470edc5884 Merge pull request #12133 from pytest-dev/dependabot/github_actions/hynek/build-and-inspect-python-package-2.0.2
build(deps): Bump hynek/build-and-inspect-python-package from 2.0.1 to 2.0.2
2024-03-20 11:21:40 +02:00
Ran Benita
07a08028de Merge pull request #12132 from pytest-dev/dependabot/github_actions/peter-evans/create-pull-request-6.0.2
build(deps): Bump peter-evans/create-pull-request from 6.0.0 to 6.0.2
2024-03-20 11:20:50 +02:00
Ran Benita
42d3960fea Merge pull request #12131 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-rerunfailures-14.0
build(deps): Bump pytest-rerunfailures from 13.0 to 14.0 in /testing/plugins_integration
2024-03-20 11:20:09 +02:00
Sebastian Meyer
e7bf216516 doc: add versionadded to ExceptionInfo.group_contains (#12141) 2024-03-19 19:54:26 -03:00
pre-commit-ci[bot]
532782a228 [pre-commit.ci] pre-commit autoupdate (#12137)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.3.2 → v0.3.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.3.2...v0.3.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-03-18 22:51:23 +00:00
dependabot[bot]
bf8b088bbe build(deps): Bump hynek/build-and-inspect-python-package
Bumps [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) from 2.0.1 to 2.0.2.
- [Release notes](https://github.com/hynek/build-and-inspect-python-package/releases)
- [Changelog](https://github.com/hynek/build-and-inspect-python-package/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hynek/build-and-inspect-python-package/compare/v2.0.1...v2.0.2)

---
updated-dependencies:
- dependency-name: hynek/build-and-inspect-python-package
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 03:12:49 +00:00
dependabot[bot]
027464975f build(deps): Bump peter-evans/create-pull-request from 6.0.0 to 6.0.2
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 6.0.0 to 6.0.2.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](b1ddad2c99...70a41aba78)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 03:12:45 +00:00
dependabot[bot]
6be5c9d88c build(deps): Bump pytest-rerunfailures in /testing/plugins_integration
Bumps [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) from 13.0 to 14.0.
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/13.0...14.0)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-18 03:02:28 +00:00
Ran Benita
2607fe8b47 Merge pull request #12126 from pytest-dev/update-plugin-list/patch-70c11582a
[automated] Update plugin list
2024-03-17 21:22:45 +02:00
pytest bot
2e69f31444 [automated] Update plugin list 2024-03-17 00:20:04 +00:00
John Litborn
70c11582aa Don't add fixture finalizer if the value is cached (#11833)
Fixes #1489
2024-03-16 23:45:56 +02:00
Ran Benita
c203f1615c Merge pull request #12121 from jakkdl/test_scope_fixture_caching
Add tests to ensure setup&finalization for scoped fixtures only run once.
2024-03-16 12:21:53 +02:00
jakkdl
bec0e9caf8 Add tests to ensure setup&finalization for scoped fixtures only run once. 2024-03-15 12:26:28 +01:00
Tobias Stoeckmann
2e5da5d2fb doc: fix typos (#12118)
* doc: add missing word

* doc: fix typos

Typos found with codespell
2024-03-14 16:36:11 +00:00
pre-commit-ci[bot]
c0532dda18 [pre-commit.ci] pre-commit autoupdate (#12115)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
Co-authored-by: Ran Benita <ran@unusedvar.com>
2024-03-13 15:30:18 +02:00
Ran Benita
14437788f0 Merge pull request #12108 from pytest-dev/bluetech-patch-1
doc: add versionadded to `Stash` and `StashKey`
2024-03-11 18:36:16 +02:00
Ran Benita
7eaaf370bb doc: add versionadded to Stash and StashKey
Fixes #12107
2024-03-11 18:22:16 +02:00
dependabot[bot]
6c9e107681 build(deps): Bump softprops/action-gh-release from 1 to 2 (#12106)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 11:48:06 -03:00
dependabot[bot]
122b43439c build(deps): Bump django in /testing/plugins_integration (#12103)
Bumps [django](https://github.com/django/django) from 5.0.2 to 5.0.3.
- [Commits](https://github.com/django/django/compare/5.0.2...5.0.3)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 07:05:41 +01:00
dependabot[bot]
a29ea1bc32 build(deps): Bump pypa/gh-action-pypi-publish from 1.8.12 to 1.8.14 (#12105)
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.12 to 1.8.14.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.12...v1.8.14)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-11 07:04:19 +01:00
Ran Benita
0a442a9599 doc/flaky: remove box/flaky plugin suggestion (#12100)
The plugin is abandoned and no longer working with new pytest versions.

I also reordered a bit to put pytest-rerunfailures first since it seems most maintained and is under pytest-dev.
2024-03-10 11:51:04 -03:00
Ran Benita
520ff29c07 Merge pull request #12096 from bluetech/staticmethod-instance
python: fix instance handling in static and class method tests
2024-03-10 16:29:06 +02:00
github-actions[bot]
b777b05c0e [automated] Update plugin list (#12098)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-03-10 09:57:13 -03:00
Ran Benita
b90da34a86 Merge pull request #12095 from nicoddemus/cherry-pick-release
Cherry pick release 8.1.1
2024-03-09 19:39:15 +02:00
Ran Benita
0dc0360351 python: fix instance handling in static and class method tests
and also fixes a regression in pytest 8.0.0 where `setup_method` crashes
if the class has static or class method tests.

It is allowed to have a test class with static/class methods which
request non-static/class method fixtures (including `setup_method`
xunit-fixture). I take it as a given that we need to support this
somewhat odd scenario (stdlib unittest also supports it).

This raises a question -- when a staticmethod test requests a bound
fixture, what is that fixture's `self`?

stdlib unittest says - a fresh instance for the test.

Previously, pytest said - some instance that is shared by all
static/class methods. This is definitely broken since it breaks test
isolation.

Change pytest to behave like stdlib unittest here.

In practice, this means stopping to rely on `self.obj.__self__` to get
to the instance from the test function's binding. This doesn't work
because staticmethods are not bound to anything.

Instead, keep the instance explicitly and use that.

BTW, I think this will allow us to change `Class`'s fixture collection
(`parsefactories`) to happen on the class itself instead of a class
instance, allowing us to avoid one class instantiation. But needs more
work.

Fixes #12065.
2024-03-09 19:35:54 +02:00
Bruno Oliveira
140c777590 Merge pull request #12094 from pytest-dev/release-8.1.1
Prepare release 8.1.1

(cherry picked from commit abb0cf4922)
2024-03-09 08:52:31 -03:00
Ran Benita
774f0c44e6 fixtures: only call instance property once in function
No need to compute the property multiple times.
2024-03-09 10:16:41 +02:00
Ran Benita
006058f1f9 fixtures: update outdated comment
No longer does unittest stuff. Also the rest of the sentence is not
really necessary for a docstring.
2024-03-09 10:16:41 +02:00
Ran Benita
437eb86edd Merge pull request #12092 from bluetech/fixture-cleanup
fixtures: a few more cleanups
2024-03-09 08:55:37 +02:00
Levon Saldamli
9033d4d3ff Parse args from file (#12085)
Co-authored-by: Ran Benita <ran@unusedvar.com>
Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
2024-03-09 08:51:52 +02:00
Bruno Oliveira
2ccc73be9a Merge pull request #12087 from nicoddemus/revert-path-deprecations
Revert legacy path removals
2024-03-08 20:06:47 -03:00
Ran Benita
ff551b7685 fixtures: simplify a bit of code 2024-03-09 00:02:06 +02:00
Ran Benita
f5de111357 fixtures: check scope mismatch in getfixturevalue already-cached case
This makes sure the scope is always compatible, and also allows using
`getfixturevalue` in `pytest_fixture_setup` so less internal magic.
2024-03-09 00:02:06 +02:00
Ran Benita
71671f60b5 fixtures: improve fixture scope mismatch message
- Separate the requesting from the requested.

- Avoid the term "factory", I think most people don't distinguish
  between "fixture" and "fixture function" (i.e. "factory") and would
  find the term "factory" unfamiliar.
2024-03-09 00:02:06 +02:00
Ran Benita
a9d1f55a0f fixtures: simplify scope checking
There are two non-optimal things in the current way scope checking is
done:

- It runs on `SubRequest`, but doesn't use the `SubRequest's scope,
  which is confusing. Instead it takes `invoking_scope` and
  `requested_scope`.

- Because `_check_scope` is only defined on `SubRequest` and not
  `TopRequest`, `_compute_fixture_value` first creates the `SubRequest`
  only then checks the scope (hence the need for the previous point).

Instead, also define `_check_scope` on `TopRequest` (always valid), and
remove `invoking_scope`, using `self._scope` instead.
2024-03-09 00:02:06 +02:00
Ran Benita
83887c066b Merge pull request #12089 from bluetech/unittest-obj
unittest: make `obj` work more like `Function`/`Class`
2024-03-08 20:50:27 +02:00
Ran Benita
4a8bd5f2e2 Merge pull request #12057 from pytest-dev/dependabot/github_actions/pypa/gh-action-pypi-publish-1.8.12
build(deps): Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.8.12
2024-03-08 14:07:30 +02:00
Ran Benita
51595d9000 Merge pull request #12056 from pytest-dev/dependabot/pip/testing/plugins_integration/twisted-24.3.0
build(deps): Bump twisted from 23.10.0 to 24.3.0 in /testing/plugins_integration
2024-03-08 14:07:02 +02:00
Ran Benita
1a5e0eb71d unittest: make obj work more like Function/Class
Previously, the `obj` of a `TestCaseFunction` (the unittest plugin item
type) was the unbound method. This is unlike regular `Class` where the
`obj` is a bound method to a fresh instance.

This difference necessitated several special cases in in places outside
of the unittest plugin, such as `FixtureDef` and `FixtureRequest`, and
made things a bit harder to understand.

Instead, match how the python plugin does it, including collecting
fixtures from a fresh instance.

The downside is that now this instance for fixture-collection is kept
around in memory, but it's the same as `Class` so nothing new. Users
should only initialize stuff in `setUp`/`setUpClass` and similar
methods, and not in `__init__` which is generally off-limits in
`TestCase` subclasses.

I am not sure why there was a difference in the first place, though I
will say the previous unittest approach is probably the preferable one,
but first let's get consistency.
2024-03-08 14:03:26 +02:00
Bruno Oliveira
221097517b Add changelog entry for #12069 2024-03-07 19:50:33 -03:00
Bruno Oliveira
dacee1f11d Revert "Remove deprecated py.path hook arguments"
This reverts commit a98f02d423.
2024-03-07 19:50:33 -03:00
Bruno Oliveira
303cd0d48a Revert "Remove deprecated py.path (fspath) node constructor arguments"
This reverts commit 6c89f9261c.
2024-03-07 19:50:33 -03:00
Bruno Oliveira
86945f9a1f Rename 'testing' extra to 'dev' (#12052)
Minor, but seems `dev` is more standard for the development extras than `testing`, being the default for tools like `poetry`.
2024-03-07 19:12:19 -03:00
Bruno Oliveira
03e54712dd Do not import duplicated modules with --importmode=importlib (#12074)
Regression brought up by #11475.
2024-03-04 12:44:56 -03:00
Bruno Oliveira
18c0e5075f Merge pull request #12075 from nicoddemus/yank-8.1.0
Yank version 8.1.0
2024-03-04 12:21:12 -03:00
Bruno Oliveira
6ee02a3e6c Yank version 8.1.0
Related to #12069
2024-03-04 12:18:14 -03:00
Bruno Oliveira
0951cbc6e4 Merge pull request #12070 from RonnyPfannschmidt/lift
add myself as tidelifter
2024-03-04 10:01:53 -03:00
Ronny Pfannschmidt
37b3401091 add myself to tidelift 2024-03-04 12:59:54 +01:00
dependabot[bot]
13558b9f53 build(deps): Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.8.12
Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.11 to 1.8.12.
- [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases)
- [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.11...v1.8.12)

---
updated-dependencies:
- dependency-name: pypa/gh-action-pypi-publish
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 03:47:21 +00:00
dependabot[bot]
23bdc643c9 build(deps): Bump twisted in /testing/plugins_integration
Bumps [twisted](https://github.com/twisted/twisted) from 23.10.0 to 24.3.0.
- [Release notes](https://github.com/twisted/twisted/releases)
- [Changelog](https://github.com/twisted/twisted/blob/trunk/NEWS.rst)
- [Commits](https://github.com/twisted/twisted/compare/twisted-23.10.0...twisted-24.3.0)

---
updated-dependencies:
- dependency-name: twisted
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-04 03:25:34 +00:00
140 changed files with 3915 additions and 1473 deletions

View File

@@ -19,6 +19,11 @@ jobs:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
timeout-minutes: 10
# Required by attest-build-provenance-github.
permissions:
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
with:
@@ -26,7 +31,9 @@ jobs:
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v2.0.1
uses: hynek/build-and-inspect-python-package@v2.5.0
with:
attest-build-provenance-github: 'true'
deploy:
if: github.repository == 'pytest-dev/pytest'
@@ -47,7 +54,7 @@ jobs:
path: dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@v1.8.11
uses: pypa/gh-action-pypi-publish@v1.8.14
- name: Push tag
run: |
@@ -94,7 +101,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@v1
uses: softprops/action-gh-release@v2
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 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."
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."
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.0.1
uses: hynek/build-and-inspect-python-package@v2.5.0
build:
needs: [package]
@@ -55,6 +55,7 @@ jobs:
"windows-py310",
"windows-py311",
"windows-py312",
"windows-py313",
"ubuntu-py38",
"ubuntu-py38-pluggy",
@@ -63,12 +64,14 @@ jobs:
"ubuntu-py310",
"ubuntu-py311",
"ubuntu-py312",
"ubuntu-py313",
"ubuntu-pypy3",
"macos-py38",
"macos-py39",
"macos-py310",
"macos-py312",
"macos-py313",
"doctesting",
"plugins",
@@ -97,9 +100,13 @@ jobs:
os: windows-latest
tox_env: "py311"
- name: "windows-py312"
python: "3.12-dev"
python: "3.12"
os: windows-latest
tox_env: "py312"
- name: "windows-py313"
python: "3.13-dev"
os: windows-latest
tox_env: "py313"
- name: "ubuntu-py38"
python: "3.8"
@@ -128,10 +135,15 @@ jobs:
tox_env: "py311"
use_coverage: true
- name: "ubuntu-py312"
python: "3.12-dev"
python: "3.12"
os: ubuntu-latest
tox_env: "py312"
use_coverage: true
- name: "ubuntu-py313"
python: "3.13-dev"
os: ubuntu-latest
tox_env: "py313"
use_coverage: true
- name: "ubuntu-pypy3"
python: "pypy-3.8"
os: ubuntu-latest
@@ -151,9 +163,13 @@ jobs:
os: macos-latest
tox_env: "py310-xdist"
- name: "macos-py312"
python: "3.12-dev"
python: "3.12"
os: macos-latest
tox_env: "py312-xdist"
- name: "macos-py313"
python: "3.13-dev"
os: macos-latest
tox_env: "py313-xdist"
- name: "plugins"
python: "3.12"

View File

@@ -46,7 +46,7 @@ jobs:
run: python scripts/update-plugin-list.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@b1ddad2c994a25fbc81a28b3ec0e368bb2021c50
uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83
with:
commit-message: '[automated] Update plugin list'
author: 'pytest bot <pytestbot@users.noreply.github.com>'

1
.gitignore vendored
View File

@@ -51,6 +51,7 @@ 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.2.2"
rev: "v0.4.1"
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -26,7 +26,7 @@ repos:
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
rev: v1.9.0
hooks:
- id: mypy
files: ^(src/|testing/|scripts/)
@@ -34,7 +34,7 @@ repos:
additional_dependencies:
- iniconfig>=1.1.0
- attrs>=19.2.0
- pluggy>=1.4.0
- pluggy>=1.5.0
- packaging
- tomli
- types-pkg_resources
@@ -43,13 +43,20 @@ repos:
# on <3.11
- exceptiongroup>=1.0.0rc8
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "1.7.0"
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

11
AUTHORS
View File

@@ -36,6 +36,7 @@ Andrey Paramonov
Andrzej Klajnert
Andrzej Ostrowski
Andy Freeland
Anita Hammer
Anthon van der Neut
Anthony Shaw
Anthony Sottile
@@ -101,6 +102,7 @@ Cyrus Maden
Damian Skrzypczak
Daniel Grana
Daniel Hahler
Daniel Miller
Daniel Nuri
Daniel Sánchez Castelló
Daniel Valenzuela Zenteno
@@ -118,6 +120,7 @@ Daw-Ran Liou
Debi Mishra
Denis Kirisov
Denivy Braiam Rück
Dheeraj C K
Dhiren Serai
Diego Russo
Dmitry Dygalo
@@ -137,6 +140,7 @@ Endre Galaczi
Eric Hunsberger
Eric Liu
Eric Siegerman
Eric Yuan
Erik Aronesty
Erik Hasse
Erik M. Bray
@@ -188,6 +192,7 @@ Jake VanderPlas
Jakob van Santen
Jakub Mitoraj
James Bourbeau
James Frost
Jan Balster
Janne Vanhala
Jason R. Coombs
@@ -235,6 +240,7 @@ Kyle Altendorf
Lawrence Mitchell
Lee Kamentsky
Lev Maximov
Levon Saldamli
Lewis Cowles
Llandy Riveron Del Risco
Loic Esteve
@@ -284,6 +290,7 @@ Mike Lundy
Milan Lesnek
Miro Hrončok
mrbean-bremen
Nathan Goldbaum
Nathaniel Compton
Nathaniel Waisbrot
Ned Batchelder
@@ -318,6 +325,7 @@ Pierre Sassoulas
Pieter Mulder
Piotr Banaszkiewicz
Piotr Helm
Poulami Sau
Prakhar Gurunani
Prashant Anand
Prashant Sharma
@@ -352,6 +360,7 @@ Sadra Barikbin
Saiprasad Kale
Samuel Colvin
Samuel Dion-Girardeau
Samuel Jirovec
Samuel Searles-Bryant
Samuel Therrien (Avasam)
Samuele Pedroni
@@ -431,9 +440,11 @@ Xixi Zhao
Xuan Luong
Xuecong Liao
Yannick Péroux
Yao Xiao
Yoav Caspi
Yuliang Shao
Yusuke Kadowaki
Yutian Li
Yuval Shimon
Zac Hatfield-Dodds
Zachary Kneupper

View File

@@ -5,10 +5,6 @@ Contribution getting started
Contributions are highly welcomed and appreciated. Every little bit of help counts,
so do not hesitate!
.. contents::
:depth: 2
:backlinks: none
.. _submitfeedback:
@@ -297,12 +293,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 ``testing`` extra::
an editable install with the ``dev`` extra::
$ python3 -m venv .venv
$ source .venv/bin/activate # Linux
$ .venv/Scripts/activate.bat # Windows
$ pip install -e ".[testing]"
$ pip install -e ".[dev]"
Afterwards, you can edit the files and run pytest normally::

View File

@@ -25,6 +25,7 @@ The current list of contributors receiving funding are:
* `@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
@@ -56,3 +57,4 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the
.. _`@nicoddemus`: https://github.com/nicoddemus
.. _`@The-Compiler`: https://github.com/The-Compiler
.. _`@RonnyPfannschmidt`: https://github.com/RonnyPfannschmidt

View File

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

View File

@@ -0,0 +1,7 @@
<style>
.logo {text-align: center;}
</style>
<a class="logo" href="{{ pathto('contents') }}">
<img src="{{ pathto('_static/pytest1.png', 1) }}" width="70%" height="70%" text="Pytest Logo"/>
</a>

View File

@@ -1,14 +0,0 @@
{#
basic/searchbox.html with heading removed.
#}
{%- if pagename != "search" and builder != "singlehtml" %}
<div id="searchbox" style="display: none" role="search">
<div class="searchformwrapper">
<form class="search" action="{{ pathto('search') }}" method="get">
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
<input type="submit" value="{{ _('Go') }}" />
</form>
</div>
</div>
<script>document.getElementById('searchbox').style.display = "block"</script>
{%- endif %}

View File

@@ -0,0 +1,7 @@
<style>
ul {list-style: none;}
li {margin: 0.4em 0;}
@media (min-width: 46em) {
#features {width: 50%;}
}
</style>

View File

@@ -6,6 +6,11 @@ Release announcements
:maxdepth: 2
release-8.2.2
release-8.2.1
release-8.2.0
release-8.1.2
release-8.1.1
release-8.1.0
release-8.0.2
release-8.0.1

View File

@@ -62,7 +62,7 @@ New Features
- new "-q" option which decreases verbosity and prints a more
nose/unittest-style "dot" output.
- many many more detailed improvements details
- many, many, more detailed improvements details
Fixes
-----------------------
@@ -109,7 +109,7 @@ Important Notes
in conftest.py files. They will cause nothing special.
- removed support for calling the pre-1.0 collection API of "run()" and "join"
- removed reading option values from conftest.py files or env variables.
This can now be done much much better and easier through the ini-file
This can now be done much, much, better and easier through the ini-file
mechanism and the "addopts" entry in particular.
- removed the "disabled" attribute in test classes. Use the skipping
and pytestmark mechanism to skip or xfail a test class.

View File

@@ -4,7 +4,7 @@ pytest-2.2.2: bug fixes
pytest-2.2.2 (updated to 2.2.3 to fix packaging issues) is a minor
backward-compatible release of the versatile py.test testing tool. It
contains bug fixes and a few refinements particularly to reporting with
"--collectonly", see below for betails.
"--collectonly", see below for details.
For general information see here:

View File

@@ -181,7 +181,7 @@ Bug fixes:
partially failed (finalizers would not always be called before)
- fix issue320 - fix class scope for fixtures when mixed with
module-level functions. Thanks Anatloy Bubenkoff.
module-level functions. Thanks Anatoly Bubenkoff.
- you can specify "-q" or "-qq" to get different levels of "quieter"
reporting (thanks Katarzyna Jachim)

View File

@@ -83,7 +83,7 @@ holger krekel
Thanks Ralph Schmitt for the precise failure example.
- fix issue244 by implementing special index for parameters to only use
indices for paramentrized test ids
indices for parametrized test ids
- fix issue287 by running all finalizers but saving the exception
from the first failing finalizer and re-raising it so teardown will

View File

@@ -73,7 +73,7 @@ holger krekel
- cleanup setup.py a bit and specify supported versions. Thanks Jurko
Gospodnetic for the PR.
- change XPASS colour to yellow rather then red when tests are run
- change XPASS colour to yellow rather than red when tests are run
with -v.
- fix issue473: work around mock putting an unbound method into a class

View File

@@ -55,7 +55,7 @@ holger krekel
github. See https://pytest.org/en/stable/contributing.html .
Thanks to Anatoly for pushing and initial work on this.
- fix issue650: new option ``--docttest-ignore-import-errors`` which
- fix issue650: new option ``--doctest-ignore-import-errors`` which
will turn import errors in doctests into skips. Thanks Charles Cloud
for the complete PR.

View File

@@ -0,0 +1,18 @@
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

@@ -0,0 +1,18 @@
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

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

View File

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

View File

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

View File

@@ -49,7 +49,7 @@ place on 20th, 21st, 22nd, 24th and 25th. On the 23rd we took a break
day for some hot hiking in the Black Forest.
Sprint activity was organised heavily around pairing, with plenty of group
discusssions to take advantage of the high bandwidth, and lightning talks
discussions to take advantage of the high bandwidth, and lightning talks
as well.

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:527
cache -- .../_pytest/cacheprovider.py:560
Return a cache object that can persist state between testing sessions.
cache.get(key, default)
@@ -33,7 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Values can be any object handled by the json stdlib module.
capsysbinary -- .../_pytest/capture.py:1008
capsysbinary -- .../_pytest/capture.py:1003
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:1035
capfd -- .../_pytest/capture.py:1030
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:1062
capfdbinary -- .../_pytest/capture.py:1057
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:981
capsys -- .../_pytest/capture.py:976
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:737
doctest_namespace [session scope] -- .../_pytest/doctest.py:738
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:1346
pytestconfig [session scope] -- .../_pytest/fixtures.py:1338
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:283
record_property -- .../_pytest/junitxml.py:284
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:306
record_xml_attribute -- .../_pytest/junitxml.py:307
Add extra xml attributes to the tag for the calling test.
The fixture is callable with ``name, value``. The value is
automatically XML-encoded.
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:344
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:345
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
This is suitable to writing global information regarding the entire test
@@ -170,10 +170,10 @@ 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:317
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:303
Return a :class:`pytest.TempdirFactory` instance for the test session.
tmpdir -- .../_pytest/legacypath.py:324
tmpdir -- .../_pytest/legacypath.py:310
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
@@ -192,7 +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:601
caplog -- .../_pytest/logging.py:602
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:32
monkeypatch -- .../_pytest/monkeypatch.py:33
A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or
@@ -227,16 +227,16 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
To undo modifications done by the fixture in a contained scope,
use :meth:`context() <pytest.MonkeyPatch.context>`.
recwarn -- .../_pytest/recwarn.py:31
recwarn -- .../_pytest/recwarn.py:32
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
on warning categories.
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:241
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:242
Return a :class:`pytest.TempPathFactory` instance for the test session.
tmp_path -- .../_pytest/tmpdir.py:256
tmp_path -- .../_pytest/tmpdir.py:257
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.

View File

@@ -28,9 +28,183 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 8.1.0 (2024-03-03)
pytest 8.2.2 (2024-06-04)
=========================
Bug Fixes
---------
- `#12355 <https://github.com/pytest-dev/pytest/issues/12355>`_: Fix possible catastrophic performance slowdown on a certain parametrization pattern involving many higher-scoped parameters.
- `#12367 <https://github.com/pytest-dev/pytest/issues/12367>`_: Fix a regression in pytest 8.2.0 where unittest class instances (a fresh one is created for each test) were not released promptly on test teardown but only on session teardown.
- `#12381 <https://github.com/pytest-dev/pytest/issues/12381>`_: Fix possible "Directory not empty" crashes arising from concurent cache dir (``.pytest_cache``) creation. Regressed in pytest 8.2.0.
Improved Documentation
----------------------
- `#12290 <https://github.com/pytest-dev/pytest/issues/12290>`_: Updated Sphinx theme to use Furo instead of Flask, enabling Dark mode theme.
- `#12356 <https://github.com/pytest-dev/pytest/issues/12356>`_: Added a subsection to the documentation for debugging flaky tests to mention
lack of thread safety in pytest as a possible source of flakyness.
- `#12363 <https://github.com/pytest-dev/pytest/issues/12363>`_: The documentation webpages now links to a canonical version to reduce outdated documentation in search engine results.
pytest 8.2.1 (2024-05-19)
=========================
Improvements
------------
- `#12334 <https://github.com/pytest-dev/pytest/issues/12334>`_: Support for Python 3.13 (beta1 at the time of writing).
Bug Fixes
---------
- `#12120 <https://github.com/pytest-dev/pytest/issues/12120>`_: Fix `PermissionError` crashes arising from directories which are not selected on the command-line.
- `#12191 <https://github.com/pytest-dev/pytest/issues/12191>`_: Keyboard interrupts and system exits are now properly handled during the test collection.
- `#12300 <https://github.com/pytest-dev/pytest/issues/12300>`_: Fixed handling of 'Function not implemented' error under squashfuse_ll, which is a different way to say that the mountpoint is read-only.
- `#12308 <https://github.com/pytest-dev/pytest/issues/12308>`_: Fix a regression in pytest 8.2.0 where the permissions of automatically-created ``.pytest_cache`` directories became ``rwx------`` instead of the expected ``rwxr-xr-x``.
Trivial/Internal Changes
------------------------
- `#12333 <https://github.com/pytest-dev/pytest/issues/12333>`_: pytest releases are now attested using the recent `Artifact Attestation <https://github.blog/2024-05-02-introducing-artifact-attestations-now-in-public-beta/>`_ support from GitHub, allowing users to verify the provenance of pytest's sdist and wheel artifacts.
pytest 8.2.0 (2024-04-27)
=========================
Breaking Changes
----------------
- `#12089 <https://github.com/pytest-dev/pytest/pull/12089>`_: pytest now requires that :class:`unittest.TestCase` subclasses can be instantiated freely using ``MyTestCase('runTest')``.
If the class doesn't allow this, you may see an error during collection such as ``AttributeError: 'MyTestCase' object has no attribute 'runTest'``.
Classes which do not override ``__init__``, or do not access the test method in ``__init__`` using ``getattr`` or similar, are unaffected.
Classes which do should take care to not crash when ``"runTest"`` is given, as is shown in `unittest.TestCases's implementation <https://github.com/python/cpython/blob/51aefc5bf907ddffaaf083ded0de773adcdf08c8/Lib/unittest/case.py#L419-L426>`_.
Alternatively, consider using :meth:`setUp <unittest.TestCase.setUp>` instead of ``__init__``.
If you run into this issue using ``tornado.AsyncTestCase``, please see `issue 12263 <https://github.com/pytest-dev/pytest/issues/12263>`_.
If you run into this issue using an abstract ``TestCase`` subclass, please see `issue 12275 <https://github.com/pytest-dev/pytest/issues/12275>`_.
Historical note: the effect of this change on custom TestCase implementations was not properly considered initially, this is why it was done in a minor release. We apologize for the inconvenience.
Deprecations
------------
- `#12069 <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
--------
@@ -55,7 +229,7 @@ Improvements
- `#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`.
Previously 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.
@@ -86,6 +260,9 @@ Improvements
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.
@@ -100,7 +277,6 @@ Bug Fixes
- `#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
----------------------
@@ -120,6 +296,28 @@ Trivial/Internal Changes
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)
=========================
@@ -1274,7 +1472,7 @@ Deprecations
``__init__`` method, they should take ``**kwargs``. See
:ref:`uncooperative-constructors-deprecated` for details.
Note that a deprection warning is only emitted when there is a conflict in the
Note that a deprecation warning is only emitted when there is a conflict in the
arguments pytest expected to pass. This deprecation was already part of pytest
7.0.0rc1 but wasn't documented.
@@ -1316,7 +1514,7 @@ Breaking Changes
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffected.
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
@@ -1824,7 +2022,7 @@ Bug Fixes
the ``tmp_path``/``tmpdir`` fixture). Now the directories are created with
private permissions.
pytest used to silently use a pre-existing ``/tmp/pytest-of-<username>`` directory,
pytest used to silently use a preexisting ``/tmp/pytest-of-<username>`` directory,
even if owned by another user. This means another user could pre-create such a
directory and gain control of another user's temporary directory. Now such a
condition results in an error.
@@ -2551,7 +2749,7 @@ Features
also changes ``sys.modules`` as a side-effect), which works but has a number of drawbacks, like requiring test modules
that don't live in packages to have unique names (as they need to reside under a unique name in ``sys.modules``).
``--import-mode=importlib`` uses more fine grained import mechanisms from ``importlib`` which don't
``--import-mode=importlib`` uses more fine-grained import mechanisms from ``importlib`` which don't
require pytest to change ``sys.path`` or ``sys.modules`` at all, eliminating much of the drawbacks
of the previous mode.
@@ -2568,7 +2766,7 @@ Improvements
------------
- :issue:`4375`: The ``pytest`` command now suppresses the ``BrokenPipeError`` error message that
is printed to stderr when the output of ``pytest`` is piped and and the pipe is
is printed to stderr when the output of ``pytest`` is piped and the pipe is
closed by the piped-to program (common examples are ``less`` and ``head``).
@@ -2870,7 +3068,7 @@ Breaking Changes
This hook has been marked as deprecated and not been even called by pytest for over 10 years now.
- :issue:`6673`: Reversed / fix meaning of "+/-" in error diffs. "-" means that sth. expected is missing in the result and "+" means that there are unexpected extras in the result.
- :issue:`6673`: Reversed / fix meaning of "+/-" in error diffs. "-" means that something expected is missing in the result and "+" means that there are unexpected extras in the result.
- :issue:`6737`: The ``cached_result`` attribute of ``FixtureDef`` is now set to ``None`` when
@@ -4475,7 +4673,7 @@ Bug Fixes
Improved Documentation
----------------------
- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability liminations
- :issue:`4974`: Update docs for ``pytest_cmdline_parse`` hook to note availability limitations
@@ -6333,7 +6531,7 @@ Features
Bug Fixes
---------
- Fix hanging pexpect test on MacOS by using flush() instead of wait().
- Fix hanging pexpect test on macOS by using flush() instead of wait().
(:issue:`2022`)
- Fix restoring Python state after in-process pytest runs with the
@@ -6381,7 +6579,7 @@ Trivial/Internal Changes
------------------------
- Show a simple and easy error when keyword expressions trigger a syntax error
(for example, ``"-k foo and import"`` will show an error that you can not use
(for example, ``"-k foo and import"`` will show an error that you cannot use
the ``import`` keyword in expressions). (:issue:`2953`)
- Change parametrized automatic test id generation to use the ``__name__``
@@ -8157,7 +8355,7 @@ time or change existing behaviors in order to make them less surprising/more use
one will also have a "reprec" attribute with the recorded events/reports.
- fix monkeypatch.setattr("x.y", raising=False) to actually not raise
if "y" is not a pre-existing attribute. Thanks Florian Bruhin.
if "y" is not a preexisting attribute. Thanks Florian Bruhin.
- fix issue741: make running output from testdir.run copy/pasteable
Thanks Bruno Oliveira.
@@ -8213,7 +8411,7 @@ time or change existing behaviors in order to make them less surprising/more use
- fix issue854: autouse yield_fixtures defined as class members of
unittest.TestCase subclasses now work as expected.
Thannks xmo-odoo for the report and Bruno Oliveira for the PR.
Thanks xmo-odoo for the report and Bruno Oliveira for the PR.
- fix issue833: --fixtures now shows all fixtures of collected test files, instead of just the
fixtures declared on the first one.
@@ -8317,7 +8515,7 @@ time or change existing behaviors in order to make them less surprising/more use
github. See https://pytest.org/en/stable/contributing.html .
Thanks to Anatoly for pushing and initial work on this.
- fix issue650: new option ``--docttest-ignore-import-errors`` which
- fix issue650: new option ``--doctest-ignore-import-errors`` which
will turn import errors in doctests into skips. Thanks Charles Cloud
for the complete PR.
@@ -8505,7 +8703,7 @@ time or change existing behaviors in order to make them less surprising/more use
- cleanup setup.py a bit and specify supported versions. Thanks Jurko
Gospodnetic for the PR.
- change XPASS colour to yellow rather then red when tests are run
- change XPASS colour to yellow rather than red when tests are run
with -v.
- fix issue473: work around mock putting an unbound method into a class
@@ -8678,7 +8876,7 @@ time or change existing behaviors in order to make them less surprising/more use
Thanks Ralph Schmitt for the precise failure example.
- fix issue244 by implementing special index for parameters to only use
indices for paramentrized test ids
indices for parametrized test ids
- fix issue287 by running all finalizers but saving the exception
from the first failing finalizer and re-raising it so teardown will
@@ -8686,7 +8884,7 @@ time or change existing behaviors in order to make them less surprising/more use
it might be the cause for other finalizers to fail.
- fix ordering when mock.patch or other standard decorator-wrappings
are used with test methods. This fixues issue346 and should
are used with test methods. This fixes issue346 and should
help with random "xdist" collection failures. Thanks to
Ronny Pfannschmidt and Donald Stufft for helping to isolate it.
@@ -8943,7 +9141,7 @@ Bug fixes:
partially failed (finalizers would not always be called before)
- fix issue320 - fix class scope for fixtures when mixed with
module-level functions. Thanks Anatloy Bubenkoff.
module-level functions. Thanks Anatoly Bubenkoff.
- you can specify "-q" or "-qq" to get different levels of "quieter"
reporting (thanks Katarzyna Jachim)
@@ -9365,7 +9563,7 @@ Bug fixes:
unexpected exceptions
- fix issue47: timing output in junitxml for test cases is now correct
- fix issue48: typo in MarkInfo repr leading to exception
- fix issue49: avoid confusing error when initizaliation partially fails
- fix issue49: avoid confusing error when initialization partially fails
- fix issue44: env/username expansion for junitxml file path
- show releaselevel information in test runs for pypy
- reworked doc pages for better navigation and PDF generation
@@ -9490,7 +9688,7 @@ Bug fixes:
collection-before-running semantics were not
setup as with pytest 1.3.4. Note, however, that
the recommended and much cleaner way to do test
parametraization remains the "pytest_generate_tests"
parameterization remains the "pytest_generate_tests"
mechanism, see the docs.
2.0.0 (2010-11-25)

View File

@@ -15,9 +15,7 @@
#
# The full version, including alpha/beta/rc tags.
# The short X.Y version.
import os
import shutil
import sys
from textwrap import dedent
from typing import TYPE_CHECKING
@@ -65,7 +63,6 @@ latex_elements = {
# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
"pallets_sphinx_themes",
"pygments_pytest",
"sphinx.ext.autodoc",
"sphinx.ext.autosummary",
@@ -140,10 +137,6 @@ add_module_names = False
# output. They are ignored by default.
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@@ -200,6 +193,7 @@ nitpick_ignore = [
("py:class", "_tracing.TagTracerSub"),
("py:class", "warnings.WarningMessage"),
# Undocumented type aliases
("py:class", "LEGACY_PATH"),
("py:class", "_PluggyPlugin"),
# TypeVars
("py:class", "_pytest._code.code.E"),
@@ -215,12 +209,9 @@ nitpick_ignore = [
# -- Options for HTML output ---------------------------------------------------
sys.path.append(os.path.abspath("_themes"))
html_theme_path = ["_themes"]
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = "flask"
html_theme = "furo"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
@@ -265,18 +256,24 @@ html_favicon = "img/favicon.png"
html_sidebars = {
"index": [
"slim_searchbox.html",
"sidebar/brand.html",
"sidebar/search.html",
"sidebar/scroll-start.html",
"sidebarintro.html",
"globaltoc.html",
"links.html",
"sourcelink.html",
"sidebar/scroll-end.html",
"style.html",
],
"**": [
"slim_searchbox.html",
"sidebar/brand.html",
"sidebar/search.html",
"sidebar/scroll-start.html",
"globaltoc.html",
"relations.html",
"links.html",
"sourcelink.html",
"sidebar/scroll-end.html",
"style.html",
],
}
@@ -315,6 +312,9 @@ html_show_sourcelink = False
# Output file base name for HTML help builder.
htmlhelp_basename = "pytestdoc"
# The base URL which points to the root of the HTML documentation. It is used
# to indicate the location of document using the canonical link relation (#12363).
html_baseurl = "https://docs.pytest.org/en/stable/"
# -- Options for LaTeX output --------------------------------------------------
@@ -336,10 +336,6 @@ latex_documents = [
)
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
latex_logo = "img/pytest1.png"
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False
@@ -394,7 +390,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 shat should be inserted after the pages created by sphinx.
# HTML files that should be inserted after the pages created by sphinx.
# The format is a list of tuples containing the path and title.
# epub_post_files = []

View File

@@ -7,10 +7,6 @@ This page lists all pytest features that are currently deprecated or have been r
The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives
should be used instead.
.. contents::
:depth: 3
:local:
Deprecated Features
-------------------
@@ -19,7 +15,80 @@ 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>`.
.. _legacy-path-hooks-deprecated:
.. _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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -62,6 +131,33 @@ Changed ``hookwrapper`` attributes:
* ``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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -208,73 +304,6 @@ an appropriate period of deprecation has passed.
Some breaking changes which could not be deprecated are also listed.
.. _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.
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
.. versionremoved:: 8.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).
.. _nose-deprecation:
Support for tests written for nose
@@ -429,7 +458,7 @@ Now :class:`~pytest.Class` collects the test methods directly.
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
using a check such as ``if isinstance(node, Instance): return``.
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
However, to keep such uses working, a dummy type has been instanced in ``pytest.Instance`` and ``_pytest.python.Instance``,
and importing it emits a deprecation warning. This was removed in pytest 8.

View File

@@ -21,7 +21,7 @@ class ManifestDirectory(pytest.Directory):
@pytest.hookimpl
def pytest_collect_directory(path, parent):
# Use our custom collector for directories containing a `mainfest.json` file.
# Use our custom collector for directories containing a `manifest.json` file.
if path.joinpath("manifest.json").is_file():
return ManifestDirectory.from_parent(parent=parent, path=path)
# Otherwise fallback to the standard behavior.

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-195>
<Dir parametrize.rst-200>
<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-195>
<Dir parametrize.rst-200>
<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-195>
<Dir parametrize.rst-200>
<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-196>
<Dir pythoncollection.rst-201>
<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-196>
<Dir pythoncollection.rst-201>
<Dir CWD>
<Module pythoncollection.py>
<Function test_function>

View File

@@ -18,7 +18,7 @@ System state
Broadly speaking, a flaky test indicates that the test relies on some system state that is not being appropriately controlled - the test environment is not sufficiently isolated. Higher level tests are more likely to be flaky as they rely on more state.
Flaky tests sometimes appear when a test suite is run in parallel (such as use of pytest-xdist). This can indicate a test is reliant on test ordering.
Flaky tests sometimes appear when a test suite is run in parallel (such as use of `pytest-xdist`_). This can indicate a test is reliant on test ordering.
- Perhaps a different test is failing to clean up after itself and leaving behind data which causes the flaky test to fail.
- The flaky test is reliant on data from a previous test that doesn't clean up after itself, and in parallel runs that previous test is not always present
@@ -30,9 +30,22 @@ Overly strict assertion
Overly strict assertions can cause problems with floating point comparison as well as timing issues. :func:`pytest.approx` is useful here.
Thread safety
~~~~~~~~~~~~~
Pytest features
^^^^^^^^^^^^^^^
pytest is single-threaded, executing its tests always in the same thread, sequentially, never spawning any threads itself.
Even in case of plugins which run tests in parallel, for example `pytest-xdist`_, usually work by spawning multiple *processes* and running tests in batches, without using multiple threads.
It is of course possible (and common) for tests and fixtures to spawn threads themselves as part of their testing workflow (for example, a fixture that starts a server thread in the background, or a test which executes production code that spawns threads), but some care must be taken:
* Make sure to eventually wait on any spawned threads -- for example at the end of a test, or during the teardown of a fixture.
* Avoid using primitives provided by pytest (:func:`pytest.warns`, :func:`pytest.raises`, etc) from multiple threads, as they are not thread-safe.
If your test suite uses threads and your are seeing flaky test results, do not discount the possibility that the test is implicitly using global state in pytest itself.
Related features
^^^^^^^^^^^^^^^^
Xfail strict
~~~~~~~~~~~~
@@ -52,10 +65,9 @@ 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:
@@ -106,7 +118,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
^^^^^^^^^
@@ -124,3 +136,6 @@ Resources
* `Flaky Tests at Google and How We Mitigate Them <https://testing.googleblog.com/2016/05/flaky-tests-at-google-and-how-we.html>`_ by John Micco, 2016
* `Where do Google's flaky tests come from? <https://testing.googleblog.com/2017/04/where-do-our-flaky-tests-come-from.html>`_ by Jeff Listfield, 2017
.. _pytest-xdist: https://github.com/pytest-dev/pytest-xdist

View File

@@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 8.1.0
pytest 8.2.2
.. _`simpletest`:
@@ -274,7 +274,7 @@ Continue reading
Check out additional pytest resources to help you customize tests for your unique workflow:
* ":ref:`usage`" for command line invocation examples
* ":ref:`existingtestsuite`" for working with pre-existing tests
* ":ref:`existingtestsuite`" for working with preexisting tests
* ":ref:`mark`" for information on the ``pytest.mark`` mechanism
* ":ref:`fixtures`" for providing a functional baseline to your tests
* ":ref:`plugins`" for managing and writing plugins

View File

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

View File

@@ -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-214>
<Dir fixtures.rst-219>
<Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]>

View File

@@ -294,9 +294,47 @@ Now if we increase verbosity even more:
test_verbosity_example.py:19: AssertionError
========================= short test summary info ==========================
FAILED test_verbosity_example.py::test_words_fail - AssertionError: asser...
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: ass...
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: a...
FAILED test_verbosity_example.py::test_words_fail - AssertionError: assert ['banana', 'apple', 'grapes', 'melon', 'kiwi'] == ['banana', 'apple', 'orange', 'melon', 'kiwi']
At index 2 diff: 'grapes' != 'orange'
Full diff:
[
'banana',
'apple',
- 'orange',
? ^ ^^
+ 'grapes',
? ^ ^ +
'melon',
'kiwi',
]
FAILED test_verbosity_example.py::test_numbers_fail - AssertionError: assert {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4} == {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
Common items:
{'0': 0}
Left contains 4 more items:
{'1': 1, '2': 2, '3': 3, '4': 4}
Right contains 4 more items:
{'10': 10, '20': 20, '30': 30, '40': 40}
Full diff:
{
'0': 0,
- '10': 10,
? - -
+ '1': 1,
- '20': 20,
? - -
+ '2': 2,
- '30': 30,
? - -
+ '3': 3,
- '40': 40,
? - -
+ '4': 4,
}
FAILED test_verbosity_example.py::test_long_text_fail - AssertionError: assert 'hello world' in 'Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet '
======================= 3 failed, 1 passed in 0.12s ========================
Notice now that:

View File

@@ -17,7 +17,8 @@ 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.
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>`).
**Run tests in a module**
@@ -91,6 +92,28 @@ 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,
supressing it, or raising a different exception entirely.
suppressing it, or raising a different exception entirely.
For more information, consult the
:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.

View File

@@ -1,16 +1,16 @@
:orphan:
.. sidebar:: Next Open Trainings and Events
- `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 2024 (`date poll <https://nuudel.digitalcourage.de/2tEsEpRcwMNcAXVO>`_)
Also see :doc:`previous talks and blogposts <talks>`.
.. _features:
.. sidebar:: **Next Open Trainings and Events**
- `pytest development sprint <https://github.com/pytest-dev/sprint>`_, **June 17th -- 22nd 2024**, Klaus (AT) / Remote
- `pytest tips and tricks for a better testsuite <https://ep2024.europython.eu/session/pytest-tips-and-tricks-for-a-better-testsuite>`_, at `Europython 2024 <https://ep2024.europython.eu/>`_, **July 8th -- 14th 2024** (3h), Prague (CZ)
- `pytest: Professionelles Testen (nicht nur) für Python <https://pretalx.com/workshoptage-2024/talk/9VUHYB/>`_, at `CH Open Workshoptage <https://workshoptage.ch/>`_, **September 2nd 2024**, HSLU Rotkreuz (CH)
- `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), **March 4th -- 6th 2025**, Leipzig (DE) / Remote
Also see :doc:`previous talks and blogposts <talks>`
pytest: helps you write better programs
=======================================
@@ -24,7 +24,6 @@ scale to support complex functional testing for applications and libraries.
**PyPI package name**: :pypi:`pytest`
A quick example
---------------
@@ -87,7 +86,7 @@ Features
Documentation
-------------
* :ref:`Get started <get-started>` - install pytest and grasp its basics just twenty minutes
* :ref:`Get started <get-started>` - install pytest and grasp its basics in 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

@@ -8,7 +8,7 @@ If you used older version of the ``py`` distribution (which
included the py.test command line tool and Python name space)
you accessed helpers and possibly collection classes through
the ``py.test`` Python namespaces. The new ``pytest``
Python module flaty provides the same objects, following
Python module flatly provides the same objects, following
these renaming rules::
py.test.XYZ -> pytest.XYZ

View File

@@ -39,7 +39,7 @@ Built-in fixtures
Store and retrieve values across pytest runs.
:fixture:`doctest_namespace`
Provide a dict injected into the docstests namespace.
Provide a dict injected into the doctests namespace.
:fixture:`monkeypatch`
Temporarily modify classes, functions, dictionaries,

File diff suppressed because it is too large Load Diff

View File

@@ -7,9 +7,6 @@ API Reference
This page contains the full reference to pytest's API.
.. contents::
:depth: 3
:local:
Constants
---------
@@ -59,11 +56,19 @@ pytest.fail
.. autofunction:: pytest.fail(reason, [pytrace=True, msg=None])
.. class:: pytest.fail.Exception
The exception raised by :func:`pytest.fail`.
pytest.skip
~~~~~~~~~~~
.. autofunction:: pytest.skip(reason, [allow_module_level=False, msg=None])
.. class:: pytest.skip.Exception
The exception raised by :func:`pytest.skip`.
.. _`pytest.importorskip ref`:
pytest.importorskip
@@ -76,11 +81,19 @@ pytest.xfail
.. autofunction:: pytest.xfail
.. class:: pytest.xfail.Exception
The exception raised by :func:`pytest.xfail`.
pytest.exit
~~~~~~~~~~~
.. autofunction:: pytest.exit(reason, [returncode=None, msg=None])
.. class:: pytest.exit.Exception
The exception raised by :func:`pytest.exit`.
pytest.main
~~~~~~~~~~~
@@ -136,7 +149,7 @@ pytest.freeze_includes
Marks
-----
Marks can be used apply meta data to *test functions* (but not fixtures), which can then be accessed by
Marks can be used to apply metadata to *test functions* (but not fixtures), which can then be accessed by
fixtures or plugins.
@@ -246,9 +259,10 @@ Marks a test function as *expected to fail*.
to specify ``reason`` (see :ref:`condition string <string conditions>`).
:keyword str reason:
Reason why the test function is marked as xfail.
:keyword Type[Exception] raises:
:keyword raises:
Exception class (or tuple of classes) expected to be raised by the test function; other exceptions will fail the test.
Note that subclasses of the classes passed will also result in a match (similar to how the ``except`` statement works).
:type raises: Type[:py:exc:`Exception`]
:keyword bool run:
Whether the test function should actually be executed. If ``False``, the function will always xfail and will
@@ -1117,6 +1131,11 @@ 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
@@ -1279,8 +1298,7 @@ passed multiple times. The expected format is ``name=value``. For example::
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 you are testing namespace packages installed into a virtual environment and it is important for
your packages to be imported using their full namespace package name.
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>`__.
@@ -1919,7 +1937,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
general:
-k EXPRESSION Only run tests which match the given substring
expression. An expression is a Python evaluatable
expression. An expression is a Python evaluable
expression where all names are substring-matched
against test names and their parent classes.
Example: -k 'test_method or test_other' matches all

View File

@@ -1,5 +1,4 @@
pallets-sphinx-themes
pluggy>=1.2.0
pluggy>=1.5.0
pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0
sphinx>=7
@@ -8,4 +7,5 @@ sphinxcontrib-svg2pdfconverter
# Pin packaging because it no longer handles 'latest' version, which
# is the version that is assigned to the docs.
# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045.
packaging <22
packaging
furo

View File

@@ -31,6 +31,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Testing",
"Topic :: Utilities",
@@ -43,11 +44,11 @@ dependencies = [
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
"iniconfig",
"packaging",
"pluggy<2.0,>=1.4",
"pluggy<2.0,>=1.5",
'tomli>=1; python_version < "3.11"',
]
[project.optional-dependencies]
testing = [
dev = [
"argcomplete",
"attrs>=19.2",
"hypothesis>=3.56",
@@ -165,6 +166,119 @@ lines-after-imports = 2
"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
@@ -281,6 +395,7 @@ template = "changelog/_template.rst"
showcontent = true
[tool.mypy]
files = ["src", "testing", "scripts"]
mypy_path = ["src"]
check_untyped_defs = true
disallow_any_generics = true
@@ -293,3 +408,4 @@ warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
no_implicit_reexport = true
warn_unused_ignores = true

View File

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

View File

@@ -13,6 +13,7 @@ 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

View File

@@ -1,5 +1,6 @@
# mypy: disallow-untyped-defs
"""Invoke development tasks."""
import argparse
import os
from pathlib import Path

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") # type:ignore[assignment]
version_tuple = (0, 0, "unknown")

View File

@@ -52,7 +52,7 @@ from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
if sys.version_info[:2] < (3, 11):
if sys.version_info < (3, 11):
from exceptiongroup import BaseExceptionGroup
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
@@ -279,9 +279,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,12 +378,10 @@ 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]
@@ -426,15 +424,14 @@ class Traceback(List[TracebackEntry]):
# which generates code objects that have hash/value equality
# XXX needs a test
key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
# print "checking for recursion at", key
values = cache.setdefault(key, [])
# Since Python 3.13 f_locals is a proxy, freeze it.
loc = dict(entry.frame.f_locals)
if values:
f = entry.frame
loc = f.f_locals
for otherloc in values:
if otherloc == loc:
return i
values.append(entry.frame.f_locals)
values.append(loc)
return None
@@ -705,7 +702,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[:2] <= (3, 11) and isinstance(exc, HTTPError):
if sys.version_info < (3, 12) and isinstance(exc, HTTPError):
notes = []
else:
raise
@@ -788,6 +785,8 @@ 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
@@ -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 # type: ignore[attr-defined]
obj = obj.place_as
try:
code = Code.from_function(obj)

View File

@@ -47,12 +47,10 @@ 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

@@ -9,6 +9,7 @@ from typing import Optional
from typing import Sequence
from typing import TextIO
from ..compat import assert_never
from .wcwidth import wcswidth
@@ -209,6 +210,8 @@ 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:

View File

@@ -41,7 +41,7 @@ _winerrnomap = {
3: errno.ENOENT,
17: errno.EEXIST,
18: errno.EXDEV,
13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable
13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailable
22: errno.ENOTDIR,
20: errno.ENOTDIR,
267: errno.ENOTDIR,

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""local path implementation."""
from __future__ import annotations
import atexit
@@ -205,12 +206,10 @@ 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)
@@ -225,7 +224,7 @@ class Stat:
raise NotImplementedError("XXX win32")
import pwd
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined]
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined,unused-ignore]
return entry[0]
@property
@@ -235,7 +234,7 @@ class Stat:
raise NotImplementedError("XXX win32")
import grp
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined]
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined,unused-ignore]
return entry[0]
def isdir(self):
@@ -253,7 +252,7 @@ def getuserid(user):
import pwd
if not isinstance(user, int):
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined,unused-ignore]
return user
@@ -261,7 +260,7 @@ def getgroupid(group):
import grp
if not isinstance(group, int):
group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
group = grp.getgrnam(group)[2] # type:ignore[attr-defined,unused-ignore]
return group
@@ -318,7 +317,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]
return error.checked_call(os.readlink, self.strpath) # type: ignore[arg-type,return-value,unused-ignore]
def mklinkto(self, oldname):
"""Posix style hard link to another name."""
@@ -757,15 +756,11 @@ 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, # type:ignore[arg-type]
encoding=encoding,
)
return error.checked_call(open, self.strpath, mode)
@@ -841,7 +836,7 @@ class LocalPath:
def copy(self, target, mode=False, stat=False):
"""Copy path to target.
If mode is True, will copy copy permission from path to target.
If mode is True, will copy permission from path to target.
If stat is True, copy permission, last modification
time, last access time, and flags from path to target.
"""
@@ -966,12 +961,10 @@ 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."""
@@ -1054,7 +1047,7 @@ class LocalPath:
def pypkgpath(self):
"""Return the Python package path by looking for the last
directory upwards which still contains an __init__.py.
Return None if a pkgpath can not be determined.
Return None if a pkgpath cannot be determined.
"""
pkgpath = None
for parent in self.parts(reverse=True):
@@ -1277,13 +1270,7 @@ class LocalPath:
if rootdir is None:
rootdir = cls.get_temproot()
# 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]
)
path = error.checked_call(tempfile.mkdtemp, dir=str(rootdir))
return cls(path)
@classmethod

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Support for presenting detailed information in failing assertions."""
import sys
from typing import Any
from typing import Generator

View File

@@ -289,15 +289,13 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
else:
from importlib.abc import TraversableResources
def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore
def get_resource_reader(self, name: str) -> TraversableResources:
if sys.version_info < (3, 11):
from importlib.readers import FileReader
else:
from importlib.resources.readers import FileReader
return FileReader( # type:ignore[no-any-return]
types.SimpleNamespace(path=self._rewritten_names[name])
)
return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
def _write_pyc_fp(
@@ -586,7 +584,7 @@ def _get_assertion_exprs(src: bytes) -> Dict[int, str]:
# multi-line assert with message
elif lineno in seen_lines:
lines[-1] = lines[-1][:offset]
# multi line assert with escapd newline before message
# multi line assert with escaped newline before message
else:
lines.append(line[:offset])
_write_and_reset()
@@ -672,9 +670,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."""
@@ -975,7 +973,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 # type: ignore[attr-defined]
target_id = name.target.id
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])
@@ -1173,7 +1171,10 @@ def try_makedirs(cache_dir: Path) -> bool:
return False
except OSError as e:
# as of now, EROFS doesn't have an equivalent OSError-subclass
if e.errno == errno.EROFS:
#
# squashfuse_ll returns ENOSYS "OSError: [Errno 38] Function not
# implemented" for a read-only error
if e.errno in {errno.EROFS, errno.ENOSYS}:
return False
raise
return True

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Utilities for assertion debugging."""
import collections.abc
import os
import pprint
@@ -222,10 +223,9 @@ def assertrepr_compare(
except outcomes.Exit:
raise
except Exception:
repr_crash = _pytest._code.ExceptionInfo.from_current()._getreprcrash()
explanation = [
"(pytest_assertion plugin: representation of details failed: {}.".format(
_pytest._code.ExceptionInfo.from_current()._getreprcrash()
),
f"(pytest_assertion plugin: representation of details failed: {repr_crash}.",
" Probably an object has a faulty __repr__.)",
]
@@ -325,7 +325,7 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
def _compare_eq_iterable(
left: Iterable[Any],
right: Iterable[Any],
highligher: _HighlightFunc,
highlighter: _HighlightFunc,
verbose: int = 0,
) -> List[str]:
if verbose <= 0 and not running_on_ci():
@@ -340,7 +340,7 @@ def _compare_eq_iterable(
# "right" is the expected base against which we compare "left",
# see https://github.com/pytest-dev/pytest/issues/3333
explanation.extend(
highligher(
highlighter(
"\n".join(
line.rstrip()
for line in difflib.ndiff(right_formatting, left_formatting)

View File

@@ -1,11 +1,14 @@
# mypy: allow-untyped-defs
"""Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version.
import dataclasses
import errno
import json
import os
from pathlib import Path
import tempfile
from typing import Dict
from typing import final
from typing import Generator
@@ -122,6 +125,10 @@ 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.
@@ -140,7 +147,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)
res.mkdir(exist_ok=True, parents=True)
self._mkdir(res)
return res
def _getvaluepath(self, key: str) -> Path:
@@ -177,19 +184,13 @@ class Cache:
"""
path = self._getvaluepath(key)
try:
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)
self._mkdir(path.parent)
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,17 +203,49 @@ class Cache:
with f:
f.write(data)
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")
def _ensure_cache_dir_and_supporting_files(self) -> None:
"""Create the cache dir and its supporting files."""
if self._cachedir.is_dir():
return
gitignore_path = self._cachedir.joinpath(".gitignore")
msg = "# Created by pytest automatically.\n*\n"
gitignore_path.write_text(msg, 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)
cachedir_tag_path = self._cachedir.joinpath("CACHEDIR.TAG")
cachedir_tag_path.write_bytes(CACHEDIR_TAG_CONTENT)
# Reset permissions to the default, see #12308.
# Note: there's no way to get the current umask atomically, eek.
umask = os.umask(0o022)
os.umask(umask)
path.chmod(0o777 - umask)
with open(path.joinpath("README.md"), "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)
try:
path.rename(self._cachedir)
except OSError as e:
# If 2 concurrent pytests both race to the rename, the loser
# gets "Directory not empty" from the rename. In this case,
# everything is handled so just continue (while letting the
# temporary directory be cleaned up).
if e.errno != errno.ENOTEMPTY:
raise
else:
# Create a directory in place of the one we just moved so that
# `TemporaryDirectory`'s cleanup doesn't complain.
#
# TODO: pass ignore_cleanup_errors=True when we no longer support python < 3.10.
# See https://github.com/python/cpython/issues/74168. Note that passing
# delete=False would do the wrong thing in case of errors and isn't supported
# until python 3.12.
path.mkdir()
class LFPluginCollWrapper:
@@ -229,7 +262,7 @@ class LFPluginCollWrapper:
# Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths
# Use stable sort to priorize last failed.
# Use stable sort to prioritize last failed.
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
return node.path in lf_paths
@@ -432,7 +465,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) # type: ignore[no-any-return]
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True)
def pytest_sessionfinish(self) -> None:
config = self.config

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Per-test stdout/stderr capturing mechanism."""
import abc
import collections
import contextlib
@@ -105,17 +106,16 @@ 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]
if not hasattr(stream, "buffer"): # type: ignore[unreachable,unused-ignore]
return
buffered = hasattr(stream.buffer, "raw")
raw_stdout = stream.buffer.raw if buffered else stream.buffer # type: ignore[attr-defined]
raw_stdout = stream.buffer.raw if hasattr(stream.buffer, "raw") else stream.buffer
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined]
if not isinstance(raw_stdout, io._WindowsConsoleIO): # type: ignore[attr-defined,unused-ignore]
return
def _reopen_stdio(f, mode):
if not buffered and mode[0] == "w":
if not hasattr(stream.buffer, "raw") and mode[0] == "w":
buffering = 0
else:
buffering = -1
@@ -482,12 +482,9 @@ class FDCaptureBase(CaptureBase[AnyStr]):
self._state = "initialized"
def __repr__(self) -> str:
return "<{} {} oldfd={} _state={!r} tmpfile={!r}>".format(
self.__class__.__name__,
self.targetfd,
self.targetfd_save,
self._state,
self.tmpfile,
return (
f"<{self.__class__.__name__} {self.targetfd} oldfd={self.targetfd_save} "
f"_state={self._state!r} tmpfile={self.tmpfile!r}>"
)
def _assert_state(self, op: str, states: Tuple[str, ...]) -> None:
@@ -621,12 +618,9 @@ class MultiCapture(Generic[AnyStr]):
self.err: Optional[CaptureBase[AnyStr]] = err
def __repr__(self) -> str:
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,
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}>"
)
def start_capturing(self) -> None:
@@ -735,8 +729,9 @@ class CaptureManager:
self._capture_fixture: Optional[CaptureFixture[Any]] = None
def __repr__(self) -> str:
return "<CaptureManager _method={!r} _global_capturing={!r} _capture_fixture={!r}>".format(
self._method, self._global_capturing, self._capture_fixture
return (
f"<CaptureManager _method={self._method!r} _global_capturing={self._global_capturing!r} "
f"_capture_fixture={self._capture_fixture!r}>"
)
def is_capturing(self) -> Union[str, bool]:

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Python version compatibility code."""
from __future__ import annotations
import dataclasses
@@ -16,6 +17,22 @@ from typing import Callable
from typing import Final
from typing import NoReturn
import py
#: constant to prepare valuing pylib path replacements/lazy proxies later on
# intended for removal in pytest 8.0 or 9.0
# fmt: off
# intentional space to create a fake difference for the verification
LEGACY_PATH = py.path. local
# fmt: on
def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
return LEGACY_PATH(path)
# fmt: off
# Singleton type for NOTSET, as described in:
@@ -36,7 +53,7 @@ def iscoroutinefunction(func: object) -> bool:
def syntax, and doesn't contain yield), or a function decorated with
@asyncio.coroutine.
Note: copied and modified from Python 3.5's builtin couroutines.py to avoid
Note: copied and modified from Python 3.5's builtin coroutines.py to avoid
importing asyncio directly, which in turns also initializes the "logging"
module as a side-effect (see issue #8).
"""
@@ -86,7 +103,6 @@ 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.
@@ -97,9 +113,8 @@ def getfuncargnames(
* Aren't bound with functools.partial.
* Aren't replaced with mocks.
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 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 name parameter should be the original name in which the function was collected.
"""
@@ -137,7 +152,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 is_method or (
if (
# 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
@@ -289,7 +304,7 @@ def get_user_id() -> int | None:
# mypy follows the version and platform checking expectation of PEP 484:
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
# Containment checks are too complex for mypy v1.5.0 and cause failure.
if sys.platform == "win32" or sys.platform == "emscripten": # noqa: PLR1714
if sys.platform == "win32" or sys.platform == "emscripten":
# win32 does not have a getuid() function.
# Emscripten has a return 0 stub.
return None

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Command line options, ini-file and conftest.py processing."""
import argparse
import collections.abc
import copy
@@ -38,15 +39,18 @@ from typing import TYPE_CHECKING
from typing import Union
import warnings
import pluggy
from pluggy import HookimplMarker
from pluggy import HookimplOpts
from pluggy import HookspecMarker
from pluggy import HookspecOpts
from pluggy import PluginManager
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
@@ -148,7 +152,9 @@ 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:
@@ -183,6 +189,11 @@ 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:
@@ -451,7 +462,7 @@ class PytestPluginManager(PluginManager):
# (see issue #1073).
if not name.startswith("pytest_"):
return None
# Ignore names which can not be hooks.
# Ignore names which cannot be hooks.
if name == "pytest_plugins":
return None
@@ -563,8 +574,8 @@ class PytestPluginManager(PluginManager):
self._noconftest = noconftest
self._using_pyargs = pyargs
foundanchor = False
for intitial_path in args:
path = str(intitial_path)
for initial_path in args:
path = str(initial_path)
# remove node-id syntax
i = path.find("::")
if i != -1:
@@ -1068,7 +1079,7 @@ class Config:
self._store = self.stash
self.trace = self.pluginmanager.trace.root.get("config")
self.hook = self.pluginmanager.hook # type: ignore[assignment]
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
self._inicache: Dict[str, Any] = {}
self._override_ini: Sequence[str] = ()
self._opt2dest: Dict[str, str] = {}

View File

@@ -415,6 +415,7 @@ 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.
@@ -425,8 +426,7 @@ class MyOptionParser(argparse.ArgumentParser):
msg = f"{self.prog}: error: {message}"
if hasattr(self._parser, "_config_source_hint"):
# Type ignored because the attribute is set dynamically.
msg = f"{msg} ({self._parser._config_source_hint})" # type: ignore
msg = f"{msg} ({self._parser._config_source_hint})"
raise UsageError(self.format_usage() + msg)
@@ -448,7 +448,7 @@ class MyOptionParser(argparse.ArgumentParser):
getattr(parsed, FILE_OR_DIR).extend(unrecognized)
return parsed
if sys.version_info[:2] < (3, 9): # pragma: no cover
if sys.version_info < (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

@@ -0,0 +1,85 @@
from __future__ import annotations
import functools
from pathlib import Path
from typing import Any
from typing import Mapping
import warnings
import pluggy
from ..compat import LEGACY_PATH
from ..compat import legacy_path
from ..deprecated import HOOK_LEGACY_PATH_ARG
# hookname: (Path, LEGACY_PATH)
imply_paths_hooks: Mapping[str, tuple[str, str]] = {
"pytest_ignore_collect": ("collection_path", "path"),
"pytest_collect_file": ("file_path", "path"),
"pytest_pycollect_makemodule": ("module_path", "path"),
"pytest_report_header": ("start_path", "startdir"),
"pytest_report_collectionfinish": ("start_path", "startdir"),
}
def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
if Path(fspath) != path:
raise ValueError(
f"Path({fspath!r}) != {path!r}\n"
"if both path and fspath are given they need to be equal"
)
class PathAwareHookProxy:
"""
this helper wraps around hook callers
until pluggy supports fixingcalls, this one will do
it currently doesn't return full hook caller proxies for fixed hooks,
this may have to be changed later depending on bugs
"""
def __init__(self, hook_relay: pluggy.HookRelay) -> None:
self._hook_relay = hook_relay
def __dir__(self) -> list[str]:
return dir(self._hook_relay)
def __getattr__(self, key: str) -> pluggy.HookCaller:
hook: pluggy.HookCaller = getattr(self._hook_relay, key)
if key not in imply_paths_hooks:
self.__dict__[key] = hook
return hook
else:
path_var, fspath_var = imply_paths_hooks[key]
@functools.wraps(hook)
def fixed_hook(**kw: Any) -> Any:
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:
warnings.warn(
HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg=fspath_var, pathlib_path_arg=path_var
),
stacklevel=2,
)
if path_value is not None:
if fspath_value is not None:
_check_path(path_value, fspath_value)
else:
fspath_value = legacy_path(path_value)
else:
assert fspath_value is not None
path_value = Path(fspath_value)
kw[path_var] = path_value
kw[fspath_var] = fspath_value
return hook(**kw)
fixed_hook.name = hook.name # type: ignore[attr-defined]
fixed_hook.spec = hook.spec # type: ignore[attr-defined]
fixed_hook.__name__ = key
self.__dict__[key] = fixed_hook
return fixed_hook # type: ignore[return-value]

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Interactive debugging with PDB, the Python Debugger."""
import argparse
import functools
import sys
@@ -154,9 +155,7 @@ class pytestPDB:
def _get_pdb_wrapper_class(cls, pdb_cls, capman: Optional["CaptureManager"]):
import _pytest.config
# Type ignored because mypy doesn't support "dynamic"
# inheritance like this.
class PytestPdbWrapper(pdb_cls): # type: ignore[valid-type,misc]
class PytestPdbWrapper(pdb_cls):
_pytest_capman = capman
_continued = False

View File

@@ -36,6 +36,21 @@ YIELD_FIXTURE = PytestDeprecationWarning(
PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.")
HOOK_LEGACY_PATH_ARG = UnformattedWarning(
PytestRemovedIn9Warning,
"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,
"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",
)
HOOK_LEGACY_MARKING = UnformattedWarning(
PytestDeprecationWarning,
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Discover and run doctests in modules and test files."""
import bdb
from contextlib import contextmanager
import functools

View File

@@ -23,6 +23,7 @@ from typing import List
from typing import MutableMapping
from typing import NoReturn
from typing import Optional
from typing import OrderedDict
from typing import overload
from typing import Sequence
from typing import Set
@@ -35,6 +36,7 @@ import warnings
import _pytest
from _pytest import nodes
from _pytest._code import getfslineno
from _pytest._code import Source
from _pytest._code.code import FormattedExcinfo
from _pytest._code.code import TerminalRepr
from _pytest._io import TerminalWriter
@@ -51,6 +53,7 @@ from _pytest.compat import NotSetType
from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import MARKED_FIXTURE
@@ -68,13 +71,11 @@ from _pytest.scope import HIGH_SCOPES
from _pytest.scope import Scope
if sys.version_info[:2] < (3, 11):
if sys.version_info < (3, 11):
from exceptiongroup import BaseExceptionGroup
if TYPE_CHECKING:
from typing import Deque
from _pytest.main import Session
from _pytest.python import CallSpec2
from _pytest.python import Function
@@ -205,16 +206,18 @@ def get_parametrized_fixture_keys(
def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]] = {}
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, Deque[nodes.Item]]] = {}
items_by_argkey: Dict[
Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]
] = {}
for scope in HIGH_SCOPES:
scoped_argkeys_cache = argkeys_cache[scope] = {}
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(deque)
scoped_items_by_argkey = items_by_argkey[scope] = defaultdict(OrderedDict)
for item in items:
keys = dict.fromkeys(get_parametrized_fixture_keys(item, scope), None)
if keys:
scoped_argkeys_cache[item] = keys
for key in keys:
scoped_items_by_argkey[key].append(item)
scoped_items_by_argkey[key][item] = None
items_dict = dict.fromkeys(items, None)
return list(
reorder_items_atscope(items_dict, argkeys_cache, items_by_argkey, Scope.Session)
@@ -224,17 +227,19 @@ def reorder_items(items: Sequence[nodes.Item]) -> List[nodes.Item]:
def fix_cache_order(
item: nodes.Item,
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
) -> None:
for scope in HIGH_SCOPES:
scoped_items_by_argkey = items_by_argkey[scope]
for key in argkeys_cache[scope].get(item, []):
items_by_argkey[scope][key].appendleft(item)
scoped_items_by_argkey[key][item] = None
scoped_items_by_argkey[key].move_to_end(item, last=False)
def reorder_items_atscope(
items: Dict[nodes.Item, None],
argkeys_cache: Dict[Scope, Dict[nodes.Item, Dict[FixtureArgKey, None]]],
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, "Deque[nodes.Item]"]],
items_by_argkey: Dict[Scope, Dict[FixtureArgKey, OrderedDict[nodes.Item, None]]],
scope: Scope,
) -> Dict[nodes.Item, None]:
if scope is Scope.Function or len(items) < 3:
@@ -323,7 +328,7 @@ class FuncFixtureInfo:
working_set = set(self.initialnames)
while working_set:
argname = working_set.pop()
# Argname may be smth not included in the original names_closure,
# Argname may be something not included in the original names_closure,
# in which case we ignore it. This currently happens with pseudo
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
# So they introduce the new dependency 'request' which might have
@@ -388,6 +393,14 @@ class FixtureRequest(abc.ABC):
"""Scope string, one of "function", "class", "module", "package", "session"."""
return self._scope.value
@abc.abstractmethod
def _check_scope(
self,
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
requested_scope: Scope,
) -> None:
raise NotImplementedError()
@property
def fixturenames(self) -> List[str]:
"""Names of all active fixtures in this request."""
@@ -401,45 +414,10 @@ class FixtureRequest(abc.ABC):
"""Underlying collection node (depends on current request scope)."""
raise NotImplementedError()
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None)
if fixturedefs is None:
# We arrive here because of a dynamic call to
# getfixturevalue(argname) usage which was naturally
# not known at parsing/collection time.
fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
if fixturedefs is not None:
self._arg2fixturedefs[argname] = fixturedefs
# No fixtures defined with this name.
if fixturedefs is None:
raise FixtureLookupError(argname, self)
# The are no fixtures with this name applicable for the function.
if not fixturedefs:
raise FixtureLookupError(argname, self)
# A fixture may override another fixture with the same name, e.g. a
# fixture in a module can override a fixture in a conftest, a fixture in
# a class can override a fixture in the module, and so on.
# An overriding fixture can request its own name (possibly indirectly);
# in this case it gets the value of the fixture it overrides, one level
# up.
# Check how many `argname`s deep we are, and take the next one.
# `fixturedefs` is sorted from furthest to closest, so use negative
# indexing to go in reverse.
index = -1
for request in self._iter_chain():
if request.fixturename == argname:
index -= 1
# If already consumed all of the available levels, fail.
if -index > len(fixturedefs):
raise FixtureLookupError(argname, self)
return fixturedefs[index]
@property
def config(self) -> Config:
"""The pytest config object associated with this request."""
return self._pyfuncitem.config # type: ignore[no-any-return]
return self._pyfuncitem.config
@property
def function(self):
@@ -462,12 +440,9 @@ class FixtureRequest(abc.ABC):
@property
def instance(self):
"""Instance (can be None) on which test function was collected."""
# unittest support hack, see _pytest.unittest.TestCaseFunction.
try:
return self._pyfuncitem._testcase # type: ignore[attr-defined]
except AttributeError:
function = getattr(self, "function", None)
return getattr(function, "__self__", None)
if self.scope != "function":
return None
return getattr(self._pyfuncitem, "instance", None)
@property
def module(self):
@@ -494,7 +469,7 @@ class FixtureRequest(abc.ABC):
@property
def session(self) -> "Session":
"""Pytest session object."""
return self._pyfuncitem.session # type: ignore[no-any-return]
return self._pyfuncitem.session
@abc.abstractmethod
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
@@ -538,6 +513,11 @@ class FixtureRequest(abc.ABC):
:raises pytest.FixtureLookupError:
If the given fixture could not be found.
"""
# Note that in addition to the use case described in the docstring,
# getfixturevalue() is also called by pytest itself during item and fixture
# setup to evaluate the fixtures that are requested statically
# (using function parameters, autouse, etc).
fixturedef = self._get_active_fixturedef(argname)
assert fixturedef.cached_result is not None, (
f'The fixture value for "{argname}" is not available. '
@@ -558,38 +538,53 @@ class FixtureRequest(abc.ABC):
def _get_active_fixturedef(
self, argname: str
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
if argname == "request":
cached_result = (self, [0], None)
return PseudoFixtureDef(cached_result, Scope.Function)
# If we already finished computing a fixture by this name in this item,
# return it.
fixturedef = self._fixture_defs.get(argname)
if fixturedef is None:
try:
fixturedef = self._getnextfixturedef(argname)
except FixtureLookupError:
if argname == "request":
cached_result = (self, [0], None)
return PseudoFixtureDef(cached_result, Scope.Function)
raise
self._compute_fixture_value(fixturedef)
self._fixture_defs[argname] = fixturedef
return fixturedef
if fixturedef is not None:
self._check_scope(fixturedef, fixturedef._scope)
return fixturedef
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
values = [request._fixturedef for request in self._iter_chain()]
values.reverse()
return values
# Find the appropriate fixturedef.
fixturedefs = self._arg2fixturedefs.get(argname, None)
if fixturedefs is None:
# We arrive here because of a dynamic call to
# getfixturevalue(argname) which was naturally
# not known at parsing/collection time.
fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
if fixturedefs is not None:
self._arg2fixturedefs[argname] = fixturedefs
# No fixtures defined with this name.
if fixturedefs is None:
raise FixtureLookupError(argname, self)
# The are no fixtures with this name applicable for the function.
if not fixturedefs:
raise FixtureLookupError(argname, self)
# A fixture may override another fixture with the same name, e.g. a
# fixture in a module can override a fixture in a conftest, a fixture in
# a class can override a fixture in the module, and so on.
# An overriding fixture can request its own name (possibly indirectly);
# in this case it gets the value of the fixture it overrides, one level
# up.
# Check how many `argname`s deep we are, and take the next one.
# `fixturedefs` is sorted from furthest to closest, so use negative
# indexing to go in reverse.
index = -1
for request in self._iter_chain():
if request.fixturename == argname:
index -= 1
# If already consumed all of the available levels, fail.
if -index > len(fixturedefs):
raise FixtureLookupError(argname, self)
fixturedef = fixturedefs[index]
def _compute_fixture_value(self, fixturedef: "FixtureDef[object]") -> None:
"""Create a SubRequest based on "self" and call the execute method
of the given FixtureDef object.
This will force the FixtureDef object to throw away any previous
results and compute a new fixture value, which will be stored into
the FixtureDef object itself.
"""
# prepare a subrequest object before calling fixture function
# (latter managed by fixturedef)
argname = fixturedef.argname
funcitem = self._pyfuncitem
# Prepare a SubRequest object for calling the fixture.
try:
callspec = funcitem.callspec
callspec = self._pyfuncitem.callspec
except AttributeError:
callspec = None
if callspec is not None and argname in callspec.params:
@@ -601,59 +596,55 @@ class FixtureRequest(abc.ABC):
param = NOTSET
param_index = 0
scope = fixturedef._scope
has_params = fixturedef.params is not None
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
if has_params and fixtures_not_supported:
msg = (
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
f"Node id: {funcitem.nodeid}\n"
f"Function type: {type(funcitem).__name__}"
)
fail(msg, pytrace=False)
if has_params:
frame = inspect.stack()[3]
frameinfo = inspect.getframeinfo(frame[0])
source_path = absolutepath(frameinfo.filename)
source_lineno = frameinfo.lineno
try:
source_path_str = str(
source_path.relative_to(funcitem.config.rootpath)
)
except ValueError:
source_path_str = str(source_path)
msg = (
"The requested fixture has no parameter defined for test:\n"
" {}\n\n"
"Requested fixture '{}' defined in:\n{}"
"\n\nRequested here:\n{}:{}".format(
funcitem.nodeid,
fixturedef.argname,
getlocation(fixturedef.func, funcitem.config.rootpath),
source_path_str,
source_lineno,
)
)
fail(msg, pytrace=False)
self._check_fixturedef_without_param(fixturedef)
self._check_scope(fixturedef, scope)
subrequest = SubRequest(
self, scope, param, param_index, fixturedef, _ispytest=True
)
# Check if a higher-level scoped fixture accesses a lower level one.
subrequest._check_scope(argname, self._scope, scope)
try:
# Call the fixture function.
fixturedef.execute(request=subrequest)
finally:
self._schedule_finalizers(fixturedef, subrequest)
# Make sure the fixture value is cached, running it if it isn't
fixturedef.execute(request=subrequest)
def _schedule_finalizers(
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
) -> None:
# If fixture function failed it might have registered finalizers.
finalizer = functools.partial(fixturedef.finish, request=subrequest)
subrequest.node.addfinalizer(finalizer)
self._fixture_defs[argname] = fixturedef
return fixturedef
def _check_fixturedef_without_param(self, fixturedef: "FixtureDef[object]") -> None:
"""Check that this request is allowed to execute this fixturedef without
a param."""
funcitem = self._pyfuncitem
has_params = fixturedef.params is not None
fixtures_not_supported = getattr(funcitem, "nofuncargs", False)
if has_params and fixtures_not_supported:
msg = (
f"{funcitem.name} does not support fixtures, maybe unittest.TestCase subclass?\n"
f"Node id: {funcitem.nodeid}\n"
f"Function type: {type(funcitem).__name__}"
)
fail(msg, pytrace=False)
if has_params:
frame = inspect.stack()[3]
frameinfo = inspect.getframeinfo(frame[0])
source_path = absolutepath(frameinfo.filename)
source_lineno = frameinfo.lineno
try:
source_path_str = str(source_path.relative_to(funcitem.config.rootpath))
except ValueError:
source_path_str = str(source_path)
location = getlocation(fixturedef.func, funcitem.config.rootpath)
msg = (
"The requested fixture has no parameter defined for test:\n"
f" {funcitem.nodeid}\n\n"
f"Requested fixture '{fixturedef.argname}' defined in:\n"
f"{location}\n\n"
f"Requested here:\n"
f"{source_path_str}:{source_lineno}"
)
fail(msg, pytrace=False)
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
values = [request._fixturedef for request in self._iter_chain()]
values.reverse()
return values
@final
@@ -673,6 +664,14 @@ class TopRequest(FixtureRequest):
def _scope(self) -> Scope:
return Scope.Function
def _check_scope(
self,
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
requested_scope: Scope,
) -> None:
# TopRequest always has function scope so always valid.
pass
@property
def node(self):
return self._pyfuncitem
@@ -744,56 +743,38 @@ class SubRequest(FixtureRequest):
def _check_scope(
self,
argname: str,
invoking_scope: Scope,
requested_fixturedef: Union["FixtureDef[object]", PseudoFixtureDef[object]],
requested_scope: Scope,
) -> None:
if argname == "request":
if isinstance(requested_fixturedef, PseudoFixtureDef):
return
if invoking_scope > requested_scope:
if self._scope > requested_scope:
# Try to report something helpful.
text = "\n".join(self._factorytraceback())
argname = requested_fixturedef.argname
fixture_stack = "\n".join(
self._format_fixturedef_line(fixturedef)
for fixturedef in self._get_fixturestack()
)
requested_fixture = self._format_fixturedef_line(requested_fixturedef)
fail(
f"ScopeMismatch: You tried to access the {requested_scope.value} scoped "
f"fixture {argname} with a {invoking_scope.value} scoped request object, "
f"involved factories:\n{text}",
f"fixture {argname} with a {self._scope.value} scoped request object. "
f"Requesting fixture stack:\n{fixture_stack}\n"
f"Requested fixture:\n{requested_fixture}",
pytrace=False,
)
def _factorytraceback(self) -> List[str]:
lines = []
for fixturedef in self._get_fixturestack():
factory = fixturedef.func
fs, lineno = getfslineno(factory)
if isinstance(fs, Path):
session: Session = self._pyfuncitem.session
p = bestrelpath(session.path, fs)
else:
p = fs
lines.append(
"%s:%d: def %s%s"
% (p, lineno + 1, factory.__name__, inspect.signature(factory))
)
return lines
def _format_fixturedef_line(self, fixturedef: "FixtureDef[object]") -> str:
factory = fixturedef.func
path, lineno = getfslineno(factory)
if isinstance(path, Path):
path = bestrelpath(self._pyfuncitem.session.path, path)
signature = inspect.signature(factory)
return f"{path}:{lineno + 1}: def {factory.__name__}{signature}"
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
self._fixturedef.addfinalizer(finalizer)
def _schedule_finalizers(
self, fixturedef: "FixtureDef[object]", subrequest: "SubRequest"
) -> None:
# If the executing fixturedef was not explicitly requested in the argument list (via
# getfixturevalue inside the fixture call) then ensure this fixture def will be finished
# first.
if (
fixturedef.argname not in self._fixture_defs
and fixturedef.argname not in self._pyfuncitem.fixturenames
):
fixturedef.addfinalizer(
functools.partial(self._fixturedef.finish, request=self)
)
super()._schedule_finalizers(fixturedef, subrequest)
@final
class FixtureLookupError(LookupError):
@@ -887,13 +868,6 @@ class FixtureLookupErrorRepr(TerminalRepr):
tw.line("%s:%d" % (os.fspath(self.filename), self.firstlineno + 1))
def fail_fixturefunc(fixturefunc, msg: str) -> NoReturn:
fs, lineno = getfslineno(fixturefunc)
location = f"{fs}:{lineno + 1}"
source = _pytest._code.Source(fixturefunc)
fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, pytrace=False)
def call_fixture_func(
fixturefunc: "_FixtureFunc[FixtureValue]", request: FixtureRequest, kwargs
) -> FixtureValue:
@@ -923,7 +897,13 @@ def _teardown_yield_fixture(fixturefunc, it) -> None:
except StopIteration:
pass
else:
fail_fixturefunc(fixturefunc, "fixture function has more than one 'yield'")
fs, lineno = getfslineno(fixturefunc)
fail(
f"fixture function has more than one 'yield':\n\n"
f"{Source(fixturefunc).indent()}\n"
f"{fs}:{lineno + 1}",
pytrace=False,
)
def _eval_scope_callable(
@@ -965,7 +945,6 @@ class FixtureDef(Generic[FixtureValue]):
func: "_FixtureFunc[FixtureValue]",
scope: Union[Scope, _ScopeName, Callable[[str, Config], _ScopeName], None],
params: Optional[Sequence[object]],
unittest: bool = False,
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None,
@@ -1011,9 +990,7 @@ class FixtureDef(Generic[FixtureValue]):
# a parameter value.
self.ids: Final = ids
# The names requested by the fixtures.
self.argnames: Final = getfuncargnames(func, name=argname, is_method=unittest)
# Whether the fixture was collected from a unittest TestCase class.
self.unittest: Final = unittest
self.argnames: Final = getfuncargnames(func, name=argname)
# If the fixture was executed, the current value of the fixture.
# Can change if the fixture is executed with different parameters.
self.cached_result: Optional[_FixtureCachedResult[FixtureValue]] = None
@@ -1049,15 +1026,25 @@ class FixtureDef(Generic[FixtureValue]):
raise BaseExceptionGroup(msg, exceptions[::-1])
def execute(self, request: SubRequest) -> FixtureValue:
# Get required arguments and register our own finish()
# with their finalization.
"""Return the value of this fixture, executing it if not cached."""
# Ensure that the dependent fixtures requested by this fixture are loaded.
# This needs to be done before checking if we have a cached value, since
# if a dependent fixture has their cache invalidated, e.g. due to
# parametrization, they finalize themselves and fixtures depending on it
# (which will likely include this fixture) setting `self.cached_result = None`.
# See #4871
requested_fixtures_that_should_finalize_us = []
for argname in self.argnames:
fixturedef = request._get_active_fixturedef(argname)
if argname != "request":
# PseudoFixtureDef is only for "request".
assert isinstance(fixturedef, FixtureDef)
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
# Saves requested fixtures in a list so we later can add our finalizer
# to them, ensuring that if a requested fixture gets torn down we get torn
# down first. This is generally handled by SetupState, but still currently
# needed when this fixture is not parametrized but depends on a parametrized
# fixture.
if not isinstance(fixturedef, PseudoFixtureDef):
requested_fixtures_that_should_finalize_us.append(fixturedef)
# Check for (and return) cached value/exception.
my_cache_key = self.cache_key(request)
if self.cached_result is not None:
cache_key = self.cached_result[1]
@@ -1075,12 +1062,26 @@ class FixtureDef(Generic[FixtureValue]):
self.finish(request)
assert self.cached_result is None
# Add finalizer to requested fixtures we saved previously.
# We make sure to do this after checking for cached value to avoid
# adding our finalizer multiple times. (#12135)
finalizer = functools.partial(self.finish, request=request)
for parent_fixture in requested_fixtures_that_should_finalize_us:
parent_fixture.addfinalizer(finalizer)
ihook = request.node.ihook
result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
try:
# Setup the fixture, run the code in it, and cache the value
# in self.cached_result
result = ihook.pytest_fixture_setup(fixturedef=self, request=request)
finally:
# schedule our finalizer, even if the setup failed
request.node.addfinalizer(finalizer)
return result
def cache_key(self, request: SubRequest) -> object:
return request.param_index if not hasattr(request, "param") else request.param
return getattr(request, "param", None)
def __repr__(self) -> str:
return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"
@@ -1090,27 +1091,23 @@ def resolve_fixture_function(
fixturedef: FixtureDef[FixtureValue], request: FixtureRequest
) -> "_FixtureFunc[FixtureValue]":
"""Get the actual callable that can be called to obtain the fixture
value, dealing with unittest-specific instances and bound methods."""
value."""
fixturefunc = fixturedef.func
if fixturedef.unittest:
if request.instance is not None:
# Bind the unbound method to the TestCase instance.
fixturefunc = fixturedef.func.__get__(request.instance) # type: ignore[union-attr]
else:
# The fixture function needs to be bound to the actual
# request.instance so that code working with "fixturedef" behaves
# as expected.
if request.instance is not None:
# Handle the case where fixture is defined not in a test class, but some other class
# (for example a plugin class with a fixture), see #2270.
if hasattr(fixturefunc, "__self__") and not isinstance(
request.instance,
fixturefunc.__self__.__class__, # type: ignore[union-attr]
):
return fixturefunc
fixturefunc = getimfunc(fixturedef.func)
if fixturefunc != fixturedef.func:
fixturefunc = fixturefunc.__get__(request.instance) # type: ignore[union-attr]
# The fixture function needs to be bound to the actual
# request.instance so that code working with "fixturedef" behaves
# as expected.
instance = request.instance
if instance is not None:
# Handle the case where fixture is defined not in a test class, but some other class
# (for example a plugin class with a fixture), see #2270.
if hasattr(fixturefunc, "__self__") and not isinstance(
instance,
fixturefunc.__self__.__class__,
):
return fixturefunc
fixturefunc = getimfunc(fixturedef.func)
if fixturefunc != fixturedef.func:
fixturefunc = fixturefunc.__get__(instance)
return fixturefunc
@@ -1120,11 +1117,7 @@ def pytest_fixture_setup(
"""Execution of fixture setup."""
kwargs = {}
for argname in fixturedef.argnames:
fixdef = request._get_active_fixturedef(argname)
assert fixdef.cached_result is not None
result, arg_cache_key, exc = fixdef.cached_result
request._check_scope(argname, request._scope, fixdef._scope)
kwargs[argname] = result
kwargs[argname] = request.getfixturevalue(argname)
fixturefunc = resolve_fixture_function(fixturedef, request)
my_cache_key = fixturedef.cache_key(request)
@@ -1148,12 +1141,13 @@ def wrap_function_to_error_out_if_called_directly(
) -> FixtureFunction:
"""Wrap the given fixture function so we can raise an error about it being called directly,
instead of used as an argument in a test function."""
name = fixture_marker.name or function.__name__
message = (
'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
f'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n'
"but are created automatically when test functions request them as parameters.\n"
"See https://docs.pytest.org/en/stable/explanation/fixtures.html for more information about fixtures, and\n"
"https://docs.pytest.org/en/stable/deprecations.html#calling-fixtures-directly about how to update your code."
).format(name=fixture_marker.name or function.__name__)
)
@functools.wraps(function)
def result(*args, **kwargs):
@@ -1220,8 +1214,7 @@ def fixture(
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = ...,
name: Optional[str] = ...,
) -> FixtureFunction:
...
) -> FixtureFunction: ...
@overload
@@ -1235,8 +1228,7 @@ def fixture(
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = ...,
name: Optional[str] = None,
) -> FixtureFunctionMarker:
...
) -> FixtureFunctionMarker: ...
def fixture(
@@ -1364,6 +1356,33 @@ def pytest_addoption(parser: Parser) -> None:
default=[],
help="List of default fixtures to be used with this project",
)
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",
)
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 _get_direct_parametrize_args(node: nodes.Node) -> Set[str]:
@@ -1614,7 +1633,6 @@ class FixtureManager:
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None,
autouse: bool = False,
unittest: bool = False,
) -> None:
"""Register a fixture
@@ -1635,8 +1653,6 @@ class FixtureManager:
The fixture's IDs.
:param autouse:
Whether this is an autouse fixture.
:param unittest:
Set this if this is a unittest fixture.
"""
fixture_def = FixtureDef(
config=self.config,
@@ -1645,7 +1661,6 @@ class FixtureManager:
func=func,
scope=scope,
params=params,
unittest=unittest,
ids=ids,
_ispytest=True,
)
@@ -1667,8 +1682,6 @@ class FixtureManager:
def parsefactories(
self,
node_or_obj: nodes.Node,
*,
unittest: bool = ...,
) -> None:
raise NotImplementedError()
@@ -1677,8 +1690,6 @@ class FixtureManager:
self,
node_or_obj: object,
nodeid: Optional[str],
*,
unittest: bool = ...,
) -> None:
raise NotImplementedError()
@@ -1686,8 +1697,6 @@ class FixtureManager:
self,
node_or_obj: Union[nodes.Node, object],
nodeid: Union[str, NotSetType, None] = NOTSET,
*,
unittest: bool = False,
) -> None:
"""Collect fixtures from a collection node or object.
@@ -1695,7 +1704,7 @@ class FixtureManager:
If `node_or_object` is a collection node (with an underlying Python
object), the node's object is traversed and the node's nodeid is used to
determine the fixtures' visibilty. `nodeid` must not be specified in
determine the fixtures' visibility. `nodeid` must not be specified in
this case.
If `node_or_object` is an object (e.g. a plugin), the object is
@@ -1739,7 +1748,6 @@ class FixtureManager:
func=func,
scope=marker.scope,
params=marker.params,
unittest=unittest,
ids=marker.ids,
autouse=marker.autouse,
)
@@ -1771,3 +1779,137 @@ class FixtureManager:
for fixturedef in fixturedefs:
if fixturedef.baseid in parentnodeids:
yield fixturedef
def show_fixtures_per_test(config: Config) -> Union[int, ExitCode]:
from _pytest.main import wrap_session
return wrap_session(config, _show_fixtures_per_test)
_PYTEST_DIR = Path(_pytest.__file__).parent
def _pretty_fixture_path(invocation_dir: Path, func) -> str:
loc = Path(getlocation(func, invocation_dir))
prefix = Path("...", "_pytest")
try:
return str(prefix / loc.relative_to(_PYTEST_DIR))
except ValueError:
return bestrelpath(invocation_dir, loc)
def _show_fixtures_per_test(config: Config, session: "Session") -> None:
import _pytest.config
session.perform_collect()
invocation_dir = config.invocation_params.dir
tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose")
def get_best_relpath(func) -> str:
loc = getlocation(func, invocation_dir)
return bestrelpath(invocation_dir, Path(loc))
def write_fixture(fixture_def: FixtureDef[object]) -> None:
argname = fixture_def.argname
if verbose <= 0 and argname.startswith("_"):
return
prettypath = _pretty_fixture_path(invocation_dir, 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()
invocation_dir = config.invocation_params.dir
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, invocation_dir)
if (fixturedef.argname, loc) in seen:
continue
seen.add((fixturedef.argname, loc))
available.append(
(
len(fixturedef.baseid),
fixturedef.func.__module__,
_pretty_fixture_path(invocation_dir, 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)

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__ # type: ignore[attr-defined]
package_path = package.__path__
path, prefix = package_path[0], package.__name__ + "."
for _, name, is_package in pkgutil.iter_modules([path]):
if is_package:

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Version info, help messages, tracing configuration."""
from argparse import Action
import os
import sys

View File

@@ -1,6 +1,7 @@
# mypy: allow-untyped-defs
"""Hook specifications for pytest plugins which are invoked by pytest itself
and by builtin plugins."""
from pathlib import Path
from typing import Any
from typing import Dict
@@ -14,6 +15,8 @@ from typing import Union
from pluggy import HookspecMarker
from .deprecated import HOOK_LEGACY_PATH_ARG
if TYPE_CHECKING:
import pdb
@@ -22,6 +25,7 @@ if TYPE_CHECKING:
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ExceptionRepr
from _pytest.compat import LEGACY_PATH
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.config import ExitCode
@@ -295,9 +299,23 @@ def pytest_collection_finish(session: "Session") -> None:
"""
@hookspec(firstresult=True)
def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[bool]:
"""Return True to prevent considering this path for collection.
@hookspec(
firstresult=True,
warn_on_impl_args={
"path": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="path", pathlib_path_arg="collection_path"
),
},
)
def pytest_ignore_collect(
collection_path: Path, path: "LEGACY_PATH", config: "Config"
) -> Optional[bool]:
"""Return ``True`` to ignore this path for collection.
Return ``None`` to let other plugins ignore the path for collection.
Returning ``False`` will forcefully *not* ignore this path for collection,
without giving a chance for other plugins to ignore this path.
This hook is consulted for all files and directories prior to calling
more specific hooks.
@@ -310,10 +328,8 @@ def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[b
.. versionchanged:: 7.0.0
The ``collection_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter.
.. versionchanged:: 8.0.0
The ``path`` parameter has been removed.
equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
Use in conftest plugins
=======================
@@ -354,7 +370,16 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
"""
def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Collector]":
@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]":
"""Create a :class:`~pytest.Collector` for the given path, or None if not relevant.
For best results, the returned collector should be a subclass of
@@ -367,10 +392,8 @@ def pytest_collect_file(file_path: Path, parent: "Collector") -> "Optional[Colle
.. versionchanged:: 7.0.0
The ``file_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter.
.. versionchanged:: 8.0.0
The ``path`` parameter was removed.
equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
Use in conftest plugins
=======================
@@ -466,8 +489,17 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
# -------------------------------------------------------------------------
@hookspec(firstresult=True)
def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]:
@hookspec(
firstresult=True,
warn_on_impl_args={
"path": HOOK_LEGACY_PATH_ARG.format(
pylib_path_arg="path", pathlib_path_arg="module_path"
),
},
)
def pytest_pycollect_makemodule(
module_path: Path, path: "LEGACY_PATH", parent
) -> Optional["Module"]:
"""Return a :class:`pytest.Module` collector or None for the given path.
This hook will be called for each matching test module path.
@@ -483,8 +515,7 @@ def pytest_pycollect_makemodule(module_path: Path, parent) -> Optional["Module"]
The ``module_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter.
.. versionchanged:: 8.0.0
The ``path`` parameter has been removed in favor of ``module_path``.
The ``path`` parameter has been deprecated in favor of ``fspath``.
Use in conftest plugins
=======================
@@ -624,7 +655,7 @@ def pytest_runtest_protocol(
- ``pytest_runtest_logreport(report)``
- ``pytest_exception_interact(call, report)`` if an interactive exception occurred
- Call phase, if the the setup passed and the ``setuponly`` pytest option is not set:
- Call phase, if the setup passed and the ``setuponly`` pytest option is not set:
- ``call = pytest_runtest_call(item)`` (wrapped in ``CallInfo(when="call")``)
- ``report = pytest_runtest_makereport(item, call)``
- ``pytest_runtest_logreport(report)``
@@ -834,7 +865,7 @@ def pytest_fixture_setup(
) -> Optional[object]:
"""Perform fixture setup execution.
:param fixturdef:
:param fixturedef:
The fixture definition object.
:param request:
The fixture request object.
@@ -864,7 +895,7 @@ def pytest_fixture_post_finalizer(
the fixture result ``fixturedef.cached_result`` is still available (not
``None``).
:param fixturdef:
:param fixturedef:
The fixture definition object.
:param request:
The fixture request object.
@@ -991,8 +1022,15 @@ 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
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
) -> Union[str, List[str]]:
"""Return a string or list of strings to be displayed as header info for terminal reporting.
@@ -1009,10 +1047,8 @@ def pytest_report_header( # type:ignore[empty-body]
.. versionchanged:: 7.0.0
The ``start_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter.
.. versionchanged:: 8.0.0
The ``startdir`` parameter has been removed.
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
Use in conftest plugins
=======================
@@ -1021,9 +1057,17 @@ def pytest_report_header( # type:ignore[empty-body]
"""
@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,
startdir: "LEGACY_PATH",
items: Sequence["Item"],
) -> Union[str, List[str]]:
"""Return a string or list of strings to be displayed after collection
@@ -1047,10 +1091,8 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
.. versionchanged:: 7.0.0
The ``start_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter.
.. versionchanged:: 8.0.0
The ``startdir`` parameter has been removed.
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
Use in conftest plugins
=======================

View File

@@ -7,6 +7,7 @@ Based on initial code from Ross Lawley.
Output conforms to
https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
"""
from datetime import datetime
import functools
import os
@@ -60,7 +61,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))
@@ -142,7 +143,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.keys():
for key in self.attrs:
if key in families[self.family]["testcase"]:
temp_attrs[key] = self.attrs[key]
self.attrs = temp_attrs
@@ -261,7 +262,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[assignment]
self.to_xml = lambda: data # type: ignore[method-assign]
def _warn_incompatibility_with_xunit2(

View File

@@ -1,7 +1,7 @@
# mypy: allow-untyped-defs
"""Add backward compatibility support for the legacy py path type."""
import dataclasses
import os
from pathlib import Path
import shlex
import subprocess
@@ -14,9 +14,9 @@ from typing import Union
from iniconfig import SectionWrapper
import py
from _pytest.cacheprovider import Cache
from _pytest.compat import LEGACY_PATH
from _pytest.compat import legacy_path
from _pytest.config import Config
from _pytest.config import hookimpl
from _pytest.config import PytestPluginManager
@@ -39,20 +39,6 @@ if TYPE_CHECKING:
import pexpect
#: constant to prepare valuing pylib path replacements/lazy proxies later on
# intended for removal in pytest 8.0 or 9.0
# fmt: off
# intentional space to create a fake difference for the verification
LEGACY_PATH = py.path. local
# fmt: on
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
return LEGACY_PATH(path)
@final
class Testdir:
"""
@@ -398,7 +384,7 @@ def Config_inifile(self: Config) -> Optional[LEGACY_PATH]:
return legacy_path(str(self.inipath)) if self.inipath else None
def Session_stardir(self: Session) -> LEGACY_PATH:
def Session_startdir(self: Session) -> LEGACY_PATH:
"""The path from which pytest was invoked.
Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
@@ -453,7 +439,7 @@ def pytest_load_initial_conftests(early_config: Config) -> None:
mp.setattr(Config, "inifile", property(Config_inifile), raising=False)
# Add Session.startdir property.
mp.setattr(Session, "startdir", property(Session_stardir), raising=False)
mp.setattr(Session, "startdir", property(Session_startdir), raising=False)
# Add pathlist configuration type.
mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type)

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Access and control log capturing."""
from contextlib import contextmanager
from contextlib import nullcontext
from datetime import datetime
@@ -209,7 +210,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) # type: ignore[attr-defined]
auto_indent = self._get_auto_indent(record.auto_indent)
else:
auto_indent = self._auto_indent
@@ -512,7 +513,7 @@ class LogCaptureFixture:
:return: The original disabled logging level.
"""
original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
original_disable_level: int = logger_obj.manager.disable
if isinstance(level, str):
# Try to translate the level string to an int for `logging.disable()`

View File

@@ -37,6 +37,7 @@ from _pytest.config import hookimpl
from _pytest.config import PytestPluginManager
from _pytest.config import UsageError
from _pytest.config.argparsing import Parser
from _pytest.config.compat import PathAwareHookProxy
from _pytest.fixtures import FixtureManager
from _pytest.outcomes import exit
from _pytest.pathlib import absolutepath
@@ -557,6 +558,7 @@ class Session(nodes.Collector):
super().__init__(
name="",
path=config.rootpath,
fspath=None,
parent=None,
config=config,
session=self,
@@ -694,7 +696,7 @@ class Session(nodes.Collector):
proxy: pluggy.HookRelay
if remove_mods:
# One or more conftests are not in use at this path.
proxy = FSHookProxy(pm, remove_mods) # type: ignore[arg-type,assignment]
proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment]
else:
# All plugins are active for this fspath.
proxy = self.config.hook
@@ -734,14 +736,12 @@ 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(
self, args: Optional[Sequence[str]] = ..., genitems: bool = ...
) -> Sequence[Union[nodes.Item, nodes.Collector]]:
...
) -> Sequence[Union[nodes.Item, nodes.Collector]]: ...
def perform_collect(
self, args: Optional[Sequence[str]] = None, genitems: bool = True

View File

@@ -78,7 +78,7 @@ def pytest_addoption(parser: Parser) -> None:
default="",
metavar="EXPRESSION",
help="Only run tests which match the given substring expression. "
"An expression is a Python evaluatable expression "
"An expression is a Python evaluable expression "
"where all names are substring-matched against test names "
"and their parent classes. Example: -k 'test_method or test_"
"other' matches all test functions and classes whose name "

View File

@@ -342,7 +342,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[misc]
def __call__(self, arg: Markable) -> Markable: # type: ignore[overload-overlap]
pass
@overload
@@ -433,13 +433,11 @@ if TYPE_CHECKING:
from _pytest.scope import _ScopeName
class _SkipMarkDecorator(MarkDecorator):
@overload # type: ignore[override,misc,no-overload-impl]
def __call__(self, arg: Markable) -> Markable:
...
@overload # type: ignore[override,no-overload-impl]
def __call__(self, arg: Markable) -> Markable: ...
@overload
def __call__(self, reason: str = ...) -> "MarkDecorator":
...
def __call__(self, reason: str = ...) -> "MarkDecorator": ...
class _SkipifMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
@@ -447,13 +445,11 @@ if TYPE_CHECKING:
condition: Union[str, bool] = ...,
*conditions: Union[str, bool],
reason: str = ...,
) -> MarkDecorator:
...
) -> MarkDecorator: ...
class _XfailMarkDecorator(MarkDecorator):
@overload # type: ignore[override,misc,no-overload-impl]
def __call__(self, arg: Markable) -> Markable:
...
@overload # type: ignore[override,no-overload-impl]
def __call__(self, arg: Markable) -> Markable: ...
@overload
def __call__(
@@ -466,8 +462,7 @@ if TYPE_CHECKING:
None, Type[BaseException], Tuple[Type[BaseException], ...]
] = ...,
strict: bool = ...,
) -> MarkDecorator:
...
) -> MarkDecorator: ...
class _ParametrizeMarkDecorator(MarkDecorator):
def __call__( # type: ignore[override]
@@ -483,8 +478,7 @@ if TYPE_CHECKING:
]
] = ...,
scope: Optional[_ScopeName] = ...,
) -> MarkDecorator:
...
) -> MarkDecorator: ...
class _UsefixturesMarkDecorator(MarkDecorator):
def __call__(self, *fixtures: str) -> MarkDecorator: # type: ignore[override]

View File

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

View File

@@ -3,6 +3,7 @@ import abc
from functools import cached_property
from inspect import signature
import os
import pathlib
from pathlib import Path
from typing import Any
from typing import Callable
@@ -29,8 +30,11 @@ from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback
from _pytest.compat import LEGACY_PATH
from _pytest.config import Config
from _pytest.config import ConftestImportFailure
from _pytest.config.compat import _check_path
from _pytest.deprecated import NODE_CTOR_FSPATH_ARG
from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import NodeKeywords
@@ -55,6 +59,29 @@ tracebackcutdir = Path(_pytest.__file__).parent
_T = TypeVar("_T")
def _imply_path(
node_type: Type["Node"],
path: Optional[Path],
fspath: Optional[LEGACY_PATH],
) -> Path:
if fspath is not None:
warnings.warn(
NODE_CTOR_FSPATH_ARG.format(
node_type_name=node_type.__name__,
),
stacklevel=6,
)
if path is not None:
if fspath is not None:
_check_path(path, fspath)
return path
else:
assert fspath is not None
return Path(fspath)
_NodeType = TypeVar("_NodeType", bound="Node")
@@ -110,6 +137,13 @@ class Node(abc.ABC, metaclass=NodeMeta):
leaf nodes.
"""
# Implemented in the legacypath plugin.
#: 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
# Use __slots__ to make attribute access faster.
# Note that __dict__ is still available.
__slots__ = (
@@ -129,6 +163,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
parent: "Optional[Node]" = None,
config: Optional[Config] = None,
session: "Optional[Session]" = None,
fspath: Optional[LEGACY_PATH] = None,
path: Optional[Path] = None,
nodeid: Optional[str] = None,
) -> None:
@@ -154,11 +189,10 @@ class Node(abc.ABC, metaclass=NodeMeta):
raise TypeError("session or parent must be provided")
self.session = parent.session
if path is None:
if path is None and fspath is None:
path = getattr(parent, "path", None)
assert path is not None
#: Filesystem path where this node was collected from (can be None).
self.path = path
self.path: pathlib.Path = _imply_path(type(self), path, fspath=fspath)
# The explicit annotation is to avoid publicly exposing NodeKeywords.
#: Keywords/markers collected from all scopes.
@@ -329,12 +363,10 @@ class Node(abc.ABC, metaclass=NodeMeta):
yield node, mark
@overload
def get_closest_marker(self, name: str) -> Optional[Mark]:
...
def get_closest_marker(self, name: str) -> 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
@@ -529,6 +561,7 @@ class FSCollector(Collector, abc.ABC):
def __init__(
self,
fspath: Optional[LEGACY_PATH] = None,
path_or_parent: Optional[Union[Path, Node]] = None,
path: Optional[Path] = None,
name: Optional[str] = None,
@@ -544,8 +577,8 @@ class FSCollector(Collector, abc.ABC):
elif isinstance(path_or_parent, Path):
assert path is None
path = path_or_parent
assert path is not None
path = _imply_path(type(self), path, fspath=fspath)
if name is None:
name = path.name
if parent is not None and parent.path != path:
@@ -585,11 +618,12 @@ class FSCollector(Collector, abc.ABC):
cls,
parent,
*,
fspath: Optional[LEGACY_PATH] = None,
path: Optional[Path] = None,
**kw,
) -> "Self":
"""The public constructor."""
return super().from_parent(parent=parent, path=path, **kw)
return super().from_parent(parent=parent, fspath=fspath, path=path, **kw)
class File(FSCollector, abc.ABC):
@@ -731,7 +765,7 @@ class Item(Node, abc.ABC):
and lineno is a 0-based line number.
"""
location = self.reportinfo()
path = absolutepath(os.fspath(location[0]))
path = absolutepath(location[0])
relfspath = self.session._node_location_to_relpath(path)
assert type(location[2]) is str
return (relfspath, location[1], location[2])

View File

@@ -11,6 +11,8 @@ from typing import Protocol
from typing import Type
from typing import TypeVar
from .warning_types import PytestDeprecationWarning
class OutcomeException(BaseException):
"""OutcomeException and its subclass instances indicate and contain info
@@ -112,6 +114,9 @@ def exit(
:param returncode:
Return code to be used when exiting pytest. None means the same as ``0`` (no error), same as :func:`sys.exit`.
:raises pytest.exit.Exception:
The exception that is raised.
"""
__tracebackhide__ = True
raise Exit(reason, returncode)
@@ -140,6 +145,9 @@ def skip(
Defaults to False.
:raises pytest.skip.Exception:
The exception that is raised.
.. note::
It is better to use the :ref:`pytest.mark.skipif ref` marker when
possible to declare a test to be skipped under certain conditions
@@ -161,6 +169,9 @@ def fail(reason: str = "", pytrace: bool = True) -> NoReturn:
:param pytrace:
If False, msg represents the full failure information and no
python traceback will be reported.
:raises pytest.fail.Exception:
The exception that is raised.
"""
__tracebackhide__ = True
raise Failed(msg=reason, pytrace=pytrace)
@@ -186,13 +197,20 @@ def xfail(reason: str = "") -> NoReturn:
It is better to use the :ref:`pytest.mark.xfail ref` marker when
possible to declare a test to be xfailed under certain conditions
like known bugs or missing features.
:raises pytest.xfail.Exception:
The exception that is raised.
"""
__tracebackhide__ = True
raise XFailed(reason)
def importorskip(
modname: str, minversion: Optional[str] = None, reason: Optional[str] = None
modname: str,
minversion: Optional[str] = None,
reason: Optional[str] = None,
*,
exc_type: Optional[Type[ImportError]] = None,
) -> Any:
"""Import and return the requested module ``modname``, or skip the
current test if the module cannot be imported.
@@ -205,30 +223,84 @@ 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.
:raises pytest.skip.Exception:
If the module cannot be imported.
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 ImportError as exc:
except exc_type as exc:
# Do not raise or issue warnings inside the catch_warnings() block.
if reason is None:
reason = f"could not import {modname!r}: {exc}"
raise Skipped(reason, allow_module_level=True) from None
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
mod = sys.modules[modname]
if minversion is None:
return mod

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Submit failure or test session information to a pastebin service."""
from io import StringIO
import tempfile
from typing import IO

View File

@@ -1,4 +1,3 @@
# mypy: allow-untyped-defs
import atexit
import contextlib
from enum import Enum
@@ -8,6 +7,7 @@ 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,6 +22,7 @@ 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
@@ -58,7 +59,7 @@ _IGNORED_WINERRORS = (
)
def _ignore_error(exception):
def _ignore_error(exception: Exception) -> bool:
return (
getattr(exception, "errno", None) in _IGNORED_ERRORS
or getattr(exception, "winerror", None) in _IGNORED_WINERRORS
@@ -70,7 +71,7 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
def on_rm_rf_error(
func,
func: Optional[Callable[..., Any]],
path: str,
excinfo: Union[
BaseException,
@@ -172,7 +173,7 @@ def rm_rf(path: Path) -> None:
def find_prefixed(root: Path, prefix: str) -> Iterator["os.DirEntry[str]"]:
"""Find all elements in root that begin with the prefix, case insensitive."""
"""Find all elements in root that begin with the prefix, case-insensitive."""
l_prefix = prefix.lower()
for x in os.scandir(root):
if x.name.lower().startswith(l_prefix):
@@ -195,7 +196,7 @@ def find_suffixes(root: Path, prefix: str) -> Iterator[str]:
return extract_suffixes(find_prefixed(root, prefix), prefix)
def parse_num(maybe_num) -> int:
def parse_num(maybe_num: str) -> int:
"""Parse number path suffixes, returns -1 on error."""
try:
return int(maybe_num)
@@ -263,7 +264,9 @@ def create_cleanup_lock(p: Path) -> Path:
return lock_path
def register_cleanup_lock_removal(lock_path: Path, register=atexit.register):
def register_cleanup_lock_removal(
lock_path: Path, register: Any = atexit.register
) -> Any:
"""Register a cleanup function for removing a lock, by default on atexit."""
pid = os.getpid()
@@ -354,7 +357,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
yield Path(entry)
def cleanup_dead_symlinks(root: Path):
def cleanup_dead_symlinks(root: Path) -> None:
for left_dir in root.iterdir():
if left_dir.is_symlink():
if not left_dir.resolve().exists():
@@ -458,10 +461,14 @@ 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, dst, **kwargs):
def symlink_or_skip(
src: Union["os.PathLike[str]", str],
dst: Union["os.PathLike[str]", str],
**kwargs: Any,
) -> None:
"""Make a symlink, or skip the test in case symlinks are not supported."""
try:
os.symlink(str(src), str(dst), **kwargs)
os.symlink(src, dst, **kwargs)
except OSError as e:
skip(f"symlinks not supported: {e}")
@@ -539,6 +546,10 @@ def import_path(
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
)
@@ -615,23 +626,53 @@ def _import_module_using_spec(
: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).
Note: we can probably drop insert_missing_modules altogether: instead of
generating module names such as "src.tests.test_foo", which require intermediate
empty modules, we might just as well generate unique module names like
"src_tests_test_foo".
"""
# 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 is not None:
if spec_matches_module_path(spec, module_path):
break
else:
spec = importlib.util.spec_from_file_location(module_name, str(module_path))
if spec is not None:
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
@@ -639,6 +680,16 @@ def _import_module_using_spec(
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"):
@@ -692,34 +743,31 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
otherwise "src.tests.test_foo" is not importable by ``__import__``.
"""
module_parts = module_name.split(".")
child_module: Union[ModuleType, None] = None
module: Union[ModuleType, None] = None
child_name: str = ""
while module_name:
if module_name not in modules:
try:
# 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:
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
# Add child attribute to the parent that can reference the child
# modules.
if not hasattr(module, child_name):
setattr(module, child_name, child_module)
modules[module_name] = module
# Keep track of the child module while moving up the tree.
child_module, child_name = module, module_name.rpartition(".")[-1]
if not hasattr(parent_module, child_name):
setattr(parent_module, child_name, modules[module_name])
module_parts.pop(-1)
module_name = ".".join(module_parts)
@@ -728,7 +776,7 @@ def resolve_package_path(path: Path) -> Optional[Path]:
"""Return the Python package path by looking for the last
directory upwards which still contains an __init__.py.
Returns None if it can not be determined.
Returns None if it cannot be determined.
"""
result = None
for parent in itertools.chain((path,), path.parents):
@@ -758,39 +806,79 @@ def resolve_pkg_root_and_module_name(
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
until we find a directory that is reachable from sys.path, which marks it as a namespace package:
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
# https://packaging.python.org/en/latest/guides/packaging-namespace-packages/
if consider_namespace_packages:
# Go upwards in the hierarchy, if we find a parent path included
# in sys.path, it means the package found by resolve_package_path()
# actually belongs to a namespace package.
for parent in pkg_root.parents:
# If any of the parent paths has a __init__.py, it means it is not
# a namespace package (see the docs linked above).
if (parent / "__init__.py").is_file():
break
if str(parent) in sys.path:
# Point the pkg_root to the root of the namespace package.
pkg_root = parent
break
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
names = list(path.with_suffix("").relative_to(pkg_root).parts)
if names[-1] == "__init__":
names.pop()
module_name = ".".join(names)
return pkg_root, module_name
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."""
@@ -836,13 +924,13 @@ def visit(
yield from visit(entry.path, recurse)
def absolutepath(path: Union[Path, str]) -> Path:
def absolutepath(path: "Union[str, os.PathLike[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(str(path)))
return Path(os.path.abspath(path))
def commonpath(path1: Path, path2: Path) -> Optional[Path]:

View File

@@ -3,6 +3,7 @@
PYTEST_DONT_REWRITE
"""
import collections.abc
import contextlib
from fnmatch import fnmatch
@@ -245,8 +246,7 @@ 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
@@ -289,7 +289,8 @@ class HookRecorder:
__tracebackhide__ = True
i = 0
entries = list(entries)
backlocals = sys._getframe(1).f_locals
# Since Python 3.13, f_locals is not a dict, but eval requires a dict.
backlocals = dict(sys._getframe(1).f_locals)
while entries:
name, check = entries.pop(0)
for ind, call in enumerate(self.calls[i:]):
@@ -327,15 +328,13 @@ 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(
@@ -344,8 +343,7 @@ class HookRecorder:
"pytest_collectreport",
"pytest_runtest_logreport",
),
) -> Sequence[Union[CollectReport, TestReport]]:
...
) -> Sequence[Union[CollectReport, TestReport]]: ...
def getreports(
self,
@@ -390,15 +388,13 @@ 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(
@@ -407,8 +403,7 @@ class HookRecorder:
"pytest_collectreport",
"pytest_runtest_logreport",
),
) -> Sequence[Union[CollectReport, TestReport]]:
...
) -> Sequence[Union[CollectReport, TestReport]]: ...
def getfailures(
self,
@@ -766,6 +761,9 @@ class Pytester:
) -> Path:
items = list(files.items())
if ext is None:
raise TypeError("ext must not be None")
if ext and not ext.startswith("."):
raise ValueError(
f"pytester.makefile expects a file extension, try .{ext} instead of {ext}"

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Python test discovery, setup and run of test functions."""
import abc
from collections import Counter
from collections import defaultdict
@@ -39,20 +40,18 @@ 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.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
@@ -69,7 +68,6 @@ 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
@@ -86,27 +84,7 @@ 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",
@@ -135,16 +113,6 @@ 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)
@@ -208,7 +176,12 @@ def pytest_collect_directory(
path: Path, parent: nodes.Collector
) -> Optional[nodes.Collector]:
pkginit = path / "__init__.py"
if pkginit.is_file():
try:
has_pkginit = pkginit.is_file()
except PermissionError:
# See https://github.com/pytest-dev/pytest/issues/12120#issuecomment-2106349096.
return None
if has_pkginit:
return Package.from_parent(parent, path=path)
return None
@@ -301,10 +274,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 staticmethod, a class or a module.
a class or a module.
"""
node = self.getparent(Function)
return getattr(node.obj, "__self__", None) if node is not None else None
# Overridden by Function.
return None
@property
def obj(self):
@@ -665,6 +638,7 @@ class Package(nodes.Directory):
def __init__(
self,
fspath: Optional[LEGACY_PATH],
parent: nodes.Collector,
# NOTE: following args are unused:
config=None,
@@ -676,6 +650,7 @@ class Package(nodes.Directory):
# super().__init__(self, fspath, parent=parent)
session = parent.session
super().__init__(
fspath=fspath,
path=path,
parent=parent,
config=config,
@@ -1314,7 +1289,6 @@ class Metafunc:
func=get_direct_param_fixture_func,
scope=scope_,
params=None,
unittest=False,
ids=None,
_ispytest=True,
)
@@ -1522,137 +1496,6 @@ 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(invocation_dir: Path, func) -> str:
loc = Path(getlocation(func, invocation_dir))
prefix = Path("...", "_pytest")
try:
return str(prefix / loc.relative_to(_PYTEST_DIR))
except ValueError:
return bestrelpath(invocation_dir, 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()
invocation_dir = config.invocation_params.dir
tw = _pytest.config.create_terminal_writer(config)
verbose = config.getvalue("verbose")
def get_best_relpath(func) -> str:
loc = getlocation(func, invocation_dir)
return bestrelpath(invocation_dir, 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(invocation_dir, 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()
invocation_dir = config.invocation_params.dir
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, invocation_dir)
if (fixturedef.argname, loc) in seen:
continue
seen.add((fixturedef.argname, loc))
available.append(
(
len(fixturedef.baseid),
fixturedef.func.__module__,
_pretty_fixture_path(invocation_dir, 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.
@@ -1700,7 +1543,8 @@ class Function(PyobjMixin, nodes.Item):
super().__init__(name, parent, config=config, session=session)
if callobj is not NOTSET:
self.obj = callobj
self._obj = callobj
self._instance = getattr(callobj, "__self__", None)
#: Original function name, without any decorations (for example
#: parametrization adds a ``"[...]"`` suffix to function names), used to access
@@ -1750,12 +1594,31 @@ class Function(PyobjMixin, nodes.Item):
"""Underlying python 'function' object."""
return getimfunc(self.obj)
def _getobj(self):
assert self.parent is not None
@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):
if isinstance(self.parent, Class):
# Each Function gets a fresh class instance.
parent_obj = self.parent.newinstance()
return 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)

View File

@@ -142,7 +142,7 @@ class ApproxNumpy(ApproxBase):
)
return f"approx({list_scalars!r})"
def _repr_compare(self, other_side: "ndarray") -> List[str]:
def _repr_compare(self, other_side: Union["ndarray", List[Any]]) -> List[str]:
import itertools
import math
@@ -163,10 +163,14 @@ class ApproxNumpy(ApproxBase):
self._approx_scalar, self.expected.tolist()
)
if np_array_shape != other_side.shape:
# 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:
return [
"Impossible to compare arrays with different shapes.",
f"Shapes: {np_array_shape} and {other_side.shape}",
f"Shapes: {np_array_shape} and {other_side_as_array.shape}",
]
number_of_elements = self.expected.size
@@ -175,7 +179,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, index)
other_value = get_value_from_nested_list(other_side_as_array, index)
if approx_value != other_value:
abs_diff = abs(approx_value.expected - other_value)
max_abs_diff = max(max_abs_diff, abs_diff)
@@ -188,7 +192,7 @@ class ApproxNumpy(ApproxBase):
message_data = [
(
str(index),
str(get_value_from_nested_list(other_side, index)),
str(get_value_from_nested_list(other_side_as_array, index)),
str(get_value_from_nested_list(approx_side_as_seq, index)),
)
for index in different_ids
@@ -393,7 +397,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) # type: ignore[arg-type]
abs(self.expected)
):
return str(self.expected)
@@ -437,8 +441,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)): # type: ignore[arg-type]
return self.nan_ok and math.isnan(abs(actual)) # type: ignore[arg-type]
if math.isnan(abs(self.expected)):
return self.nan_ok and math.isnan(abs(actual))
# Infinity shouldn't be approximately equal to anything but itself, but
# if there's a relative tolerance, it will be infinite and infinity
@@ -446,11 +450,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)): # type: ignore[arg-type]
if math.isinf(abs(self.expected)):
return False
# Return true if the two numbers are within the tolerance.
result: bool = abs(self.expected - actual) <= self.tolerance
result: bool = abs(self.expected - actual) <= self.tolerance # type: ignore[arg-type]
return result
# Ignore type because of https://github.com/python/mypy/issues/4266.
@@ -769,8 +773,7 @@ def raises(
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
*,
match: Optional[Union[str, Pattern[str]]] = ...,
) -> "RaisesContext[E]":
...
) -> "RaisesContext[E]": ...
@overload
@@ -779,8 +782,7 @@ def raises(
func: Callable[..., Any],
*args: Any,
**kwargs: Any,
) -> _pytest._code.ExceptionInfo[E]:
...
) -> _pytest._code.ExceptionInfo[E]: ...
def raises(

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Record warnings during test function execution."""
from pprint import pformat
import re
from types import TracebackType
@@ -43,13 +44,11 @@ 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(func: Callable[..., T], *args: Any, **kwargs: Any) -> T: ...
def deprecated_call(
@@ -91,8 +90,7 @@ def warns(
expected_warning: Union[Type[Warning], Tuple[Type[Warning], ...]] = ...,
*,
match: Optional[Union[str, Pattern[str]]] = ...,
) -> "WarningsChecker":
...
) -> "WarningsChecker": ...
@overload
@@ -101,8 +99,7 @@ def warns(
func: Callable[..., T],
*args: Any,
**kwargs: Any,
) -> T:
...
) -> T: ...
def warns(
@@ -184,8 +181,7 @@ class WarningsRecorder(warnings.catch_warnings): # type:ignore[type-arg]
def __init__(self, *, _ispytest: bool = False) -> None:
check_ispytest(_ispytest)
# Type ignored due to the way typeshed handles warnings.catch_warnings.
super().__init__(record=True) # type: ignore[call-arg]
super().__init__(record=True)
self._entered = False
self._list: List[warnings.WarningMessage] = []

View File

@@ -72,8 +72,7 @@ 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"):
@@ -606,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,5 +1,6 @@
# mypy: allow-untyped-defs
"""Basic collect and runtest protocol implementations."""
import bdb
import dataclasses
import os
@@ -38,7 +39,7 @@ from _pytest.outcomes import Skipped
from _pytest.outcomes import TEST_OUTCOME
if sys.version_info[:2] < (3, 11):
if sys.version_info < (3, 11):
from exceptiongroup import BaseExceptionGroup
if TYPE_CHECKING:
@@ -84,7 +85,7 @@ def pytest_terminal_summary(terminalreporter: "TerminalReporter") -> None:
dlist.append(rep)
if not dlist:
return
dlist.sort(key=lambda x: x.duration, reverse=True) # type: ignore[no-any-return]
dlist.sort(key=lambda x: x.duration, reverse=True)
if not durations:
tr.write_sep("=", "slowest durations")
else:
@@ -387,7 +388,9 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
return list(collector.collect())
call = CallInfo.from_call(collect, "collect")
call = CallInfo.from_call(
collect, "collect", reraise=(KeyboardInterrupt, SystemExit)
)
longrepr: Union[None, Tuple[str, int, str], str, TerminalRepr] = None
if not call.excinfo:
outcome: Literal["passed", "skipped", "failed"] = "passed"
@@ -395,8 +398,7 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport:
skip_exceptions = [Skipped]
unittest = sys.modules.get("unittest")
if unittest is not None:
# Type ignored because unittest is loaded dynamically.
skip_exceptions.append(unittest.SkipTest) # type: ignore
skip_exceptions.append(unittest.SkipTest)
if isinstance(call.excinfo.value, tuple(skip_exceptions)):
outcome = "skipped"
r_ = collector._repr_failure_py(call.excinfo, "line")

View File

@@ -58,7 +58,7 @@ def pytest_fixture_post_finalizer(
if config.option.setupshow:
_show_fixture_action(fixturedef, request.config, "TEARDOWN")
if hasattr(fixturedef, "cached_param"):
del fixturedef.cached_param # type: ignore[attr-defined]
del fixturedef.cached_param
def _show_fixture_action(
@@ -87,7 +87,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)}]") # type: ignore[attr-defined]
tw.write(f"[{saferepr(fixturedef.cached_param, maxsize=42)}]")
tw.flush()

View File

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

View File

@@ -19,6 +19,8 @@ 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__ = ()
@@ -61,6 +63,8 @@ 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",)

View File

@@ -40,7 +40,7 @@ def pytest_addoption(parser: Parser) -> None:
@pytest.hookimpl
def pytest_configure(config: Config) -> None:
if config.option.stepwise_skip:
# allow --stepwise-skip to work on it's own merits.
# allow --stepwise-skip to work on its own merits.
config.option.stepwise = True
if config.getoption("stepwise"):
config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin")

View File

@@ -3,6 +3,7 @@
This is a good source for looking at the various reporting hooks.
"""
import argparse
from collections import Counter
import dataclasses
@@ -1266,7 +1267,7 @@ class TerminalReporter:
def _set_main_color(self) -> None:
unknown_types: List[str] = []
for found_type in self.stats.keys():
for found_type in self.stats:
if found_type: # setup/teardown reports have an empty key, ignore them
if found_type not in KNOWN_TYPES and found_type not in unknown_types:
unknown_types.append(found_type)
@@ -1407,11 +1408,11 @@ def _get_line_with_reprcrash_message(
except AttributeError:
pass
else:
if not running_on_ci():
if running_on_ci() or config.option.verbose >= 2:
msg = f" - {msg}"
else:
available_width = tw.fullwidth - line_width
msg = _format_trimmed(" - {}", msg, available_width)
else:
msg = f" - {msg}"
if msg is not None:
line += msg

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Support for providing temporary directories to test functions."""
import dataclasses
import os
from pathlib import Path

View File

@@ -1,5 +1,6 @@
# mypy: allow-untyped-defs
"""Discover and run std-library "unittest" style tests."""
import sys
import traceback
import types
@@ -15,7 +16,6 @@ from typing import TYPE_CHECKING
from typing import Union
import _pytest._code
from _pytest.compat import getimfunc
from _pytest.compat import is_async_function
from _pytest.config import hookimpl
from _pytest.fixtures import FixtureRequest
@@ -32,6 +32,9 @@ from _pytest.runner import CallInfo
import pytest
if sys.version_info[:2] < (3, 11):
from exceptiongroup import ExceptionGroup
if TYPE_CHECKING:
import unittest
@@ -63,6 +66,14 @@ class UnitTestCase(Class):
# to declare that our children do not support funcargs.
nofuncargs = True
def newinstance(self):
# TestCase __init__ takes the method (test) name. The TestCase
# constructor treats the name "runTest" as a special no-op, so it can be
# used when a dummy instance is needed. While unittest.TestCase has a
# default, some subclasses omit the default (#9610), so always supply
# it.
return self.obj("runTest")
def collect(self) -> Iterable[Union[Item, Collector]]:
from unittest import TestLoader
@@ -76,23 +87,22 @@ class UnitTestCase(Class):
self._register_unittest_setup_class_fixture(cls)
self._register_setup_class_fixture()
self.session._fixturemanager.parsefactories(self, unittest=True)
self.session._fixturemanager.parsefactories(self.newinstance(), self.nodeid)
loader = TestLoader()
foundsomething = False
for name in loader.getTestCaseNames(self.obj):
x = getattr(self.obj, name)
if not getattr(x, "__test__", True):
continue
funcobj = getimfunc(x)
yield TestCaseFunction.from_parent(self, name=name, callobj=funcobj)
yield TestCaseFunction.from_parent(self, name=name)
foundsomething = True
if not foundsomething:
runtest = getattr(self.obj, "runTest", None)
if runtest is not None:
ut = sys.modules.get("twisted.trial.unittest", None)
# Type ignored because `ut` is an opaque module.
if ut is None or runtest != ut.TestCase.runTest: # type: ignore
if ut is None or runtest != ut.TestCase.runTest:
yield TestCaseFunction.from_parent(self, name="runTest")
def _register_unittest_setup_class_fixture(self, cls: type) -> None:
@@ -104,6 +114,20 @@ class UnitTestCase(Class):
return None
cleanup = getattr(cls, "doClassCleanups", lambda: None)
def process_teardown_exceptions() -> None:
# tearDown_exceptions is a list set in the class containing exc_infos for errors during
# teardown for the class.
exc_infos = getattr(cls, "tearDown_exceptions", None)
if not exc_infos:
return
exceptions = [exc for (_, exc, _) in exc_infos]
# If a single exception, raise it directly as this provides a more readable
# error (hopefully this will improve in #12255).
if len(exceptions) == 1:
raise exceptions[0]
else:
raise ExceptionGroup("Unittest class cleanup errors", exceptions)
def unittest_setup_class_fixture(
request: FixtureRequest,
) -> Generator[None, None, None]:
@@ -118,6 +142,7 @@ class UnitTestCase(Class):
# follow this here.
except Exception:
cleanup()
process_teardown_exceptions()
raise
yield
try:
@@ -125,6 +150,7 @@ class UnitTestCase(Class):
teardown()
finally:
cleanup()
process_teardown_exceptions()
self.session._fixturemanager._register_fixture(
# Use a unique name to speed up lookup.
@@ -169,32 +195,29 @@ class UnitTestCase(Class):
class TestCaseFunction(Function):
nofuncargs = True
_excinfo: Optional[List[_pytest._code.ExceptionInfo[BaseException]]] = None
_testcase: Optional["unittest.TestCase"] = None
def _getobj(self):
assert self.parent is not None
# Unlike a regular Function in a Class, where `item.obj` returns
# a *bound* method (attached to an instance), TestCaseFunction's
# `obj` returns an *unbound* method (not attached to an instance).
# This inconsistency is probably not desirable, but needs some
# consideration before changing.
return getattr(self.parent.obj, self.originalname) # type: ignore[attr-defined]
def _getinstance(self):
assert isinstance(self.parent, UnitTestCase)
return self.parent.obj(self.name)
# Backward compat for pytest-django; can be removed after pytest-django
# updates + some slack.
@property
def _testcase(self):
return self.instance
def setup(self) -> None:
# A bound method to be called during teardown() if set (see 'runtest()').
self._explicit_tearDown: Optional[Callable[[], None]] = None
assert self.parent is not None
self._testcase = self.parent.obj(self.name) # type: ignore[attr-defined]
self._obj = getattr(self._testcase, self.name)
super().setup()
def teardown(self) -> None:
super().teardown()
if self._explicit_tearDown is not None:
self._explicit_tearDown()
self._explicit_tearDown = None
self._testcase = None
self._obj = None
self._instance = None
super().teardown()
def startTest(self, testcase: "unittest.TestCase") -> None:
pass
@@ -292,14 +315,14 @@ class TestCaseFunction(Function):
def runtest(self) -> None:
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
assert self._testcase is not None
testcase = self.instance
assert testcase is not None
maybe_wrap_pytest_function_for_tracing(self)
# Let the unittest framework handle async functions.
if is_async_function(self.obj):
# Type ignored because self acts as the TestResult, but is not actually one.
self._testcase(result=self) # type: ignore[arg-type]
testcase(result=self)
else:
# When --pdb is given, we want to postpone calling tearDown() otherwise
# when entering the pdb prompt, tearDown() would have probably cleaned up
@@ -311,16 +334,16 @@ class TestCaseFunction(Function):
assert isinstance(self.parent, UnitTestCase)
skipped = _is_skipped(self.obj) or _is_skipped(self.parent.obj)
if self.config.getoption("usepdb") and not skipped:
self._explicit_tearDown = self._testcase.tearDown
setattr(self._testcase, "tearDown", lambda *args: None)
self._explicit_tearDown = testcase.tearDown
setattr(testcase, "tearDown", lambda *args: None)
# We need to update the actual bound method with self.obj, because
# wrap_pytest_function_for_tracing replaces self.obj by a wrapper.
setattr(self._testcase, self.name, self.obj)
setattr(testcase, self.name, self.obj)
try:
self._testcase(result=self) # type: ignore[arg-type]
testcase(result=self)
finally:
delattr(self._testcase, self.name)
delattr(testcase, self.name)
def _traceback_filter(
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
@@ -349,9 +372,7 @@ def pytest_runtest_makereport(item: Item, call: CallInfo[None]) -> None:
# its own nose.SkipTest. For unittest TestCases, SkipTest is already
# handled internally, and doesn't reach here.
unittest = sys.modules.get("unittest")
if (
unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest) # type: ignore[attr-defined]
):
if unittest and call.excinfo and isinstance(call.excinfo.value, unittest.SkipTest):
excinfo = call.excinfo
call2 = CallInfo[None].from_call(
lambda: pytest.skip(str(excinfo.value)), call.when

View File

@@ -1,5 +1,6 @@
# PYTHON_ARGCOMPLETE_OK
"""pytest: unit and functional testing with Python."""
from _pytest import __version__
from _pytest import version_tuple
from _pytest._code import ExceptionInfo

View File

@@ -16,8 +16,8 @@ import pytest
@contextlib.contextmanager
def ignore_encoding_warning():
with warnings.catch_warnings():
with contextlib.suppress(NameError): # new in 3.10
warnings.simplefilter("ignore", EncodingWarning) # type: ignore [name-defined]
if sys.version_info > (3, 10):
warnings.simplefilter("ignore", EncodingWarning)
yield
@@ -667,7 +667,7 @@ class TestLocalPath(CommonFSTests):
assert p == os.path.expanduser("~")
@pytest.mark.skipif(
not sys.platform.startswith("win32"), reason="case insensitive only on windows"
not sys.platform.startswith("win32"), reason="case-insensitive only on windows"
)
def test_eq_hash_are_case_insensitive_on_windows(self):
a = local("/some/path")
@@ -822,7 +822,7 @@ class TestLocalPath(CommonFSTests):
# depending on how the paths are used), but > 4096 (which is the
# Linux' limitation) - the behaviour of paths with names > 4096 chars
# is undetermined
newfilename = "/test" * 60 # type:ignore[unreachable]
newfilename = "/test" * 60 # type:ignore[unreachable,unused-ignore]
l1 = tmpdir.join(newfilename)
l1.ensure(file=True)
l1.write_text("foo", encoding="utf-8")
@@ -898,7 +898,7 @@ class TestExecutionOnWindows:
class TestExecution:
pytestmark = skiponwin32
def test_sysfind_no_permisson_ignored(self, monkeypatch, tmpdir):
def test_sysfind_no_permission_ignored(self, monkeypatch, tmpdir):
noperm = tmpdir.ensure("noperm", dir=True)
monkeypatch.setenv("PATH", str(noperm), prepend=":")
noperm.chmod(0)
@@ -1368,8 +1368,8 @@ class TestPOSIXLocalPath:
assert realpath.basename == "file"
def test_owner(self, path1, tmpdir):
from grp import getgrgid # type:ignore[attr-defined]
from pwd import getpwuid # type:ignore[attr-defined]
from grp import getgrgid # type:ignore[attr-defined,unused-ignore]
from pwd import getpwuid # type:ignore[attr-defined,unused-ignore]
stat = path1.stat()
assert stat.path == path1

View File

@@ -2,6 +2,7 @@
import dataclasses
import importlib.metadata
import os
from pathlib import Path
import subprocess
import sys
import types
@@ -541,6 +542,32 @@ class TestGeneralUsage:
res = pytester.runpytest(p)
res.assert_outcomes(passed=3)
# Warning ignore because of:
# https://github.com/python/cpython/issues/85308
# Can be removed once Python<3.12 support is dropped.
@pytest.mark.filterwarnings("ignore:'encoding' argument not specified")
def test_command_line_args_from_file(
self, pytester: Pytester, tmp_path: Path
) -> None:
pytester.makepyfile(
test_file="""
import pytest
class TestClass:
@pytest.mark.parametrize("a", ["x","y"])
def test_func(self, a):
pass
"""
)
tests = [
"test_file.py::TestClass::test_func[x]",
"test_file.py::TestClass::test_func[y]",
"-q",
]
args_file = pytester.maketxtfile(tests="\n".join(tests))
result = pytester.runpytest(f"@{args_file}")
result.assert_outcomes(failed=0, passed=2)
class TestInvocationVariants:
def test_earlyinit(self, pytester: Pytester) -> None:

View File

@@ -1,6 +1,7 @@
# mypy: allow-untyped-defs
from __future__ import annotations
import fnmatch
import importlib
import io
import operator
@@ -28,7 +29,7 @@ import pytest
if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle
if sys.version_info[:2] < (3, 11):
if sys.version_info < (3, 11):
from exceptiongroup import ExceptionGroup
@@ -180,7 +181,7 @@ class TestTraceback_f_g_h:
def test_traceback_cut_excludepath(self, pytester: Pytester) -> None:
p = pytester.makepyfile("def f(): raise ValueError")
with pytest.raises(ValueError) as excinfo:
import_path(p, root=pytester.path, consider_namespace_packages=False).f() # type: ignore[attr-defined]
import_path(p, root=pytester.path, consider_namespace_packages=False).f()
basedir = Path(pytest.__file__).parent
newtraceback = excinfo.traceback.cut(excludepath=basedir)
for x in newtraceback:
@@ -237,7 +238,7 @@ class TestTraceback_f_g_h:
n += 1
f(n)
excinfo = pytest.raises(RuntimeError, f, 8)
excinfo = pytest.raises(RecursionError, f, 8)
traceback = excinfo.traceback
recindex = traceback.recursionindex()
assert recindex == 3
@@ -373,7 +374,10 @@ def test_excinfo_no_sourcecode():
except ValueError:
excinfo = _pytest._code.ExceptionInfo.from_current()
s = str(excinfo.traceback[-1])
assert s == " File '<string>':1 in <module>\n ???\n"
# TODO: Since Python 3.13b1 under pytest-xdist, the * is `import
# sys;exec(eval(sys.stdin.readline()))` (execnet bootstrap code)
# instead of `???` like before. Is this OK?
fnmatch.fnmatch(s, " File '<string>':1 in <module>\n *\n")
def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
@@ -1515,7 +1519,7 @@ def test_cwd_deleted(pytester: Pytester) -> None:
result.stderr.no_fnmatch_line("*INTERNALERROR*")
def test_regression_nagative_line_index(pytester: Pytester) -> None:
def test_regression_negative_line_index(pytester: Pytester) -> None:
"""
With Python 3.10 alphas, there was an INTERNALERROR reported in
https://github.com/pytest-dev/pytest/pull/8227

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