Compare commits

..

272 Commits

Author SHA1 Message Date
Ran Benita
71849cc05c Merge pull request #12055 from bluetech/cherry-pick-release
Cherry-pick 8.1.0 release notes
2024-03-03 23:29:47 +02:00
Ran Benita
e410705561 Cherry-pick 8.1.0 release notes
(cherry picked from commit 0a536810dc)
2024-03-03 23:27:02 +02:00
Ran Benita
00043f7f10 Merge pull request #12038 from bluetech/fixtures-rm-arg2index
fixtures: avoid mutable arg2index state in favor of looking up the request chain
2024-03-03 15:45:34 +02:00
Ran Benita
f4e10251a4 Merge pull request #12048 from bluetech/fixture-teardown-excgroup
fixtures: use exception group when multiple finalizers raise in fixture teardown
2024-03-03 15:09:36 +02:00
Ran Benita
43492f5707 Merge pull request #12051 from jakkdl/test_debugging_pythonbreakpoint
[minor/QoL] monkeypatch.delenv PYTHONBREAKPOINT in two tests that previously failed/skipped
2024-03-03 15:07:19 +02:00
github-actions[bot]
82fe28dae4 [automated] Update plugin list (#12049)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-03-03 12:50:42 +00:00
jakkdl
5e2ee7175c monkeypatch.delenv PYTHONBREAKPOINT in two tests that previously failed/skipped 2024-03-03 13:48:29 +01:00
Bruno Oliveira
89ee4493cc Merge pull request #11997 from nicoddemus/11475-importlib
Change importlib to first try to import modules using the standard mechanism
2024-03-03 09:43:14 -03:00
mrbean-bremen
8248946a55 Do not collect symlinked tests under Windows (#12050)
The check for short paths under Windows via os.path.samefile, introduced in #11936, also found similar tests in symlinked tests in the GH Actions CI.

Fixes #12039.

Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
2024-03-03 09:41:31 -03:00
Ran Benita
434282e17f fixtures: use exception group when multiple finalizers raise in fixture teardown
Previously, if more than one fixture finalizer raised, only the first
was reported, and the other errors were lost.

Use an exception group to report them all. This is similar to the change
we made in node teardowns (in `SetupState`).
2024-03-02 23:39:11 +02:00
Bruno Oliveira
d6134bc21e doc: document consider_namespace_packages option 2024-03-02 16:13:48 -03:00
Bruno Oliveira
aac720abc9 testing/test_pathlib: parametrize namespace package option
Test with namespace packages support even when it will not find namespace packages to ensure it will at least not give weird results or crashes.
2024-03-02 16:13:48 -03:00
Bruno Oliveira
111c0d910e Add consider_namespace_packages ini option
Fix #11475
2024-03-02 16:13:48 -03:00
Bruno Oliveira
199d4e2b73 pathlib: import signature and docs for import_path 2024-03-02 16:13:48 -03:00
Bruno Oliveira
5746b8e696 doc: update and improve import mode docs 2024-03-02 16:13:48 -03:00
Bruno Oliveira
c85fce39b6 Change importlib to first try to import modules using the standard mechanism
As detailed in
https://github.com/pytest-dev/pytest/issues/11475#issuecomment-1937043670,
currently with `--import-mode=importlib` pytest will try to import every
file by using a unique module name, regardless if that module could be
imported using the normal import mechanism without touching `sys.path`.

This has the consequence that non-test modules available in `sys.path`
(via other mechanism, such as being installed into a virtualenv,
PYTHONPATH, etc) would end up being imported as standalone modules,
instead of imported with their expected module names.

To illustrate:

```
.env/
  lib/
    site-packages/
      anndata/
        core.py
```

Given `anndata` is installed into the virtual environment, `python -c
"import anndata.core"` works, but pytest with `importlib` mode would
import that module as a standalone module named
`".env.lib.site-packages.anndata.core"`, because importlib module was
designed to import test files which are not reachable from `sys.path`,
but now it is clear that normal modules should be imported using the
standard mechanisms if possible.

Now `imporlib` mode will first try to import the module normally,
without changing `sys.path`, and if that fails it falls back to
importing the module as a standalone module.

This also makes `importlib` respect namespace packages.

This supersedes #11931.

Fix #11475
Close #11931
2024-03-02 16:13:48 -03:00
Bruno Oliveira
067daf9f7d pathlib: consider namespace packages in resolve_pkg_root_and_module_name
This applies to `append` and `prepend` import modes; support for
`importlib` mode will be added in a separate change.
2024-03-02 16:13:48 -03:00
Bruno Oliveira
5867426455 pathlib: handle filenames starting with . in module_name_from_path 2024-03-02 16:13:48 -03:00
Bruno Oliveira
4dea18308b pathlib: extract a function _import_module_using_spec
Will be reused.
2024-03-02 16:13:48 -03:00
Bruno Oliveira
7524e60d8f pathlib: extract a function resolve_pkg_root_and_module_name
Will be reused.
2024-03-02 16:13:48 -03:00
Bruno Oliveira
dcf01fd39a testing/test_pathlib: add an importlib test
Ensure the implementation isn't changed to trigger such a bug.
2024-03-02 16:13:48 -03:00
Bruno Oliveira
300ceb435e testing/test_doctest: make test_importmode more realistic 2024-03-02 16:13:48 -03:00
Bruno Oliveira
887e251abb testing/test_pathlib: remove test_issue131_on__init__
The test seems wrong, and we haven't been able to figure out what it's
trying to test (the original issue is lost in time).
2024-03-02 16:13:48 -03:00
Ran Benita
6ed005161d Merge pull request #12043 from bluetech/pyargs-root
Fix collection failures due to permission errors when using `--pyargs`
2024-03-02 21:09:20 +02:00
Ran Benita
31026a2df2 main: only include package parents in collection tree for --pyargs collection arguments
(diff better viewed ignoring whitespace)

In pytest<8, the collection tree for `pyargs` arguments in an invocation
like this:

    pytest --collect-only --pyargs pyflakes.test.test_undefined_names

looked like this:

```
<Package test>
  <Module test_undefined_names.py>
    <UnitTestCase Test>
      <TestCaseFunction test_annotationUndefined>
      ... snipped ...
```

The pytest 8 collection improvements changed it to this:

```
<Dir pytest>
  <Dir .tox>
    <Dir venv>
      <Dir lib>
        <Dir python3.11>
          <Dir site-packages>
            <Package pyflakes>
              <Package test>
                <Module test_undefined_names.py>
                  <UnitTestCase Test>
                    <TestCaseFunction test_annotationUndefined>
                    ... snipped ...
```

Besides being egregious (and potentially even worse than the above,
going all the way to the root, for system-installed packages, as is
apparently common in CI), this also caused permission errors when trying
to probe some of those intermediate directories.

This change makes `--pyargs` arguments no longer try to add parent
directories to the collection tree according to the `--confcutdir` like
they're regular arguments. Instead, only add the parents that are in the
import path. This now looks like this:

```
<Package .tox/venv/lib/python3.11/site-packages/pyflakes>
  <Package test>
    <Module test_undefined_names.py>
      <UnitTestCase Test>
        <TestCaseFunction test_annotationUndefined>
        ... snipped ...
```

Fix #11904.
2024-03-02 20:26:02 +02:00
Ran Benita
f20e32c982 main: slight refactor to collection argument parents logic
No logical change, preparation for the next commit.
2024-03-02 20:26:02 +02:00
Ran Benita
1612d4e393 main: add module_name to CollectionArgument
This is available when the argument is a `--pyargs` argument (resolved
from a python module path). Will be used in an upcoming commit.
2024-03-02 20:26:02 +02:00
Ran Benita
5e0d11746c main: model the result of resolve_collection_arguments as a dataclass
In preparation of adding more info to it.
2024-03-02 20:25:57 +02:00
Ran Benita
b6bf58abe8 Merge pull request #12042 from donhui/docs-0301
docs: update plugins number
2024-03-01 15:20:33 +02:00
Ran Benita
ff4c3b2873 main: add missing import importlib.util
Used in `resolve_collection_argument`. It's implicitly imported by some
other import, but some type checkers don't recognize this.
2024-03-01 13:28:27 +02:00
donghui
4db5e53709 docs: update plugins number 2024-03-01 05:43:57 +08:00
Ran Benita
c967d508d6 Merge pull request #12037 from bluetech/fixtures-getnextfixturedef-parent
fixtures: remove unneeded optimization from `_getnextfixturedef`
2024-02-28 09:28:15 +02:00
Ran Benita
bd45ccd2ca fixtures: avoid mutable arg2index state in favor of looking up the request chain
pytest allows a fixture to request its own name (directly or
indirectly), in which case the fixture with the same name but one level
up is used.

To know which fixture should be used next, pytest keeps a mutable
item-global dict `_arg2index` which maintains this state. This is not
great:

- Mutable state like this is hard to understand and reason about.

- It is conceptually buggy; the indexing is global (e.g. if requesting
  `fix1` and `fix2`, the indexing is shared between them), but actually
  different branches of the subrequest tree should not affect each
  other.

  This is not an issue in practice because pytest keeps a cache of the
  fixturedefs it resolved anyway (`_fixture_defs`), but if the cache is
  removed it becomes evident.

Instead of the `_arg2index` state, count how many `argname`s deep we are
in the subrequest tree ("the fixture stack") and use that for the index.

This way, no global mutable state and the logic is very localized and
easier to understand.

This is slower, however fixture stacks should not be so deep that this
matters much, I hope.
2024-02-27 22:24:26 +02:00
Ran Benita
c83c1c4bda fixtures: add _iter_chain helper method
Will be reused in the next commit.
2024-02-27 22:24:26 +02:00
Ran Benita
686f9e0720 fixtures: remove unneeded optimization from _getnextfixturedef
According to my understanding, this code, which handles obtaining the
relevant fixturedefs when a dynamic `getfixturevalue` is used, has an
optimization where it only grabs fixturedefs that are visible to the
*parent* of the item, instead of the item itself, under the assumption
that a fixturedef can't be visible to a single item, only to a
collector.

Remove this optimization for the following reasons:
- It doesn't save much (one loop iteration in `matchfactories`)
- It slightly complicates the complex fixtures code
- If some plugin wants to make a fixture visible only to a single item,
  why not let it?
- In the static case (`getfixtureclosure`), this optimization is not
  done (despite the confusing name `parentnode`, it is *not* the parent
  node). This is inconsistent.
2024-02-27 22:22:07 +02:00
dependabot[bot]
98b008ff6c build(deps): Bump anyio[curio,trio] in /testing/plugins_integration (#12033)
Bumps [anyio[curio,trio]](https://github.com/agronholm/anyio) from 4.2.0 to 4.3.0.
- [Release notes](https://github.com/agronholm/anyio/releases)
- [Changelog](https://github.com/agronholm/anyio/blob/master/docs/versionhistory.rst)
- [Commits](https://github.com/agronholm/anyio/compare/4.2.0...4.3.0)

---
updated-dependencies:
- dependency-name: anyio[curio,trio]
  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-02-26 10:51:07 -03:00
Bruno Oliveira
73edefdd64 Merge pull request #12031 from pytest-dev/update-plugin-list/patch-affc652f1
[automated] Update plugin list
2024-02-24 21:50:44 -03:00
Bruno Oliveira
ffd727e9d6 Fix mention of the prefix for pytest plugins in plugin_list 2024-02-24 21:35:51 -03:00
pytest bot
b6eb985d55 [automated] Update plugin list 2024-02-25 00:19:33 +00:00
Ran Benita
affc652f1c Merge pull request #12030 from bluetech/cherry-pick-release
Cherry pick 8.0.2 release notes
2024-02-25 00:31:52 +02:00
Ran Benita
7460b1aa31 Cherry pick 8.0.2 release notes
(cherry picked from commit e53f798932)
2024-02-25 00:25:49 +02:00
Patrick Lannigan
84bd31de64 New verbosity_test_case ini option (#11653)
Allow for the output of test case execution to be controlled independently from the application verbosity level. 

`verbosity_test_case` is the new ini setting to adjust this functionality.

Fix #11639
2024-02-24 16:27:54 -03:00
Ran Benita
a2a9aa6cde Merge pull request #12027 from bluetech/sys-last-exc
runner: add support for `sys.last_exc` for post-mortem debugging on Python>=3.12
2024-02-23 22:41:14 +02:00
robotherapist
c09f74619b runner: add support for sys.last_exc for post-mortem debugging on Python>=3.12
Fix #11850
2024-02-23 15:59:32 +02:00
Ran Benita
b510adf9ef Merge pull request #12023 from bluetech/fixtures-cleanups-2
fixtures: remove an unneeded suppress
2024-02-23 15:35:17 +02:00
Ran Benita
93cd7ba857 Merge pull request #12022 from bluetech/revert-11721
Revert "Fix teardown error reporting when `--maxfail=1` (#11721)"
2024-02-23 15:34:32 +02:00
Bruno Oliveira
8d9b95dcdb Fix collection of short paths on Windows (#11936)
Passing a short path in the command line was causing the matchparts check to fail, because ``Path(short_path) != Path(long_path)``.

Using ``os.path.samefile`` as fallback ensures the comparsion works on Windows when comparing short/long paths.

Fix #11895
2024-02-23 07:51:15 -03:00
Ran Benita
ad651ddfce fixtures: remove an unneeded suppress
In `CallSpec2.setmulti` the `params` and `_arg2scope` dicts are always
set together.

Further, the `get_parametrized_fixture_keys` accesses `_arg2scope` for
all argnames without a check, which I think rules out that the code
protects against plugin shenanigans.

After removing the suppress, adjust the comment and code to make more
sense.
2024-02-23 11:51:49 +02:00
Ran Benita
00d9640abc Revert "Fix teardown error reporting when --maxfail=1 (#11721)"
Fix #12021.
Reopens #11706.

This reverts commit 12b9bd5801.

This change caused a bad regression in pytest-xdist:
https://github.com/pytest-dev/pytest-xdist/issues/1024

pytest-xdist necessarily has special handling of `--maxfail` and session
fixture teardown get executed multiple times with the change.

Since I'm not sure how to adapt pytest-xdist myself, revert for now.

I kept the sticky `shouldstop`/`shouldfail` changes as they are good
ideas regardless I think.
2024-02-23 11:45:26 +02:00
Ran Benita
010ce2ab0f Add typing to from_parent return values (#11916)
Up to now the return values of `from_parent` were untyped, this is an
attempt to make it work with `typing.Self`.
2024-02-22 23:35:57 -08:00
Eric Larson
1640f2e454 ENH: Improve warning stacklevel (#12014)
* ENH: Improve warning stacklevel

* TST: Add test

* TST: Ping

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* MAINT: Changelog

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-22 22:11:05 -08:00
Bruno Oliveira
59e9a58cc9 Remove Anthony from Tidelift participants (#12020)
As discussed in the core development channels, @asottile asked to no longer be a Tidelift participant, at least for the time being.
2024-02-22 21:04:11 -03:00
Ran Benita
691d8fcafb Merge pull request #12019 from bluetech/fixtures-simplify
fixtures: remove a no longer needed sort
2024-02-22 21:52:56 +02:00
Ben Leith
c5c729e27a Add --log-file-mode option to the logging plugin, enabling appending to log-files (#11979)
Previously, the mode was hard-coded to be "w" which truncates the file before logging.

Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
2024-02-21 12:02:19 +00:00
Ran Benita
9bea1be215 fixtures: remove a no longer needed sort
Dicts these days preserve order, so the sort is no longer needed to
achieve determinism.

As shown by the `test_dynamic_parametrized_ordering` test, this can
change the ordering of items, but only in equivalent ways (same number
of setups/teardowns per scope), it will just respect the user's given
ordering better (hence `vxlan` items now ordered before `vlan` items
compared to the previous ordering).
2024-02-21 10:17:25 +02:00
Ran Benita
2007cf6296 fixtures: remove a level of indentation
A bit easier to follow.
2024-02-21 10:17:24 +02:00
Ran Benita
79def57cc6 python: small code cleanup 2024-02-21 10:17:24 +02:00
Ran Benita
8a410d0ba6 Merge pull request #12013 from bluetech/doctest-test
testing: add a regression test for `setup_module` + `--doctest-modules`
2024-02-20 22:22:01 +02:00
Ran Benita
a37cff3ca8 testing: add a regression test for setup_module + --doctest-modules
Refs #12011 (only fails on 8.0.1, not main).
2024-02-20 09:36:00 +02:00
pre-commit-ci[bot]
95f21f96cc [pre-commit.ci] pre-commit autoupdate (#12012)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.1 → v0.2.2](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.1...v0.2.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-19 21:36:03 -03:00
Bruno Oliveira
998fee1679 Clarify PytestPluginManager._is_in_confcutdir (#12007)
Follow up to #12006, let's put some comments clarifying `is_in_confcutdir` semantics, as this is not the first time someone misunderstands it.

Also removed an obsolete comment in `_loadconftestmodules`: we already set the `confcutdir` based on `rootdir`/`initfile` if not explicitly given.

Co-authored-by: Ran Benita <ran@unusedvar.com>
2024-02-18 17:27:47 -03:00
Bruno Oliveira
40011b838b Allow Sphinx 7.x (#12002)
Thanks to https://github.com/pytest-dev/pytest/issues/11988#issuecomment-1950318888, the problem was our custom template.

The solution was to copy the template from Sphinx 7 and remove the header.
2024-02-18 07:21:05 -03:00
Bruno Oliveira
b37696703b Merge pull request #12003 from pytest-dev/update-plugin-list/patch-cefb3e227
[automated] Update plugin list
2024-02-17 22:35:26 -03:00
Bruno Oliveira
abf6a60567 Fix 'pytest_' mention: it was being considered a rst reference 2024-02-17 22:21:30 -03:00
pytest bot
52da8dd66c [automated] Update plugin list 2024-02-18 00:19:26 +00:00
Bruno Oliveira
cefb3e2277 Disallow Sphinx 6 and 7 (#12000)
Using Sphinx 6.x and 7.x the search bar disappears. Restrict to Sphinx 5.x for now until we find a solution.

Reverts #11568
Fixes #11988
2024-02-17 17:46:58 -03:00
Bruno Oliveira
5f241f388b Revert sys.platform verification to simple comparison (#11998)
As the comment above it states, we need to keep the comparison simple so mypy can understand it, otherwise it will fail to properly handle this on Windows and will flag ``os.getuid()`` missing.
2024-02-17 16:10:21 -03:00
Ran Benita
aebeb33137 Merge pull request #11996 from bluetech/getstatementrange-index-error
code: fix `IndexError` crash in `getstatementrange_ast`
2024-02-17 18:59:10 +02:00
Ran Benita
9f13d41d7b code: fix IndexError crash in getstatementrange_ast
Fix #11953.
2024-02-17 18:43:55 +02:00
Ran Benita
984478109f Merge pull request #11995 from bluetech/cherry-pick-release
Cherry pick 8.0.1 release notes
2024-02-17 00:27:55 +02:00
Ran Benita
22b541e4eb Merge pull request #11993 from pytest-dev/release-8.0.1
Prepare release 8.0.1

(cherry picked from commit 68524d4858)
2024-02-17 00:11:27 +02:00
Ran Benita
fbe18fc7a9 Merge pull request #11991 from bluetech/warns-fix
recwarn: fix pytest.warns handling of Warnings with multiple arguments
2024-02-16 14:29:19 +02:00
Reagan Lee
9b838f491f recwarn: fix pytest.warns handling of Warnings with multiple arguments
Fix #11906
2024-02-16 14:14:24 +02:00
Ran Benita
6ef0cf150a Merge pull request #11959 from eerovaher/warn-message-type
Allow using `warnings.warn()` with a `Warning`
2024-02-16 14:00:37 +02:00
Eero Vaher
0475b1c925 Allow using warnings.warn() with Warnings
Test:

`warnings.warn()` expects that its first argument is a `str` or a
`Warning`, but since 9454fc38d3
`pytest.warns()` no longer allows `Warning` instances unless the first
argument the `Warning` was initialized with is a `str`. Furthermore, if
the `Warning` was created without arguments then `pytest.warns()` raises
an unexpected `IndexError`. The new tests reveal the problem.

Fix:

`pytest.warns()` now allows using `warnings.warn()` with a `Warning`
instance, as is required by Python, with one exception. If the warning
used is a `UserWarning` that was created by passing it arguments and the
first argument was not a `str` then `pytest.raises()` still considers
that an error. This is because if an invalid type was used in
`warnings.warn()` then Python creates a `UserWarning` anyways and it
becomes impossible for `pytest` to figure out if that was done
automatically or not.

[ran: rebased on previous commit]
2024-02-16 13:41:59 +02:00
Ran Benita
dcf9da92be recwarn: minor style improvements
In preparation for next commit.
2024-02-16 13:41:27 +02:00
Ran Benita
8e56795b77 Merge pull request #11989 from bluetech/plugin-list-underscore
scripts/update-plugin-list: include packages starting with "pytest_" (underscore)
2024-02-16 13:01:25 +02:00
Ran Benita
718cd40015 Merge pull request #11920 from bluetech/warn-skip
recwarn: let base exceptions propagate through `pytest.warns` again
2024-02-16 12:22:52 +02:00
Ran Benita
288201b8f5 recwarn: let base exceptions propagate through pytest.warns again
Fix #11907.
2024-02-16 12:07:56 +02:00
Ran Benita
c6313a06fb scripts/update-plugin-list: include packages starting with "pytest_" (underscore)
Fix #11985.
2024-02-16 12:03:38 +02:00
dependabot[bot]
5d7a5a9343 build(deps): Bump codecov/codecov-action from 3 to 4 (#11921)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  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-02-14 16:11:30 -03:00
dependabot[bot]
cbb5c82c39 build(deps): Bump peter-evans/create-pull-request from 5.0.2 to 6.0.0 (#11922)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 5.0.2 to 6.0.0.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](153407881e...b1ddad2c99)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  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-02-14 16:11:20 -03:00
Bruno Oliveira
acafd003aa Consider pyproject.toml files for config if no other config files were found (#11962)
Today `pyproject.toml` is the standard for declaring a Python project root, so seems reasonable to consider it for the ini configuration (and specially `rootdir`) in case we do not find other suitable candidates.

Related to #11311
2024-02-14 16:08:45 -03:00
Bruno Oliveira
46e6fb12c7 Consider paths and pathlists relative to cwd in case of an absent ini-file (#11963)
Previously this would trigger an `AssertionError`.

While it could be considered a bug-fix, but given it now can be relied upon, it is probably better to consider it an improvement.

Fix #11311
2024-02-14 15:53:28 -03:00
Dave Hall
fe7907d78c Add logot to plugin list (#11972) 2024-02-14 15:50:34 -03:00
Ran Benita
5bb1363435 Merge pull request #11957 from bluetech/fix-session-collect-order
main: fix reversed collection order in Session
2024-02-13 09:44:13 +02:00
pre-commit-ci[bot]
ca84327f15 [pre-commit.ci] pre-commit autoupdate (#11967)
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.2.0 → v0.2.1](https://github.com/astral-sh/ruff-pre-commit/compare/v0.2.0...v0.2.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-13 06:19:40 +01:00
dependabot[bot]
3b798e5422 build(deps): Bump pytest-asyncio in /testing/plugins_integration (#11965)
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.23.3 to 0.23.5.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.23.3...v0.23.5)

---
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-02-12 11:06:14 -03:00
Pierre Sassoulas
526b971221 Merge pull request #11961 Add some autofixable refactor rules from pylint 2024-02-11 19:52:29 +01:00
github-actions[bot]
23dfb52974 [automated] Update plugin list (#11964)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-02-10 21:50:24 -03:00
Pierre Sassoulas
7da04c963f [ruff] Add unwanted pylint message to the ignore list 2024-02-10 23:42:36 +01:00
Pierre Sassoulas
b922270ce7 [performance] Micro-optimization in '_traceback_filter'
Should be around 40% faster according to this simplified small benchmark:

python -m timeit "a=[0, 1, 2, 3, 4];b=list((e if i in {0, len(a) -1} else str(e)) for i, e in enumerate(a))"
200000 loops, best of 5: 1.12 usec per loop
python -m timeit "a=[0, 1, 2, 3, 4];b=list((a[0], *(str(e) for e in a[1:-1]), a[-1]))"
500000 loops, best of 5: 651 nsec per loop

python -m timeit "a=[0, 1, 2, 3, 4,5,6,7,8,9,10,11,12,13,14,15,16];b=list((e if i in {0, len(a) -1} else str(e)) for i, e in enumerate(a))"
100000 loops, best of 5: 3.31 usec per loop
python -m timeit "a=[0, 1, 2, 3, 4,5,6,7,8,9,10,11,12,13,14,15,16];b=list((a[0], *(str(e) for e in a[1:-1]), a[-1]))"
200000 loops, best of 5: 1.72 usec per loop
2024-02-10 20:23:48 +01:00
Pierre Sassoulas
1180348303 [ruff] Add 'consider-using-in' from pylint
See https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/consider-using-in.html
An automated fix from ruff is available (but it's unsafe for now).
2024-02-10 15:47:44 +01:00
Florian Bruhin
7690a0ddf1 Improve error message when using @pytest.fixture twice (#11670)
* Improve error message when using @pytest.fixture twice

While obvious in hindsight, this error message confused me. I thought my fixture
function was used in a test function twice, since the wording is ambiguous.

Also, the error does not tell me *which* function is the culprit.

Finally, this adds a test, which wasn't done in
cfd16d0dac where this was originally implemented.

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-02-09 14:59:04 +01:00
Ran Benita
29a5f94428 Merge pull request #11956 from bluetech/ruff-warning
Fix ruff deprecation warning + enable few more rules
2024-02-09 15:32:11 +02:00
Ran Benita
ea57c40c43 main: fix reversed collection order in Session
Since we're working with a stack (last in first out), we need to append
to it in reverse to preserve the order when popped.

Fix #11937.
2024-02-09 15:24:44 +02:00
Ran Benita
a182e10b06 Enable lint PGH004 - Use specific rule codes when using noqa 2024-02-09 11:14:36 +02:00
Ran Benita
d1ee6d154f Enable flake8-pie 2024-02-09 11:08:33 +02:00
Ran Benita
d02618d173 Fix ruff deprecation warning
warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `pyproject.toml`:
  - 'ignore' -> 'lint.ignore'
  - 'select' -> 'lint.select'
2024-02-09 10:48:05 +02:00
whysage
9454fc38d3 closes: 10865 Fix muted exception (#11804)
* feat: 10865

* feat: 10865 refactor code and tests

* feat: 10865 add test skip for pypy

* feat: 10865 add test with valid warning

* feat: 10865 fix v2 for codecov

* feat: 10865 fix conflict
2024-02-07 16:47:56 -08:00
Ran Benita
101328aba5 Merge pull request #11947 from bluetech/hypothesis-revert
Revert "testing: temporarily disable test due to hypothesis issue (#1836)"
2024-02-07 22:44:58 +02:00
Ran Benita
42785cca44 Revert "testing: temporarily disable test due to hypothesis issue (#11836)"
This reverts commit 5cd0535395.
2024-02-07 22:13:23 +02:00
Ran Benita
6c0b6c2f92 Merge pull request #11941 from bluetech/doctest-parsefactories
doctest: fix autouse fixtures possibly not getting picked up
2024-02-07 22:09:17 +02:00
Ran Benita
9cd14b4ffb doctest: fix autouse fixtures possibly not getting picked up
Fix #11929.

Figured out what's going on. We have the following collection tree:

```
<Dir pyspacewar>
  <Dir src>
    <Package pyspacewar>
      <Package tests>
        <DoctestModule test_main.py>
          <DoctestItem pyspacewar.tests.test_main.doctest_main>
```

And the `test_main.py` contains an autouse fixture (`fake_game_ui`) that
`doctest_main` needs in order to run properly. The fixture doesn't run!
It doesn't run because nothing collects the fixtures from (calls
`parsefactories()` on) the `test_main.py` `DoctestModule`.

How come it only started happening with commit
ab63ebb3dc07b89670b96ae97044f48406c44fa0? Turns out it mostly only
worked accidentally. Each `DoctestModule` is also collected as a normal
`Module`, with the `Module` collected after the `DoctestModule`. For
example, if we add a non-doctest test to `test_main.py`, the collection
tree looks like this:

```
<Dir pyspacewar>
  <Dir src>
    <Package pyspacewar>
      <Package tests>
        <DoctestModule test_main.py>
          <DoctestItem pyspacewar.tests.test_main.doctest_main>
        <Module test_main.py>
          <Function test_it>
```

Now, `Module` *does* collect fixtures. When autouse fixtures are
collected, they are added to the `_nodeid_autousenames` dict.

Before ab63ebb3dc, `DoctestItem` consults
`_nodeid_autousenames` at *setup* time. At this point, the `Module` has
collected and so it ended up picking the autouse fixture (this relies on
another "accident", that the `DoctestModule` and `Module` have the same
node ID).

After ab63ebb3dc, `DoctestItem` consults
`_nodeid_autousenames` at *collection* time (= when it's created). At
this point, the `Module` hasn't collected yet, so the autouse fixture is
not picked out.

The fix is simple -- have `DoctestModule.collect()` call
`parsefactories`. From some testing I've done it shouldn't have negative
consequences (I hope).
2024-02-07 21:53:51 +02:00
dependabot[bot]
4c894f20a1 build(deps): Bump django in /testing/plugins_integration (#11946)
Bumps [django](https://github.com/django/django) from 5.0 to 5.0.2.
- [Commits](https://github.com/django/django/compare/5.0...5.0.2)

---
updated-dependencies:
- dependency-name: django
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 15:37:56 -03:00
Ran Benita
6e5008f19f doctest: don't open code the module import
Currently, `DoctestModule` does `import_path` on its own. This changes
it to use `importtestmodule` as `Module` does. The behavioral changes
are:

- Much better error messages on import errors.

- Handles a few more error cases (see `importtestmodule`). This
  technically expands the cover of `--doctest-ignore-import-errors` but
  I think it makes sense.

- Considers `pytest_plugins` in the module.

- Populates `self.obj` as properly (without double-imports) as is
  expected from a `PyCollector`.

This is also needed for the next commit.
2024-02-06 23:46:23 +02:00
Pierre Sassoulas
b9d02c5b53 Merge pull request #11932 Add pre-commit hook and fix pyproject.toml
- [pyproject-fmt] Add pre-commit hook and autofix existing
- Proper setuptools version for pyproject.toml
2024-02-06 15:14:07 +01:00
Pierre Sassoulas
d1095426c1 Update setuptools version to the one that support pyproject.toml 2024-02-06 14:40:31 +01:00
Pierre Sassoulas
757778f5f6 [pyproject-fmt] Add pre-commit hook and autofix existing 2024-02-06 14:40:31 +01:00
Pierre Sassoulas
404d31a942 Merge pull request #11930 from Pierre-Sassoulas/setup.cfg-to-pyproject.toml
Migration from ``setup.cfg`` to ``pyproject.toml``
2024-02-05 22:40:44 +01:00
Pierre Sassoulas
0d91539614 Merge pull request #11935 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2024-02-05 21:46:11 +01:00
pre-commit-ci[bot]
835e8032f4 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/astral-sh/ruff-pre-commit: v0.1.15 → v0.2.0](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.15...v0.2.0)
2024-02-05 20:29:38 +00:00
Pierre Sassoulas
c76ea72331 [pyproject.toml] Moving 'package_data' (py.typed) from setup.cfg 2024-02-05 21:04:33 +01:00
Pierre Sassoulas
dab1583ce1 [pyproject.toml] Remove setup_requires / package_dir in setup.cfg already exists 2024-02-05 21:04:12 +01:00
Pierre Sassoulas
411c2cbf9d [pyproject.toml] Translate dynamic option to pyproject.toml 2024-02-05 21:03:28 +01:00
Pierre Sassoulas
c3583dee0b [pyproject.toml] Translate 'metadata:platforms' to pyproject.toml 2024-02-05 21:03:26 +01:00
Pierre Sassoulas
a1d99e4bdd [pyproject.toml] Move information from setup.cfg to pyproject.toml 2024-02-05 21:02:32 +01:00
Ran Benita
aaa9ca7327 Merge pull request #11918 from pytest-dev/update-plugin-list/patch-20b18f0f9
[automated] Update plugin list
2024-02-05 13:49:09 +02:00
Ran Benita
29094983d7 Merge pull request #11923 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-django-4.8.0
build(deps): Bump pytest-django from 4.7.0 to 4.8.0 in /testing/plugins_integration
2024-02-05 13:32:11 +02:00
Ran Benita
b8e56557c6 Merge pull request #11925 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-sugar-1.0.0
build(deps): Bump pytest-sugar from 0.9.7 to 1.0.0 in /testing/plugins_integration
2024-02-05 13:31:50 +02:00
Ran Benita
304ccb4ad7 Merge pull request #11928 from bluetech/setup-cfg-cleanups
Some setup.cfg cleanups
2024-02-05 13:29:49 +02:00
Ran Benita
90634a6060 setup.cfg: remove zip_safe
The setuptools docs say it's obsolete and no longer needed:
https://setuptools.pypa.io/en/latest/deprecated/zip_safe.html#understanding-the-zip-safe-flag
2024-02-05 12:21:37 +02:00
Ran Benita
ccbae95ad4 setup.cfg: remove redundant packages/py_modules
The `package_dir` already achieves this.
2024-02-05 12:16:52 +02:00
Ran Benita
cd9b241047 setup.cfg: remove [devpi:upload] section
Not needed since bfe2cbe875.
2024-02-05 12:08:21 +02:00
Ran Benita
5c67cb2cf5 setup.cfg: remove [check-manifest] section
Not used since 731c35fcab.
2024-02-05 11:59:48 +02:00
Ran Benita
2fc7926bae setup.cfg: remove [build_sphinx] section
Legacy thing that is no longer needed.
2024-02-05 11:59:32 +02:00
Ran Benita
79de84941a setup.cfg: move mypy configuration from setup.cfg to pyproject.toml
TOML is a nicer format than the INI format setup.cfg uses.
2024-02-05 11:59:27 +02:00
Pierre Sassoulas
0d1f4c63fa Merge pull request #11914 - Activate flake8-bugbear and flake8-pyi 2024-02-05 09:37:37 +01:00
dependabot[bot]
2d4e27daf2 build(deps): Bump pytest-sugar in /testing/plugins_integration
Bumps [pytest-sugar](https://github.com/Teemu/pytest-sugar) from 0.9.7 to 1.0.0.
- [Release notes](https://github.com/Teemu/pytest-sugar/releases)
- [Changelog](https://github.com/Teemu/pytest-sugar/blob/main/CHANGES.rst)
- [Commits](https://github.com/Teemu/pytest-sugar/compare/v0.9.7...v1.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-05 03:39:30 +00:00
dependabot[bot]
2e50788b6d build(deps): Bump pytest-django in /testing/plugins_integration
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.7.0 to 4.8.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.7.0...v4.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-05 03:39:23 +00:00
Pierre Sassoulas
3101c026b9 [flake8-bugbear] Remove hidden global state to import only once 2024-02-04 23:08:38 +01:00
Pierre Sassoulas
e193a263c7 [flake8-pyi] Add checks for flake8-pyi and fix existing 2024-02-04 19:27:23 +01:00
Pierre Sassoulas
4eb246d4e1 [flake8-bugbear] noqa B023 not bound by design 2024-02-04 19:27:23 +01:00
Pierre Sassoulas
41ff3584d7 [flake8-bugbear] Fixes a B017 we can actually fix and noqa the two others 2024-02-04 19:27:23 +01:00
Pierre Sassoulas
b62d4b3527 [flake8-bugbear] Remove misleading multiple characters in lstrip
See https://pylint.readthedocs.io/en/stable/user_guide/messages/error/bad-str-strip-call.html
2024-02-04 19:27:23 +01:00
Pierre Sassoulas
e7bab63537 [flake8-bugbear] noqa all the useless comparison that are justified 2024-02-04 19:27:23 +01:00
Pierre Sassoulas
52fba25ff9 [flake8-bugbear] Fix all the useless expressions that are justified 2024-02-04 19:27:23 +01:00
Pierre Sassoulas
fcb818b73c [flake8-bugbear] Re-raise all exceptions with proper exception chaining 2024-02-04 19:27:23 +01:00
Pierre Sassoulas
7eef4619d5 [flake8-bugbear] Add checks from flake8 bugbear 2024-02-04 19:27:16 +01:00
Ran Benita
e28f35c296 Merge pull request #11915 from bluetech/compat-string-types
compat: a couple of minor cleanups
2024-02-04 16:51:43 +02:00
pytest bot
b28bb01c4e [automated] Update plugin list 2024-02-04 00:19:52 +00:00
Ran Benita
3ba4095400 compat: inline helpers into ascii_escaped
The helpers don't add much.
2024-02-03 18:42:05 +02:00
Ran Benita
99e8129ba3 compat: get rid of STRING_TYPES
I think it only obfuscates the code, also calling `bytes` a string type
is pretty misleading in Python 3.
2024-02-03 18:38:38 +02:00
Ran Benita
20b18f0f9a Merge pull request #11913 from bluetech/ruff-version-ignore
Ignore isort on _version.py
2024-02-02 21:55:40 +02:00
Ran Benita
cb5f738858 Ignore isort on _version.py
The file is generated. This makes `ruff src/` run cleanly (when not
running through pre-commit).
2024-02-02 21:29:30 +02:00
Pierre Sassoulas
5be64c31cb Merge pull request #11912 from Pierre-Sassoulas/activate-ruff-checks
[pre-commit] Activate ruff checks and fix existing issues
2024-02-02 20:22:18 +01:00
Pierre Sassoulas
233ab89f13 [ruff] Fix all consider [*cats, garfield] instead of cats + [garfield] 2024-02-02 15:18:38 +01:00
Pierre Sassoulas
8967c527ff [ruff] Activate use next(iter(x)) instead of list(x)[0] and fix issue 2024-02-02 15:18:38 +01:00
Pierre Sassoulas
180a16a344 [ruff] Fix ambiguous characters found in string and comment 2024-02-02 15:18:38 +01:00
Pierre Sassoulas
514376fe29 [ruff] Add ruff's check and autofix existing issues 2024-02-02 15:18:38 +01:00
Pierre Sassoulas
bdfc5c80d8 Merge pull request #11901 from Pierre-Sassoulas/migrate-from-isort-to-ruff
Migrate from ``autoflake``, ``black``, ``isort``, ``pyupgrade``, ``flake8`` and ``pydocstyle``, to ``ruff``
2024-02-02 09:42:07 +01:00
Pierre Sassoulas
9ef905e7a0 [.git-blame-ignore-revs] Add migration to ruff/ruff format and blacken-docs 2024's style 2024-02-02 09:28:09 +01:00
Pierre Sassoulas
4588653b24 Migrate from autoflake, black, isort, pyupgrade, flake8 and pydocstyle, to ruff
ruff is faster and handle everything we had prior.

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

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

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

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

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

Co-authored-by: Ran Benita <ran@unusedvar.com>
2024-02-02 09:27:00 +01:00
Pierre Sassoulas
046f64751b Fix a duplicate assignment in test_config.py
Taken from https://github.com/pytest-dev/pytest/pull/11885 that was closed.
2024-01-31 13:59:25 +01:00
Pierre Sassoulas
4546d5445a Upgrade blacken-doc to black's 2024 style (#11899) 2024-01-31 13:53:21 +01:00
Bruno Oliveira
de161f8791 Merge pull request #11896 from nicoddemus/isort
Replace reorder-python-imports by isort due to black incompatibility
2024-01-31 08:04:39 -03:00
Bruno Oliveira
3be2a9d655 Update and rename .gitblameignore to .git-blame-ignore-revs
Seems like `.git-blame-ignore-revs` is the standard used and also automatically detected by GitHub:

https://github.blog/changelog/2022-03-24-ignore-commits-in-the-blame-view-beta/
2024-01-31 07:50:49 -03:00
John Litborn
e885013c6b fix incorrect example for group_contains (#11892) 2024-01-30 12:00:38 -08:00
Bruno Oliveira
8b54596639 Run pre-commit on all files
Running pre-commit on all files after replacing reorder-python-imports by isort.
2024-01-30 16:35:46 -03:00
Bruno Oliveira
899a6cf2ce Replace reorder-python-imports by isort due to black incompatibility
Unfortunately black and reorder-python-imports are no longer compatible, and from the looks of it probably will not be compatible anytime soon:

https://github.com/asottile/reorder-python-imports/issues/367
https://github.com/asottile/reorder-python-imports/issues/366
https://github.com/psf/black/issues/4175

This replaces `reorder-python-imports` by `isort` configured in a way to yield roughtly the same results.

Closes #11885
2024-01-30 16:35:45 -03:00
Clément Robert
407d984142 Fix an edge case where ExceptionInfo._stringify_exception could crash pytest.raises (#11879)
Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
Co-authored-by: Zac Hatfield-Dodds <zac.hatfield.dodds@gmail.com>
2024-01-30 17:20:30 +02:00
Bruno Oliveira
21bec6cfbe Add changelog entry about FixtureManager.getfixtureclosure changing (#11887)
As discussed in #11868.
2024-01-30 09:41:18 -03:00
Bruno Oliveira
cb57bf50b1 Pin back pytest-asyncio to 8.0.0 compatible release (#11889)
The current version (0.23.4) explicitly does not support pytest 8 yet, so we fallback to the previous release in the hope that at least our integration tests pass.
2024-01-30 06:58:22 -03:00
dependabot[bot]
c0dfc45186 build(deps): Bump hynek/build-and-inspect-python-package (#11877)
Bumps [hynek/build-and-inspect-python-package](https://github.com/hynek/build-and-inspect-python-package) from 2.0.0 to 2.0.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.0...v2.0.1)

---
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>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 10:42:51 -03:00
dependabot[bot]
5855f65d0f build(deps): Bump pytest-asyncio in /testing/plugins_integration (#11878)
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.23.3 to 0.23.4.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.23.3...v0.23.4)

---
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-01-29 10:42:27 -03:00
Russell Martin
14d3707818 Catch OSError from getpass.getuser() (#11875)
- Previously, `getpass.getuser()` would leak an ImportError if the
  USERNAME environment variable was not set on Windows because the `pwd`
  module cannot be imported.
- Starting in Python 3.13.0a3, it only raises `OSError`.

Fixes #11874
2024-01-28 23:07:18 -03:00
github-actions[bot]
8853a57532 [automated] Update plugin list (#11867)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-01-28 13:28:28 +00:00
Bruno Oliveira
878af85aef mypy: disallow untyped defs by default (#11862)
Change our mypy configuration to disallow untyped defs by default, which ensures *new* files added to the code base are fully typed.

To avoid having to type-annotate everything now, add `# mypy: allow-untyped-defs` to files which are not fully type annotated yet.

As we fully type annotate those modules, we can then just remove that directive from the top.
2024-01-28 10:12:42 -03:00
Ran Benita
e7b43b2121 Merge pull request #11859 from bluetech/numbered-dir-scandir
pathlib: speed up `make_numbered_dir` given a large tmp root
2024-01-28 00:05:39 +02:00
Ran Benita
d0f427aec4 Merge pull request #11865 from bluetech/cherry-pick-release
Cherry pick 8.0.0 release notes
2024-01-28 00:03:06 +02:00
Ran Benita
c6da0d20d2 Merge pull request #11864 from bluetech/release-8.0.0
Prepare release version 8.0.0

(cherry picked from commit 24c681d4ee)
2024-01-28 00:00:15 +02:00
Dương Quốc Khánh
a164ed6400 logging: avoid rounding microsecond to 1_000_000 (#11861)
Rounding microsecond might cause it to reach `1_000_000`, which raises a TypeError.
2024-01-27 10:40:31 -03:00
Ran Benita
eb9013d42c pathlib: speed up make_numbered_dir given a large tmp root
The function currently uses `find_suffixes` which iterates the entire
directory searching for files with the given suffix. In some cases
though, like in pytest's selftest, the directory can get big:

    $ ls /tmp/pytest-of-ran/pytest-0/
    7686

and iterating it many times can get slow.

This doesn't fix the underlying issue (iterating the directory) but at
least speeds it up a bit by using `os.scandir` instead of
`path.iterdir`. So `make_numbered_dir` is still slow for pytest's
selftests, but reduces ~10s for me.
2024-01-25 19:19:02 +02:00
Ran Benita
ac2cd72e5f Merge pull request #11856 from bluetech/pluggy-unblock
config: use `pluginmanager.unblock` instead of accessing pluggy's internals
2024-01-25 10:42:13 +02:00
Ran Benita
bca4bb0738 config: use pluginmanager.unblock instead of accessing pluggy's internals
The function was added in pluggy 1.4.0.
2024-01-25 10:20:44 +02:00
Ran Benita
2a77f8d88b Merge pull request #11854 from bluetech/runner-inline-2
runner: inline `call_runtest_hook`
2024-01-24 10:36:28 +02:00
Ran Benita
d972957303 hookspec: document conftest behavior for all hooks (#9496)
Have each hook explain how implementing it in conftests works. This is part of the functional specification of a hook.
2024-01-23 16:08:16 +02:00
Joachim B Haga
44bf7a2ec0 Mark some xfails from #10042 as non-strict (#11832)
Related to #10042, some tests in `test_debugging.py` are actually flaky and should not be considered strict xfailures.
2024-01-22 18:29:14 -03:00
Ran Benita
5ab8972bb5 runner: inline call_runtest_hook
- Reduce the common stacktrace by an entry - this is mostly for benefit
  of devs looking at crash logs.

- Reduce 6 slow `ihook` calls per test to 3.
2024-01-22 16:26:55 +02:00
github-actions[bot]
21440521fa [automated] Update plugin list (#11848)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-01-22 10:14:58 -03:00
dependabot[bot]
79ae968131 build(deps): Bump actions/cache from 3 to 4 (#11852)
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  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-01-22 10:11:09 -03:00
Ran Benita
c3fc717ff7 Merge pull request #11843 from bluetech/writing-plugins
doc/writing_plugins: correct inaccuracies re. initial conftest loading
2024-01-19 14:30:20 +02:00
clee2000
d71ef04f11 Escape skip reason in junitxml (#11842)
Co-authored-by: Bruno Oliveira <bruno@soliv.dev>
2024-01-18 22:08:26 -03:00
Ran Benita
111ad26f71 doc/writing_plugins: correct inaccuracies re. initial conftest loading 2024-01-18 22:17:55 +02:00
Ran Benita
2178ee86d7 Merge pull request #11826 from bluetech/no-cwd
Prefer using the invocation dir over CWD
2024-01-18 19:02:37 +02:00
Ran Benita
a6dd90a414 python: use invocation dir instead of cwd in fixture-printing code
We should aim to remove all `cwd()` calls except one, otherwise things
will go bad if the working directory changes. Use the invocation dir
instead.
2024-01-18 12:35:32 +02:00
Ran Benita
676f38d04a findpaths: rely on invocation_dir instead of cwd
We should aim to remove all `cwd()` calls except one, otherwise things
will go bad if the working directory changes. Use the invocation dir
instead.
2024-01-18 12:35:32 +02:00
Ran Benita
212c55218b helpconfig: add invocation dir to debug output
The current WD is not supposed to matter, the invocation dir is what
should be relevant. But keep them both for debugging.
2024-01-18 12:35:32 +02:00
Ran Benita
34fafe4c6b config: avoid Path.cwd() call in _set_initial_conftests
We should aim to remove all `cwd()` calls except one, otherwise things
will go bad if the working directory changes. Use the invocation dir
instead.
2024-01-18 12:35:32 +02:00
faph
eefc9d47fc [DOCS] Clarify tmp_path directory location and retention (#11830)
Fixes #11789 and #11790
2024-01-18 07:21:49 -03:00
Bruno Oliveira
7fd561e4ba Properly attach packages to the GH release notes (#11839)
Follow up to https://github.com/pytest-dev/pytest/pull/11754, noticed that the latest GitHub release does not contain the attached files.

Output log from the action:

```
🤔 Pattern 'dist/*' does not match any files.
```
2024-01-17 22:11:10 +00:00
Ran Benita
da9749cd97 Merge pull request #11838 from bluetech/cherry-pick-release
Prepare release version 8.0.0rc2
2024-01-17 23:53:41 +02:00
Ran Benita
ca5bbd0a9f Merge pull request #11835 from pytest-dev/release-8.0.0rc2
Prepare release version 8.0.0rc2

(cherry picked from commit 97960bdd14)
2024-01-17 23:45:21 +02:00
Ran Benita
5cd0535395 testing: temporarily disable test due to hypothesis issue (#11836)
Ref: https://github.com/pytest-dev/pytest/pull/11825#issuecomment-1894094641
2024-01-17 22:53:04 +02:00
Ran Benita
0f5aa5a7d2 Merge pull request #11825 from woutdenolf/fix_missing_fixture_issue
avoid using __file__ in pytest_plugin_registered as can be wrong on Windows
2024-01-17 15:29:29 +02:00
Ran Benita
9ea2e0a79f fixtures: avoid slow pm.get_name(plugin) call by using the new plugin_name hook parameter 2024-01-17 15:06:45 +02:00
Ran Benita
0f5ecd83c4 hookspecs: add plugin_name parameter to the pytest_plugin_registered hook
We have a use case for this in the next commit.

The name can be obtained by using `manager.get_name(plugin)`, however
this is currently O(num plugins) in pluggy, which would be good to
avoid. Besides, it seems generally useful.
2024-01-17 15:06:42 +02:00
Ran Benita
6b9bba2edb pre-commit: add pluggy to mypy deps
Otherwise mypy doesn't fully recognize pluggy's typing for some reason
or another.
2024-01-17 15:01:55 +02:00
woutdenolf
6e9f566d79 avoid using __file__ in pytest_plugin_registered as can be wrong on Windows 2024-01-17 15:01:04 +02:00
Ran Benita
a6708b9254 Merge pull request #11822 from bluetech/doc-hookspec
hookspec: minor doc tweaks
2024-01-16 18:34:33 +02:00
Florian Bruhin
e895c9d38c doc: Remove sold out training (#11823) 2024-01-16 14:53:10 +01:00
Ran Benita
c973ccb622 hookspec: modernize a reference 2024-01-15 23:47:19 +02:00
Ran Benita
dd1447cfe5 hookspec: move pytest_load_initial_conftests up
Reflect the order in which the plugins are called.
2024-01-15 23:46:07 +02:00
Ran Benita
9ad8b9fc36 hookspec: remove explicit :param types
Duplicates info in the type annotations which sphinx understands.
2024-01-15 23:35:53 +02:00
Florian Bruhin
348e6de102 doc: Update training dates and add pytest sprint (#11819) 2024-01-15 21:04:08 +01:00
Ran Benita
9af6d46371 Merge pull request #11817 from bluetech/conftesterror-cleanup
config: stop using exception triplets in `ConftestImportError`
2024-01-15 13:26:46 +02:00
Ran Benita
e1074f9c3d config: stop using exception triplets in ConftestImportError
In recent python versions all of the info is on the exception object
itself so no reason to deal with the annoying tuple.
2024-01-15 09:47:55 +02:00
Ran Benita
6e74601466 Merge pull request #11815 from bluetech/iter_parents-rename
nodes: rename `iterparents()` -> `iter_parents()`
2024-01-15 09:46:59 +02:00
Ronny Pfannschmidt
3acbdc2f79 Merge pull request #11814 from bluetech/pycache-ignore-collect
main,python: move `__pycache__` ignore to `pytest_ignore_collect`
2024-01-14 17:26:34 +01:00
Ran Benita
707642ad35 nodes: rename iterparents() -> iter_parents()
After the fact I remembered there is `node.iter_markers()` so let's be
consistent with that rather than with `listchain()`.
2024-01-14 15:17:41 +02:00
Ran Benita
2413d1b214 main,python: move __pycache__ ignore to pytest_ignore_collect
This removes one thing that directory collectors need to worry about.

This adds one hook dispatch per `__pycache__` file, but I think it's
worth it for consistency.
2024-01-14 15:05:15 +02:00
Ran Benita
2bb0eca347 Merge pull request #11795 from lesteve/improve-assert-mod-not-in-mods-error-message
Improve assert mod not in mods error message
2024-01-14 13:39:14 +02:00
Loïc Estève
1c9d6834fd Improve assert mod not in mods error message
[ran: tweaked message, made the formatting lazy]
2024-01-14 13:21:54 +02:00
github-actions[bot]
c6ed86453f [automated] Update plugin list (#11811)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-01-14 01:04:53 -03:00
Ran Benita
e403bbf1a9 Merge pull request #11708 from fcharras/FIX/crash_during_conftest_collection
FIX key formating divergence when inspecting plugin dictionary.
2024-01-13 21:45:36 +02:00
Franck Charras
a7c2549321 Fix assert mod not in mods crash
Fix #27806.

Co-authored-by: Loïc Estève <loic.esteve@ymail.com>
Co-authored-by: Ran Benita <ran@unusedvar.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2024-01-13 20:19:28 +02:00
Ran Benita
d65bcd9a3b Merge pull request #11808 from bluetech/doctest-conftest
doctest: remove special conftest handling
2024-01-13 19:50:20 +02:00
Ran Benita
06dbd3c21c doctest: remove special conftest handling
(Diff better viewed ignoring whitespace)

Since e1c66ab0ad, conftest loading is
handled at the directory level before sub-nodes are collected, so there
is no need for the doctest plugin to handle it specially.

This was probably the case even before
e1c66ab0ad, but I haven't verified this.
2024-01-13 11:18:41 +02:00
Ran Benita
1b78de4e21 Merge pull request #11803 from pytest-dev/package-scope-note
Add note about package scope
2024-01-12 22:15:33 +02:00
mrbean-bremen
82fda31e99 Clarify package scope
The behavior of package scope is surprising to some
(as seen by related questions on SO), this should clarify it a bit.
2024-01-12 20:03:30 +01:00
Ran Benita
5645fa45fb Merge pull request #11801 from bluetech/node-iterchain
nodes: add `Node.iterchain()` function
2024-01-12 11:01:48 +02:00
Ran Benita
5bd5b80afd nodes: add Node.iterparents() function
This is a useful addition to the existing `listchain`. While `listchain`
returns top-to-bottom, `iterparents` is bottom-to-top and doesn't require
an internal full iteration + `reverse`.
2024-01-11 23:19:45 +02:00
Ran Benita
bd58c09500 Merge pull request #11799 from bluetech/rm-nose-compat_co_firstlineno
python: remove support for nose's `compat_co_firstlineno`
2024-01-11 13:13:30 +02:00
Faisal Fawad
996e45d66a Slight change to tmp_path documentation to more clearly illustrate its behavior (#11800) 2024-01-11 11:01:07 +00:00
Ran Benita
c7d85c5dc6 python: remove support for nose's compat_co_firstlineno
Since we're removing nose support, let's also drop support for this
attribute.

From doing a code search on github, this seems completely unused outside
of nose, except for some projects which used to use it, but no longer
do.
2024-01-10 19:22:19 +02:00
Ronny Pfannschmidt
b1c430820f Merge pull request #11794 from bluetech/fixturedef-ref-cycle
A few cleanups
2024-01-10 06:44:14 +01:00
Ran Benita
35a3863b15 config: clarify a bit of code in _importconftest 2024-01-09 23:49:03 +02:00
Ran Benita
368fa2c03e fixtures: remove unhelpful FixtureManager.{FixtureLookupError,FixtureLookupErrorRepr}
Couldn't find any reason for this indirection, nor any plugins which
rely on it. Seems like historically it was done to avoid some imports...
2024-01-09 23:33:07 +02:00
Ran Benita
372c17e228 fixtures: avoid FixtureDef <-> FixtureManager reference cycle
There is no need to store the FixtureManager on each FixtureDef.
2024-01-09 23:29:58 +02:00
Ran Benita
c4a356eaee Merge pull request #11718 from pytest-dev/dependabot/github_actions/hynek/build-and-inspect-python-package-2.0.0
build(deps): Bump hynek/build-and-inspect-python-package from 1.5.4 to 2.0.0
2024-01-09 21:44:43 +02:00
dependabot[bot]
2270cab1c2 build(deps): Bump actions/download-artifact from 3 to 4
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-09 21:12:42 +02:00
dependabot[bot]
956d0e5e9d 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 1.5.4 to 2.0.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/v1.5.4...v2.0.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-09 21:12:27 +02:00
pre-commit-ci[bot]
7bc8385924 [pre-commit.ci] pre-commit autoupdate (#11792)
updates:
- [github.com/PyCQA/flake8: 6.1.0 → 7.0.0](https://github.com/PyCQA/flake8/compare/6.1.0...7.0.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-01-08 21:19:35 +00:00
Ran Benita
9b7e10a3c0 Merge pull request #11788 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-asyncio-0.23.3
build(deps): Bump pytest-asyncio from 0.23.2 to 0.23.3 in /testing/plugins_integration
2024-01-08 22:40:47 +02:00
dependabot[bot]
913d93debb build(deps): Bump pytest-asyncio in /testing/plugins_integration
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.23.2 to 0.23.3.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.23.2...v0.23.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-01-08 20:24:22 +00:00
Ran Benita
97dfc3429e Merge pull request #11785 from bluetech/matchfactories-nodes
fixtures: match fixtures based on actual node hierarchy, not textual nodeids
2024-01-08 22:23:08 +02:00
Ran Benita
992d0f082f fixtures: match fixtures based on actual node hierarchy, not textual nodeids
Refs #11662.

--- Problem

Each fixture definition has a "visibility", the `FixtureDef.baseid`
attribute. This is nodeid-like string. When a certain `node` requests a
certain fixture name, we match node's nodeid against the fixture
definitions with this name.

The matching currently happens on the *textual* representation of the
nodeid - we split `node.nodeid` to its "parent nodeids" and then check
if the fixture's `baseid` is in there.

While this has worked so far, we really should try to avoid textual
manipulation of nodeids as much as possible. It has also caused problem
in an odd case of a `Package` in the root directory: the `Package` gets
nodeid `.`, while a `Module` in it gets nodeid `test_module.py`. And
textually, `.` is not a parent of `test_module.py`.

--- Solution

Avoid this entirely by just checking the node hierarchy itself. This is
made possible by the fact that we now have proper `Directory` nodes
(`Dir` or `Package`) for the entire hierarchy.

Also do the same for `_getautousenames` which is a similar deal.

The `iterparentnodeids` function is no longer used and is removed.
2024-01-08 21:36:51 +02:00
Ran Benita
b968f63ca5 Merge pull request #11780 from bluetech/register-fixture
Add an internal "register fixture" API and use it replace object patching for fixture injection
2024-01-08 21:24:54 +02:00
Ran Benita
c8792bd080 python,unittest: replace obj fixture patching with FixtureManager._register_fixture
Instead of modifying user objects like modules and classes that we
really shouldn't be touching, use the new `_register_fixture` internal
API to do it directly.
2024-01-08 21:02:59 +02:00
Ran Benita
3234c79ee5 fixtures: add an internal API for registering a fixture
Add a function on the `FixtureManager` to register a fixture with
pytest. Currently this can only be done through `parsefactories`.

My aim is to eventually make something like this available to plugins,
as it's a pretty common need.
2024-01-08 21:02:59 +02:00
Ran Benita
851b72f289 Merge pull request #11776 from bluetech/unittest-xunit-inline
unittest: inline `_make_xunit_fixture`
2024-01-08 21:02:24 +02:00
Ran Benita
1d7349d18c Merge pull request #11774 from bluetech/fspath-cleanups
Small `fspath` cleanups
2024-01-08 21:01:56 +02:00
github-actions[bot]
5747a6c06e [automated] Update plugin list (#11784)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2024-01-07 10:20:38 -03:00
Fabian Sturm
13eacdad8a Add summary for xfails with -rxX option (#11574)
Co-authored-by: Brian Okken <1568356+okken@users.noreply.github.com>
2024-01-05 09:59:19 -03:00
Ran Benita
a616adf3ae unittest: inline _make_xunit_fixture
The indirection makes things harder to follow in this case IMO.
2024-01-05 14:37:03 +02:00
Ran Benita
685e52ec30 nodes: fix attribute name fspath -> path in get_fslocation_from_item 2024-01-04 22:32:34 +02:00
Ran Benita
7b4ab8134e fixtures: remove unnecessary fspath workaround 2024-01-04 22:32:34 +02:00
Ran Benita
c2a4a8d518 Merge pull request #11769 from neutrinoceros/fix_warns_docstring
Fix a mistake in pytest.warns' docstring (expect_warning accepts tuples, not any sequence)
2024-01-04 14:27:49 +02:00
Clément Robert
ac96256272 Fix a mistake in pytest.warns' docstring (expect_warning accepts tuples, not any sequence) 2024-01-04 11:51:12 +01:00
Bruno Oliveira
d38193646d Update docstring of scripts/generate-gh-release-notes.py (#11767)
Follow up to #11754.
2024-01-04 07:29:20 -03:00
Bruno Oliveira
cd07177906 Merge pull request #11754 from nicoddemus/release-notes
Improve GitHub release workflow
2024-01-03 20:14:48 -03:00
Bruno Oliveira
6321b74fae Enable type-checking in scripts/ 2024-01-03 19:47:56 -03:00
Bruno Oliveira
5aa289e478 Improve GitHub release workflow
This changes the existing script to just generate the release notes and delegate the actual publishing to the `softprops/action-gh-release@v1` action.

This allows us to delete the custom code, which failed recently in https://github.com/pytest-dev/pytest/actions/runs/7370258570/job/20056477756.
2024-01-03 19:47:56 -03:00
Ben Brown
12b9bd5801 Fix teardown error reporting when --maxfail=1 (#11721)
Co-authored-by: Ran Benita <ran@unusedvar.com>
2024-01-03 19:39:24 +02:00
Ran Benita
f017df443a Merge pull request #11757 from bluetech/rm-removed-in-8
Remove pytest 8 deprecations
2024-01-03 16:53:10 +02:00
Ran Benita
1ba07450e4 doc/deprecations: fix incorrect title level 2024-01-03 14:29:45 +02:00
Ran Benita
215f4d1fab Remove PytestRemovedIn8Warning
Per our deprecation policy.
2024-01-03 14:29:45 +02:00
Ran Benita
6c89f9261c Remove deprecated py.path (fspath) node constructor arguments 2024-01-03 14:29:45 +02:00
Ran Benita
a98f02d423 Remove deprecated py.path hook arguments 2024-01-03 14:29:42 +02:00
Marc Bresson
effc2b0529 Clarified markers ini property. Fix #11738 (#11739) 2024-01-03 14:20:54 +02:00
Ran Benita
2c5c97b6d1 Merge pull request #11760 from bluetech/fix-highlight-empty-source
terminalwriter: fix crash trying to highlight empty source
2024-01-03 09:38:29 +02:00
Ran Benita
cb5a42c836 terminalwriter: fix crash trying to highlight empty source
For quick checking I don't know how we can reach here with an empty
source, so test just checks the function directly.

Fix #11758.
2024-01-02 19:37:24 +02:00
Ran Benita
0f18a7fe5e Remove deprecated nose support 2024-01-02 12:20:47 +02:00
Ran Benita
0591569b4b Remove deprecated pytest.{exit,fail,skip}(msg=...) argument 2024-01-02 12:20:47 +02:00
Ran Benita
477959ef7d Remove deprecated pytest.Instance backward compat 2024-01-02 12:20:47 +02:00
Ran Benita
4147c92b21 Remove deprecated pytest.warns(None) 2024-01-02 12:20:47 +02:00
Ran Benita
10fbb2325f Remove deprecated Parser.addoption backward compatibilities 2024-01-02 12:20:47 +02:00
Ran Benita
1f8b39ed32 Remove deprecated --strict option 2024-01-02 12:20:47 +02:00
Ran Benita
f4e7b0d6e0 Remove deprecated pytest_cmdline_preparse hook 2024-01-02 12:20:47 +02:00
Ran Benita
f13724e2e3 Remove deprecated {FSCollector,Package}.{gethookproxy,isinitpath} 2024-01-02 12:20:47 +02:00
Ran Benita
a53984a55b Merge pull request #11756 from pytest-dev/cherry-pick-release
Cherry pick 8.0.0rc1 release notes
2024-01-02 11:20:03 +02:00
Ran Benita
d3c7ba310c Merge pull request #11744 from pytest-dev/release-8.0.0rc1
Prepare release 8.0.0rc1

(cherry picked from commit 665e4e58d3)
2024-01-02 10:59:26 +02:00
262 changed files with 6864 additions and 5703 deletions

View File

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

View File

@@ -26,7 +26,7 @@ jobs:
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5.4
uses: hynek/build-and-inspect-python-package@v2.0.1
deploy:
if: github.repository == 'pytest-dev/pytest'
@@ -41,7 +41,7 @@ jobs:
- uses: actions/checkout@v4
- name: Download Package
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: Packages
path: dist

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@v1.5.4
uses: hynek/build-and-inspect-python-package@v2.0.1
build:
needs: [package]
@@ -173,7 +173,7 @@ jobs:
persist-credentials: false
- name: Download Package
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: Packages
path: dist
@@ -205,7 +205,7 @@ jobs:
- name: Upload coverage to Codecov
if: "matrix.use_coverage"
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
continue-on-error: true
with:
fail_ci_if_error: true

View File

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

View File

@@ -1,14 +1,10 @@
repos:
- repo: https://github.com/psf/black
rev: 23.12.1
hooks:
- id: black
args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs
rev: 1.16.0
hooks:
- id: blacken-docs
additional_dependencies: [black==23.7.0]
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: "v0.2.2"
hooks:
- id: ruff
args: ["--fix"]
- id: ruff-format
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
@@ -20,37 +16,11 @@ repos:
- id: debug-statements
exclude: _pytest/(debugging|hookspec).py
language_version: python3
- repo: https://github.com/PyCQA/autoflake
rev: v2.2.1
- repo: https://github.com/adamchainz/blacken-docs
rev: 1.16.0
hooks:
- id: autoflake
name: autoflake
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"]
language: python
files: \.py$
- repo: https://github.com/PyCQA/flake8
rev: 6.1.0
hooks:
- id: flake8
language_version: python3
additional_dependencies:
- flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.12.0
hooks:
- id: reorder-python-imports
args: ['--application-directories=.:src', --py38-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
args: [--py38-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.5.0
hooks:
- id: setup-cfg-fmt
args: ["--max-py-version=3.12", "--include-version-classifiers"]
- id: blacken-docs
additional_dependencies: [black==24.1.1]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
@@ -64,7 +34,7 @@ repos:
additional_dependencies:
- iniconfig>=1.1.0
- attrs>=19.2.0
- pluggy
- pluggy>=1.4.0
- packaging
- tomli
- types-pkg_resources
@@ -72,6 +42,12 @@ repos:
# for mypy running on python>=3.11 since exceptiongroup is only a dependency
# on <3.11
- exceptiongroup>=1.0.0rc8
- repo: https://github.com/tox-dev/pyproject-fmt
rev: "1.7.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: rst

View File

@@ -56,6 +56,7 @@ Babak Keyvani
Barney Gale
Ben Brown
Ben Gartner
Ben Leith
Ben Webb
Benjamin Peterson
Benjamin Schubert
@@ -93,6 +94,7 @@ Christopher Dignam
Christopher Gilling
Claire Cecil
Claudio Madotto
Clément M.T. Robert
CrazyMerlyn
Cristian Vera
Cyrus Maden
@@ -126,6 +128,8 @@ Edison Gustavo Muenz
Edoardo Batini
Edson Tadeu M. Manoel
Eduardo Schettino
Edward Haigh
Eero Vaher
Eli Boyarski
Elizaveta Shashkova
Éloi Rivard
@@ -141,6 +145,7 @@ Evgeny Seliverstov
Fabian Sturm
Fabien Zarifian
Fabio Zadrozny
faph
Felix Hofstätter
Felix Nieuwenhuizen
Feng Ma
@@ -244,6 +249,7 @@ Marc Mueller
Marc Schlaich
Marcelo Duarte Trevisani
Marcin Bachry
Marc Bresson
Marco Gorelli
Mark Abramowitz
Mark Dickinson
@@ -277,6 +283,7 @@ Mike Hoyle (hoylemd)
Mike Lundy
Milan Lesnek
Miro Hrončok
mrbean-bremen
Nathaniel Compton
Nathaniel Waisbrot
Ned Batchelder
@@ -338,6 +345,7 @@ Ronny Pfannschmidt
Ross Lawley
Ruaridh Williamson
Russel Winder
Russell Martin
Ryan Puddephatt
Ryan Wooden
Sadra Barikbin
@@ -412,6 +420,7 @@ Vivaan Verma
Vlad Dragos
Vlad Radziuk
Vladyslav Rachek
Volodymyr Kochetkov
Volodymyr Piskun
Wei Lin
Wil Cooley

View File

@@ -27,9 +27,6 @@
:target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main
:alt: pre-commit.ci status
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
:target: https://www.codetriage.com/pytest-dev/pytest
@@ -97,12 +94,12 @@ Features
- `Modular fixtures <https://docs.pytest.org/en/stable/explanation/fixtures.html>`_ for
managing small or parametrized long-lived test resources
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial)
test suites out of the box
- Python 3.8+ or PyPy3
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
- Rich plugin architecture, with over 1300+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
Documentation

View File

@@ -23,7 +23,6 @@ members of the `contributors team`_ interested in receiving funding.
The current list of contributors receiving funding are:
* `@asottile`_
* `@nicoddemus`_
* `@The-Compiler`_
@@ -55,6 +54,5 @@ funds. Just drop a line to one of the `@pytest-dev/tidelift-admins`_ or use the
.. _`@pytest-dev/tidelift-admins`: https://github.com/orgs/pytest-dev/teams/tidelift-admins/members
.. _`agreement`: https://tidelift.com/docs/lifting/agreement
.. _`@asottile`: https://github.com/asottile
.. _`@nicoddemus`: https://github.com/nicoddemus
.. _`@The-Compiler`: https://github.com/The-Compiler

View File

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

View File

@@ -4,6 +4,7 @@
# FastFilesCompleter 0.7383 1.0760
import timeit
imports = [
"from argcomplete.completers import FilesCompleter as completer",
"from _pytest._argcomplete import FastFilesCompleter as completer",

View File

@@ -1,5 +1,6 @@
import pytest
SKIP = True

View File

@@ -1,5 +1,6 @@
from unittest import TestCase # noqa: F401
for i in range(15000):
exec(
f"""

View File

@@ -5,11 +5,10 @@
<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"
placeholder="Search"/>
<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 type="text/javascript">$('#searchbox').show(0);</script>
<script>document.getElementById('searchbox').style.display = "block"</script>
{%- endif %}

View File

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

View File

@@ -6,6 +6,9 @@ Release announcements
:maxdepth: 2
release-8.1.0
release-8.0.2
release-8.0.1
release-8.0.0
release-8.0.0rc2
release-8.0.0rc1

View File

@@ -0,0 +1,21 @@
pytest-8.0.1
=======================================
pytest 8.0.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
* Clément Robert
* Pierre Sassoulas
* Ran Benita
Happy testing,
The pytest Development Team

View File

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

View File

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

View File

@@ -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:526
cache -- .../_pytest/cacheprovider.py:527
Return a cache object that can persist state between testing sessions.
cache.get(key, default)
@@ -43,7 +43,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsysbinary):
@@ -51,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:1036
capfd -- .../_pytest/capture.py:1035
Enable text capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
@@ -61,7 +60,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfd):
@@ -69,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:1064
capfdbinary -- .../_pytest/capture.py:1062
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
@@ -79,7 +77,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfdbinary):
@@ -87,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:980
capsys -- .../_pytest/capture.py:981
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsys.readouterr()`` method
@@ -97,7 +94,6 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
@@ -105,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:743
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.
@@ -119,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:1365
pytestconfig [session scope] -- .../_pytest/fixtures.py:1346
Session-scoped fixture that returns the session's :class:`pytest.Config`
object.
@@ -129,7 +125,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
if pytestconfig.getoption("verbose") > 0:
...
record_property -- .../_pytest/junitxml.py:284
record_property -- .../_pytest/junitxml.py:283
Add extra properties to the calling test.
User properties become part of the test report and are available to the
@@ -143,13 +139,13 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
def test_function(record_property):
record_property("example_key", 1)
record_xml_attribute -- .../_pytest/junitxml.py:307
record_xml_attribute -- .../_pytest/junitxml.py:306
Add extra xml attributes to the tag for the calling test.
The fixture is callable with ``name, value``. The value is
automatically XML-encoded.
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:345
record_testsuite_property [session scope] -- .../_pytest/junitxml.py:344
Record a new ``<property>`` tag as child of the root ``<testsuite>``.
This is suitable to writing global information regarding the entire test
@@ -174,18 +170,18 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
:issue:`7767` for details.
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:300
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:317
Return a :class:`pytest.TempdirFactory` instance for the test session.
tmpdir -- .../_pytest/legacypath.py:307
tmpdir -- .../_pytest/legacypath.py:324
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
``--basetemp`` is used then it is cleared each session. See
:ref:`temporary directory location and retention`.
The returned object is a `legacy_path`_ object.
@@ -196,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:594
caplog -- .../_pytest/logging.py:601
Access and control log capturing.
Captured logs are available through the following properties/methods::
@@ -207,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:30
monkeypatch -- .../_pytest/monkeypatch.py:32
A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or
@@ -231,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:30
recwarn -- .../_pytest/recwarn.py:31
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:239
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:241
Return a :class:`pytest.TempPathFactory` instance for the test session.
tmp_path -- .../_pytest/tmpdir.py:254
tmp_path -- .../_pytest/tmpdir.py:256
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
@@ -249,8 +245,8 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
and old bases are removed after 3 sessions, to aid in debugging.
This behavior can be configured with :confval:`tmp_path_retention_count` and
:confval:`tmp_path_retention_policy`.
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
If ``--basetemp`` is used then it is cleared each session. See
:ref:`temporary directory location and retention`.
The returned object is a :class:`pathlib.Path` object.

View File

@@ -28,6 +28,137 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 8.1.0 (2024-03-03)
=========================
Features
--------
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: Added the new :confval:`consider_namespace_packages` configuration option, defaulting to ``False``.
If set to ``True``, pytest will attempt to identify modules that are part of `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__ when importing modules.
- `#11653 <https://github.com/pytest-dev/pytest/issues/11653>`_: Added the new :confval:`verbosity_test_cases` configuration option for fine-grained control of test execution verbosity.
See :ref:`Fine-grained verbosity <pytest.fine_grained_verbosity>` for more details.
Improvements
------------
- `#10865 <https://github.com/pytest-dev/pytest/issues/10865>`_: :func:`pytest.warns` now validates that :func:`warnings.warn` was called with a `str` or a `Warning`.
Currently in Python it is possible to use other types, however this causes an exception when :func:`warnings.filterwarnings` is used to filter those warnings (see `CPython #103577 <https://github.com/python/cpython/issues/103577>`__ for a discussion).
While this can be considered a bug in CPython, we decided to put guards in pytest as the error message produced without this check in place is confusing.
- `#11311 <https://github.com/pytest-dev/pytest/issues/11311>`_: When using ``--override-ini`` for paths in invocations without a configuration file defined, the current working directory is used
as the relative directory.
Previoulsy this would raise an :class:`AssertionError`.
- `#11475 <https://github.com/pytest-dev/pytest/issues/11475>`_: :ref:`--import-mode=importlib <import-mode-importlib>` now tries to import modules using the standard import mechanism (but still without changing :py:data:`sys.path`), falling back to importing modules directly only if that fails.
This means that installed packages will be imported under their canonical name if possible first, for example ``app.core.models``, instead of having the module name always be derived from their path (for example ``.env310.lib.site_packages.app.core.models``).
- `#11801 <https://github.com/pytest-dev/pytest/issues/11801>`_: Added the :func:`iter_parents() <_pytest.nodes.Node.iter_parents>` helper method on nodes.
It is similar to :func:`listchain <_pytest.nodes.Node.listchain>`, but goes from bottom to top, and returns an iterator, not a list.
- `#11850 <https://github.com/pytest-dev/pytest/issues/11850>`_: Added support for :data:`sys.last_exc` for post-mortem debugging on Python>=3.12.
- `#11962 <https://github.com/pytest-dev/pytest/issues/11962>`_: In case no other suitable candidates for configuration file are found, a ``pyproject.toml`` (even without a ``[tool.pytest.ini_options]`` table) will be considered as the configuration file and define the ``rootdir``.
- `#11978 <https://github.com/pytest-dev/pytest/issues/11978>`_: Add ``--log-file-mode`` option to the logging plugin, enabling appending to log-files. This option accepts either ``"w"`` or ``"a"`` and defaults to ``"w"``.
Previously, the mode was hard-coded to be ``"w"`` which truncates the file before logging.
- `#12047 <https://github.com/pytest-dev/pytest/issues/12047>`_: When multiple finalizers of a fixture raise an exception, now all exceptions are reported as an exception group.
Previously, only the first exception was reported.
Bug Fixes
---------
- `#11904 <https://github.com/pytest-dev/pytest/issues/11904>`_: Fixed a regression in pytest 8.0.0 that would cause test collection to fail due to permission errors when using ``--pyargs``.
This change improves the collection tree for tests specified using ``--pyargs``, see :pull:`12043` for a comparison with pytest 8.0 and <8.
- `#12011 <https://github.com/pytest-dev/pytest/issues/12011>`_: Fixed a regression in 8.0.1 whereby ``setup_module`` xunit-style fixtures are not executed when ``--doctest-modules`` is passed.
- `#12014 <https://github.com/pytest-dev/pytest/issues/12014>`_: Fix the ``stacklevel`` used when warning about marks used on fixtures.
- `#12039 <https://github.com/pytest-dev/pytest/issues/12039>`_: Fixed a regression in ``8.0.2`` where tests created using :fixture:`tmp_path` have been collected multiple times in CI under Windows.
Improved Documentation
----------------------
- `#11790 <https://github.com/pytest-dev/pytest/issues/11790>`_: Documented the retention of temporary directories created using the ``tmp_path`` fixture in more detail.
Trivial/Internal Changes
------------------------
- `#11785 <https://github.com/pytest-dev/pytest/issues/11785>`_: Some changes were made to private functions which may affect plugins which access them:
- ``FixtureManager._getautousenames()`` now takes a ``Node`` itself instead of the nodeid.
- ``FixtureManager.getfixturedefs()`` now takes the ``Node`` itself instead of the nodeid.
- The ``_pytest.nodes.iterparentnodeids()`` function is removed without replacement.
Prefer to traverse the node hierarchy itself instead.
If you really need to, copy the function from the previous pytest release.
pytest 8.0.2 (2024-02-24)
=========================
Bug Fixes
---------
- `#11895 <https://github.com/pytest-dev/pytest/issues/11895>`_: Fix collection on Windows where initial paths contain the short version of a path (for example ``c:\PROGRA~1\tests``).
- `#11953 <https://github.com/pytest-dev/pytest/issues/11953>`_: Fix an ``IndexError`` crash raising from ``getstatementrange_ast``.
- `#12021 <https://github.com/pytest-dev/pytest/issues/12021>`_: Reverted a fix to `--maxfail` handling in pytest 8.0.0 because it caused a regression in pytest-xdist whereby session fixture teardowns may get executed multiple times when the max-fails is reached.
pytest 8.0.1 (2024-02-16)
=========================
Bug Fixes
---------
- `#11875 <https://github.com/pytest-dev/pytest/issues/11875>`_: Correctly handle errors from :func:`getpass.getuser` in Python 3.13.
- `#11879 <https://github.com/pytest-dev/pytest/issues/11879>`_: Fix an edge case where ``ExceptionInfo._stringify_exception`` could crash :func:`pytest.raises`.
- `#11906 <https://github.com/pytest-dev/pytest/issues/11906>`_: Fix regression with :func:`pytest.warns` using custom warning subclasses which have more than one parameter in their `__init__`.
- `#11907 <https://github.com/pytest-dev/pytest/issues/11907>`_: Fix a regression in pytest 8.0.0 whereby calling :func:`pytest.skip` and similar control-flow exceptions within a :func:`pytest.warns()` block would get suppressed instead of propagating.
- `#11929 <https://github.com/pytest-dev/pytest/issues/11929>`_: Fix a regression in pytest 8.0.0 whereby autouse fixtures defined in a module get ignored by the doctests in the module.
- `#11937 <https://github.com/pytest-dev/pytest/issues/11937>`_: Fix a regression in pytest 8.0.0 whereby items would be collected in reverse order in some circumstances.
pytest 8.0.0 (2024-01-27)
=========================
@@ -61,6 +192,8 @@ Bug Fixes
- `#11706 <https://github.com/pytest-dev/pytest/issues/11706>`_: Fix reporting of teardown errors in higher-scoped fixtures when using `--maxfail` or `--stepwise`.
NOTE: This change was reverted in pytest 8.0.2 to fix a `regression <https://github.com/pytest-dev/pytest-xdist/issues/1024>`_ it caused in pytest-xdist.
- `#11758 <https://github.com/pytest-dev/pytest/issues/11758>`_: Fixed ``IndexError: string index out of range`` crash in ``if highlighted[-1] == "\n" and source[-1] != "\n"``.
This bug was introduced in pytest 8.0.0rc1.
@@ -266,11 +399,15 @@ These are breaking changes where deprecation was not possible.
therefore fail on the newly-re-emitted warnings.
- The internal ``FixtureManager.getfixtureclosure`` method has changed. Plugins which use this method or
which subclass ``FixtureManager`` and overwrite that method will need to adapt to the change.
Deprecations
------------
- `#10465 <https://github.com/pytest-dev/pytest/issues/10465>`_: Test functions returning a value other than ``None`` will now issue a :class:`pytest.PytestWarning` instead of :class:`pytest.PytestRemovedIn8Warning`, meaning this will stay a warning instead of becoming an error in the future.
- `#10465 <https://github.com/pytest-dev/pytest/issues/10465>`_: Test functions returning a value other than ``None`` will now issue a :class:`pytest.PytestWarning` instead of ``pytest.PytestRemovedIn8Warning``, meaning this will stay a warning instead of becoming an error in the future.
- `#3664 <https://github.com/pytest-dev/pytest/issues/3664>`_: Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work.
@@ -1300,7 +1437,7 @@ Deprecations
See :ref:`the deprecation note <diamond-inheritance-deprecated>` for full details.
- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: :hook:`pytest_cmdline_preparse` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead.
- `#8592 <https://github.com/pytest-dev/pytest/issues/8592>`_: ``pytest_cmdline_preparse`` has been officially deprecated. It will be removed in a future release. Use :hook:`pytest_load_initial_conftests` instead.
See :ref:`the deprecation note <cmdline-preparse-deprecated>` for full details.

View File

@@ -23,6 +23,7 @@ from typing import TYPE_CHECKING
from _pytest import __version__ as version
if TYPE_CHECKING:
import sphinx.application
@@ -199,7 +200,6 @@ 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"),
@@ -441,9 +441,10 @@ intersphinx_mapping = {
def configure_logging(app: "sphinx.application.Sphinx") -> None:
"""Configure Sphinx's WarningHandler to handle (expected) missing include."""
import sphinx.util.logging
import logging
import sphinx.util.logging
class WarnLogFilter(logging.Filter):
def filter(self, record: logging.LogRecord) -> bool:
"""Ignore warnings about missing include with "only" directive.

View File

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

View File

@@ -19,170 +19,6 @@ 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>`.
.. _nose-deprecation:
Support for tests written for nose
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.2
Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated.
``nose`` has been in maintenance mode-only for years, and maintaining the plugin is not trivial as it spills
over the code base (see :issue:`9886` for more details).
setup/teardown
^^^^^^^^^^^^^^
One thing that might catch users by surprise is that plain ``setup`` and ``teardown`` methods are not pytest native,
they are in fact part of the ``nose`` support.
.. code-block:: python
class Test:
def setup(self):
self.resource = make_resource()
def teardown(self):
self.resource.close()
def test_foo(self):
...
def test_bar(self):
...
Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`xunit-method-setup`), so the above should be changed to:
.. code-block:: python
class Test:
def setup_method(self):
self.resource = make_resource()
def teardown_method(self):
self.resource.close()
def test_foo(self):
...
def test_bar(self):
...
This is easy to do in an entire code base by doing a simple find/replace.
@with_setup
^^^^^^^^^^^
Code using `@with_setup <with-setup-nose>`_ such as this:
.. code-block:: python
from nose.tools import with_setup
def setup_some_resource():
...
def teardown_some_resource():
...
@with_setup(setup_some_resource, teardown_some_resource)
def test_foo():
...
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
.. code-block:: python
import pytest
def setup_some_resource():
...
def teardown_some_resource():
...
@pytest.fixture
def some_resource():
setup_some_resource()
yield
teardown_some_resource()
def test_foo(some_resource):
...
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
.. _instance-collector-deprecation:
The ``pytest.Instance`` collector
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 7.0
The ``pytest.Instance`` collector type has been removed.
Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`.
Now :class:`~pytest.Class` collects the test methods directly.
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
using a check such as ``if isinstance(node, Instance): return``.
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
and importing it emits a deprecation warning. This will be removed in pytest 8.
.. _node-ctor-fspath-deprecation:
``fspath`` argument for Node constructors replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`,
the ``fspath`` argument to :class:`~_pytest.nodes.Node` constructors like
:func:`pytest.Function.from_parent()` and :func:`pytest.Class.from_parent()`
is now deprecated.
Plugins which construct nodes should pass the ``path`` argument, of type
:class:`pathlib.Path`, instead of the ``fspath`` argument.
Plugins which implement custom items and collectors are encouraged to replace
``fspath`` parameters (``py.path.local``) with ``path`` parameters
(``pathlib.Path``), and drop any other usage of the ``py`` library if possible.
If possible, plugins with custom items should use :ref:`cooperative
constructors <uncooperative-constructors-deprecated>` to avoid hardcoding
arguments they only pass on to the superclass.
.. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes (the
new attribute being ``path``) is **the opposite** of the situation for
hooks, :ref:`outlined below <legacy-path-hooks-deprecated>` (the old
argument being ``path``).
This is an unfortunate artifact due to historical reasons, which should be
resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion).
Due to the ongoing migration of methods like :meth:`~pytest.Item.reportinfo`
which still is expected to return a ``py.path.local`` object, nodes still have
both ``fspath`` (``py.path.local``) and ``path`` (``pathlib.Path``) attributes,
no matter what argument was used in the constructor. We expect to deprecate the
``fspath`` attribute in a future release.
.. _legacy-path-hooks-deprecated:
Configuring hook specs/impls using markers
@@ -197,13 +33,11 @@ have been available since years and should be used instead.
.. code-block:: python
@pytest.mark.tryfirst
def pytest_runtest_call():
...
def pytest_runtest_call(): ...
# or
def pytest_runtest_call():
...
def pytest_runtest_call(): ...
pytest_runtest_call.tryfirst = True
@@ -213,8 +47,7 @@ should be changed to:
.. code-block:: python
@pytest.hookimpl(tryfirst=True)
def pytest_runtest_call():
...
def pytest_runtest_call(): ...
Changed ``hookimpl`` attributes:
@@ -229,31 +62,6 @@ Changed ``hookwrapper`` attributes:
* ``historic``
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments:
* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) <pytest_ignore_collect>` as equivalent to ``path``
* :hook:`pytest_collect_file(file_path: pathlib.Path) <pytest_collect_file>` as equivalent to ``path``
* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) <pytest_pycollect_makemodule>` as equivalent to ``path``
* :hook:`pytest_report_header(start_path: pathlib.Path) <pytest_report_header>` as equivalent to ``startdir``
* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) <pytest_report_collectionfinish>` as equivalent to ``startdir``
The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments.
.. note::
The name of the :class:`~_pytest.nodes.Node` arguments and attributes,
:ref:`outlined above <node-ctor-fspath-deprecation>` (the new attribute
being ``path``) is **the opposite** of the situation for hooks (the old
argument being ``path``).
This is an unfortunate artifact due to historical reasons, which should be
resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion).
Directly constructing internal classes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -273,62 +81,6 @@ Directly constructing the following classes is now deprecated:
These constructors have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.
.. _cmdline-preparse-deprecated:
Passing ``msg=`` to ``pytest.skip``, ``pytest.fail`` or ``pytest.exit``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
Passing the keyword argument ``msg`` to :func:`pytest.skip`, :func:`pytest.fail` or :func:`pytest.exit`
is now deprecated and ``reason`` should be used instead. This change is to bring consistency between these
functions and the ``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which already accept a ``reason`` argument.
.. code-block:: python
def test_fail_example():
# old
pytest.fail(msg="foo")
# new
pytest.fail(reason="bar")
def test_skip_example():
# old
pytest.skip(msg="foo")
# new
pytest.skip(reason="bar")
def test_exit_example():
# old
pytest.exit(msg="foo")
# new
pytest.exit(reason="bar")
Implementing the ``pytest_cmdline_preparse`` hook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
Implementing the :hook:`pytest_cmdline_preparse` hook has been officially deprecated.
Implement the :hook:`pytest_load_initial_conftests` hook instead.
.. code-block:: python
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None:
...
# becomes:
def pytest_load_initial_conftests(
early_config: Config, parser: Parser, args: List[str]
) -> None:
...
.. _diamond-inheritance-deprecated:
Diamond inheritance between :class:`pytest.Collector` and :class:`pytest.Item`
@@ -381,7 +133,7 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of
deprecation warning is now raised.
Applying a mark to a fixture function
-------------------------------------
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.4
@@ -391,38 +143,13 @@ Applying a mark to a fixture function never had any effect, but it is a common u
@pytest.mark.usefixtures("clean_database")
@pytest.fixture
def user() -> User:
...
def user() -> User: ...
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
Backward compatibilities in ``Parser.addoption``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 2.4
Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
scheduled for removal in pytest 8 (deprecated since pytest 2.4.0):
- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead.
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
Using ``pytest.warns(None)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
:func:`pytest.warns(None) <pytest.warns>` is now deprecated because it was frequently misused.
Its correct usage was checking that the code emits at least one warning of any type - like ``pytest.warns()``
or ``pytest.warns(Warning)``.
See :ref:`warns use cases` for examples.
Returning non-None value in test functions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -463,19 +190,6 @@ The proper fix is to change the `return` to an `assert`:
assert foo(a, b) == result
The ``--strict`` command-line option
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.2
The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
better conveys what the option does.
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
flag for all strictness related options (``--strict-markers`` and ``--strict-config``
at the moment, more might be introduced in the future).
The ``yield_fixture`` function/decorator
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -494,6 +208,294 @@ 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.2
.. versionremoved:: 8.0
Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated.
``nose`` has been in maintenance mode-only for years, and maintaining the plugin is not trivial as it spills
over the code base (see :issue:`9886` for more details).
setup/teardown
^^^^^^^^^^^^^^
One thing that might catch users by surprise is that plain ``setup`` and ``teardown`` methods are not pytest native,
they are in fact part of the ``nose`` support.
.. code-block:: python
class Test:
def setup(self):
self.resource = make_resource()
def teardown(self):
self.resource.close()
def test_foo(self): ...
def test_bar(self): ...
Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`xunit-method-setup`), so the above should be changed to:
.. code-block:: python
class Test:
def setup_method(self):
self.resource = make_resource()
def teardown_method(self):
self.resource.close()
def test_foo(self): ...
def test_bar(self): ...
This is easy to do in an entire code base by doing a simple find/replace.
@with_setup
^^^^^^^^^^^
Code using `@with_setup <with-setup-nose>`_ such as this:
.. code-block:: python
from nose.tools import with_setup
def setup_some_resource(): ...
def teardown_some_resource(): ...
@with_setup(setup_some_resource, teardown_some_resource)
def test_foo(): ...
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
.. code-block:: python
import pytest
def setup_some_resource(): ...
def teardown_some_resource(): ...
@pytest.fixture
def some_resource():
setup_some_resource()
yield
teardown_some_resource()
def test_foo(some_resource): ...
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
The ``compat_co_firstlineno`` attribute
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Nose inspects this attribute on function objects to allow overriding the function's inferred line number.
Pytest no longer respects this attribute.
Passing ``msg=`` to ``pytest.skip``, ``pytest.fail`` or ``pytest.exit``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
.. versionremoved:: 8.0
Passing the keyword argument ``msg`` to :func:`pytest.skip`, :func:`pytest.fail` or :func:`pytest.exit`
is now deprecated and ``reason`` should be used instead. This change is to bring consistency between these
functions and the ``@pytest.mark.skip`` and ``@pytest.mark.xfail`` markers which already accept a ``reason`` argument.
.. code-block:: python
def test_fail_example():
# old
pytest.fail(msg="foo")
# new
pytest.fail(reason="bar")
def test_skip_example():
# old
pytest.skip(msg="foo")
# new
pytest.skip(reason="bar")
def test_exit_example():
# old
pytest.exit(msg="foo")
# new
pytest.exit(reason="bar")
.. _instance-collector-deprecation:
The ``pytest.Instance`` collector
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionremoved:: 7.0
The ``pytest.Instance`` collector type has been removed.
Previously, Python test methods were collected as :class:`~pytest.Class` -> ``Instance`` -> :class:`~pytest.Function`.
Now :class:`~pytest.Class` collects the test methods directly.
Most plugins which reference ``Instance`` do so in order to ignore or skip it,
using a check such as ``if isinstance(node, Instance): return``.
Such plugins should simply remove consideration of ``Instance`` on pytest>=7.
However, to keep such uses working, a dummy type has been instanted in ``pytest.Instance`` and ``_pytest.python.Instance``,
and importing it emits a deprecation warning. This was removed in pytest 8.
Using ``pytest.warns(None)``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
.. versionremoved:: 8.0
:func:`pytest.warns(None) <pytest.warns>` is now deprecated because it was frequently misused.
Its correct usage was checking that the code emits at least one warning of any type - like ``pytest.warns()``
or ``pytest.warns(Warning)``.
See :ref:`warns use cases` for examples.
Backward compatibilities in ``Parser.addoption``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 2.4
.. versionremoved:: 8.0
Several behaviors of :meth:`Parser.addoption <pytest.Parser.addoption>` are now
removed in pytest 8 (deprecated since pytest 2.4.0):
- ``parser.addoption(..., help=".. %default ..")`` - use ``%(default)s`` instead.
- ``parser.addoption(..., type="int/string/float/complex")`` - use ``type=int`` etc. instead.
The ``--strict`` command-line option
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 6.2
.. versionremoved:: 8.0
The ``--strict`` command-line option has been deprecated in favor of ``--strict-markers``, which
better conveys what the option does.
We have plans to maybe in the future to reintroduce ``--strict`` and make it an encompassing
flag for all strictness related options (``--strict-markers`` and ``--strict-config``
at the moment, more might be introduced in the future).
.. _cmdline-preparse-deprecated:
Implementing the ``pytest_cmdline_preparse`` hook
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: 7.0
.. versionremoved:: 8.0
Implementing the ``pytest_cmdline_preparse`` hook has been officially deprecated.
Implement the :hook:`pytest_load_initial_conftests` hook instead.
.. code-block:: python
def pytest_cmdline_preparse(config: Config, args: List[str]) -> None: ...
# becomes:
def pytest_load_initial_conftests(
early_config: Config, parser: Parser, args: List[str]
) -> None: ...
Collection changes in pytest 8
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -907,8 +909,7 @@ Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated
(50, 500),
],
)
def test_foo(a, b):
...
def test_foo(a, b): ...
This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization
call.
@@ -931,8 +932,7 @@ To update the code, use ``pytest.param``:
(50, 500),
],
)
def test_foo(a, b):
...
def test_foo(a, b): ...
.. _pytest_funcarg__ prefix deprecated:
@@ -1083,15 +1083,13 @@ This is just a matter of renaming the fixture as the API is the same:
.. code-block:: python
def test_foo(record_xml_property):
...
def test_foo(record_xml_property): ...
Change to:
.. code-block:: python
def test_foo(record_property):
...
def test_foo(record_property): ...
.. _passing command-line string to pytest.main deprecated:
@@ -1253,8 +1251,7 @@ Example of usage:
.. code-block:: python
class MySymbol:
...
class MySymbol: ...
def pytest_namespace():

View File

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

View File

@@ -2,6 +2,7 @@ import os.path
import pytest
mydir = os.path.dirname(__file__)

View File

@@ -1,6 +1,7 @@
import os.path
import shutil
failure_demo = os.path.join(os.path.dirname(__file__), "failure_demo.py")
pytest_plugins = ("pytester",)

View File

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

View File

@@ -1,5 +1,6 @@
"""Module containing a parametrized tests testing cross-python serialization
via the pickle module."""
import shutil
import subprocess
import textwrap
@@ -32,14 +33,12 @@ class Python:
dumpfile = self.picklefile.with_name("dump.py")
dumpfile.write_text(
textwrap.dedent(
r"""
rf"""
import pickle
f = open({!r}, 'wb')
s = pickle.dump({!r}, f, protocol=2)
f = open({str(self.picklefile)!r}, 'wb')
s = pickle.dump({obj!r}, f, protocol=2)
f.close()
""".format(
str(self.picklefile), obj
)
"""
)
)
subprocess.run((self.pythonpath, str(dumpfile)), check=True)
@@ -48,17 +47,15 @@ class Python:
loadfile = self.picklefile.with_name("load.py")
loadfile.write_text(
textwrap.dedent(
r"""
rf"""
import pickle
f = open({!r}, 'rb')
f = open({str(self.picklefile)!r}, 'rb')
obj = pickle.load(f)
f.close()
res = eval({!r})
res = eval({expression!r})
if not res:
raise SystemExit(1)
""".format(
str(self.picklefile), expression
)
"""
)
)
print(loadfile)

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-193>
<Dir parametrize.rst-195>
<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-193>
<Dir parametrize.rst-195>
<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-193>
<Dir parametrize.rst-195>
<Module test_backends.py>
<Function test_db_initialized[d1]>
<Function test_db_initialized[d2]>
@@ -503,10 +503,10 @@ Running it results in some skips if we don't have all the python interpreters in
.. code-block:: pytest
. $ pytest -rs -q multipython.py
ssssssssssssssssssssssss... [100%]
ssssssssssss...ssssssssssss [100%]
========================= short test summary info ==========================
SKIPPED [12] multipython.py:68: 'python3.9' not found
SKIPPED [12] multipython.py:68: 'python3.10' not found
SKIPPED [12] multipython.py:65: 'python3.9' not found
SKIPPED [12] multipython.py:65: 'python3.11' not found
3 passed, 24 skipped in 0.12s
Parametrization of optional implementations/imports

View File

@@ -152,7 +152,7 @@ The test collection would look like this:
configfile: pytest.ini
collected 2 items
<Dir pythoncollection.rst-194>
<Dir pythoncollection.rst-196>
<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-194>
<Dir pythoncollection.rst-196>
<Dir CWD>
<Module pythoncollection.py>
<Function test_function>

View File

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

View File

@@ -1,5 +1,6 @@
import pytest
xfail = pytest.mark.xfail

View File

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

View File

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

View File

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

View File

@@ -99,8 +99,7 @@ sets. pytest-2.3 introduces a decorator for use on the factory itself:
.. code-block:: python
@pytest.fixture(params=["mysql", "pg"])
def db(request):
... # use request.param
def db(request): ... # use request.param
Here the factory will be invoked twice (with the respective "mysql"
and "pg" values set as ``request.param`` attributes) and all of
@@ -141,8 +140,7 @@ argument:
.. code-block:: python
@pytest.fixture()
def db(request):
...
def db(request): ...
The name under which the funcarg resource can be requested is ``db``.
@@ -151,8 +149,7 @@ aka:
.. code-block:: python
def pytest_funcarg__db(request):
...
def pytest_funcarg__db(request): ...
But it is then not possible to define scoping and parametrization.

View File

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

View File

@@ -227,8 +227,7 @@ to use strings:
@pytest.mark.skipif("sys.version_info >= (3,3)")
def test_function():
...
def test_function(): ...
During test function setup the skipif condition is evaluated by calling
``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains
@@ -262,8 +261,7 @@ configuration value which you might have added:
.. code-block:: python
@pytest.mark.skipif("not config.getvalue('db')")
def test_function():
...
def test_function(): ...
The equivalent with "boolean conditions" is:

View File

@@ -154,7 +154,7 @@ method to test for exceptions returned as part of an :class:`ExceptionGroup`:
.. code-block:: python
def test_exception_in_group():
with pytest.raises(RuntimeError) as excinfo:
with pytest.raises(ExceptionGroup) as excinfo:
raise ExceptionGroup(
"Group message",
[
@@ -176,7 +176,7 @@ exception at a specific level; exceptions contained directly in the top
.. code-block:: python
def test_exception_in_group_at_given_depth():
with pytest.raises(RuntimeError) as excinfo:
with pytest.raises(ExceptionGroup) as excinfo:
raise ExceptionGroup(
"Group message",
[

View File

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

View File

@@ -494,7 +494,7 @@ Fixtures are created when first requested by a test, and are destroyed based on
* ``function``: the default scope, the fixture is destroyed at the end of the test.
* ``class``: the fixture is destroyed during teardown of the last test in the class.
* ``module``: the fixture is destroyed during teardown of the last test in the module.
* ``package``: the fixture is destroyed during teardown of the last test in the package.
* ``package``: the fixture is destroyed during teardown of the last test in the package where the fixture is defined, including sub-packages and sub-directories within it.
* ``session``: the fixture is destroyed at the end of the test session.
.. note::
@@ -1418,7 +1418,7 @@ Running the above tests results in the following test IDs being used:
rootdir: /home/sweet/project
collected 12 items
<Dir fixtures.rst-212>
<Dir fixtures.rst-214>
<Module test_anothersmtp.py>
<Function test_showhelo[smtp.gmail.com]>
<Function test_showhelo[mail.python.org]>
@@ -1721,8 +1721,7 @@ You can specify multiple fixtures like this:
.. code-block:: python
@pytest.mark.usefixtures("cleandir", "anotherfixture")
def test():
...
def test(): ...
and you may specify fixture usage at the test module level using :globalvar:`pytestmark`:
@@ -1750,8 +1749,7 @@ into an ini-file:
@pytest.mark.usefixtures("my_other_fixture")
@pytest.fixture
def my_fixture_that_sadly_wont_use_my_other_fixture():
...
def my_fixture_that_sadly_wont_use_my_other_fixture(): ...
This generates a deprecation warning, and will become an error in Pytest 8.

View File

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

View File

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

View File

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

View File

@@ -325,7 +325,9 @@ This is done by setting a verbosity level in the configuration file for the spec
``pytest --no-header`` with a value of ``2`` would have the same output as the previous example, but each test inside
the file is shown by a single character in the output.
(Note: currently this is the only option available, but more might be added in the future).
:confval:`verbosity_test_cases`: Controls how verbose the test execution output should be when pytest is executed.
Running ``pytest --no-header`` with a value of ``2`` would have the same output as the first verbosity example, but each
test inside the file gets its own line in the output.
.. _`pytest.detailed_failed_tests_usage`:

View File

@@ -47,8 +47,7 @@ which may be passed an optional ``reason``:
.. code-block:: python
@pytest.mark.skip(reason="no way of currently testing this")
def test_the_unknown():
...
def test_the_unknown(): ...
Alternatively, it is also possible to skip imperatively during test execution or setup
@@ -93,8 +92,7 @@ when run on an interpreter earlier than Python3.10:
@pytest.mark.skipif(sys.version_info < (3, 10), reason="requires python3.10 or higher")
def test_function():
...
def test_function(): ...
If the condition evaluates to ``True`` during collection, the test function will be skipped,
with the specified reason appearing in the summary when using ``-rs``.
@@ -112,8 +110,7 @@ You can share ``skipif`` markers between modules. Consider this test module:
@minversion
def test_function():
...
def test_function(): ...
You can import the marker and reuse it in another test module:
@@ -124,8 +121,7 @@ You can import the marker and reuse it in another test module:
@minversion
def test_anotherfunction():
...
def test_anotherfunction(): ...
For larger test suites it's usually a good idea to have one file
where you define the markers which you then consistently apply
@@ -232,8 +228,7 @@ expect a test to fail:
.. code-block:: python
@pytest.mark.xfail
def test_function():
...
def test_function(): ...
This test will run but no traceback will be reported when it fails. Instead, terminal
reporting will list it in the "expected to fail" (``XFAIL``) or "unexpectedly
@@ -275,8 +270,7 @@ that condition as the first parameter:
.. code-block:: python
@pytest.mark.xfail(sys.platform == "win32", reason="bug in a 3rd party library")
def test_function():
...
def test_function(): ...
Note that you have to pass a reason as well (see the parameter description at
:ref:`pytest.mark.xfail ref`).
@@ -289,8 +283,7 @@ You can specify the motive of an expected failure with the ``reason`` parameter:
.. code-block:: python
@pytest.mark.xfail(reason="known parser issue")
def test_function():
...
def test_function(): ...
``raises`` parameter
@@ -302,8 +295,7 @@ a single exception, or a tuple of exceptions, in the ``raises`` argument.
.. code-block:: python
@pytest.mark.xfail(raises=RuntimeError)
def test_function():
...
def test_function(): ...
Then the test will be reported as a regular failure if it fails with an
exception not mentioned in ``raises``.
@@ -317,8 +309,7 @@ even executed, use the ``run`` parameter as ``False``:
.. code-block:: python
@pytest.mark.xfail(run=False)
def test_function():
...
def test_function(): ...
This is specially useful for xfailing tests that are crashing the interpreter and should be
investigated later.
@@ -334,8 +325,7 @@ You can change this by setting the ``strict`` keyword-only parameter to ``True``
.. code-block:: python
@pytest.mark.xfail(strict=True)
def test_function():
...
def test_function(): ...
This will make ``XPASS`` ("unexpectedly passing") results from this test to fail the test suite.

View File

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

View File

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

View File

@@ -1,8 +1,11 @@
:orphan:
.. sidebar:: Next Open Trainings
.. 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/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
- `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>`.
@@ -74,11 +77,11 @@ Features
- :ref:`Modular fixtures <fixture>` for managing small or parametrized long-lived test resources
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
- Can run :ref:`unittest <unittest>` (including trial) test suites out of the box
- Python 3.8+ or PyPy 3
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
- Rich plugin architecture, with over 1300+ :ref:`external plugins <plugin-list>` and thriving community
Documentation

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -164,8 +164,7 @@ Add warning filters to marked test items.
.. code-block:: python
@pytest.mark.filterwarnings("ignore:.*usage will be deprecated.*:DeprecationWarning")
def test_foo():
...
def test_foo(): ...
.. _`pytest.mark.parametrize ref`:
@@ -276,8 +275,7 @@ For example:
.. code-block:: python
@pytest.mark.timeout(10, "slow", method="thread")
def test_function():
...
def test_function(): ...
Will create and attach a :class:`Mark <pytest.Mark>` object to the collected
:class:`Item <pytest.Item>`, which can then be accessed by fixtures or hooks with
@@ -294,8 +292,7 @@ Example for using multiple custom markers:
@pytest.mark.timeout(10, "slow", method="thread")
@pytest.mark.slow
def test_function():
...
def test_function(): ...
When :meth:`Node.iter_markers <_pytest.nodes.Node.iter_markers>` or :meth:`Node.iter_markers_with_node <_pytest.nodes.Node.iter_markers_with_node>` is used with multiple markers, the marker closest to the function will be iterated over first. The above example will result in ``@pytest.mark.slow`` followed by ``@pytest.mark.timeout(...)``.
@@ -643,8 +640,6 @@ Bootstrapping hooks called for plugins registered early enough (internal and set
.. hook:: pytest_load_initial_conftests
.. autofunction:: pytest_load_initial_conftests
.. hook:: pytest_cmdline_preparse
.. autofunction:: pytest_cmdline_preparse
.. hook:: pytest_cmdline_parse
.. autofunction:: pytest_cmdline_parse
.. hook:: pytest_cmdline_main
@@ -1209,9 +1204,6 @@ Custom warnings generated in some situations such as improper usage or deprecate
.. autoclass:: pytest.PytestReturnNotNoneWarning
:show-inheritance:
.. autoclass:: pytest.PytestRemovedIn8Warning
:show-inheritance:
.. autoclass:: pytest.PytestRemovedIn9Warning
:show-inheritance:
@@ -1282,6 +1274,19 @@ passed multiple times. The expected format is ``name=value``. For example::
variables, that will be expanded. For more information about cache plugin
please refer to :ref:`cache_provider`.
.. confval:: consider_namespace_packages
Controls if pytest should attempt to identify `namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages>`__
when collecting Python modules. Default is ``False``.
Set to ``True`` if 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.
Only `native namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#native-namespace-packages>`__
are supported, with no plans to support `legacy namespace packages <https://packaging.python.org/en/latest/guides/packaging-namespace-packages/#legacy-namespace-packages>`__.
.. versionadded:: 8.1
.. confval:: console_output_style
Sets the console output style while running tests:
@@ -1873,6 +1878,19 @@ passed multiple times. The expected format is ``name=value``. For example::
"auto" can be used to explicitly use the global verbosity level.
.. confval:: verbosity_test_cases
Set a verbosity level specifically for test case execution related output, overriding the application wide level.
.. code-block:: ini
[pytest]
verbosity_test_cases = 2
Defaults to application wide verbosity level (via the ``-v`` command-line option). A special value of
"auto" can be used to explicitly use the global verbosity level.
.. confval:: xfail_strict
If set to ``True``, tests marked with ``@pytest.mark.xfail`` that actually succeed will by default fail the
@@ -2036,7 +2054,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
failure
--doctest-glob=pat Doctests file matching pattern, default: test*.txt
--doctest-ignore-import-errors
Ignore doctest ImportErrors
Ignore doctest collection errors
--doctest-continue-on-failure
For a given doctest, continue to run after the first
failure
@@ -2085,6 +2103,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
--log-cli-date-format=LOG_CLI_DATE_FORMAT
Log date format used by the logging module
--log-file=LOG_FILE Path to a file when logging will be written to
--log-file-mode={w,a}
Log file open mode
--log-file-level=LOG_FILE_LEVEL
Log file logging level
--log-file-format=LOG_FILE_FORMAT
@@ -2100,7 +2120,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
markers (linelist): Markers for test functions
markers (linelist): Register new markers for test functions
empty_parameter_set_mark (string):
Default marker for empty parametersets
norecursedirs (args): Directory patterns to avoid for recursion
@@ -2110,6 +2130,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
Each line specifies a pattern for
warnings.filterwarnings. Processed after
-W/--pythonwarnings.
consider_namespace_packages (bool):
Consider namespace packages when resolving module
names during import
usefixtures (args): List of default fixtures to be used with this
project
python_files (args): Glob-style file patterns for Python test module
@@ -2128,6 +2151,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
progress information ("progress" (percentage) |
"count" | "progress-even-when-capture-no" (forces
progress even when capture=no)
verbosity_test_cases (string):
Specify a verbosity level for test case execution,
overriding the main level. Higher levels will
provide more detailed information about each test
case executed.
xfail_strict (bool): Default for the strict parameter of xfail markers
when not given explicitly (default: False)
tmp_path_retention_count (string):
@@ -2175,6 +2203,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
log_cli_date_format (string):
Default value for --log-cli-date-format
log_file (string): Default value for --log-file
log_file_mode (string):
Default value for --log-file-mode
log_file_level (string):
Default value for --log-file-level
log_file_format (string):

View File

@@ -2,7 +2,7 @@ pallets-sphinx-themes
pluggy>=1.2.0
pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0
sphinx>=5,<8
sphinx>=7
sphinxcontrib-trio
sphinxcontrib-svg2pdfconverter
# Pin packaging because it no longer handles 'latest' version, which

View File

@@ -3,6 +3,7 @@ from pathlib import Path
import requests
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"

View File

@@ -1,13 +1,178 @@
[build-system]
requires = [
"setuptools>=45.0",
"setuptools-scm[toml]>=6.2.3",
[project]
name = "pytest"
description = "pytest: simple powerful testing with Python"
readme = "README.rst"
keywords = [
"test",
"unittest",
]
license = {text = "MIT"}
authors = [
{name = "Holger Krekel"},
{name = "Bruno Oliveira"},
{name = "Ronny Pfannschmidt"},
{name = "Floris Bruynooghe"},
{name = "Brianna Laugher"},
{name = "Florian Bruhin"},
{name = "Others (See AUTHORS)"},
]
requires-python = ">=3.8"
classifiers = [
"Development Status :: 6 - Mature",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX",
"Operating System :: Unix",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries",
"Topic :: Software Development :: Testing",
"Topic :: Utilities",
]
dynamic = [
"version",
]
dependencies = [
'colorama; sys_platform == "win32"',
'exceptiongroup>=1.0.0rc8; python_version < "3.11"',
"iniconfig",
"packaging",
"pluggy<2.0,>=1.4",
'tomli>=1; python_version < "3.11"',
]
[project.optional-dependencies]
testing = [
"argcomplete",
"attrs>=19.2",
"hypothesis>=3.56",
"mock",
"pygments>=2.7.2",
"requests",
"setuptools",
"xmlschema",
]
[project.urls]
Changelog = "https://docs.pytest.org/en/stable/changelog.html"
Homepage = "https://docs.pytest.org/en/latest/"
Source = "https://github.com/pytest-dev/pytest"
Tracker = "https://github.com/pytest-dev/pytest/issues"
Twitter = "https://twitter.com/pytestdotorg"
[project.scripts]
"py.test" = "pytest:console_main"
pytest = "pytest:console_main"
[build-system]
build-backend = "setuptools.build_meta"
requires = [
"setuptools>=61",
"setuptools-scm[toml]>=6.2.3",
]
[tool.setuptools.package-data]
"_pytest" = ["py.typed"]
"pytest" = ["py.typed"]
[tool.setuptools_scm]
write_to = "src/_pytest/_version.py"
[tool.black]
target-version = ['py38']
[tool.ruff]
src = ["src"]
line-length = 88
[tool.ruff.format]
docstring-code-format = true
[tool.ruff.lint]
select = [
"B", # bugbear
"D", # pydocstyle
"E", # pycodestyle
"F", # pyflakes
"I", # isort
"PYI", # flake8-pyi
"UP", # pyupgrade
"RUF", # ruff
"W", # pycodestyle
"PIE", # flake8-pie
"PGH004", # pygrep-hooks - Use specific rule codes when using noqa
"PLE", # pylint error
"PLW", # pylint warning
"PLR1714", # Consider merging multiple comparisons
]
ignore = [
# bugbear ignore
"B004", # Using `hasattr(x, "__call__")` to test if x is callable is unreliable.
"B007", # Loop control variable `i` not used within loop body
"B009", # Do not call `getattr` with a constant attribute value
"B010", # [*] Do not call `setattr` with a constant attribute value.
"B011", # Do not `assert False` (`python -O` removes these calls)
"B028", # No explicit `stacklevel` keyword argument found
# pycodestyle ignore
# pytest can do weird low-level things, and we usually know
# what we're doing when we use type(..) is ...
"E721", # Do not compare types, use `isinstance()`
# pydocstyle ignore
"D100", # Missing docstring in public module
"D101", # Missing docstring in public class
"D102", # Missing docstring in public method
"D103", # Missing docstring in public function
"D104", # Missing docstring in public package
"D105", # Missing docstring in magic method
"D106", # Missing docstring in public nested class
"D107", # Missing docstring in `__init__`
"D209", # [*] Multi-line docstring closing quotes should be on a separate line
"D205", # 1 blank line required between summary line and description
"D400", # First line should end with a period
"D401", # First line of docstring should be in imperative mood
"D402", # First line should not be the function's signature
"D404", # First word of the docstring should not be "This"
"D415", # First line should end with a period, question mark, or exclamation point
# ruff ignore
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
# pylint ignore
"PLW0603", # Using the global statement
"PLW0120", # remove the else and dedent its contents
"PLW2901", # for loop variable overwritten by assignment target
"PLR5501", # Use `elif` instead of `else` then `if`
]
[tool.ruff.lint.pycodestyle]
# In order to be able to format for 88 char in ruff format
max-line-length = 120
[tool.ruff.lint.pydocstyle]
convention = "pep257"
[tool.ruff.lint.isort]
force-single-line = true
combine-as-imports = true
force-sort-within-sections = true
order-by-type = false
known-local-folder = ["pytest", "_pytest"]
lines-after-imports = 2
[tool.ruff.lint.per-file-ignores]
"src/_pytest/_py/**/*.py" = ["B", "PYI"]
"src/_pytest/_version.py" = ["I001"]
"testing/python/approx.py" = ["B015"]
[tool.check-wheel-contents]
# check-wheel-contents is executed by the build-and-inspect-python-package action.
# W009: Wheel contains multiple toplevel library entries
ignore = "W009"
[tool.pyproject-fmt]
indent = 4
[tool.pytest.ini_options]
minversion = "2.0"
addopts = "-rfEX -p pytester --strict-markers"
@@ -67,7 +232,6 @@ markers = [
"uses_pexpect",
]
[tool.towncrier]
package = "pytest"
package_dir = "src"
@@ -116,10 +280,16 @@ template = "changelog/_template.rst"
name = "Trivial/Internal Changes"
showcontent = true
[tool.black]
target-version = ['py38']
# check-wheel-contents is executed by the build-and-inspect-python-package action.
[tool.check-wheel-contents]
# W009: Wheel contains multiple toplevel library entries
ignore = "W009"
[tool.mypy]
mypy_path = ["src"]
check_untyped_defs = true
disallow_any_generics = true
disallow_untyped_defs = true
ignore_missing_imports = true
show_error_codes = true
strict_equality = true
warn_redundant_casts = true
warn_return_any = true
warn_unreachable = true
warn_unused_configs = true
no_implicit_reexport = true

View File

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

View File

@@ -14,8 +14,8 @@ After that, it will create a release using the `release` tox environment, and pu
`pytest bot <pytestbot@gmail.com>` commit author.
"""
import argparse
import re
from pathlib import Path
import re
from subprocess import check_call
from subprocess import check_output
from subprocess import run
@@ -79,7 +79,7 @@ def prepare_release_pr(
)
except InvalidFeatureRelease as e:
print(f"{Fore.RED}{e}")
raise SystemExit(1)
raise SystemExit(1) from None
print(f"Version: {Fore.CYAN}{version}")

View File

@@ -107,7 +107,7 @@ def pre_release(
def changelog(version: str, write_out: bool = False) -> None:
addopts = [] if write_out else ["--draft"]
check_call(["towncrier", "--yes", "--version", version] + addopts)
check_call(["towncrier", "--yes", "--version", version, *addopts])
def main() -> None:

View File

@@ -1,6 +1,6 @@
# mypy: disallow-untyped-defs
import sys
from subprocess import call
import sys
def main() -> int:

View File

@@ -11,13 +11,13 @@ from typing import TypedDict
import packaging.version
import platformdirs
import tabulate
import wcwidth
from requests_cache import CachedResponse
from requests_cache import CachedSession
from requests_cache import OriginalResponse
from requests_cache import SQLiteCache
import tabulate
from tqdm import tqdm
import wcwidth
FILE_HEAD = r"""
@@ -29,7 +29,7 @@ Pytest Plugin List
==================
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
It includes PyPI projects whose names begin with ``pytest-`` or ``pytest_`` and a handful of manually selected projects.
Packages classified as inactive are excluded.
For detailed insights into how this list is generated,
@@ -61,6 +61,7 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
)
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert",
"logot",
"nuts",
"flask_fixture",
}
@@ -86,7 +87,6 @@ def project_response_with_refresh(
force refresh in case of last serial mismatch
"""
response = session.get(f"https://pypi.org/pypi/{name}/json")
if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial:
response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True)
@@ -110,7 +110,10 @@ def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
return {
name: p["_last-serial"]
for p in response.json()["projects"]
if (name := p["name"]).startswith("pytest-") or name in ADDITIONAL_PROJECTS
if (
(name := p["name"]).startswith(("pytest-", "pytest_"))
or name in ADDITIONAL_PROJECTS
)
}
@@ -185,7 +188,6 @@ def iter_plugins() -> Iterator[PluginInfo]:
def plugin_definitions(plugins: Iterable[PluginInfo]) -> Iterator[str]:
"""Return RST for the plugin list that fits better on a vertical page."""
for plugin in plugins:
yield dedent(
f"""
@@ -210,7 +212,7 @@ def main() -> None:
f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(".. only:: not latex\n\n")
wcwidth # reference library that must exist for tabulate to work
_ = wcwidth # reference library that must exist for tabulate to work
plugin_table = tabulate.tabulate(plugins, headers="keys", tablefmt="rst")
f.write(indent(plugin_table, " "))
f.write("\n\n")

105
setup.cfg
View File

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

View File

@@ -1,7 +1,8 @@
__all__ = ["__version__", "version_tuple"]
try:
from ._version import version as __version__, version_tuple
from ._version import version as __version__
from ._version import version_tuple
except ImportError: # pragma: no cover
# broken installation, we don't even try
# unknown only works because we do poor mans version compare

View File

@@ -61,10 +61,11 @@ If things do not work right away:
which should throw a KeyError: 'COMPLINE' (which is properly set by the
global argcomplete script).
"""
import argparse
from glob import glob
import os
import sys
from glob import glob
from typing import Any
from typing import List
from typing import Optional

View File

@@ -1,4 +1,5 @@
"""Python inspection/code generation API."""
from .code import Code
from .code import ExceptionInfo
from .code import filter_traceback
@@ -9,6 +10,7 @@ from .code import TracebackEntry
from .source import getrawcode
from .source import Source
__all__ = [
"Code",
"ExceptionInfo",

View File

@@ -1,14 +1,15 @@
# mypy: allow-untyped-defs
import ast
import dataclasses
import inspect
import os
import re
import sys
import traceback
from inspect import CO_VARARGS
from inspect import CO_VARKEYWORDS
from io import StringIO
import os
from pathlib import Path
import re
import sys
import traceback
from traceback import format_exception_only
from types import CodeType
from types import FrameType
@@ -50,6 +51,7 @@ from _pytest.deprecated import check_ispytest
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup
@@ -486,9 +488,10 @@ class ExceptionInfo(Generic[E]):
.. versionadded:: 7.4
"""
assert (
exception.__traceback__
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
assert exception.__traceback__, (
"Exceptions passed to ExcInfo.from_exception(...)"
" must have a non-None __traceback__."
)
exc_info = (type(exception), exception, exception.__traceback__)
return cls.from_exc_info(exc_info, exprinfo)
@@ -587,9 +590,7 @@ class ExceptionInfo(Generic[E]):
def __repr__(self) -> str:
if self._excinfo is None:
return "<ExceptionInfo for raises contextmanager>"
return "<{} {} tblen={}>".format(
self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback)
)
return f"<{self.__class__.__name__} {saferepr(self._excinfo[1])} tblen={len(self.traceback)}>"
def exconly(self, tryshort: bool = False) -> str:
"""Return the exception as a string.
@@ -698,10 +699,21 @@ class ExceptionInfo(Generic[E]):
return fmt.repr_excinfo(self)
def _stringify_exception(self, exc: BaseException) -> str:
try:
notes = getattr(exc, "__notes__", [])
except KeyError:
# 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):
notes = []
else:
raise
return "\n".join(
[
str(exc),
*getattr(exc, "__notes__", []),
*notes,
]
)
@@ -1006,13 +1018,8 @@ class FormattedExcinfo:
extraline: Optional[str] = (
"!!! Recursion error detected, but an error occurred locating the origin of recursion.\n"
" The following exception happened when comparing locals in the stack frame:\n"
" {exc_type}: {exc_msg}\n"
" Displaying first and last {max_frames} stack frames out of {total}."
).format(
exc_type=type(e).__name__,
exc_msg=str(e),
max_frames=max_frames,
total=len(traceback),
f" {type(e).__name__}: {e!s}\n"
f" Displaying first and last {max_frames} stack frames out of {len(traceback)}."
)
# Type ignored because adding two instances of a List subtype
# currently incorrectly has type List instead of the subtype.
@@ -1219,7 +1226,6 @@ class ReprEntry(TerminalRepr):
the "E" prefix) using syntax highlighting, taking care to not highlighting the ">"
character, as doing so might break line continuations.
"""
if not self.lines:
return

View File

@@ -1,10 +1,10 @@
# mypy: allow-untyped-defs
import ast
from bisect import bisect_right
import inspect
import textwrap
import tokenize
import types
import warnings
from bisect import bisect_right
from typing import Iterable
from typing import Iterator
from typing import List
@@ -12,6 +12,7 @@ from typing import Optional
from typing import overload
from typing import Tuple
from typing import Union
import warnings
class Source:
@@ -196,7 +197,9 @@ def getstatementrange_ast(
# by using the BlockFinder helper used which inspect.getsource() uses itself.
block_finder = inspect.BlockFinder()
# If we start with an indented line, put blockfinder to "started" mode.
block_finder.started = source.lines[start][0].isspace()
block_finder.started = (
bool(source.lines[start]) and source.lines[start][0].isspace()
)
it = ((x + "\n") for x in source.lines[start:end])
try:
for tok in tokenize.generate_tokens(lambda: next(it)):

View File

@@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
# This module was imported from the cpython standard library
# (https://github.com/python/cpython/) at commit
# c5140945c723ae6c4b7ee81ff720ac8ea4b52cfd (python3.12).
@@ -14,9 +15,9 @@
# useful, thank small children who sleep at night.
import collections as _collections
import dataclasses as _dataclasses
from io import StringIO as _StringIO
import re
import types as _types
from io import StringIO as _StringIO
from typing import Any
from typing import Callable
from typing import Dict

View File

@@ -19,8 +19,8 @@ def _format_repr_exception(exc: BaseException, obj: object) -> str:
raise
except BaseException as exc:
exc_info = f"unpresentable exception ({_try_repr_or_str(exc)})"
return "<[{} raised in repr()] {} object at 0x{:x}>".format(
exc_info, type(obj).__name__, id(obj)
return (
f"<[{exc_info} raised in repr()] {type(obj).__name__} object at 0x{id(obj):x}>"
)
@@ -108,7 +108,6 @@ def saferepr(
This function is a wrapper around the Repr/reprlib functionality of the
stdlib.
"""
return SafeRepr(maxsize, use_ascii).repr(obj)

View File

@@ -1,4 +1,5 @@
"""Helper functions for writing to terminals and files."""
import os
import shutil
import sys
@@ -183,9 +184,7 @@ class TerminalWriter:
"""
if indents and len(indents) != len(lines):
raise ValueError(
"indents size ({}) should have same size as lines ({})".format(
len(indents), len(lines)
)
f"indents size ({len(indents)}) should have same size as lines ({len(lines)})"
)
if not indents:
indents = [""] * len(lines)
@@ -233,17 +232,17 @@ class TerminalWriter:
# which may lead to the previous color being propagated to the
# start of the expression, so reset first.
return "\x1b[0m" + highlighted
except pygments.util.ClassNotFound:
except pygments.util.ClassNotFound as e:
raise UsageError(
"PYTEST_THEME environment variable had an invalid value: '{}'. "
"Only valid pygment styles are allowed.".format(
os.getenv("PYTEST_THEME")
)
)
except pygments.util.OptionError:
) from e
except pygments.util.OptionError as e:
raise UsageError(
"PYTEST_THEME_MODE environment variable had an invalid value: '{}'. "
"The only allowed values are 'dark' and 'light'.".format(
os.getenv("PYTEST_THEME_MODE")
)
)
) from e

View File

@@ -1,5 +1,5 @@
import unicodedata
from functools import lru_cache
import unicodedata
@lru_cache(100)

View File

@@ -1,4 +1,5 @@
"""create errno-specific classes for IO or os calls."""
from __future__ import annotations
import errno
@@ -8,6 +9,7 @@ from typing import Callable
from typing import TYPE_CHECKING
from typing import TypeVar
if TYPE_CHECKING:
from typing_extensions import ParamSpec

View File

@@ -1,16 +1,13 @@
# mypy: allow-untyped-defs
"""local path implementation."""
from __future__ import annotations
import atexit
from contextlib import contextmanager
import fnmatch
import importlib.util
import io
import os
import posixpath
import sys
import uuid
import warnings
from contextlib import contextmanager
from os.path import abspath
from os.path import dirname
from os.path import exists
@@ -19,18 +16,23 @@ from os.path import isdir
from os.path import isfile
from os.path import islink
from os.path import normpath
import posixpath
from stat import S_ISDIR
from stat import S_ISLNK
from stat import S_ISREG
import sys
from typing import Any
from typing import Callable
from typing import cast
from typing import Literal
from typing import overload
from typing import TYPE_CHECKING
import uuid
import warnings
from . import error
# Moved from local.py.
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
@@ -450,7 +452,7 @@ class LocalPath:
def ensure_dir(self, *args):
"""Ensure the path joined with args is a directory."""
return self.ensure(*args, **{"dir": True})
return self.ensure(*args, dir=True)
def bestrelpath(self, dest):
"""Return a string which is a relative path from self
@@ -675,7 +677,7 @@ class LocalPath:
else:
kw.setdefault("dirname", dirname)
kw.setdefault("sep", self.sep)
obj.strpath = normpath("%(dirname)s%(sep)s%(basename)s" % kw)
obj.strpath = normpath("{dirname}{sep}{basename}".format(**kw))
return obj
def _getbyspec(self, spec: str) -> list[str]:
@@ -760,7 +762,10 @@ class LocalPath:
# 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]
io.open,
self.strpath,
mode,
encoding=encoding, # type:ignore[arg-type]
)
return error.checked_call(open, self.strpath, mode)
@@ -779,11 +784,11 @@ class LocalPath:
valid checkers::
file=1 # is a file
file=0 # is not a file (may not even exist)
dir=1 # is a dir
link=1 # is a link
exists=1 # exists
file = 1 # is a file
file = 0 # is not a file (may not even exist)
dir = 1 # is a dir
link = 1 # is a link
exists = 1 # exists
You can specify multiple checker definitions, for example::
@@ -1100,9 +1105,7 @@ class LocalPath:
modname = self.purebasename
spec = importlib.util.spec_from_file_location(modname, str(self))
if spec is None or spec.loader is None:
raise ImportError(
f"Can't find module {modname} at location {str(self)}"
)
raise ImportError(f"Can't find module {modname} at location {self!s}")
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
@@ -1167,7 +1170,8 @@ class LocalPath:
where the 'self' path points to executable.
The process is directly invoked and not through a system shell.
"""
from subprocess import Popen, PIPE
from subprocess import PIPE
from subprocess import Popen
popen_opts.pop("stdout", None)
popen_opts.pop("stderr", None)
@@ -1277,7 +1281,8 @@ class LocalPath:
# 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]
tempfile.mkdtemp,
dir=str(rootdir), # type:ignore[arg-type]
)
return cls(path)

View File

@@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Support for presenting detailed information in failing assertions."""
import sys
from typing import Any
@@ -15,6 +16,7 @@ from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.nodes import Item
if TYPE_CHECKING:
from _pytest.main import Session
@@ -128,7 +130,6 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
reporting via the pytest_assertrepr_compare hook. This sets up this custom
comparison for the test.
"""
ihook = item.ihook
def callbinrepr(op, left: object, right: object) -> Optional[str]:

View File

@@ -1,5 +1,7 @@
"""Rewrite assertion AST to produce nice error messages."""
import ast
from collections import defaultdict
import errno
import functools
import importlib.abc
@@ -9,13 +11,12 @@ import io
import itertools
import marshal
import os
from pathlib import Path
from pathlib import PurePath
import struct
import sys
import tokenize
import types
from collections import defaultdict
from pathlib import Path
from pathlib import PurePath
from typing import Callable
from typing import Dict
from typing import IO
@@ -33,15 +34,17 @@ from _pytest._io.saferepr import DEFAULT_REPR_MAX_SIZE
from _pytest._io.saferepr import saferepr
from _pytest._version import version
from _pytest.assertion import util
from _pytest.assertion.util import ( # noqa: F401
format_explanation as _format_explanation,
)
from _pytest.config import Config
from _pytest.main import Session
from _pytest.pathlib import absolutepath
from _pytest.pathlib import fnmatch_ex
from _pytest.stash import StashKey
# fmt: off
from _pytest.assertion.util import format_explanation as _format_explanation # noqa:F401, isort:skip
# fmt:on
if TYPE_CHECKING:
from _pytest.assertion import AssertionState
@@ -858,9 +861,10 @@ class AssertionRewriter(ast.NodeVisitor):
the expression is false.
"""
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
from _pytest.warning_types import PytestAssertRewriteWarning
import warnings
from _pytest.warning_types import PytestAssertRewriteWarning
# TODO: This assert should not be needed.
assert self.module_path is not None
warnings.warn_explicit(
@@ -921,7 +925,7 @@ class AssertionRewriter(ast.NodeVisitor):
# If any hooks implement assert_pass hook
hook_impl_test = ast.If(
self.helper("_check_if_assertion_pass_impl"),
self.expl_stmts + [hook_call_pass],
[*self.expl_stmts, hook_call_pass],
[],
)
statements_pass = [hook_impl_test]
@@ -1002,7 +1006,7 @@ class AssertionRewriter(ast.NodeVisitor):
if i:
fail_inner: List[ast.stmt] = []
# cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa: F821
self.expl_stmts = fail_inner
# Check if the left operand is a ast.NamedExpr and the value has already been visited
if (
@@ -1016,9 +1020,7 @@ class AssertionRewriter(ast.NodeVisitor):
]
):
pytest_temp = self.variable()
self.variables_overwrite[self.scope][
v.left.target.id
] = v.left # type:ignore[assignment]
self.variables_overwrite[self.scope][v.left.target.id] = v.left # type:ignore[assignment]
v.left.target.id = pytest_temp
self.push_format_context()
res, expl = self.visit(v)
@@ -1062,9 +1064,7 @@ class AssertionRewriter(ast.NodeVisitor):
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
self.scope, {}
):
arg = self.variables_overwrite[self.scope][
arg.id
] # type:ignore[assignment]
arg = self.variables_overwrite[self.scope][arg.id] # type:ignore[assignment]
res, expl = self.visit(arg)
arg_expls.append(expl)
new_args.append(res)
@@ -1072,9 +1072,7 @@ class AssertionRewriter(ast.NodeVisitor):
if isinstance(
keyword.value, ast.Name
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
keyword.value = self.variables_overwrite[self.scope][
keyword.value.id
] # type:ignore[assignment]
keyword.value = self.variables_overwrite[self.scope][keyword.value.id] # type:ignore[assignment]
res, expl = self.visit(keyword.value)
new_kwargs.append(ast.keyword(keyword.arg, res))
if keyword.arg:
@@ -1111,13 +1109,9 @@ class AssertionRewriter(ast.NodeVisitor):
if isinstance(
comp.left, ast.Name
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
comp.left = self.variables_overwrite[self.scope][
comp.left.id
] # type:ignore[assignment]
comp.left = self.variables_overwrite[self.scope][comp.left.id] # type:ignore[assignment]
if isinstance(comp.left, ast.NamedExpr):
self.variables_overwrite[self.scope][
comp.left.target.id
] = comp.left # type:ignore[assignment]
self.variables_overwrite[self.scope][comp.left.target.id] = comp.left # type:ignore[assignment]
left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})"
@@ -1135,9 +1129,7 @@ class AssertionRewriter(ast.NodeVisitor):
and next_operand.target.id == left_res.id
):
next_operand.target.id = self.variable()
self.variables_overwrite[self.scope][
left_res.id
] = next_operand # type:ignore[assignment]
self.variables_overwrite[self.scope][left_res.id] = next_operand # type:ignore[assignment]
next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = f"({next_expl})"

View File

@@ -3,6 +3,7 @@
Current default behaviour is to truncate assertion explanations at
terminal lines, unless running with an assertions verbosity level of at least 2 or running on CI.
"""
from typing import List
from typing import Optional
@@ -91,7 +92,8 @@ def _truncate_explanation(
else:
# Add proper ellipsis when we were able to fit a full line exactly
truncated_explanation[-1] = "..."
return truncated_explanation + [
return [
*truncated_explanation,
"",
f"...Full output truncated ({truncated_line_count} line"
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",

View File

@@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Utilities for assertion debugging."""
import collections.abc
import os
@@ -14,13 +15,14 @@ from typing import Protocol
from typing import Sequence
from unicodedata import normalize
import _pytest._code
from _pytest import outcomes
import _pytest._code
from _pytest._io.pprint import PrettyPrinter
from _pytest._io.saferepr import saferepr
from _pytest._io.saferepr import saferepr_unlimited
from _pytest.config import Config
# The _reprcompare attribute on the util module is used by the new assertion
# interpretation code and assertion rewriter to detect this plugin was
# loaded and in turn call the hooks defined here as part of the
@@ -231,8 +233,8 @@ def assertrepr_compare(
return None
if explanation[0] != "":
explanation = [""] + explanation
return [summary] + explanation
explanation = ["", *explanation]
return [summary, *explanation]
def _compare_eq_any(
@@ -301,8 +303,8 @@ def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]:
if i > 42:
i -= 10 # Provide some context
explanation += [
"Skipping {} identical trailing "
"characters in diff, use -v to show".format(i)
f"Skipping {i} identical trailing "
"characters in diff, use -v to show"
]
left = left[:-i]
right = right[:-i]

View File

@@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version.
@@ -31,6 +32,7 @@ from _pytest.nodes import Directory
from _pytest.nodes import File
from _pytest.reports import TestReport
README_CONTENT = """\
# pytest cache directory #
@@ -111,6 +113,7 @@ class Cache:
"""
check_ispytest(_ispytest)
import warnings
from _pytest.warning_types import PytestCacheWarning
warnings.warn(
@@ -366,15 +369,13 @@ class LFPlugin:
noun = "failure" if self._previously_failed_count == 1 else "failures"
suffix = " first" if self.config.getoption("failedfirst") else ""
self._report_status = "rerun previous {count} {noun}{suffix}".format(
count=self._previously_failed_count, suffix=suffix, noun=noun
self._report_status = (
f"rerun previous {self._previously_failed_count} {noun}{suffix}"
)
if self._skipped_files > 0:
files_noun = "file" if self._skipped_files == 1 else "files"
self._report_status += " (skipped {files} {files_noun})".format(
files=self._skipped_files, files_noun=files_noun
)
self._report_status += f" (skipped {self._skipped_files} {files_noun})"
else:
self._report_status = "no previously failed tests, "
if self.config.getoption("last_failed_no_failures") == "none":

View File

@@ -1,11 +1,12 @@
# mypy: allow-untyped-defs
"""Per-test stdout/stderr capturing mechanism."""
import abc
import collections
import contextlib
import io
from io import UnsupportedOperation
import os
import sys
from io import UnsupportedOperation
from tempfile import TemporaryFile
from types import TracebackType
from typing import Any
@@ -38,6 +39,7 @@ from _pytest.nodes import File
from _pytest.nodes import Item
from _pytest.reports import CollectReport
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
@@ -596,7 +598,8 @@ if sys.version_info >= (3, 11) or TYPE_CHECKING:
else:
class CaptureResult(
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
collections.namedtuple("CaptureResult", ["out", "err"]), # noqa: PYI024
Generic[AnyStr],
):
"""The result of :method:`caplog.readouterr() <pytest.CaptureFixture.readouterr>`."""
@@ -789,9 +792,7 @@ class CaptureManager:
current_fixture = self._capture_fixture.request.fixturename
requested_fixture = capture_fixture.request.fixturename
capture_fixture.request.raiseerror(
"cannot use {} and {} at the same time".format(
requested_fixture, current_fixture
)
f"cannot use {requested_fixture} and {current_fixture} at the same time"
)
self._capture_fixture = capture_fixture
@@ -987,7 +988,6 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
@@ -1015,7 +1015,6 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsysbinary):
@@ -1043,7 +1042,6 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfd):
@@ -1071,7 +1069,6 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_system_echo(capfdbinary):

View File

@@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Python version compatibility code."""
from __future__ import annotations
@@ -5,35 +6,15 @@ import dataclasses
import enum
import functools
import inspect
import os
import sys
from inspect import Parameter
from inspect import signature
import os
from pathlib import Path
import sys
from typing import Any
from typing import Callable
from typing import Final
from typing import NoReturn
from typing import TypeVar
import py
_T = TypeVar("_T")
_S = TypeVar("_S")
#: constant to prepare valuing pylib path replacements/lazy proxies later on
# intended for removal in pytest 8.0 or 9.0
# 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
@@ -41,7 +22,7 @@ def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum):
token = 0
NOTSET: Final = NotSetType.token # noqa: E305
NOTSET: Final = NotSetType.token
# fmt: on
@@ -191,25 +172,13 @@ _non_printable_ascii_translate_table.update(
)
def _translate_non_printable(s: str) -> str:
return s.translate(_non_printable_ascii_translate_table)
STRING_TYPES = bytes, str
def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace")
def ascii_escaped(val: bytes | str) -> str:
r"""If val is pure ASCII, return it as an str, otherwise, escape
bytes objects into a sequence of escaped bytes:
b'\xc3\xb4\xc5\xd6' -> r'\xc3\xb4\xc5\xd6'
and escapes unicode objects into a sequence of escaped unicode
ids, e.g.:
and escapes strings into a sequence of escaped unicode ids, e.g.:
r'4\nV\U00043efa\x0eMXWB\x1e\u3028\u15fd\xcd\U0007d944'
@@ -220,10 +189,10 @@ def ascii_escaped(val: bytes | str) -> str:
a UTF-8 string.
"""
if isinstance(val, bytes):
ret = _bytes_to_ascii(val)
ret = val.decode("ascii", "backslashreplace")
else:
ret = val.encode("unicode_escape").decode("ascii")
return _translate_non_printable(ret)
return ret.translate(_non_printable_ascii_translate_table)
@dataclasses.dataclass
@@ -258,9 +227,7 @@ def get_real_func(obj):
from _pytest._io.saferepr import saferepr
raise ValueError(
("could not find real function of {start}\nstopped at {current}").format(
start=saferepr(start_obj), current=saferepr(obj)
)
f"could not find real function of {saferepr(start_obj)}\nstopped at {saferepr(obj)}"
)
if isinstance(obj, functools.partial):
obj = obj.func
@@ -322,7 +289,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":
if sys.platform == "win32" or sys.platform == "emscripten": # noqa: PLR1714
# win32 does not have a getuid() function.
# Emscripten has a return 0 stub.
return None

View File

@@ -1,23 +1,22 @@
# mypy: allow-untyped-defs
"""Command line options, ini-file and conftest.py processing."""
import argparse
import collections.abc
import copy
import dataclasses
import enum
from functools import lru_cache
import glob
import importlib.metadata
import inspect
import os
from pathlib import Path
import re
import shlex
import sys
import types
import warnings
from functools import lru_cache
from pathlib import Path
from textwrap import dedent
import types
from types import FunctionType
from types import TracebackType
from typing import Any
from typing import Callable
from typing import cast
@@ -37,24 +36,23 @@ from typing import Tuple
from typing import Type
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
import _pytest._code
import _pytest.deprecated
import _pytest.hookspec
from .compat import PathAwareHookProxy
from .exceptions import PrintHelp as PrintHelp
from .exceptions import UsageError as UsageError
from .findpaths import determine_setup
import _pytest._code
from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter
import _pytest.deprecated
import _pytest.hookspec
from _pytest.outcomes import fail
from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath
@@ -67,10 +65,12 @@ from _pytest.stash import Stash
from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING:
from .argparsing import Argument
from .argparsing import Parser
from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter
from .argparsing import Argument, Parser
_PluggyPlugin = object
@@ -114,16 +114,14 @@ class ConftestImportFailure(Exception):
def __init__(
self,
path: Path,
excinfo: Tuple[Type[Exception], Exception, TracebackType],
*,
cause: Exception,
) -> None:
super().__init__(path, excinfo)
self.path = path
self.excinfo = excinfo
self.cause = cause
def __str__(self) -> str:
return "{}: {} (from {})".format(
self.excinfo[0].__name__, self.excinfo[1], self.path
)
return f"{type(self.cause).__name__}: {self.cause} (from {self.path})"
def filter_traceback_for_conftest_import_failure(
@@ -154,7 +152,7 @@ def main(
try:
config = _prepareconfig(args, plugins)
except ConftestImportFailure as e:
exc_info = ExceptionInfo.from_exc_info(e.excinfo)
exc_info = ExceptionInfo.from_exception(e.cause)
tw = TerminalWriter(sys.stderr)
tw.line(f"ImportError while loading conftest '{e.path}'.", red=True)
exc_info.traceback = exc_info.traceback.filter(
@@ -240,7 +238,8 @@ essential_plugins = (
"helpconfig", # Provides -p.
)
default_plugins = essential_plugins + (
default_plugins = (
*essential_plugins,
"python",
"terminal",
"debugging",
@@ -252,7 +251,6 @@ default_plugins = essential_plugins + (
"monkeypatch",
"recwarn",
"pastebin",
"nose",
"assertion",
"junitxml",
"doctest",
@@ -547,7 +545,10 @@ class PytestPluginManager(PluginManager):
noconftest: bool,
rootpath: Path,
confcutdir: Optional[Path],
invocation_dir: Path,
importmode: Union[ImportMode, str],
*,
consider_namespace_packages: bool,
) -> None:
"""Load initial conftest files given a preparsed "namespace".
@@ -556,8 +557,9 @@ class PytestPluginManager(PluginManager):
All builtin and 3rd party plugins will have been loaded, however, so
common options will not confuse our logic here.
"""
current = Path.cwd()
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
self._confcutdir = (
absolutepath(invocation_dir / confcutdir) if confcutdir else None
)
self._noconftest = noconftest
self._using_pyargs = pyargs
foundanchor = False
@@ -567,37 +569,73 @@ class PytestPluginManager(PluginManager):
i = path.find("::")
if i != -1:
path = path[:i]
anchor = absolutepath(current / path)
anchor = absolutepath(invocation_dir / path)
# Ensure we do not break if what appears to be an anchor
# is in fact a very long option (#10169, #11394).
if safe_exists(anchor):
self._try_load_conftest(anchor, importmode, rootpath)
self._try_load_conftest(
anchor,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
foundanchor = True
if not foundanchor:
self._try_load_conftest(current, importmode, rootpath)
self._try_load_conftest(
invocation_dir,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
def _is_in_confcutdir(self, path: Path) -> bool:
"""Whether a path is within the confcutdir.
When false, should not load conftest.
"""
"""Whether to consider the given path to load conftests from."""
if self._confcutdir is None:
return True
# The semantics here are literally:
# Do not load a conftest if it is found upwards from confcut dir.
# But this is *not* the same as:
# Load only conftests from confcutdir or below.
# At first glance they might seem the same thing, however we do support use cases where
# we want to load conftests that are not found in confcutdir or below, but are found
# in completely different directory hierarchies like packages installed
# in out-of-source trees.
# (see #9767 for a regression where the logic was inverted).
return path not in self._confcutdir.parents
def _try_load_conftest(
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
self,
anchor: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
*,
consider_namespace_packages: bool,
) -> None:
self._loadconftestmodules(anchor, importmode, rootpath)
self._loadconftestmodules(
anchor,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
# let's also consider test* subdirs
if anchor.is_dir():
for x in anchor.glob("test*"):
if x.is_dir():
self._loadconftestmodules(x, importmode, rootpath)
self._loadconftestmodules(
x,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
def _loadconftestmodules(
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
self,
path: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
*,
consider_namespace_packages: bool,
) -> None:
if self._noconftest:
return
@@ -609,15 +647,17 @@ class PytestPluginManager(PluginManager):
if directory in self._dirpath2confmods:
return
# XXX these days we may rather want to use config.rootpath
# and allow users to opt into looking into the rootdir parent
# directories instead of requiring to specify confcutdir.
clist = []
for parent in reversed((directory, *directory.parents)):
if self._is_in_confcutdir(parent):
conftestpath = parent / "conftest.py"
if conftestpath.is_file():
mod = self._importconftest(conftestpath, importmode, rootpath)
mod = self._importconftest(
conftestpath,
importmode,
rootpath,
consider_namespace_packages=consider_namespace_packages,
)
clist.append(mod)
self._dirpath2confmods[directory] = clist
@@ -639,23 +679,39 @@ class PytestPluginManager(PluginManager):
raise KeyError(name)
def _importconftest(
self, conftestpath: Path, importmode: Union[str, ImportMode], rootpath: Path
self,
conftestpath: Path,
importmode: Union[str, ImportMode],
rootpath: Path,
*,
consider_namespace_packages: bool,
) -> types.ModuleType:
conftestpath_plugin_name = str(conftestpath)
existing = self.get_plugin(conftestpath_plugin_name)
if existing is not None:
return cast(types.ModuleType, existing)
# conftest.py files there are not in a Python package all have module
# name "conftest", and thus conflict with each other. Clear the existing
# before loading the new one, otherwise the existing one will be
# returned from the module cache.
pkgpath = resolve_package_path(conftestpath)
if pkgpath is None:
_ensure_removed_sysmodule(conftestpath.stem)
try:
del sys.modules[conftestpath.stem]
except KeyError:
pass
try:
mod = import_path(conftestpath, mode=importmode, root=rootpath)
mod = import_path(
conftestpath,
mode=importmode,
root=rootpath,
consider_namespace_packages=consider_namespace_packages,
)
except Exception as e:
assert e.__traceback__ is not None
exc_info = (type(e), e, e.__traceback__)
raise ConftestImportFailure(conftestpath, exc_info) from e
raise ConftestImportFailure(conftestpath, cause=e) from e
self._check_non_top_pytest_plugins(mod, conftestpath)
@@ -666,7 +722,7 @@ class PytestPluginManager(PluginManager):
if dirpath in path.parents or path == dirpath:
if mod in mods:
raise AssertionError(
f"While trying to load conftest path {str(conftestpath)}, "
f"While trying to load conftest path {conftestpath!s}, "
f"found that the module {mod} is already loaded with path {mod.__file__}. "
"This is not supposed to happen. Please report this issue to pytest."
)
@@ -743,13 +799,10 @@ class PytestPluginManager(PluginManager):
self.set_blocked("pytest_" + name)
else:
name = arg
# Unblock the plugin. None indicates that it has been blocked.
# There is no interface with pluggy for this.
if self._name2plugin.get(name, -1) is None:
del self._name2plugin[name]
# Unblock the plugin.
self.unblock(name)
if not name.startswith("pytest_"):
if self._name2plugin.get("pytest_" + name, -1) is None:
del self._name2plugin["pytest_" + name]
self.unblock("pytest_" + name)
self.import_plugin(arg, consider_entry_points=True)
def consider_conftest(
@@ -812,7 +865,7 @@ class PytestPluginManager(PluginManager):
def _get_plugin_specs_as_list(
specs: Union[None, types.ModuleType, str, Sequence[str]]
specs: Union[None, types.ModuleType, str, Sequence[str]],
) -> List[str]:
"""Parse a plugins specification into a list of plugin names."""
# None means empty.
@@ -833,13 +886,6 @@ def _get_plugin_specs_as_list(
)
def _ensure_removed_sysmodule(modname: str) -> None:
try:
del sys.modules[modname]
except KeyError:
pass
class Notset:
def __repr__(self):
return "<NOTSET>"
@@ -980,7 +1026,8 @@ class Config:
*,
invocation_params: Optional[InvocationParams] = None,
) -> None:
from .argparsing import Parser, FILE_OR_DIR
from .argparsing import FILE_OR_DIR
from .argparsing import Parser
if invocation_params is None:
invocation_params = self.InvocationParams(
@@ -1021,7 +1068,7 @@ class Config:
self._store = self.stash
self.trace = self.pluginmanager.trace.root.get("config")
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
self.hook = self.pluginmanager.hook # type: ignore[assignment]
self._inicache: Dict[str, Any] = {}
self._override_ini: Sequence[str] = ()
self._opt2dest: Dict[str, str] = {}
@@ -1175,7 +1222,11 @@ class Config:
noconftest=early_config.known_args_namespace.noconftest,
rootpath=early_config.rootpath,
confcutdir=early_config.known_args_namespace.confcutdir,
invocation_dir=early_config.invocation_params.dir,
importmode=early_config.known_args_namespace.importmode,
consider_namespace_packages=early_config.getini(
"consider_namespace_packages"
),
)
def _initini(self, args: Sequence[str]) -> None:
@@ -1183,8 +1234,8 @@ class Config:
args, namespace=copy.copy(self.option)
)
rootpath, inipath, inicfg = determine_setup(
ns.inifilename,
ns.file_or_dir + unknown_args,
inifile=ns.inifilename,
args=ns.file_or_dir + unknown_args,
rootdir_cmd_arg=ns.rootdir or None,
invocation_dir=self.invocation_params.dir,
)
@@ -1268,6 +1319,8 @@ class Config:
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
:param warn: Whether can issue warnings.
:returns: The args and the args source. Guaranteed to be non-empty.
"""
if args:
source = Config.ArgsSource.ARGS
@@ -1331,11 +1384,6 @@ class Config:
self._validate_plugins()
self._warn_about_skipped_plugins()
if self.known_args_namespace.strict:
self.issue_config_time_warning(
_pytest.deprecated.STRICT_OPTION, stacklevel=2
)
if self.known_args_namespace.confcutdir is None:
if self.inipath is not None:
confcutdir = str(self.inipath.parent)
@@ -1381,12 +1429,7 @@ class Config:
if Version(minver) > Version(pytest.__version__):
raise pytest.UsageError(
"%s: 'minversion' requires pytest-%s, actual pytest-%s'"
% (
self.inipath,
minver,
pytest.__version__,
)
f"{self.inipath}: 'minversion' requires pytest-{minver}, actual pytest-{pytest.__version__}'"
)
def _validate_config_options(self) -> None:
@@ -1399,8 +1442,9 @@ class Config:
return
# Imported lazily to improve start-up time.
from packaging.requirements import InvalidRequirement
from packaging.requirements import Requirement
from packaging.version import Version
from packaging.requirements import InvalidRequirement, Requirement
plugin_info = self.pluginmanager.list_plugin_distinfo()
plugin_dist_info = {dist.project_name: dist.version for _, dist in plugin_info}
@@ -1444,8 +1488,6 @@ class Config:
kwargs=dict(pluginmanager=self.pluginmanager)
)
self._preparse(args, addopts=addopts)
# XXX deprecated hook:
self.hook.pytest_cmdline_preparse(config=self, args=args)
self._parser.after_preparse = True # type: ignore
try:
args = self._parser.parse_setoption(
@@ -1574,9 +1616,11 @@ class Config:
# in this case, we already have a list ready to use.
#
if type == "paths":
# TODO: This assert is probably not valid in all cases.
assert self.inipath is not None
dp = self.inipath.parent
dp = (
self.inipath.parent
if self.inipath is not None
else self.invocation_params.dir
)
input_values = shlex.split(value) if isinstance(value, str) else value
return [dp / x for x in input_values]
elif type == "args":
@@ -1622,9 +1666,7 @@ class Config:
key, user_ini_value = ini_config.split("=", 1)
except ValueError as e:
raise UsageError(
"-o/--override-ini expects option=value style (got: {!r}).".format(
ini_config
)
f"-o/--override-ini expects option=value style (got: {ini_config!r})."
) from e
else:
if key == name:
@@ -1665,6 +1707,8 @@ class Config:
#: Verbosity type for failed assertions (see :confval:`verbosity_assertions`).
VERBOSITY_ASSERTIONS: Final = "assertions"
#: Verbosity type for test case execution (see :confval:`verbosity_test_cases`).
VERBOSITY_TEST_CASES: Final = "test_cases"
_VERBOSITY_INI_DEFAULT: Final = "auto"
def get_verbosity(self, verbosity_type: Optional[str] = None) -> int:
@@ -1682,7 +1726,6 @@ class Config:
can be used to explicitly use the global verbosity level.
Example:
.. code-block:: ini
# content of pytest.ini
@@ -1862,13 +1905,13 @@ def parse_warning_filter(
try:
action: "warnings._ActionKind" = warnings._getaction(action_) # type: ignore[attr-defined]
except warnings._OptionError as e:
raise UsageError(error_template.format(error=str(e)))
raise UsageError(error_template.format(error=str(e))) from None
try:
category: Type[Warning] = _resolve_warning_category(category_)
except Exception:
exc_info = ExceptionInfo.from_current()
exception_text = exc_info.getrepr(style="native")
raise UsageError(error_template.format(error=exception_text))
raise UsageError(error_template.format(error=exception_text)) from None
if message and escape:
message = re.escape(message)
if module and escape:
@@ -1881,7 +1924,7 @@ def parse_warning_filter(
except ValueError as e:
raise UsageError(
error_template.format(error=f"invalid lineno {lineno_!r}: {e}")
)
) from None
else:
lineno = 0
return action, message, category, module, lineno

View File

@@ -1,8 +1,8 @@
# mypy: allow-untyped-defs
import argparse
from gettext import gettext
import os
import sys
import warnings
from gettext import gettext
from typing import Any
from typing import Callable
from typing import cast
@@ -19,11 +19,9 @@ from typing import Union
import _pytest._io
from _pytest.config.exceptions import UsageError
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
from _pytest.deprecated import ARGUMENT_TYPE_STR
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
from _pytest.deprecated import check_ispytest
FILE_OR_DIR = "file_or_dir"
@@ -124,7 +122,7 @@ class Parser:
from _pytest._argcomplete import filescompleter
optparser = MyOptionParser(self, self.extra_info, prog=self.prog)
groups = self._groups + [self._anonymous]
groups = [*self._groups, self._anonymous]
for group in groups:
if group.options:
desc = group.description or group.name
@@ -200,9 +198,16 @@ class Parser:
* ``paths``: a list of :class:`pathlib.Path`, separated as in a shell
* ``pathlist``: a list of ``py.path``, separated as in a shell
For ``paths`` and ``pathlist`` types, they are considered relative to the ini-file.
In case the execution is happening without an ini-file defined,
they will be considered relative to the current working directory (for example with ``--override-ini``).
.. versionadded:: 7.0
The ``paths`` variable type.
.. versionadded:: 8.1
Use the current working directory to resolve ``paths`` and ``pathlist`` in the absence of an ini-file.
Defaults to ``string`` if ``None`` or not passed.
:param default:
Default value if no ini-file option exists but is queried.
@@ -219,7 +224,7 @@ class Parser:
def get_ini_default_for_type(
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]]
type: Optional[Literal["string", "paths", "pathlist", "args", "linelist", "bool"]],
) -> Any:
"""
Used by addini to get the default value for a given ini-option type, when
@@ -259,39 +264,15 @@ class Argument:
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
"""
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
def __init__(self, *names: str, **attrs: Any) -> None:
"""Store params in private vars for use in add_argument."""
self._attrs = attrs
self._short_opts: List[str] = []
self._long_opts: List[str] = []
if "%default" in (attrs.get("help") or ""):
warnings.warn(ARGUMENT_PERCENT_DEFAULT, stacklevel=3)
try:
typ = attrs["type"]
self.type = attrs["type"]
except KeyError:
pass
else:
# This might raise a keyerror as well, don't want to catch that.
if isinstance(typ, str):
if typ == "choice":
warnings.warn(
ARGUMENT_TYPE_STR_CHOICE.format(typ=typ, names=names),
stacklevel=4,
)
# argparse expects a type here take it from
# the type of the first element
attrs["type"] = type(attrs["choices"][0])
else:
warnings.warn(
ARGUMENT_TYPE_STR.format(typ=typ, names=names), stacklevel=4
)
attrs["type"] = Argument._typ_map[typ]
# Used in test_parseopt -> test_parse_defaultgetter.
self.type = attrs["type"]
else:
self.type = typ
try:
# Attribute existence is tested in Config._processopt.
self.default = attrs["default"]
@@ -322,11 +303,6 @@ class Argument:
self._attrs[attr] = getattr(self, attr)
except AttributeError:
pass
if self._attrs.get("help"):
a = self._attrs["help"]
a = a.replace("%default", "%(default)s")
# a = a.replace('%prog', '%(prog)s')
self._attrs["help"] = a
return self._attrs
def _set_opt_strings(self, opts: Sequence[str]) -> None:
@@ -480,7 +456,7 @@ class MyOptionParser(argparse.ArgumentParser):
) -> Optional[Tuple[Optional[argparse.Action], str, Optional[str]]]:
if not arg_string:
return None
if not arg_string[0] in self.prefix_chars:
if arg_string[0] not in self.prefix_chars:
return None
if arg_string in self._option_string_actions:
action = self._option_string_actions[arg_string]

View File

@@ -1,83 +0,0 @@
from __future__ import annotations
import functools
import warnings
from pathlib import Path
from typing import Mapping
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):
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,6 +1,6 @@
import os
import sys
from pathlib import Path
import sys
from typing import Dict
from typing import Iterable
from typing import List
@@ -37,7 +37,6 @@ def load_config_dict_from_file(
Return None if the file does not contain valid pytest configuration.
"""
# Configuration from ini files are obtained from the [pytest] section, if present.
if filepath.suffix == ".ini":
iniconfig = _parse_ini_config(filepath)
@@ -87,6 +86,7 @@ def load_config_dict_from_file(
def locate_config(
invocation_dir: Path,
args: Iterable[Path],
) -> Tuple[Optional[Path], Optional[Path], Dict[str, Union[str, List[str]]]]:
"""Search in the list of arguments for a valid ini-file for pytest,
@@ -100,20 +100,28 @@ def locate_config(
]
args = [x for x in args if not str(x).startswith("-")]
if not args:
args = [Path.cwd()]
args = [invocation_dir]
found_pyproject_toml: Optional[Path] = None
for arg in args:
argpath = absolutepath(arg)
for base in (argpath, *argpath.parents):
for config_name in config_names:
p = base / config_name
if p.is_file():
if p.name == "pyproject.toml" and found_pyproject_toml is None:
found_pyproject_toml = p
ini_config = load_config_dict_from_file(p)
if ini_config is not None:
return base, p, ini_config
if found_pyproject_toml is not None:
return found_pyproject_toml.parent, found_pyproject_toml, {}
return None, None, {}
def get_common_ancestor(paths: Iterable[Path]) -> Path:
def get_common_ancestor(
invocation_dir: Path,
paths: Iterable[Path],
) -> Path:
common_ancestor: Optional[Path] = None
for path in paths:
if not path.exists():
@@ -130,7 +138,7 @@ def get_common_ancestor(paths: Iterable[Path]) -> Path:
if shared is not None:
common_ancestor = shared
if common_ancestor is None:
common_ancestor = Path.cwd()
common_ancestor = invocation_dir
elif common_ancestor.is_file():
common_ancestor = common_ancestor.parent
return common_ancestor
@@ -162,10 +170,11 @@ CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supporte
def determine_setup(
*,
inifile: Optional[str],
args: Sequence[str],
rootdir_cmd_arg: Optional[str] = None,
invocation_dir: Optional[Path] = None,
rootdir_cmd_arg: Optional[str],
invocation_dir: Path,
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
"""Determine the rootdir, inifile and ini configuration values from the
command line arguments.
@@ -177,8 +186,7 @@ def determine_setup(
:param rootdir_cmd_arg:
The `--rootdir` command line argument, if given.
:param invocation_dir:
The working directory when pytest was invoked, if known.
If not known, the current working directory is used.
The working directory when pytest was invoked.
"""
rootdir = None
dirs = get_dirs_from_args(args)
@@ -189,8 +197,8 @@ def determine_setup(
if rootdir_cmd_arg is None:
rootdir = inipath_.parent
else:
ancestor = get_common_ancestor(dirs)
rootdir, inipath, inicfg = locate_config([ancestor])
ancestor = get_common_ancestor(invocation_dir, dirs)
rootdir, inipath, inicfg = locate_config(invocation_dir, [ancestor])
if rootdir is None and rootdir_cmd_arg is None:
for possible_rootdir in (ancestor, *ancestor.parents):
if (possible_rootdir / "setup.py").is_file():
@@ -198,22 +206,18 @@ def determine_setup(
break
else:
if dirs != [ancestor]:
rootdir, inipath, inicfg = locate_config(dirs)
rootdir, inipath, inicfg = locate_config(invocation_dir, dirs)
if rootdir is None:
if invocation_dir is not None:
cwd = invocation_dir
else:
cwd = Path.cwd()
rootdir = get_common_ancestor([cwd, ancestor])
rootdir = get_common_ancestor(
invocation_dir, [invocation_dir, ancestor]
)
if is_fs_root(rootdir):
rootdir = ancestor
if rootdir_cmd_arg:
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
if not rootdir.is_dir():
raise UsageError(
"Directory '{}' not found. Check your '--rootdir' option.".format(
rootdir
)
f"Directory '{rootdir}' not found. Check your '--rootdir' option."
)
assert rootdir is not None
return rootdir, inipath, inicfg or {}

View File

@@ -1,9 +1,9 @@
# mypy: allow-untyped-defs
"""Interactive debugging with PDB, the Python Debugger."""
import argparse
import functools
import sys
import types
import unittest
from typing import Any
from typing import Callable
from typing import Generator
@@ -13,6 +13,7 @@ from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import unittest
from _pytest import outcomes
from _pytest._code import ExceptionInfo
@@ -25,6 +26,7 @@ from _pytest.config.exceptions import UsageError
from _pytest.nodes import Node
from _pytest.reports import BaseReport
if TYPE_CHECKING:
from _pytest.capture import CaptureManager
from _pytest.runner import CallInfo
@@ -263,8 +265,7 @@ class pytestPDB:
elif capturing:
tw.sep(
">",
"PDB %s (IO-capturing turned off for %s)"
% (method, capturing),
f"PDB {method} (IO-capturing turned off for {capturing})",
)
else:
tw.sep(">", f"PDB {method}")
@@ -377,7 +378,8 @@ def _postmortem_traceback(excinfo: ExceptionInfo[BaseException]) -> types.Traceb
elif isinstance(excinfo.value, ConftestImportFailure):
# A config.ConftestImportFailure is not useful for post_mortem.
# Use the underlying exception instead:
return excinfo.value.excinfo[2]
assert excinfo.value.cause.__traceback__ is not None
return excinfo.value.cause.__traceback__
else:
assert excinfo._excinfo is not None
return excinfo._excinfo[2]

View File

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

View File

@@ -1,15 +1,15 @@
# mypy: allow-untyped-defs
"""Discover and run doctests in modules and test files."""
import bdb
from contextlib import contextmanager
import functools
import inspect
import os
from pathlib import Path
import platform
import sys
import traceback
import types
import warnings
from contextlib import contextmanager
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Dict
@@ -23,6 +23,7 @@ from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import warnings
from _pytest import outcomes
from _pytest._code.code import ExceptionInfo
@@ -39,13 +40,14 @@ from _pytest.nodes import Item
from _pytest.outcomes import OutcomeException
from _pytest.outcomes import skip
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path
from _pytest.python import Module
from _pytest.python_api import approx
from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
import doctest
from typing import Self
DOCTEST_REPORT_CHOICE_NONE = "none"
DOCTEST_REPORT_CHOICE_CDIFF = "cdiff"
@@ -105,7 +107,7 @@ def pytest_addoption(parser: Parser) -> None:
"--doctest-ignore-import-errors",
action="store_true",
default=False,
help="Ignore doctest ImportErrors",
help="Ignore doctest collection errors",
dest="doctest_ignore_import_errors",
)
group.addoption(
@@ -132,11 +134,9 @@ def pytest_collect_file(
if config.option.doctestmodules and not any(
(_is_setup_py(file_path), _is_main_py(file_path))
):
mod: DoctestModule = DoctestModule.from_parent(parent, path=file_path)
return mod
return DoctestModule.from_parent(parent, path=file_path)
elif _is_doctest(config, file_path, parent):
txt: DoctestTextfile = DoctestTextfile.from_parent(parent, path=file_path)
return txt
return DoctestTextfile.from_parent(parent, path=file_path)
return None
@@ -271,14 +271,14 @@ class DoctestItem(Item):
self._initrequest()
@classmethod
def from_parent( # type: ignore
def from_parent( # type: ignore[override]
cls,
parent: "Union[DoctestTextfile, DoctestModule]",
*,
name: str,
runner: "doctest.DocTestRunner",
dtest: "doctest.DocTest",
):
) -> "Self":
# incompatible signature due to imposed limits on subclass
"""The public named constructor."""
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
@@ -485,9 +485,9 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
return real_unwrap(func, stop=lambda obj: _is_mocked(obj) or _stop(func))
except Exception as e:
warnings.warn(
"Got %r when unwrapping %r. This is usually caused "
f"Got {e!r} when unwrapping {func!r}. This is usually caused "
"by a violation of Python's object protocol; see e.g. "
"https://github.com/pytest-dev/pytest/issues/5080" % (e, func),
"https://github.com/pytest-dev/pytest/issues/5080",
PytestWarning,
)
raise
@@ -558,24 +558,18 @@ class DoctestModule(Module):
else: # pragma: no cover
pass
if self.path.name == "conftest.py":
module = self.config.pluginmanager._importconftest(
self.path,
self.config.getoption("importmode"),
rootpath=self.config.rootpath,
)
else:
try:
module = import_path(
self.path,
root=self.config.rootpath,
mode=self.config.getoption("importmode"),
)
except ImportError:
if self.config.getvalue("doctest_ignore_import_errors"):
skip("unable to import module %r" % self.path)
else:
raise
try:
module = self.obj
except Collector.CollectError:
if self.config.getvalue("doctest_ignore_import_errors"):
skip("unable to import module %r" % self.path)
else:
raise
# While doctests currently don't support fixtures directly, we still
# need to pick up autouse fixtures.
self.session._fixturemanager.parsefactories(self)
# Uses internal doctest module parsing mechanism.
finder = MockAwareDocTestFinder()
optionflags = get_optionflags(self.config)

View File

@@ -2,11 +2,11 @@ import os
import sys
from typing import Generator
import pytest
from _pytest.config import Config
from _pytest.config.argparsing import Parser
from _pytest.nodes import Item
from _pytest.stash import StashKey
import pytest
fault_handler_original_stderr_fd_key = StashKey[int]()

View File

@@ -1,13 +1,13 @@
# mypy: allow-untyped-defs
import abc
from collections import defaultdict
from collections import deque
import dataclasses
import functools
import inspect
import os
import warnings
from collections import defaultdict
from collections import deque
from contextlib import suppress
from pathlib import Path
import sys
from typing import AbstractSet
from typing import Any
from typing import Callable
@@ -30,6 +30,7 @@ from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import warnings
import _pytest
from _pytest import nodes
@@ -67,6 +68,10 @@ from _pytest.scope import HIGH_SCOPES
from _pytest.scope import Scope
if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup
if TYPE_CHECKING:
from typing import Deque
@@ -116,22 +121,16 @@ def pytest_sessionstart(session: "Session") -> None:
def get_scope_package(
node: nodes.Item,
fixturedef: "FixtureDef[object]",
) -> Optional[Union[nodes.Item, nodes.Collector]]:
) -> Optional[nodes.Node]:
from _pytest.python import Package
current: Optional[Union[nodes.Item, nodes.Collector]] = node
while current and (
not isinstance(current, Package) or current.nodeid != fixturedef.baseid
):
current = current.parent # type: ignore[assignment]
if current is None:
return node.session
return current
for parent in node.iter_parents():
if isinstance(parent, Package) and parent.nodeid == fixturedef.baseid:
return parent
return node.session
def get_scope_node(
node: nodes.Node, scope: Scope
) -> Optional[Union[nodes.Item, nodes.Collector]]:
def get_scope_node(node: nodes.Node, scope: Scope) -> Optional[nodes.Node]:
import _pytest.python
if scope is Scope.Function:
@@ -174,33 +173,28 @@ def get_parametrized_fixture_keys(
the specified scope."""
assert scope is not Scope.Function
try:
callspec = item.callspec # type: ignore[attr-defined]
callspec: CallSpec2 = item.callspec # type: ignore[attr-defined]
except AttributeError:
pass
else:
cs: CallSpec2 = callspec
# cs.indices is random order of argnames. Need to
# sort this so that different calls to
# get_parametrized_fixture_keys will be deterministic.
for argname in sorted(cs.indices):
if cs._arg2scope[argname] != scope:
continue
return
for argname in callspec.indices:
if callspec._arg2scope[argname] != scope:
continue
item_cls = None
if scope is Scope.Session:
scoped_item_path = None
elif scope is Scope.Package:
scoped_item_path = item.path
elif scope is Scope.Module:
scoped_item_path = item.path
elif scope is Scope.Class:
scoped_item_path = item.path
item_cls = item.cls # type: ignore[attr-defined]
else:
assert_never(scope)
item_cls = None
if scope is Scope.Session:
scoped_item_path = None
elif scope is Scope.Package:
scoped_item_path = item.path
elif scope is Scope.Module:
scoped_item_path = item.path
elif scope is Scope.Class:
scoped_item_path = item.path
item_cls = item.cls # type: ignore[attr-defined]
else:
assert_never(scope)
param_index = cs.indices[argname]
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
param_index = callspec.indices[argname]
yield FixtureArgKey(argname, param_index, scoped_item_path, item_cls)
# Algorithm for sorting on a per-parametrized resource setup basis.
@@ -354,7 +348,6 @@ class FixtureRequest(abc.ABC):
pyfuncitem: "Function",
fixturename: Optional[str],
arg2fixturedefs: Dict[str, Sequence["FixtureDef[Any]"]],
arg2index: Dict[str, int],
fixture_defs: Dict[str, "FixtureDef[Any]"],
*,
_ispytest: bool = False,
@@ -368,16 +361,6 @@ class FixtureRequest(abc.ABC):
# collection. Dynamically requested fixtures (using
# `request.getfixturevalue("foo")`) are added dynamically.
self._arg2fixturedefs: Final = arg2fixturedefs
# A fixture may override another fixture with the same name, e.g. a fixture
# in a module can override a fixture in a conftest, a fixture in a class can
# override a fixture in the module, and so on.
# An overriding fixture can request its own name; in this case it gets
# the value of the fixture it overrides, one level up.
# The _arg2index state keeps the current depth in the overriding chain.
# The fixturedefs list in _arg2fixturedefs for a given name is ordered from
# furthest to closest, so we use negative indexing -1, -2, ... to go from
# last to first.
self._arg2index: Final = arg2index
# The evaluated argnames so far, mapping to the FixtureDef they resolved
# to.
self._fixture_defs: Final = fixture_defs
@@ -424,9 +407,7 @@ class FixtureRequest(abc.ABC):
# We arrive here because of a dynamic call to
# getfixturevalue(argname) usage which was naturally
# not known at parsing/collection time.
assert self._pyfuncitem.parent is not None
parentid = self._pyfuncitem.parent.nodeid
fixturedefs = self._fixturemanager.getfixturedefs(argname, parentid)
fixturedefs = self._fixturemanager.getfixturedefs(argname, self._pyfuncitem)
if fixturedefs is not None:
self._arg2fixturedefs[argname] = fixturedefs
# No fixtures defined with this name.
@@ -435,11 +416,24 @@ class FixtureRequest(abc.ABC):
# The are no fixtures with this name applicable for the function.
if not fixturedefs:
raise FixtureLookupError(argname, self)
index = self._arg2index.get(argname, 0) - 1
# The fixture requested its own name, but no remaining to override.
# 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)
self._arg2index[argname] = index
return fixturedefs[index]
@property
@@ -525,7 +519,7 @@ class FixtureRequest(abc.ABC):
:param msg:
An optional custom error message.
"""
raise self._fixturemanager.FixtureLookupError(None, self, msg)
raise FixtureLookupError(None, self, msg)
def getfixturevalue(self, argname: str) -> Any:
"""Dynamically run a named fixture function.
@@ -551,6 +545,16 @@ class FixtureRequest(abc.ABC):
)
return fixturedef.cached_result[0]
def _iter_chain(self) -> Iterator["SubRequest"]:
"""Yield all SubRequests in the chain, from self up.
Note: does *not* yield the TopRequest.
"""
current = self
while isinstance(current, SubRequest):
yield current
current = current._parent_request
def _get_active_fixturedef(
self, argname: str
) -> Union["FixtureDef[object]", PseudoFixtureDef[object]]:
@@ -568,11 +572,7 @@ class FixtureRequest(abc.ABC):
return fixturedef
def _get_fixturestack(self) -> List["FixtureDef[Any]"]:
current = self
values: List[FixtureDef[Any]] = []
while isinstance(current, SubRequest):
values.append(current._fixturedef) # type: ignore[has-type]
current = current._parent_request
values = [request._fixturedef for request in self._iter_chain()]
values.reverse()
return values
@@ -588,7 +588,6 @@ class FixtureRequest(abc.ABC):
# (latter managed by fixturedef)
argname = fixturedef.argname
funcitem = self._pyfuncitem
scope = fixturedef._scope
try:
callspec = funcitem.callspec
except AttributeError:
@@ -596,24 +595,20 @@ class FixtureRequest(abc.ABC):
if callspec is not None and argname in callspec.params:
param = callspec.params[argname]
param_index = callspec.indices[argname]
# If a parametrize invocation set a scope it will override
# the static scope defined with the fixture function.
with suppress(KeyError):
scope = callspec._arg2scope[argname]
# The parametrize invocation scope overrides the fixture's scope.
scope = callspec._arg2scope[argname]
else:
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 = (
"{name} does not support fixtures, maybe unittest.TestCase subclass?\n"
"Node id: {nodeid}\n"
"Function type: {typename}"
).format(
name=funcitem.name,
nodeid=funcitem.nodeid,
typename=type(funcitem).__name__,
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:
@@ -670,7 +665,6 @@ class TopRequest(FixtureRequest):
fixturename=None,
pyfuncitem=pyfuncitem,
arg2fixturedefs=pyfuncitem._fixtureinfo.name2fixturedefs.copy(),
arg2index={},
fixture_defs={},
_ispytest=_ispytest,
)
@@ -716,12 +710,11 @@ class SubRequest(FixtureRequest):
fixturename=fixturedef.argname,
fixture_defs=request._fixture_defs,
arg2fixturedefs=request._arg2fixturedefs,
arg2index=request._arg2index,
_ispytest=_ispytest,
)
self._parent_request: Final[FixtureRequest] = request
self._scope_field: Final = scope
self._fixturedef: Final = fixturedef
self._fixturedef: Final[FixtureDef[object]] = fixturedef
if param is not NOTSET:
self.param = param
self.param_index: Final = param_index
@@ -738,7 +731,7 @@ class SubRequest(FixtureRequest):
scope = self._scope
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
node: Optional[nodes.Node] = self._pyfuncitem
elif scope is Scope.Package:
node = get_scope_package(self._pyfuncitem, self._fixturedef)
else:
@@ -746,9 +739,7 @@ class SubRequest(FixtureRequest):
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
assert node, f'Could not obtain a node for scope "{scope}" for function {self._pyfuncitem!r}'
return node
def _check_scope(
@@ -846,14 +837,13 @@ class FixtureLookupError(LookupError):
available = set()
parent = self.request._pyfuncitem.parent
assert parent is not None
parentid = parent.nodeid
for name, fixturedefs in fm._arg2fixturedefs.items():
faclist = list(fm._matchfactories(fixturedefs, parentid))
faclist = list(fm._matchfactories(fixturedefs, parent))
if faclist:
available.add(name)
if self.argname in available:
msg = " recursive dependency involving fixture '{}' detected".format(
self.argname
msg = (
f" recursive dependency involving fixture '{self.argname}' detected"
)
else:
msg = f"fixture '{self.argname}' not found"
@@ -947,15 +937,13 @@ def _eval_scope_callable(
result = scope_callable(fixture_name=fixture_name, config=config) # type: ignore[call-arg]
except Exception as e:
raise TypeError(
"Error evaluating {} while defining fixture '{}'.\n"
"Expected a function with the signature (*, fixture_name, config)".format(
scope_callable, fixture_name
)
f"Error evaluating {scope_callable} while defining fixture '{fixture_name}'.\n"
"Expected a function with the signature (*, fixture_name, config)"
) from e
if not isinstance(result, str):
fail(
"Expected {} to return a 'str' while defining fixture '{}', but it returned:\n"
"{!r}".format(scope_callable, fixture_name, result),
f"Expected {scope_callable} to return a 'str' while defining fixture '{fixture_name}', but it returned:\n"
f"{result!r}",
pytrace=False,
)
return result
@@ -971,7 +959,7 @@ class FixtureDef(Generic[FixtureValue]):
def __init__(
self,
fixturemanager: "FixtureManager",
config: Config,
baseid: Optional[str],
argname: str,
func: "_FixtureFunc[FixtureValue]",
@@ -985,13 +973,11 @@ class FixtureDef(Generic[FixtureValue]):
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self._fixturemanager = fixturemanager
# The "base" node ID for the fixture.
#
# This is a node ID prefix. A fixture is only available to a node (e.g.
# a `Function` item) if the fixture's baseid is a parent of the node's
# nodeid (see the `iterparentnodeids` function for what constitutes a
# "parent" and a "prefix" in this context).
# a `Function` item) if the fixture's baseid is a nodeid of a parent of
# node.
#
# For a fixture found in a Collector's object (e.g. a `Module`s module,
# a `Class`'s class), the baseid is the Collector's nodeid.
@@ -1012,7 +998,7 @@ class FixtureDef(Generic[FixtureValue]):
if scope is None:
scope = Scope.Function
elif callable(scope):
scope = _eval_scope_callable(scope, argname, fixturemanager.config)
scope = _eval_scope_callable(scope, argname, config)
if isinstance(scope, str):
scope = Scope.from_user(
scope, descr=f"Fixture '{func.__name__}'", where=baseid
@@ -1042,27 +1028,25 @@ class FixtureDef(Generic[FixtureValue]):
self._finalizers.append(finalizer)
def finish(self, request: SubRequest) -> None:
exc = None
try:
while self._finalizers:
try:
func = self._finalizers.pop()
func()
except BaseException as e:
# XXX Only first exception will be seen by user,
# ideally all should be reported.
if exc is None:
exc = e
if exc:
raise exc
finally:
ihook = request.node.ihook
ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
# Even if finalization fails, we invalidate the cached fixture
# value and remove all finalizers because they may be bound methods
# which will keep instances alive.
self.cached_result = None
self._finalizers.clear()
exceptions: List[BaseException] = []
while self._finalizers:
fin = self._finalizers.pop()
try:
fin()
except BaseException as e:
exceptions.append(e)
node = request.node
node.ihook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
# Even if finalization fails, we invalidate the cached fixture
# value and remove all finalizers because they may be bound methods
# which will keep instances alive.
self.cached_result = None
self._finalizers.clear()
if len(exceptions) == 1:
raise exceptions[0]
elif len(exceptions) > 1:
msg = f'errors while tearing down fixture "{self.argname}" of {node}'
raise BaseExceptionGroup(msg, exceptions[::-1])
def execute(self, request: SubRequest) -> FixtureValue:
# Get required arguments and register our own finish()
@@ -1099,9 +1083,7 @@ class FixtureDef(Generic[FixtureValue]):
return request.param_index if not hasattr(request, "param") else request.param
def __repr__(self) -> str:
return "<FixtureDef argname={!r} scope={!r} baseid={!r}>".format(
self.argname, self.scope, self.baseid
)
return f"<FixtureDef argname={self.argname!r} scope={self.scope!r} baseid={self.baseid!r}>"
def resolve_fixture_function(
@@ -1122,7 +1104,8 @@ def resolve_fixture_function(
# 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]
request.instance,
fixturefunc.__self__.__class__, # type: ignore[union-attr]
):
return fixturefunc
fixturefunc = getimfunc(fixturedef.func)
@@ -1205,7 +1188,7 @@ class FixtureFunctionMarker:
if getattr(function, "_pytestfixturefunction", False):
raise ValueError(
"fixture is being applied more than once to the same function"
f"@pytest.fixture is being applied more than once to the same function {function.__name__!r}"
)
if hasattr(function, "pytestmark"):
@@ -1217,9 +1200,7 @@ class FixtureFunctionMarker:
if name == "request":
location = getlocation(function)
fail(
"'request' is a reserved word for fixtures, use another name:\n {}".format(
location
),
f"'request' is a reserved word for fixtures, use another name:\n {location}",
pytrace=False,
)
@@ -1244,7 +1225,7 @@ def fixture(
@overload
def fixture( # noqa: F811
def fixture(
fixture_function: None = ...,
*,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = ...,
@@ -1258,7 +1239,7 @@ def fixture( # noqa: F811
...
def fixture( # noqa: F811
def fixture(
fixture_function: Optional[FixtureFunction] = None,
*,
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" = "function",
@@ -1441,9 +1422,6 @@ class FixtureManager:
by a lookup of their FuncFixtureInfo.
"""
FixtureLookupError = FixtureLookupError
FixtureLookupErrorRepr = FixtureLookupErrorRepr
def __init__(self, session: "Session") -> None:
self.session = session
self.config: Config = session.config
@@ -1482,7 +1460,7 @@ class FixtureManager:
else:
argnames = ()
usefixturesnames = self._getusefixturesnames(node)
autousenames = self._getautousenames(node.nodeid)
autousenames = self._getautousenames(node)
initialnames = deduplicate_names(autousenames, usefixturesnames, argnames)
direct_parametrize_args = _get_direct_parametrize_args(node)
@@ -1519,10 +1497,10 @@ class FixtureManager:
self.parsefactories(plugin, nodeid)
def _getautousenames(self, nodeid: str) -> Iterator[str]:
"""Return the names of autouse fixtures applicable to nodeid."""
for parentnodeid in nodes.iterparentnodeids(nodeid):
basenames = self._nodeid_autousenames.get(parentnodeid)
def _getautousenames(self, node: nodes.Node) -> Iterator[str]:
"""Return the names of autouse fixtures applicable to node."""
for parentnode in node.listchain():
basenames = self._nodeid_autousenames.get(parentnode.nodeid)
if basenames:
yield from basenames
@@ -1544,7 +1522,6 @@ class FixtureManager:
# to re-discover fixturedefs again for each fixturename
# (discovering matching fixtures for a given name/node is expensive).
parentid = parentnode.nodeid
fixturenames_closure = list(initialnames)
arg2fixturedefs: Dict[str, Sequence[FixtureDef[Any]]] = {}
@@ -1556,7 +1533,7 @@ class FixtureManager:
continue
if argname in arg2fixturedefs:
continue
fixturedefs = self.getfixturedefs(argname, parentid)
fixturedefs = self.getfixturedefs(argname, parentnode)
if fixturedefs:
arg2fixturedefs[argname] = fixturedefs
for arg in fixturedefs[-1].argnames:
@@ -1623,6 +1600,69 @@ class FixtureManager:
# Separate parametrized setups.
items[:] = reorder_items(items)
def _register_fixture(
self,
*,
name: str,
func: "_FixtureFunc[object]",
nodeid: Optional[str],
scope: Union[
Scope, _ScopeName, Callable[[str, Config], _ScopeName], None
] = "function",
params: Optional[Sequence[object]] = None,
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None,
autouse: bool = False,
unittest: bool = False,
) -> None:
"""Register a fixture
:param name:
The fixture's name.
:param func:
The fixture's implementation function.
:param nodeid:
The visibility of the fixture. The fixture will be available to the
node with this nodeid and its children in the collection tree.
None means that the fixture is visible to the entire collection tree,
e.g. a fixture defined for general use in a plugin.
:param scope:
The fixture's scope.
:param params:
The fixture's parametrization params.
:param ids:
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,
baseid=nodeid,
argname=name,
func=func,
scope=scope,
params=params,
unittest=unittest,
ids=ids,
_ispytest=True,
)
faclist = self._arg2fixturedefs.setdefault(name, [])
if fixture_def.has_location:
faclist.append(fixture_def)
else:
# fixturedefs with no location are at the front
# so this inserts the current fixturedef after the
# existing fixturedefs from external plugins but
# before the fixturedefs provided in conftests.
i = len([f for f in faclist if not f.has_location])
faclist.insert(i, fixture_def)
if autouse:
self._nodeid_autousenames.setdefault(nodeid or "", []).append(name)
@overload
def parsefactories(
self,
@@ -1633,7 +1673,7 @@ class FixtureManager:
raise NotImplementedError()
@overload
def parsefactories( # noqa: F811
def parsefactories(
self,
node_or_obj: object,
nodeid: Optional[str],
@@ -1642,7 +1682,7 @@ class FixtureManager:
) -> None:
raise NotImplementedError()
def parsefactories( # noqa: F811
def parsefactories(
self,
node_or_obj: Union[nodes.Node, object],
nodeid: Union[str, NotSetType, None] = NOTSET,
@@ -1674,13 +1714,7 @@ class FixtureManager:
return
self._holderobjseen.add(holderobj)
autousenames = []
for name in dir(holderobj):
# ugly workaround for one of the fspath deprecated property of node
# todo: safely generalize
if isinstance(holderobj, nodes.Node) and name == "fspath":
continue
# The attribute can be an arbitrary descriptor, so the attribute
# access below can raise. safe_getatt() ignores such exceptions.
obj = safe_getattr(holderobj, name, None)
@@ -1697,38 +1731,21 @@ class FixtureManager:
# to issue a warning if called directly, so here we unwrap it in
# order to not emit the warning when pytest itself calls the
# fixture function.
obj = get_real_method(obj, holderobj)
func = get_real_method(obj, holderobj)
fixture_def = FixtureDef(
fixturemanager=self,
baseid=nodeid,
argname=name,
func=obj,
self._register_fixture(
name=name,
nodeid=nodeid,
func=func,
scope=marker.scope,
params=marker.params,
unittest=unittest,
ids=marker.ids,
_ispytest=True,
autouse=marker.autouse,
)
faclist = self._arg2fixturedefs.setdefault(name, [])
if fixture_def.has_location:
faclist.append(fixture_def)
else:
# fixturedefs with no location are at the front
# so this inserts the current fixturedef after the
# existing fixturedefs from external plugins but
# before the fixturedefs provided in conftests.
i = len([f for f in faclist if not f.has_location])
faclist.insert(i, fixture_def)
if marker.autouse:
autousenames.append(name)
if autousenames:
self._nodeid_autousenames.setdefault(nodeid or "", []).extend(autousenames)
def getfixturedefs(
self, argname: str, nodeid: str
self, argname: str, node: nodes.Node
) -> Optional[Sequence[FixtureDef[Any]]]:
"""Get FixtureDefs for a fixture name which are applicable
to a given node.
@@ -1739,18 +1756,18 @@ class FixtureManager:
an empty result is returned).
:param argname: Name of the fixture to search for.
:param nodeid: Full node id of the requesting test.
:param node: The requesting Node.
"""
try:
fixturedefs = self._arg2fixturedefs[argname]
except KeyError:
return None
return tuple(self._matchfactories(fixturedefs, nodeid))
return tuple(self._matchfactories(fixturedefs, node))
def _matchfactories(
self, fixturedefs: Iterable[FixtureDef[Any]], nodeid: str
self, fixturedefs: Iterable[FixtureDef[Any]], node: nodes.Node
) -> Iterator[FixtureDef[Any]]:
parentnodeids = set(nodes.iterparentnodeids(nodeid))
parentnodeids = {n.nodeid for n in node.iter_parents()}
for fixturedef in fixturedefs:
if fixturedef.baseid in parentnodeids:
yield fixturedef

View File

@@ -1,5 +1,6 @@
"""Provides a function to report all internal modules for using freezing
tools."""
import types
from typing import Iterator
from typing import List

View File

@@ -1,18 +1,19 @@
# mypy: allow-untyped-defs
"""Version info, help messages, tracing configuration."""
from argparse import Action
import os
import sys
from argparse import Action
from typing import Generator
from typing import List
from typing import Optional
from typing import Union
import pytest
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PrintHelp
from _pytest.config.argparsing import Parser
from _pytest.terminal import TerminalReporter
import pytest
class HelpAction(Action):
@@ -108,11 +109,11 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
path = config.option.debug
debugfile = open(path, "w", encoding="utf-8")
debugfile.write(
"versions pytest-%s, "
"python-%s\ncwd=%s\nargs=%s\n\n"
% (
"versions pytest-{}, "
"python-{}\ninvocation_dir={}\ncwd={}\nargs={}\n\n".format(
pytest.__version__,
".".join(map(str, sys.version_info)),
config.invocation_params.dir,
os.getcwd(),
config.invocation_params.args,
)
@@ -135,9 +136,7 @@ def pytest_cmdline_parse() -> Generator[None, Config, Config]:
def showversion(config: Config) -> None:
if config.option.version > 1:
sys.stdout.write(
"This is pytest version {}, imported from {}\n".format(
pytest.__version__, pytest.__file__
)
f"This is pytest version {pytest.__version__}, imported from {pytest.__file__}\n"
)
plugininfo = getpluginversioninfo(config)
if plugininfo:

View File

@@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Hook specifications for pytest plugins which are invoked by pytest itself
and by builtin plugins."""
from pathlib import Path
@@ -13,19 +14,18 @@ from typing import Union
from pluggy import HookspecMarker
from _pytest.deprecated import WARNING_CMDLINE_PREPARSE_HOOK
if TYPE_CHECKING:
import pdb
import warnings
from typing import Literal
import warnings
from _pytest._code.code import ExceptionRepr
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ExceptionRepr
from _pytest.config import _PluggyPlugin
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PytestPluginManager
from _pytest.config import _PluggyPlugin
from _pytest.config.argparsing import Parser
from _pytest.fixtures import FixtureDef
from _pytest.fixtures import SubRequest
@@ -42,7 +42,6 @@ if TYPE_CHECKING:
from _pytest.runner import CallInfo
from _pytest.terminal import TerminalReporter
from _pytest.terminal import TestShortLogReport
from _pytest.compat import LEGACY_PATH
hookspec = HookspecMarker("pytest")
@@ -57,10 +56,16 @@ def pytest_addhooks(pluginmanager: "PytestPluginManager") -> None:
"""Called at plugin registration time to allow adding new hooks via a call to
:func:`pluginmanager.add_hookspecs(module_or_class, prefix) <pytest.PytestPluginManager.add_hookspecs>`.
:param pytest.PytestPluginManager pluginmanager: The pytest plugin manager.
:param pluginmanager: The pytest plugin manager.
.. note::
This hook is incompatible with hook wrappers.
Use in conftest plugins
=======================
If a conftest plugin implements this hook, it will be called immediately
when the conftest is registered.
"""
@@ -78,6 +83,14 @@ def pytest_plugin_registered(
.. note::
This hook is incompatible with hook wrappers.
Use in conftest plugins
=======================
If a conftest plugin implements this hook, it will be called immediately
when the conftest is registered, once for each plugin registered thus far
(including itself!), and for all plugins thereafter when they are
registered.
"""
@@ -86,19 +99,13 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
"""Register argparse-style options and ini-style config values,
called once at the beginning of a test run.
.. note::
This function should be implemented only in plugins or ``conftest.py``
files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`.
:param pytest.Parser parser:
:param parser:
To add command line options, call
:py:func:`parser.addoption(...) <pytest.Parser.addoption>`.
To add ini-file values call :py:func:`parser.addini(...)
<pytest.Parser.addini>`.
:param pytest.PytestPluginManager pluginmanager:
:param pluginmanager:
The pytest plugin manager, which can be used to install :py:func:`~pytest.hookspec`'s
or :py:func:`~pytest.hookimpl`'s and allow one plugin to call another plugin's hooks
to change how command line options are added.
@@ -117,6 +124,14 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
.. note::
This hook is incompatible with hook wrappers.
Use in conftest plugins
=======================
If a conftest plugin implements this hook, it will be called immediately
when the conftest is registered.
This hook is only called for :ref:`initial conftests <pluginorder>`.
"""
@@ -124,16 +139,17 @@ def pytest_addoption(parser: "Parser", pluginmanager: "PytestPluginManager") ->
def pytest_configure(config: "Config") -> None:
"""Allow plugins and conftest files to perform initial configuration.
This hook is called for every plugin and initial conftest file
after command line options have been parsed.
After that, the hook is called for other conftest files as they are
imported.
.. note::
This hook is incompatible with hook wrappers.
:param pytest.Config config: The pytest config object.
:param config: The pytest config object.
Use in conftest plugins
=======================
This hook is called for every :ref:`initial conftest <pluginorder>` file
after command line options have been parsed. After that, the hook is called
for other conftest files as they are registered.
"""
@@ -152,55 +168,54 @@ def pytest_cmdline_parse(
Stops at first non-None result, see :ref:`firstresult`.
.. note::
This hook will only be called for plugin classes passed to the
This hook is only called for plugin classes passed to the
``plugins`` arg when using `pytest.main`_ to perform an in-process
test run.
:param pluginmanager: The pytest plugin manager.
:param args: List of arguments passed on the command line.
:returns: A pytest config object.
"""
Use in conftest plugins
=======================
@hookspec(warn_on_impl=WARNING_CMDLINE_PREPARSE_HOOK)
def pytest_cmdline_preparse(config: "Config", args: List[str]) -> None:
"""(**Deprecated**) modify command line arguments before option parsing.
This hook is considered deprecated and will be removed in a future pytest version. Consider
using :hook:`pytest_load_initial_conftests` instead.
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
:param config: The pytest config object.
:param args: Arguments passed on the command line.
"""
@hookspec(firstresult=True)
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
"""Called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop.
Stops at first non-None result, see :ref:`firstresult`.
:param config: The pytest config object.
:returns: The exit code.
This hook is not called for conftest files.
"""
def pytest_load_initial_conftests(
early_config: "Config", parser: "Parser", args: List[str]
) -> None:
"""Called to implement the loading of initial conftest files ahead
of command line option parsing.
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
"""Called to implement the loading of :ref:`initial conftest files
<pluginorder>` ahead of command line option parsing.
:param early_config: The pytest config object.
:param args: Arguments passed on the command line.
:param parser: To add command line options.
Use in conftest plugins
=======================
This hook is not called for conftest files.
"""
@hookspec(firstresult=True)
def pytest_cmdline_main(config: "Config") -> Optional[Union["ExitCode", int]]:
"""Called for performing the main command line action.
The default implementation will invoke the configure hooks and
:hook:`pytest_runtestloop`.
Stops at first non-None result, see :ref:`firstresult`.
:param config: The pytest config object.
:returns: The exit code.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
"""
@@ -243,6 +258,11 @@ def pytest_collection(session: "Session") -> Optional[object]:
counter (and returns `None`).
:param session: The pytest session object.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
"""
@@ -255,6 +275,11 @@ def pytest_collection_modifyitems(
:param session: The pytest session object.
:param config: The pytest config object.
:param items: List of item objects.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -262,13 +287,16 @@ def pytest_collection_finish(session: "Session") -> None:
"""Called after collection has been performed and modified.
:param session: The pytest session object.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@hookspec(firstresult=True)
def pytest_ignore_collect(
collection_path: Path, path: "LEGACY_PATH", config: "Config"
) -> Optional[bool]:
def pytest_ignore_collect(collection_path: Path, config: "Config") -> Optional[bool]:
"""Return True to prevent considering this path for collection.
This hook is consulted for all files and directories prior to calling
@@ -282,8 +310,18 @@ def pytest_ignore_collect(
.. versionchanged:: 7.0.0
The ``collection_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
equivalent of the ``path`` parameter.
.. versionchanged:: 8.0.0
The ``path`` parameter has been removed.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collection path, only
conftest files in parent directories of the collection path are consulted
(if the path is a directory, its own conftest file is *not* consulted - a
directory cannot ignore itself!).
"""
@@ -305,12 +343,18 @@ def pytest_collect_directory(path: Path, parent: "Collector") -> "Optional[Colle
See :ref:`custom directory collectors` for a simple example of use of this
hook.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collection path, only
conftest files in parent directories of the collection path are consulted
(if the path is a directory, its own conftest file is *not* consulted - a
directory cannot collect itself!).
"""
def pytest_collect_file(
file_path: Path, path: "LEGACY_PATH", parent: "Collector"
) -> "Optional[Collector]":
def pytest_collect_file(file_path: 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
@@ -323,8 +367,16 @@ def pytest_collect_file(
.. versionchanged:: 7.0.0
The ``file_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter. The ``path`` parameter
has been deprecated.
equivalent of the ``path`` parameter.
.. versionchanged:: 8.0.0
The ``path`` parameter was removed.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given file path, only
conftest files in parent directories of the file path are consulted.
"""
@@ -336,6 +388,13 @@ def pytest_collectstart(collector: "Collector") -> None:
:param collector:
The collector.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories are
consulted.
"""
@@ -344,6 +403,12 @@ def pytest_itemcollected(item: "Item") -> None:
:param item:
The item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -352,6 +417,13 @@ def pytest_collectreport(report: "CollectReport") -> None:
:param report:
The collect report.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories are
consulted.
"""
@@ -362,6 +434,11 @@ def pytest_deselected(items: Sequence["Item"]) -> None:
:param items:
The items.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -374,6 +451,13 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
:param collector:
The collector.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories are
consulted.
"""
@@ -383,9 +467,7 @@ def pytest_make_collect_report(collector: "Collector") -> "Optional[CollectRepor
@hookspec(firstresult=True)
def pytest_pycollect_makemodule(
module_path: Path, path: "LEGACY_PATH", parent
) -> Optional["Module"]:
def pytest_pycollect_makemodule(module_path: 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.
@@ -401,7 +483,15 @@ def pytest_pycollect_makemodule(
The ``module_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``path`` parameter.
The ``path`` parameter has been deprecated in favor of ``fspath``.
.. versionchanged:: 8.0.0
The ``path`` parameter has been removed in favor of ``module_path``.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given parent collector,
only conftest files in the collector's directory and its parent directories
are consulted.
"""
@@ -421,6 +511,13 @@ def pytest_pycollect_makeitem(
The object.
:returns:
The created items/collectors.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given collector, only
conftest files in the collector's directory and its parent directories
are consulted.
"""
@@ -432,6 +529,13 @@ def pytest_pyfunc_call(pyfuncitem: "Function") -> Optional[object]:
:param pyfuncitem:
The function item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only
conftest files in the item's directory and its parent directories
are consulted.
"""
@@ -440,6 +544,13 @@ def pytest_generate_tests(metafunc: "Metafunc") -> None:
:param metafunc:
The :class:`~pytest.Metafunc` helper for the test function.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given function definition,
only conftest files in the functions's directory and its parent directories
are consulted.
"""
@@ -457,7 +568,12 @@ def pytest_make_parametrize_id(
:param config: The pytest config object.
:param val: The parametrized value.
:param str argname: The automatic parameter name produced by pytest.
:param argname: The automatic parameter name produced by pytest.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -484,6 +600,11 @@ def pytest_runtestloop(session: "Session") -> Optional[object]:
Stops at first non-None result, see :ref:`firstresult`.
The return value is not used, but only stops further processing.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -522,6 +643,11 @@ def pytest_runtest_protocol(
Stops at first non-None result, see :ref:`firstresult`.
The return value is not used, but only stops further processing.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -536,6 +662,12 @@ def pytest_runtest_logstart(
:param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -550,6 +682,12 @@ def pytest_runtest_logfinish(
:param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -563,6 +701,12 @@ def pytest_runtest_setup(item: "Item") -> None:
:param item:
The item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -573,6 +717,12 @@ def pytest_runtest_call(item: "Item") -> None:
:param item:
The item.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -591,6 +741,12 @@ def pytest_runtest_teardown(item: "Item", nextitem: Optional["Item"]) -> None:
scheduled). This argument is used to perform exact teardowns, i.e.
calling just enough finalizers so that nextitem only needs to call
setup functions.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -607,6 +763,12 @@ def pytest_runtest_makereport(
:param call: The :class:`~pytest.CallInfo` for the phase.
Stops at first non-None result, see :ref:`firstresult`.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -615,6 +777,12 @@ def pytest_runtest_logreport(report: "TestReport") -> None:
of the setup, call and teardown runtest phases of an item.
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -628,6 +796,12 @@ def pytest_report_to_serializable(
:param config: The pytest config object.
:param report: The report.
Use in conftest plugins
=======================
Any conftest file can implement this hook. The exact details may depend
on the plugin which calls the hook.
"""
@@ -640,6 +814,12 @@ def pytest_report_from_serializable(
:hook:`pytest_report_to_serializable`.
:param config: The pytest config object.
Use in conftest plugins
=======================
Any conftest file can implement this hook. The exact details may depend
on the plugin which calls the hook.
"""
@@ -667,6 +847,13 @@ def pytest_fixture_setup(
If the fixture function returns None, other implementations of
this hook function will continue to be called, according to the
behavior of the :ref:`firstresult` option.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given fixture, only
conftest files in the fixture scope's directory and its parent directories
are consulted.
"""
@@ -681,6 +868,13 @@ def pytest_fixture_post_finalizer(
The fixture definition object.
:param request:
The fixture request object.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given fixture, only
conftest files in the fixture scope's directory and its parent directories
are consulted.
"""
@@ -694,6 +888,11 @@ def pytest_sessionstart(session: "Session") -> None:
and entering the run test loop.
:param session: The pytest session object.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
"""
@@ -705,6 +904,11 @@ def pytest_sessionfinish(
:param session: The pytest session object.
:param exitstatus: The status which pytest will return to the system.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -712,6 +916,11 @@ def pytest_unconfigure(config: "Config") -> None:
"""Called before test process is exited.
:param config: The pytest config object.
Use in conftest plugins
=======================
Any conftest file can implement this hook.
"""
@@ -734,6 +943,12 @@ def pytest_assertrepr_compare(
:param op: The operator, e.g. `"=="`, `"!="`, `"not in"`.
:param left: The left operand.
:param right: The right operand.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -762,6 +977,12 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
:param lineno: Line number of the assert statement.
:param orig: String with the original assertion.
:param expl: String with the assert explanation.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in the item's directory and its parent directories are consulted.
"""
@@ -771,7 +992,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
def pytest_report_header( # type:ignore[empty-body]
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
config: "Config", start_path: Path
) -> Union[str, List[str]]:
"""Return a string or list of strings to be displayed as header info for terminal reporting.
@@ -786,23 +1007,23 @@ def pytest_report_header( # type:ignore[empty-body]
If you want to have your line(s) displayed first, use
:ref:`trylast=True <plugin-hookorder>`.
.. note::
This function should be implemented only in plugins or ``conftest.py``
files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`.
.. versionchanged:: 7.0.0
The ``start_path`` parameter was added as a :class:`pathlib.Path`
equivalent of the ``startdir`` parameter. The ``startdir`` parameter
has been deprecated.
equivalent of the ``startdir`` parameter.
.. versionchanged:: 8.0.0
The ``startdir`` parameter has been removed.
Use in conftest plugins
=======================
This hook is only called for :ref:`initial conftests <pluginorder>`.
"""
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
@@ -826,8 +1047,15 @@ 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. The ``startdir`` parameter
has been deprecated.
equivalent of the ``startdir`` parameter.
.. versionchanged:: 8.0.0
The ``startdir`` parameter has been removed.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -856,6 +1084,11 @@ def pytest_report_teststatus( # type:ignore[empty-body]
:returns: The test status.
Stops at first non-None result, see :ref:`firstresult`.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -872,6 +1105,11 @@ def pytest_terminal_summary(
.. versionadded:: 4.2
The ``config`` parameter.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -896,7 +1134,8 @@ def pytest_warning_recorded(
* ``"runtest"``: during test execution.
:param nodeid:
Full id of the item.
Full id of the item. Empty string for warnings that are not specific to
a particular node.
:param location:
When available, holds information about the execution context of the captured
@@ -904,6 +1143,13 @@ def pytest_warning_recorded(
when the execution context is at the module level.
.. versionadded:: 6.0
Use in conftest plugins
=======================
Any conftest file can implement this hook. If the warning is specific to a
particular node, only conftest files in parent directories of the node are
consulted.
"""
@@ -927,6 +1173,12 @@ def pytest_markeval_namespace( # type:ignore[empty-body]
:param config: The pytest config object.
:returns: A dictionary of additional globals to add.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given item, only conftest
files in parent directories of the item are consulted.
"""
@@ -946,6 +1198,11 @@ def pytest_internalerror(
:param excrepr: The exception repr object.
:param excinfo: The exception info.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -955,6 +1212,11 @@ def pytest_keyboard_interrupt(
"""Called for keyboard interrupt.
:param excinfo: The exception info.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -981,6 +1243,12 @@ def pytest_exception_interact(
The call information. Contains the exception.
:param report:
The collection or test report.
Use in conftest plugins
=======================
Any conftest file can implement this hook. For a given node, only conftest
files in parent directories of the node are consulted.
"""
@@ -992,6 +1260,11 @@ def pytest_enter_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
:param config: The pytest config object.
:param pdb: The Pdb instance.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""
@@ -1003,4 +1276,9 @@ def pytest_leave_pdb(config: "Config", pdb: "pdb.Pdb") -> None:
:param config: The pytest config object.
:param pdb: The Pdb instance.
Use in conftest plugins
=======================
Any conftest plugin can implement this hook.
"""

View File

@@ -1,3 +1,4 @@
# mypy: allow-untyped-defs
"""Report test results in JUnit-XML format, for use with Jenkins and build
integration servers.
@@ -6,12 +7,11 @@ 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
import platform
import re
import xml.etree.ElementTree as ET
from datetime import datetime
from typing import Callable
from typing import Dict
from typing import List
@@ -19,8 +19,8 @@ from typing import Match
from typing import Optional
from typing import Tuple
from typing import Union
import xml.etree.ElementTree as ET
import pytest
from _pytest import nodes
from _pytest import timing
from _pytest._code.code import ExceptionRepr
@@ -32,6 +32,7 @@ from _pytest.fixtures import FixtureRequest
from _pytest.reports import TestReport
from _pytest.stash import StashKey
from _pytest.terminal import TerminalReporter
import pytest
xml_key = StashKey["LogXML"]()
@@ -273,9 +274,7 @@ def _warn_incompatibility_with_xunit2(
if xml is not None and xml.family not in ("xunit1", "legacy"):
request.node.warn(
PytestWarning(
"{fixture_name} is incompatible with junit_family '{family}' (use 'legacy' or 'xunit1')".format(
fixture_name=fixture_name, family=xml.family
)
f"{fixture_name} is incompatible with junit_family '{xml.family}' (use 'legacy' or 'xunit1')"
)
)
@@ -367,7 +366,6 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
:issue:`7767` for details.
"""
__tracebackhide__ = True
def record_func(name: str, value: object) -> None:
@@ -377,7 +375,7 @@ def record_testsuite_property(request: FixtureRequest) -> Callable[[str, object]
xml = request.config.stash.get(xml_key, None)
if xml is not None:
record_func = xml.add_global_property # noqa
record_func = xml.add_global_property
return record_func
@@ -626,7 +624,7 @@ class LogXML:
def update_testcase_duration(self, report: TestReport) -> None:
"""Accumulate total duration for nodeid from given report and update
the Junit.testcase with the new total if already created."""
if self.report_duration == "total" or report.when == self.report_duration:
if self.report_duration in {"total", report.when}:
reporter = self.node_reporter(report)
reporter.duration += getattr(report, "duration", 0.0)

View File

@@ -1,8 +1,10 @@
# 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
from pathlib import Path
from typing import Final
from typing import final
from typing import List
@@ -12,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
@@ -32,10 +34,25 @@ from _pytest.pytester import RunResult
from _pytest.terminal import TerminalReporter
from _pytest.tmpdir import TempPathFactory
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:
"""
@@ -312,8 +329,8 @@ class LegacyTmpdirPlugin:
By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
``--basetemp`` is used then it is cleared each session. See
:ref:`temporary directory location and retention`.
The returned object is a `legacy_path`_ object.

View File

@@ -1,16 +1,17 @@
# mypy: allow-untyped-defs
"""Access and control log capturing."""
import io
import logging
import os
import re
from contextlib import contextmanager
from contextlib import nullcontext
from datetime import datetime
from datetime import timedelta
from datetime import timezone
import io
from io import StringIO
import logging
from logging import LogRecord
import os
from pathlib import Path
import re
from types import TracebackType
from typing import AbstractSet
from typing import Dict
@@ -43,6 +44,7 @@ from _pytest.main import Session
from _pytest.stash import StashKey
from _pytest.terminal import TerminalReporter
if TYPE_CHECKING:
logging_StreamHandler = logging.StreamHandler[StringIO]
else:
@@ -115,7 +117,6 @@ class ColoredLevelFormatter(DatetimeFormatter):
.. warning::
This is an experimental API.
"""
assert self._fmt is not None
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
if not levelname_fmt_match:
@@ -182,7 +183,6 @@ class PercentStyleMultiline(logging.PercentStyle):
0 (auto-indent turned off) or
>0 (explicitly set indentation position).
"""
if auto_indent_option is None:
return 0
elif isinstance(auto_indent_option, bool):
@@ -298,6 +298,13 @@ def pytest_addoption(parser: Parser) -> None:
default=None,
help="Path to a file when logging will be written to",
)
add_option_ini(
"--log-file-mode",
dest="log_file_mode",
default="w",
choices=["w", "a"],
help="Log file open mode",
)
add_option_ini(
"--log-file-level",
dest="log_file_level",
@@ -624,9 +631,9 @@ def get_log_level_for_setting(config: Config, *setting_names: str) -> Optional[i
except ValueError as e:
# Python logging does not recognise this as a logging level
raise UsageError(
"'{}' is not recognized as a logging level name for "
"'{}'. Please consider passing the "
"logging level num instead.".format(log_level, setting_name)
f"'{log_level}' is not recognized as a logging level name for "
f"'{setting_name}'. Please consider passing the "
"logging level num instead."
) from e
@@ -669,7 +676,10 @@ class LoggingPlugin:
if not os.path.isdir(directory):
os.makedirs(directory)
self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
self.log_file_mode = get_option_ini(config, "log_file_mode") or "w"
self.log_file_handler = _FileHandler(
log_file, mode=self.log_file_mode, encoding="UTF-8"
)
log_file_format = get_option_ini(config, "log_file_format", "log_format")
log_file_date_format = get_option_ini(
config, "log_file_date_format", "log_date_format"
@@ -746,7 +756,7 @@ class LoggingPlugin:
fpath.parent.mkdir(exist_ok=True, parents=True)
# https://github.com/python/mypy/issues/11193
stream: io.TextIOWrapper = fpath.open(mode="w", encoding="UTF-8") # type: ignore[assignment]
stream: io.TextIOWrapper = fpath.open(mode=self.log_file_mode, encoding="UTF-8") # type: ignore[assignment]
old_stream = self.log_file_handler.setStream(stream)
if old_stream:
old_stream.close()

View File

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

View File

@@ -1,4 +1,5 @@
"""Generic mechanism for marking and selecting python functions."""
import dataclasses
from typing import AbstractSet
from typing import Collection
@@ -23,6 +24,7 @@ from _pytest.config import UsageError
from _pytest.config.argparsing import Parser
from _pytest.stash import StashKey
if TYPE_CHECKING:
from _pytest.nodes import Item
@@ -105,7 +107,7 @@ def pytest_addoption(parser: Parser) -> None:
help="show markers (builtin, plugin and per-project ones).",
)
parser.addini("markers", "Markers for test functions", "linelist")
parser.addini("markers", "Register new markers for test functions", "linelist")
parser.addini(EMPTY_PARAMETERSET_OPTION, "Default marker for empty parametersets")
@@ -267,8 +269,8 @@ def pytest_configure(config: Config) -> None:
if empty_parameterset not in ("skip", "xfail", "fail_at_collect", None, ""):
raise UsageError(
"{!s} must be one of skip, xfail or fail_at_collect"
" but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset)
f"{EMPTY_PARAMETERSET_OPTION!s} must be one of skip, xfail or fail_at_collect"
f" but it is {empty_parameterset!r}"
)

View File

@@ -14,6 +14,7 @@ The semantics are:
- ident evaluates to True of False according to a provided matcher function.
- or/and/not evaluate according to the usual boolean semantics.
"""
import ast
import dataclasses
import enum
@@ -26,6 +27,7 @@ from typing import NoReturn
from typing import Optional
from typing import Sequence
__all__ = [
"Expression",
"ParseError",

View File

@@ -1,7 +1,7 @@
# mypy: allow-untyped-defs
import collections.abc
import dataclasses
import inspect
import warnings
from typing import Any
from typing import Callable
from typing import Collection
@@ -21,6 +21,7 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import warnings
from .._code import getfslineno
from ..compat import ascii_escaped
@@ -32,6 +33,7 @@ from _pytest.deprecated import MARKED_FIXTURE
from _pytest.outcomes import fail
from _pytest.warning_types import PytestUnknownMarkWarning
if TYPE_CHECKING:
from ..nodes import Node
@@ -111,7 +113,6 @@ class ParameterSet(NamedTuple):
Enforce tuple wrapping so single argument tuple values
don't get decomposed and break tests.
"""
if isinstance(parameterset, cls):
return parameterset
if force_tuple:
@@ -271,8 +272,8 @@ class MarkDecorator:
``MarkDecorators`` are created with ``pytest.mark``::
mark1 = pytest.mark.NAME # Simple MarkDecorator
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
mark1 = pytest.mark.NAME # Simple MarkDecorator
mark2 = pytest.mark.NAME(name1=value) # Parametrized MarkDecorator
and can then be applied as decorators to test functions::
@@ -354,7 +355,7 @@ class MarkDecorator:
func = args[0]
is_class = inspect.isclass(func)
if len(args) == 1 and (istestfunc(func) or is_class):
store_mark(func, self.mark)
store_mark(func, self.mark, stacklevel=3)
return func
return self.with_args(*args, **kwargs)
@@ -393,7 +394,7 @@ def get_unpacked_marks(
def normalize_mark_list(
mark_list: Iterable[Union[Mark, MarkDecorator]]
mark_list: Iterable[Union[Mark, MarkDecorator]],
) -> Iterable[Mark]:
"""
Normalize an iterable of Mark or MarkDecorator objects into a list of marks
@@ -405,11 +406,11 @@ def normalize_mark_list(
for mark in mark_list:
mark_obj = getattr(mark, "mark", mark)
if not isinstance(mark_obj, Mark):
raise TypeError(f"got {repr(mark_obj)} instead of Mark")
raise TypeError(f"got {mark_obj!r} instead of Mark")
yield mark_obj
def store_mark(obj, mark: Mark) -> None:
def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None:
"""Store a Mark on an object.
This is used to implement the Mark declarations/decorators correctly.
@@ -419,7 +420,7 @@ def store_mark(obj, mark: Mark) -> None:
from ..fixtures import getfixturemarker
if getfixturemarker(obj) is not None:
warnings.warn(MARKED_FIXTURE, stacklevel=2)
warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel)
# Always reassign name to avoid updating pytestmark in a reference that
# was only borrowed.
@@ -503,9 +504,10 @@ class MarkGenerator:
import pytest
@pytest.mark.slowtest
def test_function():
pass
pass
applies a 'slowtest' :class:`Mark` on ``test_function``.
"""

View File

@@ -1,9 +1,9 @@
# mypy: allow-untyped-defs
"""Monkeypatching and mocking functionality."""
from contextlib import contextmanager
import os
import re
import sys
import warnings
from contextlib import contextmanager
from typing import Any
from typing import final
from typing import Generator
@@ -15,10 +15,12 @@ from typing import overload
from typing import Tuple
from typing import TypeVar
from typing import Union
import warnings
from _pytest.fixtures import fixture
from _pytest.warning_types import PytestWarning
RE_IMPORT_ERROR_NAME = re.compile(r"^No module named (.*)$")
@@ -89,9 +91,7 @@ def annotated_getattr(obj: object, name: str, ann: str) -> object:
obj = getattr(obj, name)
except AttributeError as e:
raise AttributeError(
"{!r} object at {} has no attribute {!r}".format(
type(obj).__name__, ann, name
)
f"{type(obj).__name__!r} object at {ann} has no attribute {name!r}"
) from e
return obj
@@ -141,7 +141,6 @@ class MonkeyPatch:
which undoes any patching done inside the ``with`` block upon exit.
Example:
.. code-block:: python
import functools
@@ -321,10 +320,8 @@ class MonkeyPatch:
if not isinstance(value, str):
warnings.warn( # type: ignore[unreachable]
PytestWarning(
"Value of environment variable {name} type should be str, but got "
"{value!r} (type: {type}); converted to str implicitly".format(
name=name, value=value, type=type(value).__name__
)
f"Value of environment variable {name} type should be str, but got "
f"{value!r} (type: {type(value).__name__}); converted to str implicitly"
),
stacklevel=2,
)
@@ -344,7 +341,6 @@ class MonkeyPatch:
def syspath_prepend(self, path) -> None:
"""Prepend ``path`` to ``sys.path`` list of import locations."""
if self._savesyspath is None:
self._savesyspath = sys.path[:]
sys.path.insert(0, str(path))

View File

@@ -1,9 +1,8 @@
# mypy: allow-untyped-defs
import abc
import os
import pathlib
import warnings
from functools import cached_property
from inspect import signature
import os
from pathlib import Path
from typing import Any
from typing import Callable
@@ -12,6 +11,7 @@ from typing import Iterable
from typing import Iterator
from typing import List
from typing import MutableMapping
from typing import NoReturn
from typing import Optional
from typing import overload
from typing import Set
@@ -20,6 +20,7 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import warnings
import pluggy
@@ -28,12 +29,8 @@ 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 FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
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
@@ -43,10 +40,13 @@ from _pytest.pathlib import commonpath
from _pytest.stash import Stash
from _pytest.warning_types import PytestWarning
if TYPE_CHECKING:
from typing import Self
# Imported here due to circular import.
from _pytest.main import Session
from _pytest._code.code import _TracebackStyle
from _pytest.main import Session
SEP = "/"
@@ -54,72 +54,7 @@ SEP = "/"
tracebackcutdir = Path(_pytest.__file__).parent
def iterparentnodeids(nodeid: str) -> Iterator[str]:
"""Return the parent node IDs of a given node ID, inclusive.
For the node ID
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
the result would be
""
"testing"
"testing/code"
"testing/code/test_excinfo.py"
"testing/code/test_excinfo.py::TestFormattedExcinfo"
"testing/code/test_excinfo.py::TestFormattedExcinfo::test_repr_source"
Note that / components are only considered until the first ::.
"""
pos = 0
first_colons: Optional[int] = nodeid.find("::")
if first_colons == -1:
first_colons = None
# The root Session node - always present.
yield ""
# Eagerly consume SEP parts until first colons.
while True:
at = nodeid.find(SEP, pos, first_colons)
if at == -1:
break
if at > 0:
yield nodeid[:at]
pos = at + len(SEP)
# Eagerly consume :: parts.
while True:
at = nodeid.find("::", pos)
if at == -1:
break
if at > 0:
yield nodeid[:at]
pos = at + len("::")
# The node ID itself.
if nodeid:
yield nodeid
def _imply_path(
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)
_T = TypeVar("_T")
_NodeType = TypeVar("_NodeType", bound="Node")
@@ -138,33 +73,33 @@ class NodeMeta(abc.ABCMeta):
progress on detangling the :class:`Node` classes.
"""
def __call__(self, *k, **kw):
def __call__(cls, *k, **kw) -> NoReturn:
msg = (
"Direct construction of {name} has been deprecated, please use {name}.from_parent.\n"
"See "
"https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent"
" for more details."
).format(name=f"{self.__module__}.{self.__name__}")
).format(name=f"{cls.__module__}.{cls.__name__}")
fail(msg, pytrace=False)
def _create(self, *k, **kw):
def _create(cls: Type[_T], *k, **kw) -> _T:
try:
return super().__call__(*k, **kw)
return super().__call__(*k, **kw) # type: ignore[no-any-return,misc]
except TypeError:
sig = signature(getattr(self, "__init__"))
sig = signature(getattr(cls, "__init__"))
known_kw = {k: v for k, v in kw.items() if k in sig.parameters}
from .warning_types import PytestDeprecationWarning
warnings.warn(
PytestDeprecationWarning(
f"{self} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
f"{cls} is not using a cooperative constructor and only takes {set(known_kw)}.\n"
"See https://docs.pytest.org/en/stable/deprecations.html"
"#constructors-of-custom-pytest-node-subclasses-should-take-kwargs "
"for more details."
)
)
return super().__call__(*k, **known_kw)
return super().__call__(*k, **known_kw) # type: ignore[no-any-return,misc]
class Node(abc.ABC, metaclass=NodeMeta):
@@ -175,13 +110,6 @@ 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__ = (
@@ -201,7 +129,6 @@ 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:
@@ -227,10 +154,11 @@ class Node(abc.ABC, metaclass=NodeMeta):
raise TypeError("session or parent must be provided")
self.session = parent.session
if path is None and fspath is None:
if path 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: pathlib.Path = _imply_path(type(self), path, fspath=fspath)
self.path = path
# The explicit annotation is to avoid publicly exposing NodeKeywords.
#: Keywords/markers collected from all scopes.
@@ -257,7 +185,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
self._store = self.stash
@classmethod
def from_parent(cls, parent: "Node", **kw):
def from_parent(cls, parent: "Node", **kw) -> "Self":
"""Public constructor for Nodes.
This indirection got introduced in order to enable removing
@@ -306,9 +234,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
# enforce type checks here to avoid getting a generic type error later otherwise.
if not isinstance(warning, Warning):
raise ValueError(
"warning must be an instance of Warning or subclass, got {!r}".format(
warning
)
f"warning must be an instance of Warning or subclass, got {warning!r}"
)
path, lineno = get_fslocation_from_item(self)
assert lineno is not None
@@ -335,12 +261,20 @@ class Node(abc.ABC, metaclass=NodeMeta):
def teardown(self) -> None:
pass
def listchain(self) -> List["Node"]:
"""Return list of all parent collectors up to self, starting from
the root of collection tree.
def iter_parents(self) -> Iterator["Node"]:
"""Iterate over all parent collectors starting from and including self
up to the root of the collection tree.
:returns: The nodes.
.. versionadded:: 8.1
"""
parent: Optional[Node] = self
while parent is not None:
yield parent
parent = parent.parent
def listchain(self) -> List["Node"]:
"""Return a list of all parent collectors starting from the root of the
collection tree down to and including self."""
chain = []
item: Optional[Node] = self
while item is not None:
@@ -389,7 +323,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
:param name: If given, filter the results by the name attribute.
:returns: An iterator of (node, mark) tuples.
"""
for node in reversed(self.listchain()):
for node in self.iter_parents():
for mark in node.own_markers:
if name is None or getattr(mark, "name", None) == name:
yield node, mark
@@ -433,17 +367,16 @@ class Node(abc.ABC, metaclass=NodeMeta):
self.session._setupstate.addfinalizer(fin, self)
def getparent(self, cls: Type[_NodeType]) -> Optional[_NodeType]:
"""Get the next parent node (including self) which is an instance of
"""Get the closest parent node (including self) which is an instance of
the given class.
:param cls: The node class to search for.
:returns: The node, if found.
"""
current: Optional[Node] = self
while current and not isinstance(current, cls):
current = current.parent
assert current is None or isinstance(current, cls)
return current
for node in self.iter_parents():
if isinstance(node, cls):
return node
return None
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
return excinfo.traceback
@@ -456,7 +389,7 @@ class Node(abc.ABC, metaclass=NodeMeta):
from _pytest.fixtures import FixtureLookupError
if isinstance(excinfo.value, ConftestImportFailure):
excinfo = ExceptionInfo.from_exc_info(excinfo.value.excinfo)
excinfo = ExceptionInfo.from_exception(excinfo.value.cause)
if isinstance(excinfo.value, fail.Exception):
if not excinfo.value.pytrace:
style = "value"
@@ -522,7 +455,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
* "location": a pair (path, lineno)
* "obj": a Python object that the node wraps.
* "fspath": just a path
* "path": just a path
:rtype: A tuple of (str|Path, int) with filename and 0-based line number.
"""
@@ -533,7 +466,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
obj = getattr(node, "obj", None)
if obj is not None:
return getfslineno(obj)
return getattr(node, "fspath", "unknown location"), -1
return getattr(node, "path", "unknown location"), -1
class Collector(Node, abc.ABC):
@@ -596,7 +529,6 @@ 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,
@@ -612,8 +544,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:
@@ -653,20 +585,11 @@ 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, fspath=fspath, path=path, **kw)
def gethookproxy(self, fspath: "os.PathLike[str]"):
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.gethookproxy(fspath)
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.isinitpath(path)
return super().from_parent(parent=parent, path=path, **kw)
class File(FSCollector, abc.ABC):

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