Compare commits

..

297 Commits

Author SHA1 Message Date
pytest bot bf3bcb2bf1 [automated] Update plugin list 2023-06-11 00:27:03 +00:00
Ran Benita ecb23106d8 Merge pull request #11097 from bluetech/cherry-pick-release
Cherry-pick 7.3.2 release notes
2023-06-10 22:57:31 +03:00
Ran Benita 8174a30164 changelog: add note to norecursedir/pytest_ignore_collect change (#11095) 2023-06-10 19:34:59 +00:00
Ran Benita 0142bb6687 Merge pull request #11096 from pytest-dev/release-7.3.2
Prepare release 7.3.2

(cherry picked from commit 5dcd2be466)
2023-06-10 22:31:49 +03:00
Ran Benita 52cf700f1b Merge pull request #10894 from pytest-dev/py312-ci
Python 3.12 support
2023-06-10 20:46:02 +03:00
Ran Benita 6b3ece5bdb ci; use check-latest: true for -dev pythons
When testing -dev python versions, we want to always use the latest.
2023-06-09 11:29:15 +03:00
Ran Benita 4059000834 testing/python/collect: replace use of deprecated/removed imp module 2023-06-07 17:05:52 +03:00
Ran Benita 7d5207a736 Declare support for Python 3.12 2023-06-07 17:05:52 +03:00
Ran Benita 3e65a461c7 ci: start testing Python 3.12-dev 2023-06-07 17:05:52 +03:00
Ran Benita fc3b5b2610 testing: install setuptools to fix pkg_resources import in a test
Since Python 3.12, setuptools is no longer installed by default in
venvs. We have a test which uses it, so add it to the testing extra.
2023-06-07 17:05:52 +03:00
Ran Benita 9335a0b445 Avoid ast deprecation warnings on Python 3.12
Fix #10977.
2023-06-07 17:05:52 +03:00
Ran Benita 0ded3297a9 Merge pull request #11086 from pytest-dev/update-plugin-list/patch-32de8e289
[automated] Update plugin list
2023-06-07 09:02:06 +03:00
pytest bot fc9cbbd4c4 [automated] Update plugin list 2023-06-06 14:05:44 +00:00
Ran Benita 1a17539065 Merge pull request #10853 from stefmolin/patch-1
Update fixture scope in package/directory fixture example.
2023-06-06 17:04:45 +03:00
Ran Benita 32de8e2893 Merge pull request #11082 from bluetech/norecursedir-in-hook
main: move norecursedir check to main's pytest_ignore_collect
2023-06-06 12:44:03 +03:00
Ran Benita 5d4a342a7a Merge pull request #11076 from pytest-dev/update-plugin-list/patch-b5ff089d2
[automated] Update plugin list
2023-06-06 12:37:46 +03:00
Facundo Batista 1790f17228 Introduced a hardcoded list of project to include as plugins beyond those found by their names. (#11077) 2023-06-06 12:15:57 +03:00
pre-commit-ci[bot] ee8baa2676 [pre-commit.ci] pre-commit autoupdate (#11084)
updates:
- [github.com/asottile/setup-cfg-fmt: v2.2.0 → v2.3.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.2.0...v2.3.0)
2023-06-06 12:13:38 +03:00
Ran Benita ae38b076da main: move norecursedir check to main's pytest_ignore_collect
Fix #11081
2023-06-05 17:21:45 +03:00
dependabot[bot] 85c5bd26b6 build(deps): Bump pytest-xvfb in /testing/plugins_integration (#11079)
Bumps [pytest-xvfb](https://github.com/The-Compiler/pytest-xvfb) from 2.0.0 to 3.0.0.
- [Changelog](https://github.com/The-Compiler/pytest-xvfb/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/The-Compiler/pytest-xvfb/compare/v2.0.0...v3.0.0)

---
updated-dependencies:
- dependency-name: pytest-xvfb
  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>
2023-06-05 12:06:55 +02:00
Ran Benita e1204e1e23 Merge pull request #11078 from bluetech/pkg-roots-path
main: change pkg_roots to work with `Path`s instead of string paths
2023-06-04 21:45:45 +03:00
Ran Benita 313b61471f main: change pkg_roots to work with Paths instead of string paths
- Works better on Windows (case sensitivity)
- Simpler code
2023-06-04 20:43:34 +03:00
pytest bot 1daa8129c6 [automated] Update plugin list 2023-06-04 00:26:58 +00:00
Ran Benita b5ff089d2c Merge pull request #11069 from bluetech/lf-file
cacheprovider: make file-skipping work with any File, not just Modules
2023-06-03 18:50:50 +03:00
Ran Benita fda8024622 cacheprovider: make file-skipping work with any File, not just Modules
No reason for `--lf`'s whole-file-skipping feature to not for for
non-Python files.

Fix #11068.
2023-06-03 09:32:26 +03:00
Ran Benita 4b823a42ce Merge pull request #11059 from bluetech/lf-skip-across
cacheprovider: fix file-skipping functionality across packages
2023-06-03 09:31:04 +03:00
Ran Benita 6c9b277ce4 Merge pull request #11070 from bluetech/pkg-redundant-methods
python: remove redundant methods from Package
2023-06-02 18:58:21 +03:00
Ran Benita 3de43e5102 python: remove redundant methods from Package
They are already inherited exactly the same from FSCollector.
2023-06-02 16:08:04 +03:00
Ran Benita c76ae74bd7 cacheprovider: fix file-skipping functionality across packages
Continuation of fc538c5766.
Fixes #11054 again.
2023-05-30 23:16:43 +03:00
Ran Benita 24534cdd29 Merge pull request #11043 from bluetech/confcutdir-rootpath
config: fallback confcutdir to rootpath if inipath is not set
2023-05-30 20:21:20 +03:00
Ran Benita 99c78aa93a Merge pull request #10921 from bluetech/tb-simplify-2
Fix hidden traceback entries of chained exceptions getting shown
2023-05-30 20:09:13 +03:00
Ran Benita 3a6bdcd76b Merge pull request #11055 from bluetech/lf-skipped-package
cacheprovider: fix file-skipping feature for files in packages
2023-05-30 20:04:06 +03:00
Ran Benita 4a1bba25b9 config: fallback confcutdir to rootpath if inipath is not set
Currently, if `--confcutdir` is not set, `inipath.parent` is used, and
if `initpath` is not set, then `confcutdir` is None, which means there
is no cutoff.

Having no cutoff is not great, it means we potentially start probing
stuff all the way up to the filesystem root directory. So let's add
another fallback, to `rootpath`, which is always something reasonable.
2023-05-30 19:52:59 +03:00
Alessio Izzo 9e1add75f7 Fix warlus operator behavior when called by a function (#11041)
In #10758 we introduced the support for the use of the walrus operator in the test cases. There was a case which was not handled that caused a bug report #11028. This PR aims to fix the issue and also to improve how the walrus operator is handled in the AssertionRewriter class.

Closes #11028
2023-05-30 11:59:24 -03:00
theirix 4da9026766 Handle microseconds with custom logging.Formatter (#11047)
Added handling of %f directive to print microseconds in log format options, such as log-date-format. It is impossible to do with a standard logging.Formatter because it uses time.strftime which doesn't know about milliseconds and %f. In this PR I added a custom Formatter which converts LogRecord to a datetime.datetime object and formats it with %f flag. This behaviour is enabled only if a microsecond flag is specified in a format string.

Also added a few tests to check the standard and changed behavior.

Closes #10991
2023-05-30 09:35:33 -03:00
Kenny Y 7c231baa64 Add warning when testpaths is set but paths are not found by glob (#11044)
Closes #11013

---------

Co-authored-by: Ran Benita <ran@unusedvar.com>
2023-05-30 07:06:13 -03:00
Ran Benita fc538c5766 cacheprovider: fix file-skipping feature for files in packages
`--lf` has a feature where if a certain `Module` (python file) does not
contain any failed tests, it is skipped entirely at the collector level
instead of being collected and each item skipped individually. When this
happens the collection summary looks like this:

    run-last-failure: rerun previous 1 failure (skipped 1 file)

However, this feature didn't work for `Module`s inside of `Package`s,
only for those directly beneath the `Session`.

Fix #11054.
2023-05-29 22:55:44 +03:00
dependabot[bot] fbfd4b5005 build(deps): Bump anyio[curio,trio] in /testing/plugins_integration (#11050)
Bumps [anyio[curio,trio]](https://github.com/agronholm/anyio) from 3.6.2 to 3.7.0.
- [Changelog](https://github.com/agronholm/anyio/blob/3.7.0/docs/versionhistory.rst)
- [Commits](https://github.com/agronholm/anyio/compare/3.6.2...3.7.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>
2023-05-29 07:55:42 +02:00
dependabot[bot] ec752537ea build(deps): Bump pytest-cov in /testing/plugins_integration (#11051)
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 4.0.0 to 4.1.0.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: pytest-cov
  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>
2023-05-29 07:55:13 +02:00
Ran Benita dd667336ce nodes: apply same traceback filtering for chained exceptions as main exception
Fix #1904.
2023-05-28 17:20:50 +03:00
Ran Benita 29d16d2939 Merge pull request #11046 from pytest-dev/update-plugin-list/patch-4f3f36c39
[automated] Update plugin list
2023-05-28 16:02:07 +03:00
pytest bot 5313d50e18 [automated] Update plugin list 2023-05-28 00:22:05 +00:00
Chris Mahoney 4f3f36c396 Add alias --config-file to -c (#11036)
Fixes #11031

Signed-off-by: Chris Mahoney <chrismahoey@hotmail.com>
Co-authored-by: Chris Mahoney <chrismahoey@hotmail.com>
2023-05-26 07:56:18 -03:00
Ran Benita af124c7f21 Merge pull request #11026 from bluetech/small-fixes
Small fixes and improvements
2023-05-24 21:44:03 +03:00
Bruno Oliveira 4e6d53fef5 Merge pull request #11033 from jkeifer/patch-1
nonpython example now repr all exceptions
2023-05-24 08:10:42 -03:00
Jarrett Keifer 751d726d21 nonpython example now repr all exceptions
The definition of `repr_failure` on the `YamlItem` subclass only handled the custom `YamlException` class, which hides all other errors from the user. By adding in the `super` call we ensure all other exception types also appropriately handled by `repr_failure`.
2023-05-23 20:06:05 -07:00
github-actions[bot] 9e491f430e [automated] Update plugin list (#11027)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-05-21 10:47:02 -03:00
Ran Benita 63f258f432 python: fix syntax typo 2023-05-20 21:14:29 +03:00
Ran Benita baaa67dfb9 python: simplify code in Package.collect()
The path of Package is already the `__init__.py` file, and we're already
assured it's a file.
2023-05-20 21:14:29 +03:00
Ran Benita 519f351b4f pathlib: extract scandir utility from visit
Will be used on its in some upcoming changes, but good on its own.
2023-05-20 21:14:29 +03:00
Ran Benita 5d53447a73 fixtures: use isinstance in get_scope_package
No reason for the exact type equality.
2023-05-20 21:13:48 +03:00
Ran Benita 1716d3c9bf fixtures: type annotate get_scope_package 2023-05-20 21:06:24 +03:00
Ran Benita ac699e7b25 fixtures: add type annotations and docstring to parsefactories 2023-05-20 21:06:13 +03:00
Ran Benita a5f37199a9 fixtures: inline FixtureRequest._addfinalizer
There are no longer any calls to `_addfinalizer` other than this one
place, so inline it.
2023-05-20 21:06:07 +03:00
Roberto Aldera 9fa82598a9 Use NamedTuple for pytest_report_teststatus return value (#10972)
Closes #10872

---------

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-05-19 08:24:28 -03:00
Alex Lambson ba32a3bd87 Handle disabled logging in 'caplog.set_level' and 'caplog.at_level' (#8758)
Forces requested `caplog` logging levels to be enabled if they were disabled via `logging.disable()`

`[attr-defined]` mypy error ignored in `logging.py` because there were existing errors with the imports
and `loggin.Logger.manager` is an attr set at runtime. Since it's in the standard lib I can't really fix that.

Ignored an attr-defined error in `src/_pytest/config/__init__.py` because the re-export is necessary.

Fixes #8711
2023-05-18 10:18:59 -03:00
Ville Skyttä c8641f879f Include reason in cache path warnings to aid debugging (#11005)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-05-18 10:11:47 -03:00
Ville Skyttä 6041511fb4 Spelling and grammar fixes (#11014) 2023-05-18 10:10:44 -03:00
Ronny Pfannschmidt 739408b958 Merge pull request #11009 from nicoddemus/reference-status-of-python-versions
Reference "Status of Python Versions" in backwards-compatibility policy
2023-05-17 09:51:30 +02:00
Bruno Oliveira 1636322995 Reference "Status of Python Versions" in backwards-compatibility policy
As suggested in #10981.
2023-05-16 20:24:06 -03:00
pre-commit-ci[bot] b8edacb8f1 [pre-commit.ci] pre-commit autoupdate (#11007)
updates:
- [github.com/pre-commit/mirrors-mypy: v1.2.0 → v1.3.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.2.0...v1.3.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-16 09:19:38 +02:00
github-actions[bot] 612489e2bd [automated] Update plugin list (#11001)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-05-15 08:26:41 -03:00
dependabot[bot] 383774db10 build(deps): Bump actions/stale from 5 to 8 (#11003)
Bumps [actions/stale](https://github.com/actions/stale) from 5 to 8.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v5...v8)

---
updated-dependencies:
- dependency-name: actions/stale
  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>
2023-05-15 07:29:18 +02:00
Adam J. Stewart 3b5b3cf50e monkeypatch: add support for TypedDict (#11000) 2023-05-14 22:17:00 +03:00
Bruno Oliveira 23e343af60 Fix close stale issues workflow (#10990)
Fixed how the label is configured.
2023-05-14 08:39:45 -03:00
Ronny Pfannschmidt f9a995b56d Merge pull request #10998 from bluetech/pre-commit-rm-default
pre-commit: remove `default_language_version` setting
2023-05-13 23:27:48 +02:00
Ran Benita d9d78a8aef pre-commit: remove default_language_version setting
This makes it difficult to run on newer python versions than the one
specified.
2023-05-13 22:24:30 +03:00
Bruno Oliveira 76d15231f5 Merge pull request #10988 from nicoddemus/initial-testpaths-10987
Consider testpaths for initial conftests
2023-05-12 09:58:59 -03:00
Bruno Oliveira 4cc05e7bee Fix trailing whitespace in .github/workflows/stale.yml 2023-05-12 09:34:44 -03:00
Bruno Oliveira 2d57d5c32f Do not break on very long command-line options
`_set_initial_conftests` could break on some systems if a very long
option was passed, because the `Path.exists()` call raises an
`OSError` instead of returning `False`.

Fix #10169
2023-05-12 09:34:15 -03:00
Bruno Oliveira faeb16146b Consider testpaths for initial conftests
The 'testpaths' option is meant to be identical to execute
pytest passing the 'testpaths' directories explicitly.

Fix #10987
2023-05-12 09:34:15 -03:00
Bruno Oliveira b241c0b479 Fix defaults for tmp_path_retention_count and tmp_path_retention_policy in docs 2023-05-12 09:34:15 -03:00
Bruno Oliveira 78403237cf Add workflow to close "needs information" labeled issues (#10986)
This introduces a workflow to automatically close issues with the label
`status: needs information` after a number of days of inactivity.

This work has been done manually for a number of years, but I think it is safe
to close issues with this label automatically.

Not tested yet, but it is in `debug-only` mode so we can watch what it does before
deciding to turn it on (however it needs to be in `main` for it to run).
2023-05-11 15:14:50 -03:00
leeyueh f84fea0888 Update usage.rst (#10974)
Added a note for single quotation used in Windows.
2023-05-10 22:28:52 -03:00
Ran Benita 271bdf6c23 Merge pull request #10979 from bluetech/faulthandler-no-encoding
faulthandler: avoid accessing sys.stderr.encoding
2023-05-10 14:17:54 +03:00
Ran Benita fd56968f2b Merge pull request #10970 from pytest-dev/dependabot/pip/testing/plugins_integration/django-4.2.1
build(deps): Bump django from 4.2 to 4.2.1 in /testing/plugins_integration
2023-05-10 10:18:17 +03:00
Ran Benita 5b75b0d03f Merge pull request #10978 from bzoracler/fix-pytest-code-import
fix reference to non-existent module
2023-05-10 10:15:20 +03:00
Ran Benita aac5d5d08b faulthandler: avoid accessing sys.stderr.encoding
Fixes a pytest-xdist regression after
762bb61562 (not yet released).

pytest-xdist patches sys.stderr with an object which doesn't have
`encoding`. Strictly speaking, this should be fixed there (or more
precisely, in execnet), but it will drop support for older versions
which don't want.

But in any case, the fix turns out to simplify the code, using FD
support added in Python 3.5, so it's good anyway!

Refs: https://github.com/pytest-dev/pytest-xdist/pull/900
2023-05-10 09:59:57 +03:00
bzoracler b1460f3261 fix reference to non-existent module 2023-05-10 10:48:20 +12:00
github-actions[bot] a88ae8289c [automated] Update plugin list (#10968)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-05-09 19:05:13 -03:00
pre-commit-ci[bot] 62320e4ff7 [pre-commit.ci] pre-commit autoupdate (#10975)
updates:
- https://github.com/asottile/reorder_python_importshttps://github.com/asottile/reorder-python-imports
- [github.com/asottile/pyupgrade: v3.3.2 → v3.4.0](https://github.com/asottile/pyupgrade/compare/v3.3.2...v3.4.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-05-09 11:22:50 +02:00
dependabot[bot] 6514041a35 build(deps): Bump django in /testing/plugins_integration
Bumps [django](https://github.com/django/django) from 4.2 to 4.2.1.
- [Commits](https://github.com/django/django/compare/4.2...4.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-08 03:56:49 +00:00
Brian Larsen 7d548c38e2 Improve verbose output by wrapping skip/xfail reasons with margin (#10958)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-05-06 12:15:11 -03:00
Bruno Oliveira 07eeeb8dfc Merge pull request #10957 from pytest-dev/update-plugin-list/patch-762bb6156
[automated] Update plugin list
2023-05-02 10:02:58 -04:00
pytest bot be774667c2 [automated] Update plugin list 2023-04-30 00:23:40 +00:00
Ran Benita 762bb61562 Fix couple of EncodingWarnings (#10954)
* faulthandler: fix an EncodingWarning

* _py/path: tiny change to `ensure` to silence EncodingWarning

We're not supposed to diverge here, but make this change to fix an
unavoidable EncodingWarning that is otherwise raised in pytest's test
suite. The behavior should be exactly the same besides the warning,
hopefully that won't cause confusion.
2023-04-29 11:37:22 +03:00
Sergey Kim 725de3a0d3 add flake8-pytest-style mention to goodpractices (#10939) 2023-04-28 23:47:47 +03:00
Ran Benita fcada1ea47 nodes: change _prunetraceback to return the new traceback instead of modifying excinfo
This makes it usable as a general function, and just more understandable
in general.
2023-04-28 11:47:45 +03:00
Ran Benita 6f7f89f3c4 code: make TracebackEntry immutable
TracebackEntry being mutable caught me by surprise and makes reasoning
about the exception formatting code harder. Make it a proper value.
2023-04-28 11:47:45 +03:00
Ran Benita 0a20452f78 code: inline Traceback.getcrashentry into ExceptionInfo._getreprcrash
Since `Traceback.getcrashentry` takes the `ExceptionInfo`, it is not
really independent of it and is in the wrong layer. Prevent nonsensical
mistakes by inlining it.
2023-04-28 11:47:45 +03:00
Ran Benita cc23ec91d0 code: stop storing weakref to ExceptionInfo on Traceback and TracebackEntry
TracebackEntry needs the excinfo for the `__tracebackhide__ = callback`
functionality, where `callback` accepts the excinfo.

Currently it achieves this by storing a weakref to the excinfo which
created it. I think this is not great, mixing layers and bloating the
objects.

Instead, have `ishidden` (and transitively, `Traceback.filter()`) take
the excinfo as a parameter.
2023-04-28 11:47:45 +03:00
Florian Bruhin a15f544962 doc: Fix 2024 training location (#10947) 2023-04-26 08:13:06 +02:00
pre-commit-ci[bot] 3823ce60dd [pre-commit.ci] pre-commit autoupdate (#10941)
updates:
- [github.com/PyCQA/autoflake: v2.1.0 → v2.1.1](https://github.com/PyCQA/autoflake/compare/v2.1.0...v2.1.1)
- [github.com/asottile/pyupgrade: v3.3.1 → v3.3.2](https://github.com/asottile/pyupgrade/compare/v3.3.1...v3.3.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-25 10:52:15 +02:00
Miro Hrončok e03f82c359 Filter new pkg_resources deprecations (#10938)
Fixes https://github.com/pytest-dev/pytest/issues/10815
2023-04-25 10:51:10 +02:00
Bryan Ricker 158f41fdf8 Fix documentation typo (#10942) 2023-04-25 10:49:16 +02:00
Bruno Oliveira fd6a4507ac Merge pull request #10936 from pytest-dev/update-plugin-list/patch-0860f4e91
[automated] Update plugin list
2023-04-22 22:42:05 -03:00
pytest bot 15156757b6 [automated] Update plugin list 2023-04-23 00:21:26 +00:00
Florian Bruhin 0860f4e916 Add 2024 pytest training (#10933) 2023-04-22 21:22:25 +02:00
Ran Benita 11965d1c27 Merge pull request #10920 from bluetech/testing-no-testdir
testing: remove usages of testdir that sneaked back in
2023-04-18 22:49:23 +03:00
pre-commit-ci[bot] 14be71b234 [pre-commit.ci] pre-commit autoupdate (#10926)
updates:
- [github.com/PyCQA/autoflake: v2.0.2 → v2.1.0](https://github.com/PyCQA/autoflake/compare/v2.0.2...v2.1.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-18 07:03:27 +02:00
dependabot[bot] 2d6206b89a build(deps): Bump pytest-sugar in /testing/plugins_integration (#10922)
Bumps [pytest-sugar](https://github.com/Teemu/pytest-sugar) from 0.9.5 to 0.9.7.
- [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/commits/v0.9.7)

---
updated-dependencies:
- dependency-name: pytest-sugar
  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>
2023-04-17 08:55:36 +02:00
Alex 41f57ef95d Fix pytrace=False and --tb=line reports None (#10905)
Closes #10831.

This fixes a small bug where running tests that contained
`pytest.fail(pytrace=False)` with the `--tb=line` flag set results in
 an output of "None" in the Failures section of the output, and adds
 a test to ensure the behavior is correct.
2023-04-16 20:31:45 +00:00
Ran Benita 4eca6063c8 Merge pull request #10918 from pytest-dev/update-plugin-list/patch-7834b3b07
[automated] Update plugin list
2023-04-16 19:40:51 +03:00
Ran Benita 819f5abd73 testing: remove usages of testdir that sneaked back in 2023-04-16 19:18:50 +03:00
Ran Benita adf891ab1d Merge pull request #10919 from bluetech/missing-changelog
doc: add missing changelog for #10907
2023-04-16 18:41:45 +03:00
Ran Benita f08184ba20 doc: add missing changelog for #10907 2023-04-16 18:22:51 +03:00
pytest bot 28783e5d23 [automated] Update plugin list 2023-04-16 00:21:39 +00:00
Bruno Oliveira 7834b3b07f Merge pull request #10914 from nicoddemus/cherry-pick-release-7.3.1
Merge pull request #10913 from pytest-dev/release-7.3.1
2023-04-14 15:19:13 -03:00
Bruno Oliveira ece756fcb4 Merge pull request #10913 from pytest-dev/release-7.3.1
Prepare release 7.3.1

(cherry picked from commit a1f7a204df)
2023-04-14 15:15:28 -03:00
Bruno Oliveira d380771065 Fix tmp_path regression introduced in 7.3.0 (#10911)
The problem is that we would loop over all directories of the basetemp directory searching for dead symlinks, for each test, which would compound over the test session run.

Doing the cleanup just once, at the end of the session, fixes the problem.

Fix #10896
2023-04-14 13:24:12 -03:00
Ran Benita b893d2a0fe Merge pull request #10907 from bluetech/empty-traceback
code: handle repr'ing empty tracebacks gracefully
2023-04-13 19:36:09 +03:00
Ran Benita e3b1799766 code: handle repr'ing empty tracebacks gracefully
By "empty traceback" I mean a traceback all of whose entries have been
filtered/cut/pruned out.

Currently, if an empty traceback needs to be repr'ed, the last entry
before the filtering is used instead (added in
accd962c9f).

Showing a hidden frame is not so good IMO. This commit does the
following instead:

1. Shows details of the exception.
2. Shows a message about how the full trace can be seen.

Example:

```
_____________ test _____________

E   ZeroDivisionError: division by zero
All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.
```

Also handles `--tb=native`, though there the `--full-trace` bit is not
shown.

This commit contains some pieces from
431ec6d34e (which has been reverted).

Helps towards fixing issue # 1904.

Co-authored-by: Felix Hofstätter <Felhof1@hotmail.com>
2023-04-13 19:11:37 +03:00
Ran Benita 5d1385320f Merge pull request #10901 from bluetech/exceptioninfo-from-exception
code: add `ExceptionInfo.from_exception`
2023-04-13 16:04:48 +03:00
Ran Benita eff54aece1 Merge pull request #10904 from bluetech/revert-10772
Revert "Correctly handle tracebackhide for chained exceptions (#10772)"
2023-04-13 14:51:57 +03:00
Ran Benita bc1fc3f0fc Merge branch 'main' into exceptioninfo-from-exception 2023-04-13 14:51:07 +03:00
Ran Benita 784ffa0fad Merge pull request #10900 from bluetech/exceptioninfo-from_exc_info-exp
code: drop Experimental API label from ExceptionInfo.from_exc_info
2023-04-13 14:48:56 +03:00
Ran Benita 90412827c3 Revert "Correctly handle tracebackhide for chained exceptions (#10772)"
This reverts commit 431ec6d34e.

Fix #10903.
Reopen #1904.
2023-04-12 19:23:25 +03:00
Ran Benita 424c3eebde code: add ExceptionInfo.from_exception
The old-style `sys.exc_info()` triplet is redundant nowadays with
`(type(exc), exc, exc.__traceback__)`, and is beginning to get
soft-deprecated in Python 3.12.

Add a nicer API to ExceptionInfo which takes just the exc instead of the
triplet. There are already a few internal uses which benefit.
2023-04-12 13:16:48 +03:00
Ran Benita 9c2247ec1b code: drop Experimental API label from ExceptionInfo.from_exc_info
This API is OK, I don't think we're going to change something about it
at this point.
2023-04-12 12:46:29 +03:00
Ran Benita 61f7c27ec0 Merge pull request #10893 from bluetech/py312
Python 3.12 alpha fixes
2023-04-11 23:54:47 +03:00
Ran Benita 1b81d636e2 unittest: add addDuration function for Python 3.12 support
Fix #10875

Without this, fails with

```
...
E           AttributeError: 'TestCaseFunction' object has no attribute 'addDuration'
...
E           RuntimeWarning: TestResult has no addDuration method
```
2023-04-11 13:24:32 +03:00
Ran Benita 1b196fbeaf pathlib: fix Python 3.12 rmtree(onerror=...) deprecation
Fixes #10890
Ref: https://docs.python.org/3.12/library/shutil.html#shutil.rmtree
2023-04-11 13:24:32 +03:00
pre-commit-ci[bot] 22524046cf [pre-commit.ci] pre-commit autoupdate (#10891)
updates:
- [github.com/pre-commit/mirrors-mypy: v1.1.1 → v1.2.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.1.1...v1.2.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-04-11 07:08:45 +02:00
Bruno Oliveira 6dcd652d4a Amend changelog note for removal of attrs (#10888)
As discussed in https://github.com/pytest-dev/pytest/pull/10669#issuecomment-1501497729, we should
make the reasoning behind this change more clear, as well as thank the attrs maintainers for the
many years of cooperation and support.

Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
2023-04-10 13:04:02 -03:00
dependabot[bot] be9faa68d8 build(deps): Bump peter-evans/create-pull-request from 4.2.4 to 5.0.0 (#10887)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.2.4 to 5.0.0.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/38e0b6e68b4c852a5500a94740f0e535e0d7ba54...5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5)

---
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>
2023-04-10 08:25:35 -03:00
dependabot[bot] e4e13dd913 build(deps): Bump django in /testing/plugins_integration (#10886)
Bumps [django](https://github.com/django/django) from 4.1.7 to 4.2.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/4.1.7...4.2)

---
updated-dependencies:
- dependency-name: django
  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>
2023-04-10 08:21:27 -03:00
Ran Benita 5a399030c1 Merge pull request #10884 from pytest-dev/update-plugin-list/patch-ec8e23951
[automated] Update plugin list
2023-04-09 11:11:02 +03:00
Ran Benita 19c8ef63a4 Merge pull request #10880 from bluetech/regen-rm-dataclasses-dep
tox: remove `dataclasses` dependency from `regen`
2023-04-09 10:46:11 +03:00
pytest bot 22951bba67 [automated] Update plugin list 2023-04-09 00:22:30 +00:00
Ran Benita ec8e23951d Merge pull request #10883 from bluetech/cherry-pick-release
Cherry pick 7.3.0 release notes
2023-04-09 00:53:52 +03:00
Ran Benita bf47357511 Merge pull request #10881 from pytest-dev/release-7.3.0
Prepare release 7.3.0

(cherry picked from commit cec5bfe058)
2023-04-09 00:50:37 +03:00
Ran Benita 2d2dc4a2a8 tox: remove dataclasses dependency from regen
This dep is not needed on newer Pythons.
2023-04-09 00:18:07 +03:00
Kodi Arfer 3683722bcb FormattedExcinfo.get_source: avoid crash when line number is out-of-bounds/negative
pytest could crash given pathological AST position attributes, which shouldn't happen when testing real Python code, but could happen when testing AST produced by e.g. Hylang.

Another example of the failure is in the nightly CI for the JAX project: https://github.com/google/jax/actions/runs/4607513902/jobs/8142126075

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
Co-authored-by: Jake VanderPlas <jakevdp@google.com>
2023-04-05 22:48:24 -03:00
github-actions[bot] 31d0b51039 [automated] Update plugin list (#10857)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-04-04 13:17:43 -03:00
Pierre Sassoulas 2d2f69dab5 Merge pull request #10862 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-04-04 09:04:47 +02:00
pre-commit-ci[bot] 2a39ed3461 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 23.1.0 → 23.3.0](https://github.com/psf/black/compare/23.1.0...23.3.0)
2023-04-04 06:34:42 +00:00
Stefanie Molin f1c7585184 Update fixture scope in package/directory fixture example. 2023-03-31 10:00:45 -07:00
github-actions[bot] a3b39069bc [automated] Update plugin list (#10838)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-03-26 08:32:13 -03:00
github-actions[bot] 172c832cbd [automated] Update plugin list (#10823)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-03-24 11:41:07 -03:00
dependabot[bot] 839b90db45 build(deps): Bump peter-evans/create-pull-request from 4.2.3 to 4.2.4 (#10828)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 4.2.3 to 4.2.4.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/2b011faafdcbc9ceb11414d64d0573f37c774b04...38e0b6e68b4c852a5500a94740f0e535e0d7ba54)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-24 11:40:34 -03:00
dependabot[bot] 549cc512f7 build(deps): Bump pytest-asyncio in /testing/plugins_integration (#10827)
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.20.2 to 0.21.0.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.20.2...v0.21.0)

---
updated-dependencies:
- dependency-name: pytest-asyncio
  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>
2023-03-24 11:40:06 -03:00
Ronny Pfannschmidt 2369bed1db Merge pull request #10727 from RonnyPfannschmidt/ronny/split-report-header
split up report header lines for config, rootdir and testpaths
2023-03-18 22:06:46 +01:00
Ronny Pfannschmidt 54864f0c9b bugfix: fix imports for simple example 2023-03-17 21:58:26 +01:00
Ronny Pfannschmidt ba969d2ae7 run regendoc 2023-03-17 21:58:26 +01:00
Ronny Pfannschmidt 407b330fe1 split up report header lines
i found it painful to read crammed in a single line
thus rootdir, config file and testpaths now have own lines
2023-03-17 21:58:26 +01:00
Felix Hofstätter 431ec6d34e Correctly handle tracebackhide for chained exceptions (#10772) 2023-03-15 08:10:25 -03:00
pre-commit-ci[bot] eada68b2b3 [pre-commit.ci] pre-commit autoupdate (#10814)
updates:
- [github.com/PyCQA/autoflake: v2.0.1 → v2.0.2](https://github.com/PyCQA/autoflake/compare/v2.0.1...v2.0.2)
- [github.com/pre-commit/mirrors-mypy: v1.0.1 → v1.1.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.1...v1.1.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-03-14 08:17:13 -03:00
dependabot[bot] ab069247cd build(deps): Bump pytest-rerunfailures in /testing/plugins_integration (#10812)
Bumps [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) from 11.1.1 to 11.1.2.
- [Release notes](https://github.com/pytest-dev/pytest-rerunfailures/releases)
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/11.1.1...11.1.2)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  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>
2023-03-13 08:00:07 -03:00
github-actions[bot] 7af1e4e4ed [automated] Update plugin list (#10810)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-03-12 13:04:50 -03:00
Stefanie Molin 0ae04ae629 Include pyproject.toml in help section that lists out config files with ini-options (#10807) 2023-03-11 10:59:02 -03:00
Florian Bruhin 723035be7f doc: Remove done training (#10805) 2023-03-10 20:47:03 +00:00
Alessio Izzo 6e478b0947 Fix walrus operator support in assertion rewriting (#10758)
Closes #10743
2023-03-10 07:32:36 -03:00
Paul Kehrer a869141b3d New option to allow a progress report even when capture=no (#10755) 2023-03-07 17:49:37 -03:00
Bruno Oliveira 5e98aefc92 Merge pull request #10794 from pytest-dev/update-plugin-list/patch-d5dda84ef
[automated] Update plugin list
2023-03-06 08:16:06 -03:00
Bruno Oliveira 5f47e423b2 Merge pull request #10795 from bluthej/fix-10782
Fix example in the documentation (#10782)
2023-03-05 13:49:35 -03:00
bluthej 5a61ec3d4a Fix example in the documentation (#10782) 2023-03-05 17:01:21 +01:00
Zac Hatfield-Dodds b3b44ea814 Merge pull request #10766 from rdb/fix-10765 2023-03-04 23:34:35 -08:00
pytest bot 1d48b3021d [automated] Update plugin list 2023-03-05 00:25:29 +00:00
Bruno Oliveira d5dda84ef3 Merge pull request #10793 from nicoddemus/cherry-pick-release
Merge pull request #10792 from pytest-dev/release-7.2.2
2023-03-03 16:22:47 -03:00
Bruno Oliveira 517e02e59e Merge pull request #10792 from pytest-dev/release-7.2.2
Prepare release 7.2.2

(cherry picked from commit 3ce6030f0c)
2023-03-03 16:14:11 -03:00
Bruno Oliveira 4e259590c9 Normalize how changelog entries are written (#10779)
Went over all changelog entries making sure they follow our guidelines as written at:

https://github.com/pytest-dev/pytest/blob/88c9e92258a15697c4f125ea1b930856ebe2975f/.github/PULL_REQUEST_TEMPLATE.md?plain=1#L18-L21
2023-03-03 12:53:38 -03:00
Bruno Oliveira 97a2761d72 Fix test_cmdline_python_namespace_package (#10788)
pgk_resources.declare_namespace has been deprecated, so added an ignore warnings option
to the test.
2023-03-03 12:25:33 -03:00
Billy 88c9e92258 Minor updates to fixtures docs (#10724)
Updated the c fixture to be a little more consistent with other fixtures in the corresponding image. for example both e and g both have edges connected with the fixtures that they explicitly depend on.
2023-02-28 12:42:33 -03:00
Ronny Pfannschmidt 72ad32411f Docs: be more explicit about module level skip preventing collection (#10753) 2023-02-28 12:41:31 -03:00
Bruno Oliveira cb9e8be301 Move logic to get_user_id in compat 2023-02-28 11:19:34 -03:00
Bruno Oliveira d72da480c4 Apply suggestions from code review 2023-02-28 11:02:17 -03:00
Bruno Oliveira 07e7deb4a7 Update changelog/10765.bugfix.rst 2023-02-28 10:59:23 -03:00
Bruno Oliveira 572b5657d7 Merge pull request #10767 from alexhad6/fix-typo-in-python_api.py
Fix typo in python_api.py
2023-02-27 08:59:13 -03:00
dependabot[bot] 44afed9b13 build(deps): Bump pytest-rerunfailures in /testing/plugins_integration (#10754)
Bumps [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) from 11.1 to 11.1.1.
- [Release notes](https://github.com/pytest-dev/pytest-rerunfailures/releases)
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/11.1...11.1.1)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  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>
2023-02-27 08:56:25 -03:00
github-actions[bot] 13ea4780b8 [automated] Update plugin list (#10752)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-02-27 08:55:33 -03:00
Alex Hadley 135600fca3 Fix typo in python_api.py 2023-02-24 15:04:42 -08:00
rdb c237297b3d Fix OSError in tmpdir on emscripten due to missing getuid()
Fixes #10765
2023-02-24 23:23:44 +01:00
Ronny Pfannschmidt 9ccae9a8e3 Merge pull request #10756 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2023-02-22 15:10:09 +01:00
pre-commit-ci[bot] 77152d26e7 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/mirrors-mypy: v1.0.0 → v1.0.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.0.0...v1.0.1)
2023-02-21 03:03:45 +00:00
Manuel Jacob da626e7186 Update import mode documentation to not refer to __import__() anymore. (#10747)
Nowadays, the prepend and append import modes use importlib.import_module() instead of __import__().

There was a phrase “which avoids having to use `__import__`”, in which I couldn’t just replace `__import__` by `importlib.import_module` because the latter is used (in insert_missing_modules()) also when using importlib mode. Therefore I removed the part from the sentence.
2023-02-18 18:55:46 -03:00
bitzge 051f8f1f0f Add CI and BUILD_NUMBER env var in docs (#10749) 2023-02-18 18:52:14 -03:00
Bruno Oliveira 31ad577325 Merge pull request #10741 from pytest-dev/dependabot/pip/testing/plugins_integration/django-4.1.7
build(deps): Bump django from 4.1.6 to 4.1.7 in /testing/plugins_integration
2023-02-16 06:50:38 -03:00
dependabot[bot] 835cac8d8b build(deps): Bump django in /testing/plugins_integration
Bumps [django](https://github.com/django/django) from 4.1.6 to 4.1.7.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/4.1.6...4.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-15 20:40:12 +00:00
Florian Bruhin 464f29901f Update open training (#10739) 2023-02-15 14:06:24 +00:00
Garvit Shubham aa72496d24 Fix entry-points declaration in the documentation example using Hatch
Closes #10721
2023-02-14 10:57:32 -03:00
pre-commit-ci[bot] e9f3a01392 [pre-commit.ci] pre-commit autoupdate (#10733)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.991 → v1.0.0](https://github.com/pre-commit/mirrors-mypy/compare/v0.991...v1.0.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-02-14 07:30:12 -03:00
dependabot[bot] 00c94ab01b build(deps): Bump pytest-rerunfailures in /testing/plugins_integration (#10731)
Bumps [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) from 11.0 to 11.1.
- [Release notes](https://github.com/pytest-dev/pytest-rerunfailures/releases)
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/11.0...11.1)

---
updated-dependencies:
- dependency-name: pytest-rerunfailures
  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>
2023-02-13 08:05:47 -03:00
github-actions[bot] 9048621002 [automated] Update plugin list (#10726)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-02-12 21:39:45 -03:00
Bruno Oliveira 27165cf8db Use build-and-inspect-python-package action (#10722)
This uses https://github.com/hynek/build-and-inspect-python-package to ensure our package is correct, both during testing and deploy,
2023-02-12 21:37:40 -03:00
Ilya Konstantinov 7a829cb57d Document the location tuple (#10700) 2023-02-12 11:20:53 -03:00
HTRafal 5e1c3d2477 Propagate timestamps from CallInfo to TestReport objects (#10711)
This makes it possible to correlate pytest stages with external events, and also makes it readable when TestReports are exported externall (for example with pytest-reportlog).

Closes #10710
2023-02-10 17:52:54 -03:00
pre-commit-ci[bot] 59e7d2bbc9 [pre-commit.ci] pre-commit autoupdate (#10712)
* [pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/psf/black: 22.12.0 → 23.1.0](https://github.com/psf/black/compare/22.12.0...23.1.0)
- [github.com/PyCQA/autoflake: v2.0.0 → v2.0.1](https://github.com/PyCQA/autoflake/compare/v2.0.0...v2.0.1)

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

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

* Update .pre-commit-config.yaml

* [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>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-02-07 19:30:33 -03:00
Mahesh Vashishtha af99040123 Add a note about -W vs filterwarnings. (#10713)
Closes #10687

Signed-off-by: mvashishtha <mahesh@ponder.io>
2023-02-07 19:27:34 -03:00
dependabot[bot] a2b7db7655 build(deps): Bump django in /testing/plugins_integration (#10706)
Bumps [django](https://github.com/django/django) from 4.1.5 to 4.1.6.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/4.1.5...4.1.6)

---
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>
2023-02-07 08:00:16 -03:00
github-actions[bot] 9c93c96b14 [automated] Update plugin list (#10707)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-02-07 07:59:57 -03:00
github-actions[bot] 4a46ee8bc9 [automated] Update plugin list (#10699)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-01-30 10:15:02 -03:00
Teejay 5dbfb8e108 Fix fixtures named teardown being considered by nose (#10696)
Closes #10597
2023-01-27 14:33:46 -03:00
vin01 86a1beba07 Clarify docs for match regarding escaping (#10695)
Add example using `re.escape` to escape arbitrary literal strings which might contain regular expression characters like `.` or `)`.

Closes #10595
2023-01-27 08:11:00 -03:00
Jay ca40380e99 Add check for zero denominator in approx (#10624)
Closes #10533
2023-01-24 07:07:42 -03:00
Bruno Oliveira 05eee78aaa Merge pull request #10599 from pytest-dev/fix-update-plugin-list-workflow
Fix update-plugin-list workflow due to new 'packaging'
2023-01-23 17:55:45 -03:00
Ran Benita 02893139f9 Merge pull request #10680 from bluetech/capture-typing
capture: improve typing
2023-01-23 14:38:28 +02:00
Ran Benita 8c53dbf9d7 capture: fix pyright type error
This is OK in mypy, but doesn't hurt to fix.
2023-01-23 14:12:01 +02:00
Ran Benita 54b8b40f83 capture: improve NoCapture typing 2023-01-23 14:12:01 +02:00
Ran Benita 54911acf8d capture: improve captureclass typing
Previously, the any `captureclass` arguments were Any. We need to
introduce another common base class to fix this.
2023-01-23 14:12:01 +02:00
Ran Benita c746d2b016 capture: improve SysCapture/FDCapture typing
Instead of `SysCapture`/`FDCapture` inheriting from
`SysCaptureBinary`/`FDCaptureBinary`, have both inherit from a common
`SysCaptureBase`/`FDCaptureBase`. This fixes a Liskov substitution
violation.
2023-01-23 14:12:01 +02:00
Ran Benita a3693ce503 capture: improve DontReadFromInput typing
Have `DontReadFromInput` inherit from `TextIO`, ensuring it's fully
compatible with `sys.stdin` (which has type `TextIO`).
2023-01-23 14:12:01 +02:00
Yannick PÉROUX af4143729f Allow spaces in -p arguments (#10658) 2023-01-21 08:22:44 -03:00
q0w bd7919e03d Initialize args and args_source during Config.__init__
Closes #10626

Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-01-21 08:19:54 -03:00
Ran Benita 7d4b40337b capture: fix some Anys 2023-01-21 10:39:58 +02:00
Ran Benita 6a714d7b70 capture: CaptureResult can be a namedtuple again (#10678)
mypy now supports generic NamedTuple.
2023-01-21 09:39:58 +02:00
Ran Benita 5a23eeff7a Merge pull request #10668 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-rerunfailures-11.0
build(deps): Bump pytest-rerunfailures from 10.3 to 11.0 in /testing/plugins_integration
2023-01-20 11:23:54 +02:00
Ran Benita 310b67b227 Drop attrs dependency, use dataclasses instead (#10669)
Since pytest now requires Python>=3.7, we can use the stdlib attrs
clone, dataclasses, instead of the OG package.

attrs is still somewhat nicer than dataclasses and has some extra
functionality, but for pytest usage there's not really a justification
IMO to impose the extra dependency on users when a standard alternative
exists.
2023-01-20 11:13:36 +02:00
Ramsey 4d4ed42c34 Fix crash if --cache-show and --help are passed at the same time
Closes #10592
2023-01-19 09:44:57 -03:00
Ronny Pfannschmidt 096b942ec4 Merge pull request #10660 from ikonst/2023-01-13-raises-typing
Derive pytest.raises from AbstractContextManager
2023-01-18 06:42:46 +01:00
pre-commit-ci[bot] 61cfaacec6 [pre-commit.ci] pre-commit autoupdate (#10671)
updates:
- [github.com/asottile/blacken-docs: v1.12.1 → 1.13.0](https://github.com/asottile/blacken-docs/compare/v1.12.1...1.13.0)
- [github.com/pre-commit/pygrep-hooks: v1.9.0 → v1.10.0](https://github.com/pre-commit/pygrep-hooks/compare/v1.9.0...v1.10.0)
2023-01-17 12:09:19 +02:00
dependabot[bot] 95c62eb527 build(deps): Bump pytest-rerunfailures in /testing/plugins_integration
Bumps [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) from 10.3 to 11.0.
- [Release notes](https://github.com/pytest-dev/pytest-rerunfailures/releases)
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/10.3...11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-16 03:00:46 +00:00
Bruno Oliveira 03b19945fb Merge pull request #10662 from nicoddemus/cherry-pick-release
Merge pull request #10659 from pytest-dev/release-7.2.1
2023-01-14 09:35:02 -03:00
Bruno Oliveira b2ac31cc9f Merge pull request #10659 from pytest-dev/release-7.2.1
Prepare release 7.2.1

(cherry picked from commit 94c05bc2a4)
2023-01-14 09:21:43 -03:00
Ilya Konstantinov 1a96f16401 Derive pytest.raises from AbstractContextManager
Makes `AbstractContextManager` the shared base class between "raises" and other context managers.

The motivation is for type checkers to narrow `pytest.raises(...) if x else nullcontext()` to a `ContextManager` rather than `object`.
2023-01-13 13:58:49 -05:00
dependabot[bot] 7421f3bb94 build(deps): Bump django in /testing/plugins_integration (#10643)
Bumps [django](https://github.com/django/django) from 4.1.3 to 4.1.5.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/4.1.3...4.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-13 07:16:43 -03:00
Bruno Oliveira 5e0583f4b9 Fix regen tox environment (#10640)
Since tox 4.0, the whitelist_external option has been renamed to allowlist_externals.

Co-authored-by: pytest bot <pytestbot@gmail.com>
2023-01-13 07:15:41 -03:00
s-padmanaban 8efb4bb9c1 Do not update cache from xdist worker (#10641) 2023-01-13 07:14:52 -03:00
Kadino 3ad4344656 Mitigate directory creation race condition (#10607)
Fixes https://github.com/pytest-dev/pytest/issues/10604 which could intermittently display unexpected behavior between checking if the path exists and requesting creation. This was fairly prevalent when pytest was being invoked in parallel by another test runner (CTest) and trying to create the same parent-folder for multiple XMLs. A modest amount of testing did not reproduce other filesystem race conditions.

This notably does not work around an edge case where the parent path of the XML could be created as a file instead of a folder or link. That vanishingly rare case should cause file creation to fail on the next line, with a fairly obvious exception message.
2023-01-06 09:12:24 -03:00
Bruno Oliveira 6bf7f55555 Merge pull request #10632 from danigm/fix-tests
Fix tests pygments 2.14.0
2023-01-05 12:59:24 -03:00
Daniel Garcia Moreno 61f70a5a75 Fix tests pygments 2.14.0
Fix https://github.com/pytest-dev/pytest/issues/10630
2023-01-04 10:30:28 +01:00
Bruno Oliveira 326ae0cd88 Merge pull request #10609 from yusuke-kadowaki/default_policy_all
Change the default `tmp_path_retention_policy` to `all`
2022-12-25 14:09:45 -03:00
Yusuke Kadowaki 10220d3f31 Change the default policy to all 2022-12-25 00:18:38 +09:00
Bruno Oliveira a98b00cd09 Use a more descriptive job name for update-plugin-list workflow 2022-12-21 14:01:37 -03:00
Bruno Oliveira 215ea7fd03 Fix update-plugin-list workflow due to new 'packaging'
The latest 'packaging' release has dropped support for `LegacyVersion`, meaning
that version strings which do not conform to the standard now raise an error.
2022-12-21 14:01:37 -03:00
Pierre Sassoulas b31db4809b Avoid truncation when truncating means longer output (#10446)
Fixes #6267
2022-12-17 10:24:46 -03:00
Bruno Oliveira f6adebb990 Fix tox 4.0 support and docs
Also includes pre-commit autoupdate
2022-12-14 08:57:07 -03:00
Bruno Oliveira b90e7b84d0 Remove reference to explicit pytest tox support
This reference no longer exists in tox 4.0 docs.
2022-12-14 08:38:11 -03:00
Bruno Oliveira 19807ab79a Pin packaging while building docs
Packaging >=22 no longer supports 'latest' versions, which is the version that is assigned when building the docs.

https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045

Also first install pytest, then `docs/en/requirements.txt`:

Installing the requirements last will honor pins of libraries
shared between pytest and the docs build.
2022-12-14 08:38:11 -03:00
Bruno Oliveira 3e52124185 Fix passenv setting for tox.ini
In tox 4.0 `passenv` is required to be one per-line, or comma-separated.
2022-12-14 08:35:39 -03:00
pre-commit-ci[bot] 1eca228bd5 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/psf/black: 22.10.0 → 22.12.0](https://github.com/psf/black/compare/22.10.0...22.12.0)
- [github.com/asottile/pyupgrade: v3.3.0 → v3.3.1](https://github.com/asottile/pyupgrade/compare/v3.3.0...v3.3.1)
2022-12-13 01:28:21 +00:00
Bruno Oliveira cab02e67d7 Merge pull request #10559 from pytest-dev/update-plugin-list/patch-9fbd67dd4
[automated] Update plugin list
2022-12-06 11:17:45 -03:00
Bruno Oliveira 64dbc7a0a1 Merge pull request #10560 from pytest-dev/dependabot/github_actions/peter-evans/create-pull-request-4.2.3
build(deps): Bump peter-evans/create-pull-request from 3.7.0 to 4.2.3
2022-12-06 11:16:54 -03:00
Bruno Oliveira 60d992677d Merge pull request #10563 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-12-06 11:06:16 -03:00
pre-commit-ci[bot] 0079decf29 [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.2.2 → v3.3.0](https://github.com/asottile/pyupgrade/compare/v3.2.2...v3.3.0)
2022-12-06 00:36:28 +00:00
dependabot[bot] 3a58fc2d44 build(deps): Bump peter-evans/create-pull-request from 3.7.0 to 4.2.3
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 3.7.0 to 4.2.3.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/2455e1596942c2902952003bbb574afbbe2ab2e6...2b011faafdcbc9ceb11414d64d0573f37c774b04)

---
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>
2022-12-05 03:15:13 +00:00
pytest bot 39b6bb551c [automated] Update plugin list 2022-12-04 00:20:05 +00:00
Marko Pacak 9fbd67dd4b Class methods can now be discovered as tests (#10552)
Fix #10525
2022-12-02 15:53:04 +00:00
Zac Hatfield-Dodds eca93db05b Merge pull request #10550 from danschef/use_os_sep
Use os.sep instead of os.path.sep.
2022-12-02 00:32:12 -08:00
Daniel Scheffler fb701b538c Use os.sep instead of os.path.sep.
Signed-off-by: Daniel Scheffler <danschef@gfz-potsdam.de>
2022-12-01 18:16:02 +01:00
Bruno Oliveira 314e623304 Merge pull request #10549 from yusuke-kadowaki/more_doc_update
Documentation update for the tmp_path configurations
2022-12-01 12:23:43 -03:00
Yusuke Kadowaki 62e75c7d55 Documentation update for tmp_path configurations 2022-12-01 22:29:46 +09:00
github-actions[bot] fd30759d94 [automated] Update plugin list (#10536)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2022-11-29 09:14:38 +00:00
dependabot[bot] eb984a717a build(deps): Bump pytest-rerunfailures in /testing/plugins_integration (#10537)
Bumps [pytest-rerunfailures](https://github.com/pytest-dev/pytest-rerunfailures) from 10.2 to 10.3.
- [Release notes](https://github.com/pytest-dev/pytest-rerunfailures/releases)
- [Changelog](https://github.com/pytest-dev/pytest-rerunfailures/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-rerunfailures/compare/10.2...10.3)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-29 09:55:24 +01:00
Anthony Sottile 54f0fb3c63 Merge pull request #10543 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-11-28 20:27:39 -08:00
pre-commit-ci[bot] 49a4ed14cf [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.3.0 → v4.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.3.0...v4.4.0)
- [github.com/PyCQA/autoflake: v1.7.7 → v2.0.0](https://github.com/PyCQA/autoflake/compare/v1.7.7...v2.0.0)
- [github.com/PyCQA/flake8: 5.0.4 → 6.0.0](https://github.com/PyCQA/flake8/compare/5.0.4...6.0.0)
2022-11-29 02:01:34 +00:00
Yusuke Kadowaki f513d33d5a Modify documentation to use .stash when storing test results. (#10535) 2022-11-27 22:09:56 +02:00
Prerak Patel 857e34ef85 Fix bug where file system root was erroneously be used as rootdir on Windows
Fix #10506
2022-11-23 14:46:00 -03:00
Yusuke Kadowaki 99dfc19fe6 Fix tmp_path_retention_policy crash when skipping from fixture (#10517)
Also uses the stash to save the test status.

Fix #10502
2022-11-23 10:48:29 -03:00
pre-commit-ci[bot] 56544c11b5 [pre-commit.ci] pre-commit autoupdate (#10522)
updates:
- [github.com/pre-commit/mirrors-mypy: v0.990 → v0.991](https://github.com/pre-commit/mirrors-mypy/compare/v0.990...v0.991)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-11-22 08:27:55 -03:00
github-actions[bot] 7710e18b4c [automated] Update plugin list (#10519)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2022-11-22 08:05:40 -03:00
Daniel Valenzuela 791b51d0fa Show test name when skipping from fixture (#10482)
Fixes #10457
2022-11-18 09:20:38 -03:00
Anthony Sottile bc4e70e048 Merge pull request #10504 from pytest-dev/all-repos_autofix_no-implicit-optional
remove no_implicit_optional
2022-11-17 00:30:51 -05:00
Anthony Sottile b817aa457c remove no_implicit_optional
this is the default in mypy 0.990

Committed via https://github.com/asottile/all-repos
2022-11-16 19:20:16 -05:00
Anthony Sottile 66b28912ac Merge pull request #10497 from pytest-dev/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2022-11-16 13:18:59 -05:00
Yusuke Kadowaki cca029d55e Add configuration options to control how tmp_path directories are kept (#10442)
Close #8141
2022-11-15 09:11:39 -03:00
Bruno Oliveira d5466b3917 Fix typing errors after mypy update 2022-11-15 08:53:23 -03:00
pre-commit-ci[bot] 4fce29f15d [pre-commit.ci] pre-commit autoupdate
updates:
- [github.com/asottile/pyupgrade: v3.2.0 → v3.2.2](https://github.com/asottile/pyupgrade/compare/v3.2.0...v3.2.2)
- [github.com/pre-commit/mirrors-mypy: v0.982 → v0.990](https://github.com/pre-commit/mirrors-mypy/compare/v0.982...v0.990)
2022-11-15 00:17:06 +00:00
Bruno Oliveira 69e3973d86 Merge pull request #10468 from pytest-dev/dependabot/pip/testing/plugins_integration/django-4.1.3
build(deps): Bump django from 4.1.2 to 4.1.3 in /testing/plugins_integration
2022-11-14 09:25:28 -03:00
dependabot[bot] c842893b02 build(deps): Bump django in /testing/plugins_integration
Bumps [django](https://github.com/django/django) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/4.1.2...4.1.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 11:36:55 +00:00
dependabot[bot] 506b10d295 build(deps): Bump pytest-asyncio in /testing/plugins_integration (#10494)
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.20.1 to 0.20.2.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.20.1...v0.20.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2022-11-14 11:36:00 +00:00
Bruno Oliveira 05061493cb Merge pull request #10495 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-bdd-6.1.1
build(deps): Bump pytest-bdd from 6.0.1 to 6.1.1 in /testing/plugins_integration
2022-11-14 08:09:07 -03:00
dependabot[bot] f97f3dc3a3 build(deps): Bump pytest-bdd in /testing/plugins_integration
Bumps [pytest-bdd](https://github.com/pytest-dev/pytest-bdd) from 6.0.1 to 6.1.1.
- [Release notes](https://github.com/pytest-dev/pytest-bdd/releases)
- [Changelog](https://github.com/pytest-dev/pytest-bdd/blob/master/CHANGES.rst)
- [Commits](https://github.com/pytest-dev/pytest-bdd/compare/6.0.1...6.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-14 03:00:43 +00:00
Bruno Oliveira 3c31b0132f Merge pull request #10493 from pytest-dev/update-plugin-list/patch-54d5a63d1
[automated] Update plugin list
2022-11-13 12:02:36 -03:00
pytest bot 593178d909 [automated] Update plugin list 2022-11-13 00:22:21 +00:00
Bruno Oliveira 54d5a63d14 Merge pull request #10488 from DanielVZ96/fix-test-raising-repr
Fix test_raising_repr test
2022-11-09 20:20:05 -03:00
Daniel Valenzuela b55e264a67 Fix test_raising_repr test
Closes #10473

Python <3.11 versions depend on `exceptiongroup>=1.0.0rc8`, and they released version `1.0.1`
6 days ago (2022/11/03) that as a side-effect changed the output of exceptions.
2022-11-09 19:43:10 -03:00
Bruno Oliveira 13d6114c0a Merge pull request #10484 from joukewitteveen/patch-2
scripts/update-plugin-list: Improve requirement detection
2022-11-09 10:07:12 -03:00
Jouke Witteveen b635e16d30 scripts/update-plugin-list: Improve requirement detection
PEP 566 does not require a space after the dependency name.
2022-11-08 19:05:10 +01:00
Bruno Oliveira a092b3ab36 Merge pull request #10481 from pytest-dev/update-plugin-list/patch-aa7e9de91
[automated] Update plugin list
2022-11-08 09:07:00 -03:00
pytest bot a006dabf6e [automated] Update plugin list 2022-11-08 11:13:43 +00:00
Bruno Oliveira aa7e9de91d Merge pull request #10480 from joukewitteveen/patch-1
scripts/update-plugin-list: Be liberal in accepted summaries
2022-11-08 08:11:35 -03:00
Jouke Witteveen 6aec32163d scripts/update-plugin-list: Be liberal in accepted summaries
Misconfigured packages may have a null summary in the PyPI json response.
2022-11-07 21:00:31 +01:00
Florian Bruhin 2f33ea87c8 Remove done trainings (#10471) 2022-11-04 18:38:09 +01:00
Ashish Kurmi 1ada62e237 Update dependabot for GitHub Actions (#10464)
Signed-off-by: Ashish Kurmi <akurmi@stepsecurity.io>

Signed-off-by: Ashish Kurmi <akurmi@stepsecurity.io>
2022-11-04 07:41:34 -03:00
pre-commit-ci[bot] 50b232b0cb [pre-commit.ci] pre-commit autoupdate (#10454)
updates:
- [github.com/asottile/reorder_python_imports: v3.8.5 → v3.9.0](https://github.com/asottile/reorder_python_imports/compare/v3.8.5...v3.9.0)
- [github.com/asottile/pyupgrade: v3.1.0 → v3.2.0](https://github.com/asottile/pyupgrade/compare/v3.1.0...v3.2.0)
- [github.com/asottile/setup-cfg-fmt: v2.1.0 → v2.2.0](https://github.com/asottile/setup-cfg-fmt/compare/v2.1.0...v2.2.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-31 21:37:05 -03:00
dependabot[bot] 496196b15c build(deps): Bump pytest-html in /testing/plugins_integration (#10450)
Bumps [pytest-html](https://github.com/pytest-dev/pytest-html) from 3.1.1 to 3.2.0.
- [Release notes](https://github.com/pytest-dev/pytest-html/releases)
- [Changelog](https://github.com/pytest-dev/pytest-html/blob/master/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-html/compare/v3.1.1...v3.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-10-31 12:51:34 -03:00
Hugo van Kemenade 0314b50c52 Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12 (#10452) 2022-10-31 12:49:51 -03:00
wim glenn 8e2de91bf8 Merge pull request #10449 from cj81499/patch-1
fix type in python_api.py
2022-10-30 22:26:56 -05:00
Cal Jacobson 692ab1160b add Cal Jacobson to AUTHORS 2022-10-30 22:02:18 -05:00
Cal Jacobson 549839bac5 fix type in python_api.py
excpected -> expected
2022-10-30 21:59:09 -05:00
Florian Bruhin 646a46e5f4 Edit changelog for 7.2.0 (#10423)
Fix some typos, and consolidate two entries for the same feature into one.
2022-10-25 19:47:16 +02:00
Santiago Castro f07017f91b Add the PyPI classifier for Python 3.11 (#10426) 2022-10-25 13:43:59 -03:00
Anthony Sottile a17d3b0c44 Merge pull request #10425 from pytest-dev/asottile-patch-1
upgrade pygments-pytest for 7.2.x coloring
2022-10-25 11:27:36 -04:00
Anthony Sottile bbec1ce67f upgrade pygments-pytest for 7.2.x coloring 2022-10-25 10:52:53 -04:00
Ronny Pfannschmidt 5a040aef97 Merge pull request #10412 from pytest-dev/release-7.2.0 (#10419)
Prepare release 7.2.0

(cherry picked from commit ac4e3cced9)
2022-10-25 13:12:55 +02:00
Bruno Oliveira c1d2168df6 Merge pull request #10417 from nicoddemus/publish-action-pin
Use specific tag in the gh-action-pypi-publish action
2022-10-25 08:11:33 -03:00
pre-commit-ci[bot] bbe7cbae4a [pre-commit.ci] pre-commit autoupdate (#10418)
updates:
- [github.com/PyCQA/autoflake: v1.7.6 → v1.7.7](https://github.com/PyCQA/autoflake/compare/v1.7.6...v1.7.7)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2022-10-24 20:25:15 -03:00
Bruno Oliveira deae8f47f6 Use specific tag in the gh-action-pypi-publish action
Otherwise we see this warning:

Warning:  You are using "pypa/gh-action-pypi-publish@master". The "master" branch of this project has been sunset and will not receive any updates, not even security bug fixes. Please, make sure to use a supported version. If you want to pin to v1 major version, use "pypa/gh-action-pypi-publish@release/v1". If you feel adventurous, you may opt to use use "pypa/gh-action-pypi-publish@unstable/v1" instead. A more general recommendation is to pin to exact tags or commit shas.
2022-10-24 19:23:40 -03:00
Zac Hatfield-Dodds 10f55f79af Merge pull request #10226 from Zac-HD/use-exceptiongroup-for-teardown
Use exceptiongroup for multiple errors during teardown
2022-10-24 08:44:02 -07:00
Bruno Oliveira a6d244343f Merge pull request #10416 from rettinghaus/update-actions
Update used GitHub Actions to latest versions
2022-10-24 10:32:58 -03:00
Klaus Rettinghaus 2b552c2240 update actions 2022-10-24 14:58:03 +02:00
Bruno Oliveira 54d7b9a08e Merge pull request #10415 from pytest-dev/dependabot/pip/testing/plugins_integration/pytest-asyncio-0.20.1
build(deps): Bump pytest-asyncio from 0.19.0 to 0.20.1 in /testing/plugins_integration
2022-10-24 08:12:12 -03:00
Bruno Oliveira 6afc02abca Merge pull request #10414 from pytest-dev/dependabot/pip/testing/plugins_integration/anyio-curiotrio--3.6.2
build(deps): Bump anyio[curio,trio] from 3.6.1 to 3.6.2 in /testing/plugins_integration
2022-10-24 08:11:49 -03:00
dependabot[bot] e75e2d66a0 build(deps): Bump pytest-asyncio in /testing/plugins_integration
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.19.0 to 0.20.1.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Changelog](https://github.com/pytest-dev/pytest-asyncio/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.19.0...v0.20.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-24 03:04:18 +00:00
dependabot[bot] 66db0b7522 build(deps): Bump anyio[curio,trio] in /testing/plugins_integration
Bumps [anyio[curio,trio]](https://github.com/agronholm/anyio) from 3.6.1 to 3.6.2.
- [Release notes](https://github.com/agronholm/anyio/releases)
- [Changelog](https://github.com/agronholm/anyio/blob/3.6.2/docs/versionhistory.rst)
- [Commits](https://github.com/agronholm/anyio/compare/3.6.1...3.6.2)

---
updated-dependencies:
- dependency-name: anyio[curio,trio]
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-24 03:04:15 +00:00
Zac Hatfield-Dodds 3a68c08426 Use exceptiongroup for teardown errors 2022-10-23 15:45:50 -07:00
Itxaso Aizpurua 9e1804a6ee Add --log-disable CLI option (#10371)
Fixes #7431
2022-10-23 18:23:34 -03:00
135 changed files with 6289 additions and 2523 deletions
+6
View File
@@ -9,3 +9,9 @@ updates:
allow:
- dependency-type: direct
- dependency-type: indirect
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
time: "03:00"
open-pull-requests-limit: 10
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
pull-requests: write
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
persist-credentials: true
+18 -14
View File
@@ -23,30 +23,34 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5
- name: Download Package
uses: actions/download-artifact@v3
with:
name: Packages
path: dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.pypi_token }}
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: "3.7"
- name: Install dependencies
- name: Install tox
run: |
python -m pip install --upgrade pip
pip install --upgrade build tox
- name: Build package
run: |
python -m build
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.pypi_token }}
pip install --upgrade tox
- name: Publish GitHub release notes
env:
+2 -2
View File
@@ -27,12 +27,12 @@ jobs:
pull-requests: write
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: "3.8"
+23
View File
@@ -0,0 +1,23 @@
name: close needs-information issues
on:
schedule:
- cron: "30 1 * * *"
workflow_dispatch:
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/stale@v8
with:
debug-only: false
days-before-issue-stale: 14
days-before-issue-close: 7
only-labels: "status: needs information"
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
+32 -8
View File
@@ -18,6 +18,11 @@ on:
env:
PYTEST_ADDOPTS: "--color=yes"
# Cancel running jobs for the same workflow and branch.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Set permissions at the job level.
permissions: {}
@@ -38,6 +43,7 @@ jobs:
"windows-py39",
"windows-py310",
"windows-py311",
"windows-py312",
"ubuntu-py37",
"ubuntu-py37-pluggy",
@@ -46,12 +52,13 @@ jobs:
"ubuntu-py39",
"ubuntu-py310",
"ubuntu-py311",
"ubuntu-py312",
"ubuntu-pypy3",
"macos-py37",
"macos-py38",
"macos-py39",
"macos-py310",
"macos-py312",
"docs",
"doctesting",
@@ -81,9 +88,13 @@ jobs:
os: windows-latest
tox_env: "py310-xdist"
- name: "windows-py311"
python: "3.11-dev"
python: "3.11"
os: windows-latest
tox_env: "py311"
- name: "windows-py312"
python: "3.12-dev"
os: windows-latest
tox_env: "py312"
- name: "ubuntu-py37"
python: "3.7"
@@ -111,10 +122,15 @@ jobs:
os: ubuntu-latest
tox_env: "py310-xdist"
- name: "ubuntu-py311"
python: "3.11-dev"
python: "3.11"
os: ubuntu-latest
tox_env: "py311"
use_coverage: true
- name: "ubuntu-py312"
python: "3.12-dev"
os: ubuntu-latest
tox_env: "py312"
use_coverage: true
- name: "ubuntu-pypy3"
python: "pypy-3.7"
os: ubuntu-latest
@@ -124,19 +140,19 @@ jobs:
python: "3.7"
os: macos-latest
tox_env: "py37-xdist"
- name: "macos-py38"
python: "3.8"
os: macos-latest
tox_env: "py38-xdist"
use_coverage: true
- name: "macos-py39"
python: "3.9"
os: macos-latest
tox_env: "py39-xdist"
use_coverage: true
- name: "macos-py310"
python: "3.10"
os: macos-latest
tox_env: "py310-xdist"
- name: "macos-py312"
python: "3.12-dev"
os: macos-latest
tox_env: "py312-xdist"
- name: "plugins"
python: "3.9"
@@ -163,6 +179,7 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
check-latest: ${{ endsWith(matrix.python, '-dev') }}
- name: Install dependencies
run: |
@@ -189,3 +206,10 @@ jobs:
fail_ci_if_error: true
files: ./coverage.xml
verbose: true
check-package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5
+4 -4
View File
@@ -11,7 +11,7 @@ on:
permissions: {}
jobs:
createPullRequest:
update-plugin-list:
if: github.repository_owner == 'pytest-dev'
runs-on: ubuntu-latest
permissions:
@@ -20,12 +20,12 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: 3.8
@@ -38,7 +38,7 @@ jobs:
run: python scripts/update-plugin-list.py
- name: Create Pull Request
uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5
with:
commit-message: '[automated] Update plugin list'
author: 'pytest bot <pytestbot@users.noreply.github.com>'
+13 -15
View File
@@ -1,18 +1,16 @@
default_language_version:
python: "3.10"
repos:
- repo: https://github.com/psf/black
rev: 22.10.0
rev: 23.3.0
hooks:
- id: black
args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs
rev: v1.12.1
rev: 1.13.0
hooks:
- id: blacken-docs
additional_dependencies: [black==20.8b1]
additional_dependencies: [black==23.1.0]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -23,7 +21,7 @@ repos:
exclude: _pytest/(debugging|hookspec).py
language_version: python3
- repo: https://github.com/PyCQA/autoflake
rev: v1.7.6
rev: v2.1.1
hooks:
- id: autoflake
name: autoflake
@@ -31,34 +29,34 @@ repos:
language: python
files: \.py$
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
rev: 6.0.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.8.5
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.9.0
hooks:
- id: reorder-python-imports
args: ['--application-directories=.:src', --py37-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v3.1.0
rev: v3.4.0
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.1.0
rev: v2.3.0
hooks:
- id: setup-cfg-fmt
args: ["--max-py-version=3.10", "--include-version-classifiers"]
args: ["--max-py-version=3.12", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0
rev: v1.10.0
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.982
rev: v1.3.0
hooks:
- id: mypy
files: ^(src/|testing/)
+6 -3
View File
@@ -2,9 +2,12 @@ version: 2
python:
install:
- requirements: doc/en/requirements.txt
- method: pip
path: .
# Install pytest first, then doc/en/requirements.txt.
# This order is important to honor any pins in doc/en/requirements.txt
# when the pinned library is also a dependency of pytest.
- method: pip
path: .
- requirements: doc/en/requirements.txt
build:
os: ubuntu-20.04
+28 -4
View File
@@ -8,10 +8,14 @@ Abdeali JK
Abdelrahman Elbehery
Abhijeet Kasurde
Adam Johnson
Adam Stewart
Adam Uhlir
Ahn Ki-Wook
Akiomi Kamakura
Alan Velasco
Alessio Izzo
Alex Jones
Alex Lambson
Alexander Johnson
Alexander King
Alexei Kozlenok
@@ -43,6 +47,7 @@ Ariel Pillemer
Armin Rigo
Aron Coyle
Aron Curzon
Ashish Kurmi
Aviral Verma
Aviv Palivoda
Babak Keyvani
@@ -53,10 +58,12 @@ Benjamin Peterson
Bernard Pratz
Bob Ippolito
Brian Dorsey
Brian Larsen
Brian Maissy
Brian Okken
Brianna Laugher
Bruno Oliveira
Cal Jacobson
Cal Leeming
Carl Friedrich Bolz
Carlos Jenkins
@@ -65,6 +72,7 @@ Charles Cloud
Charles Machalow
Charnjit SiNGH (CCSJ)
Cheuk Ting Ho
Chris Mahoney
Chris Lamb
Chris NeJame
Chris Rose
@@ -88,6 +96,7 @@ Daniel Grana
Daniel Hahler
Daniel Nuri
Daniel Sánchez Castelló
Daniel Valenzuela Zenteno
Daniel Wandschneider
Daniele Procida
Danielle Jenkins
@@ -122,8 +131,10 @@ Eric Siegerman
Erik Aronesty
Erik M. Bray
Evan Kepner
Evgeny Seliverstov
Fabien Zarifian
Fabio Zadrozny
Felix Hofstätter
Felix Nieuwenhuizen
Feng Ma
Florian Bruhin
@@ -158,6 +169,7 @@ Ionuț Turturică
Itxaso Aizpurua
Iwan Briquemont
Jaap Broekhuizen
Jake VanderPlas
Jakob van Santen
Jakub Mitoraj
James Bourbeau
@@ -182,10 +194,11 @@ Joseph Hunkeler
Josh Karpel
Joshua Bronson
Jurko Gospodnetić
Justyna Janczyszyn
Justice Ndou
Justyna Janczyszyn
Kale Kundert
Kamran Ahmad
Kenny Y
Karl O. Pinc
Karthikeyan Singaravelan
Katarzyna Jachim
@@ -222,6 +235,7 @@ Marcin Bachry
Marco Gorelli
Mark Abramowitz
Mark Dickinson
Marko Pacak
Markus Unterwaditzer
Martijn Faassen
Martin Altmayer
@@ -235,7 +249,6 @@ Matthias Hafner
Maxim Filipenko
Maximilian Cosmo Sitter
mbyt
Mickey Pashov
Michael Aquilina
Michael Birtwell
Michael Droettboom
@@ -244,6 +257,7 @@ Michael Krebs
Michael Seifert
Michal Wajszczuk
Michał Zięba
Mickey Pashov
Mihai Capotă
Mike Hoyle (hoylemd)
Mike Lundy
@@ -258,9 +272,9 @@ Niclas Olofsson
Nicolas Delaby
Nikolay Kondratyev
Nipunn Koorapati
Olga Matoula
Oleg Pidsadnyi
Oleg Sushchenko
Olga Matoula
Oliver Bestwalter
Omar Kohl
Omer Hadari
@@ -276,6 +290,7 @@ Paweł Adamczak
Pedro Algarvio
Petter Strandmark
Philipp Loose
Pierre Sassoulas
Pieter Mulder
Piotr Banaszkiewicz
Piotr Helm
@@ -285,15 +300,18 @@ Prashant Sharma
Pulkit Goyal
Punyashloka Biswal
Quentin Pradet
q0w
Ralf Schmitt
Ram Rachum
Ralph Giles
Ram Rachum
Ran Benita
Raphael Castaneda
Raphael Pierzina
Rafal Semik
Raquel Alegre
Ravi Chandra
Robert Holt
Roberto Aldera
Roberto Polli
Roland Puntaier
Romain Dorgueil
@@ -310,6 +328,7 @@ Samuel Searles-Bryant
Samuele Pedroni
Sanket Duthade
Sankt Petersbug
Saravanan Padmanaban
Segev Finer
Serhii Mozghovyi
Seth Junot
@@ -323,6 +342,7 @@ Srinivas Reddy Thatiparthy
Stefan Farmbauer
Stefan Scherfke
Stefan Zimmermann
Stefanie Molin
Stefano Taschini
Steffen Allner
Stephan Obermann
@@ -341,6 +361,7 @@ Thomas Grainger
Thomas Hisch
Tim Hoffmann
Tim Strazny
TJ Bruno
Tobias Diez
Tom Dalton
Tom Viner
@@ -371,7 +392,10 @@ Wouter van Ackooy
Xixi Zhao
Xuan Luong
Xuecong Liao
Yannick Péroux
Yoav Caspi
Yuliang Shao
Yusuke Kadowaki
Yuval Shimon
Zac Hatfield-Dodds
Zachary Kneupper
+1
View File
@@ -0,0 +1 @@
Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
+1
View File
@@ -0,0 +1 @@
Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
+2
View File
@@ -0,0 +1,2 @@
Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
+5
View File
@@ -0,0 +1,5 @@
When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
Previously, the last frame of the traceback was shown, even though it was hidden.
+3
View File
@@ -0,0 +1,3 @@
Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
Added :func:`TerminalReporter.wrap_write() <pytest.TerminalReporter.wrap_write>` as a helper for that.
+1
View File
@@ -0,0 +1 @@
Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
+1
View File
@@ -0,0 +1 @@
Added underlying exception to cache provider path creation and write warning messages.
+1
View File
@@ -0,0 +1 @@
Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
+1
View File
@@ -0,0 +1 @@
Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
+3
View File
@@ -0,0 +1,3 @@
When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir`.
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
+1
View File
@@ -0,0 +1 @@
Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
+7
View File
@@ -0,0 +1,7 @@
The :confval:`norecursedir` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
If after updating to this version you see that your `norecursedir` setting is not being respected,
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
Most likely, your hook returns `False` for paths it does not want to ignore,
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
+1
View File
@@ -0,0 +1 @@
Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
+3
View File
@@ -0,0 +1,3 @@
:func:`_pytest.logging.LogCaptureFixture.set_level` and :func:`_pytest.logging.LogCaptureFixture.at_level`
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
``logging.disable(LEVEL)``.
+5
View File
@@ -6,6 +6,11 @@ Release announcements
:maxdepth: 2
release-7.3.2
release-7.3.1
release-7.3.0
release-7.2.2
release-7.2.1
release-7.2.0
release-7.1.3
release-7.1.2
+25
View File
@@ -0,0 +1,25 @@
pytest-7.2.1
=======================================
pytest 7.2.1 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Anthony Sottile
* Bruno Oliveira
* Daniel Valenzuela
* Kadino
* Prerak Patel
* Ronny Pfannschmidt
* Santiago Castro
* s-padmanaban
Happy testing,
The pytest Development Team
+25
View File
@@ -0,0 +1,25 @@
pytest-7.2.2
=======================================
pytest 7.2.2 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Bruno Oliveira
* Garvit Shubham
* Mahesh Vashishtha
* Ramsey
* Ronny Pfannschmidt
* Teejay
* q0w
* vin01
Happy testing,
The pytest Development Team
+130
View File
@@ -0,0 +1,130 @@
pytest-7.3.0
=======================================
The pytest team is proud to announce the 7.3.0 release!
This release contains new features, improvements, and bug fixes,
the full list of changes is available in the changelog:
https://docs.pytest.org/en/stable/changelog.html
For complete documentation, please visit:
https://docs.pytest.org/en/stable/
As usual, you can upgrade from PyPI via:
pip install -U pytest
Thanks to all of the contributors to this release:
* Aaron Berdy
* Adam Turner
* Albert Villanova del Moral
* Alessio Izzo
* Alex Hadley
* Alice Purcell
* Anthony Sottile
* Anton Yakutovich
* Ashish Kurmi
* Babak Keyvani
* Billy
* Brandon Chinn
* Bruno Oliveira
* Cal Jacobson
* Chanvin Xiao
* Cheuk Ting Ho
* Chris Wheeler
* Daniel Garcia Moreno
* Daniel Scheffler
* Daniel Valenzuela
* EmptyRabbit
* Ezio Melotti
* Felix Hofstätter
* Florian Best
* Florian Bruhin
* Fredrik Berndtsson
* Gabriel Landau
* Garvit Shubham
* Gergely Kalmár
* HTRafal
* Hugo van Kemenade
* Ilya Konstantinov
* Itxaso Aizpurua
* James Gerity
* Jay
* John Litborn
* Jon Parise
* Jouke Witteveen
* Kadino
* Kevin C
* Kian Eliasi
* Klaus Rettinghaus
* Kodi Arfer
* Mahesh Vashishtha
* Manuel Jacob
* Marko Pacak
* MatthewFlamm
* Miro Hrončok
* Nate Meyvis
* Neil Girdhar
* Nhieuvu1802
* Nipunn Koorapati
* Ofek Lev
* Paul Kehrer
* Paul Müller
* Paul Reece
* Pax
* Pete Baughman
* Peyman Salehi
* Philipp A
* Pierre Sassoulas
* Prerak Patel
* Ramsey
* Ran Benita
* Robert O'Shea
* Ronny Pfannschmidt
* Rowin
* Ruth Comer
* Samuel Colvin
* Samuel Gaist
* Sandro Tosi
* Santiago Castro
* Shantanu
* Simon K
* Stefanie Molin
* Stephen Rosen
* Sviatoslav Sydorenko
* Tatiana Ovary
* Teejay
* Thierry Moisan
* Thomas Grainger
* Tim Hoffmann
* Tobias Diez
* Tony Narlock
* Vivaan Verma
* Wolfremium
* Yannick PÉROUX
* Yusuke Kadowaki
* Zac Hatfield-Dodds
* Zach OBrien
* aizpurua23a
* bitzge
* bluthej
* gresm
* holesch
* itxasos23
* johnkangw
* q0w
* rdb
* s-padmanaban
* skhomuti
* sommersoft
* vin01
* wim glenn
* wodny
* zx.qiu
Happy testing,
The pytest Development Team
+18
View File
@@ -0,0 +1,18 @@
pytest-7.3.1
=======================================
pytest 7.3.1 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Ran Benita
Happy testing,
The pytest Development Team
+21
View File
@@ -0,0 +1,21 @@
pytest-7.3.2
=======================================
pytest 7.3.2 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Adam J. Stewart
* Alessio Izzo
* Bruno Oliveira
* Ran Benita
Happy testing,
The pytest Development Team
+2
View File
@@ -92,3 +92,5 @@ pytest version min. Python version
5.0 - 6.1 3.5+
3.3 - 4.6 2.7, 3.4+
============== ===================
`Status of Python Versions <https://devguide.python.org/versions/>`__.
+31 -29
View File
@@ -33,25 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Values can be any object handled by the json stdlib module.
capsys -- .../_pytest/capture.py:905
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsys.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``text`` objects.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
print("hello")
captured = capsys.readouterr()
assert captured.out == "hello\n"
capsysbinary -- .../_pytest/capture.py:933
capsysbinary -- .../_pytest/capture.py:1001
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsysbinary.readouterr()``
@@ -69,7 +51,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsysbinary.readouterr()
assert captured.out == b"hello\n"
capfd -- .../_pytest/capture.py:961
capfd -- .../_pytest/capture.py:1029
Enable text capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
@@ -87,7 +69,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capfd.readouterr()
assert captured.out == "hello\n"
capfdbinary -- .../_pytest/capture.py:989
capfdbinary -- .../_pytest/capture.py:1057
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
@@ -105,7 +87,25 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capfdbinary.readouterr()
assert captured.out == b"hello\n"
doctest_namespace [session scope] -- .../_pytest/doctest.py:738
capsys -- .../_pytest/capture.py:973
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsys.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``text`` objects.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
print("hello")
captured = capsys.readouterr()
assert captured.out == "hello\n"
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 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1351
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
Session-scoped fixture that returns the session's :class:`pytest.Config`
object.
@@ -196,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:491
caplog -- .../_pytest/logging.py:498
Access and control log capturing.
Captured logs are available through the following properties/methods::
@@ -207,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
* caplog.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string
monkeypatch -- .../_pytest/monkeypatch.py:29
monkeypatch -- .../_pytest/monkeypatch.py:30
A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or
@@ -237,17 +237,19 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
on warning categories.
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:188
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:245
Return a :class:`pytest.TempPathFactory` instance for the test session.
tmp_path -- .../_pytest/tmpdir.py:203
tmp_path -- .../_pytest/tmpdir.py:260
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
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`.
The returned object is a :class:`pathlib.Path` object.
+187 -12
View File
@@ -28,6 +28,186 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 7.3.2 (2023-06-10)
=========================
Bug Fixes
---------
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
pytest 7.3.1 (2023-04-14)
=========================
Improvements
------------
- `#10875 <https://github.com/pytest-dev/pytest/issues/10875>`_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests.
- `#10890 <https://github.com/pytest-dev/pytest/issues/10890>`_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.
Bug Fixes
---------
- `#10896 <https://github.com/pytest-dev/pytest/issues/10896>`_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option.
- `#10903 <https://github.com/pytest-dev/pytest/issues/10903>`_: Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden.
This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0.
pytest 7.3.0 (2023-04-08)
=========================
Features
--------
- `#10525 <https://github.com/pytest-dev/pytest/issues/10525>`_: Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods.
- `#10755 <https://github.com/pytest-dev/pytest/issues/10755>`_: :confval:`console_output_style` now supports ``progress-even-when-capture-no`` to force the use of the progress output even when capture is disabled. This is useful in large test suites where capture may have significant performance impact.
- `#7431 <https://github.com/pytest-dev/pytest/issues/7431>`_: ``--log-disable`` CLI option added to disable individual loggers.
- `#8141 <https://github.com/pytest-dev/pytest/issues/8141>`_: Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
Improvements
------------
- `#10226 <https://github.com/pytest-dev/pytest/issues/10226>`_: If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
- `#10658 <https://github.com/pytest-dev/pytest/issues/10658>`_: Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of
``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration
file.
- `#10710 <https://github.com/pytest-dev/pytest/issues/10710>`_: Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.
- `#10727 <https://github.com/pytest-dev/pytest/issues/10727>`_: Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.
- `#10840 <https://github.com/pytest-dev/pytest/issues/10840>`_: pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`.
- `#6267 <https://github.com/pytest-dev/pytest/issues/6267>`_: The full output of a test is no longer truncated if the truncation message would be longer than
the hidden text. The line number shown has also been fixed.
Bug Fixes
---------
- `#10743 <https://github.com/pytest-dev/pytest/issues/10743>`_: The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.
- `#10765 <https://github.com/pytest-dev/pytest/issues/10765>`_: Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions.
NOTE: This change was reverted in version 7.3.1.
Improved Documentation
----------------------
- `#10782 <https://github.com/pytest-dev/pytest/issues/10782>`_: Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.
Trivial/Internal Changes
------------------------
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer directly depends on the `attrs <https://www.attrs.org/en/stable/>`__ package. While
we at pytest all love the package dearly and would like to thank the ``attrs`` team for many years of cooperation and support,
it makes sense for ``pytest`` to have as little external dependencies as possible, as this helps downstream projects.
With that in mind, we have replaced the pytest's limited internal usage to use the standard library's ``dataclasses`` instead.
Nice diffs for ``attrs`` classes are still supported though.
pytest 7.2.2 (2023-03-03)
=========================
Bug Fixes
---------
- `#10533 <https://github.com/pytest-dev/pytest/issues/10533>`_: Fixed :func:`pytest.approx` handling of dictionaries containing one or more values of `0.0`.
- `#10592 <https://github.com/pytest-dev/pytest/issues/10592>`_: Fixed crash if `--cache-show` and `--help` are passed at the same time.
- `#10597 <https://github.com/pytest-dev/pytest/issues/10597>`_: Fixed bug where a fixture method named ``teardown`` would be called as part of ``nose`` teardown stage.
- `#10626 <https://github.com/pytest-dev/pytest/issues/10626>`_: Fixed crash if ``--fixtures`` and ``--help`` are passed at the same time.
- `#10660 <https://github.com/pytest-dev/pytest/issues/10660>`_: Fixed :py:func:`pytest.raises` to return a 'ContextManager' so that type-checkers could narrow
:code:`pytest.raises(...) if ... else nullcontext()` down to 'ContextManager' rather than 'object'.
Improved Documentation
----------------------
- `#10690 <https://github.com/pytest-dev/pytest/issues/10690>`_: Added `CI` and `BUILD_NUMBER` environment variables to the documentation.
- `#10721 <https://github.com/pytest-dev/pytest/issues/10721>`_: Fixed entry-points declaration in the documentation example using Hatch.
- `#10753 <https://github.com/pytest-dev/pytest/issues/10753>`_: Changed wording of the module level skip to be very explicit
about not collecting tests and not executing the rest of the module.
pytest 7.2.1 (2023-01-13)
=========================
Bug Fixes
---------
- `#10452 <https://github.com/pytest-dev/pytest/issues/10452>`_: Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12.
- `#10457 <https://github.com/pytest-dev/pytest/issues/10457>`_: If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location.
- `#10506 <https://github.com/pytest-dev/pytest/issues/10506>`_: Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows.
- `#10607 <https://github.com/pytest-dev/pytest/issues/10607>`_: Fix a race condition when creating junitxml reports, which could occur when multiple instances of pytest execute in parallel.
- `#10641 <https://github.com/pytest-dev/pytest/issues/10641>`_: Fix a race condition when creating or updating the stepwise plugin's cache, which could occur when multiple xdist worker nodes try to simultaneously update the stepwise plugin's cache.
pytest 7.2.0 (2022-10-23)
=========================
@@ -57,6 +237,7 @@ Deprecations
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
- `#7337 <https://github.com/pytest-dev/pytest/issues/7337>`_: A deprecation warning is now emitted if a test function returns something other than `None`. This prevents a common mistake among beginners that expect that returning a `bool` (for example `return foo(a, b) == result`) would cause a test to pass or fail, instead of using `assert`. The plan is to make returning non-`None` from tests an error in the future.
Features
@@ -79,10 +260,7 @@ Improvements
- `#10381 <https://github.com/pytest-dev/pytest/issues/10381>`_: The ``--no-showlocals`` flag has been added. This can be passed directly to tests to override ``--showlocals`` declared through ``addopts``.
- `#3426 <https://github.com/pytest-dev/pytest/issues/3426>`_: Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expresed instead.
- `#7337 <https://github.com/pytest-dev/pytest/issues/7337>`_: A warning is now emitted if a test function returns something other than `None`. This prevents a common mistake among beginners that expect that returning a `bool` (for example `return foo(a, b) == result`) would cause a test to pass or fail, instead of using `assert`.
- `#3426 <https://github.com/pytest-dev/pytest/issues/3426>`_: Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expressed instead.
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
@@ -95,7 +273,7 @@ Improvements
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
:mod:`tomli`` is no longer a dependency on Python 3.11.
:mod:`tomli` is no longer a dependency on Python 3.11.
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
@@ -110,7 +288,7 @@ Improvements
- `#9883 <https://github.com/pytest-dev/pytest/issues/9883>`_: Normalize the help description of all command-line options.
- `#9920 <https://github.com/pytest-dev/pytest/issues/9920>`_: Display full crash messages in ``short test summary info``, when runng in a CI environment.
- `#9920 <https://github.com/pytest-dev/pytest/issues/9920>`_: Display full crash messages in ``short test summary info``, when running in a CI environment.
- `#9987 <https://github.com/pytest-dev/pytest/issues/9987>`_: Added support for hidden configuration file by allowing ``.pytest.ini`` as an alternative to ``pytest.ini``.
@@ -156,9 +334,6 @@ Improved Documentation
Trivial/Internal Changes
------------------------
- `#10196 <https://github.com/pytest-dev/pytest/issues/10196>`_: :class:`~pytest.PytestReturnNotNoneWarning` is now a subclass of :class:`~pytest.PytestRemovedIn8Warning`: the plan is to make returning non-``None`` from tests an error in the future.
- `#10313 <https://github.com/pytest-dev/pytest/issues/10313>`_: Made ``_pytest.doctest.DoctestItem`` export ``pytest.DoctestItem`` for
type check and runtime purposes. Made `_pytest.doctest` use internal APIs
to avoid circular imports.
@@ -173,7 +348,7 @@ Trivial/Internal Changes
- `#9984 <https://github.com/pytest-dev/pytest/issues/9984>`_: Improve the error message when we attempt to access a fixture that has been
torn down.
Add an additional sentence to the docstring explaining when it's not a good
idea to call getfixturevalue.
idea to call ``getfixturevalue``.
pytest 7.1.3 (2022-08-31)
@@ -416,7 +591,7 @@ Breaking Changes
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
@@ -3916,7 +4091,7 @@ Removals
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
Use ``Node.get_closest_marker(name)`` as a replacement.
+1 -1
View File
@@ -341,7 +341,7 @@ epub_copyright = "2013, holger krekel et alii"
# The scheme of the identifier. Typical schemes are ISBN or URL.
# epub_scheme = ''
# The unique identifier of the text. This can be a ISBN number
# The unique identifier of the text. This can be an ISBN number
# or the project homepage.
# epub_identifier = ''
+2 -2
View File
@@ -1052,7 +1052,7 @@ that are then turned into proper test methods. Example:
.. code-block:: python
def check(x, y):
assert x ** x == y
assert x**x == y
def test_squared():
@@ -1067,7 +1067,7 @@ This form of test function doesn't support fixtures properly, and users should s
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
def test_squared(x, y):
assert x ** x == y
assert x**x == y
.. _internal classes accessed through node deprecated:
@@ -17,7 +17,7 @@ def b(a, order):
@pytest.fixture
def c(a, b, order):
def c(b, order):
order.append("c")
+1
View File
@@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
" no further details known at this point.",
]
)
return super().repr_failure(excinfo)
def reportinfo(self):
return self.path, 0, f"usecase: {self.name}"
+4 -4
View File
@@ -504,9 +504,9 @@ Running it results in some skips if we don't have all the python interpreters in
. $ pytest -rs -q multipython.py
sssssssssssssssssssssssssss [100%]
========================= short test summary info ==========================
SKIPPED [9] multipython.py:29: 'python3.5' not found
SKIPPED [9] multipython.py:29: 'python3.6' not found
SKIPPED [9] multipython.py:29: 'python3.7' not found
SKIPPED [9] multipython.py:69: 'python3.5' not found
SKIPPED [9] multipython.py:69: 'python3.6' not found
SKIPPED [9] multipython.py:69: 'python3.7' not found
27 skipped in 0.12s
Indirect parametrization of optional implementations/imports
@@ -574,7 +574,7 @@ If you run this with reporting for skips enabled:
test_module.py .s [100%]
========================= short test summary info ==========================
SKIPPED [1] conftest.py:12: could not import 'opt2': No module named 'opt2'
SKIPPED [1] test_module.py:3: could not import 'opt2': No module named 'opt2'
======================= 1 passed, 1 skipped in 0.12s =======================
You'll see that we don't have an ``opt2`` module and thus the second test run
+6 -3
View File
@@ -148,7 +148,8 @@ The test collection would look like this:
$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini
rootdir: /home/sweet/project
configfile: pytest.ini
collected 2 items
<Module check_myapp.py>
@@ -209,7 +210,8 @@ You can always peek at the collection tree without running tests like this:
. $ pytest --collect-only pythoncollection.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini
rootdir: /home/sweet/project
configfile: pytest.ini
collected 3 items
<Module CWD/pythoncollection.py>
@@ -290,7 +292,8 @@ file will be left out:
$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini
rootdir: /home/sweet/project
configfile: pytest.ini
collected 0 items
======================= no tests collected in 0.12s ========================
+15 -18
View File
@@ -144,7 +144,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E 1
E 1...
E
E ...Full output truncated (7 lines hidden), use '-vv' to show
E ...Full output truncated (6 lines hidden), use '-vv' to show
failure_demo.py:60: AssertionError
_________________ TestSpecialisedExplanations.test_eq_list _________________
@@ -184,9 +184,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E Left contains 1 more item:
E {'c': 0}
E Right contains 1 more item:
E {'d': 0}...
E
E ...Full output truncated (2 lines hidden), use '-vv' to show
E {'d': 0}
E Use -v to get more diff
failure_demo.py:71: AssertionError
_________________ TestSpecialisedExplanations.test_eq_set __________________
@@ -195,16 +194,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
def test_eq_set(self):
> assert {0, 10, 11, 12} == {0, 20, 21}
E AssertionError: assert {0, 10, 11, 12} == {0, 20, 21}
E assert {0, 10, 11, 12} == {0, 20, 21}
E Extra items in the left set:
E 10
E 11
E 12
E Extra items in the right set:
E 20
E 21...
E
E ...Full output truncated (2 lines hidden), use '-vv' to show
E 21
E Use -v to get more diff
failure_demo.py:74: AssertionError
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
@@ -241,9 +239,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E which
E includes foo
E ? +++
E and a...
E
E ...Full output truncated (2 lines hidden), use '-vv' to show
E and a
E tail
failure_demo.py:84: AssertionError
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
@@ -307,9 +304,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E ['b']
E
E Drill down into differing attribute b:
E b: 'b' != 'c'...
E
E ...Full output truncated (3 lines hidden), use '-vv' to show
E b: 'b' != 'c'
E - c
E + b
failure_demo.py:108: AssertionError
________________ TestSpecialisedExplanations.test_eq_attrs _________________
@@ -334,9 +331,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E ['b']
E
E Drill down into differing attribute b:
E b: 'b' != 'c'...
E
E ...Full output truncated (3 lines hidden), use '-vv' to show
E b: 'b' != 'c'
E - c
E + b
failure_demo.py:120: AssertionError
______________________________ test_attribute ______________________________
@@ -673,7 +670,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list - asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_long - ...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dict - Asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - Assert...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - assert...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_longer_list
FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline
+14 -12
View File
@@ -691,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory:
pass
@pytest.fixture(scope="session")
@pytest.fixture(scope="package")
def db():
return DB()
@@ -892,8 +892,11 @@ here is a little example implemented via a local plugin:
.. code-block:: python
# content of conftest.py
from typing import Dict
import pytest
from pytest import StashKey, CollectReport
phase_report_key = StashKey[Dict[str, CollectReport]]()
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
@@ -902,10 +905,9 @@ here is a little example implemented via a local plugin:
outcome = yield
rep = outcome.get_result()
# set a report attribute for each phase of a call, which can
# store test results for each phase of a call, which can
# be "setup", "call", "teardown"
setattr(item, "rep_" + rep.when, rep)
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
@pytest.fixture
@@ -913,11 +915,11 @@ here is a little example implemented via a local plugin:
yield
# request.node is an "item" because we use the default
# "function" scope
if request.node.rep_setup.failed:
print("setting up a test failed!", request.node.nodeid)
elif request.node.rep_setup.passed:
if request.node.rep_call.failed:
print("executing test failed", request.node.nodeid)
report = request.node.stash[phase_report_key]
if report["setup"].failed:
print("setting up a test failed or skipped", request.node.nodeid)
elif ("call" not in report) or report["call"].failed:
print("executing test failed or skipped", request.node.nodeid)
if you then have failing tests:
@@ -955,8 +957,8 @@ and run it:
rootdir: /home/sweet/project
collected 3 items
test_module.py Esetting up a test failed! test_module.py::test_setup_fails
Fexecuting test failed test_module.py::test_call_fails
test_module.py Esetting up a test failed or skipped test_module.py::test_setup_fails
Fexecuting test failed or skipped test_module.py::test_call_fails
F
================================== ERRORS ==================================
+23 -5
View File
@@ -24,8 +24,9 @@ The first few lines should look like this:
[project]
name = "PACKAGENAME"
version = "PACKAGEVERSION"
where ``PACKAGENAME`` is the name of your package.
where ``PACKAGENAME`` and ``PACKAGEVERSION`` are the name and version of your package respectively.
You can then install your package in "editable" mode by running from the same directory:
@@ -50,8 +51,8 @@ Conventions for Python test discovery
* In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_.
* From those files, collect test items:
* ``test`` prefixed test functions or methods outside of class
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method)
* ``test`` prefixed test functions or methods outside of class.
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method). Methods decorated with ``@staticmethod`` and ``@classmethods`` are also considered.
For examples of how to customize your test discovery :doc:`/example/pythoncollection`.
@@ -270,8 +271,8 @@ tox
Once you are done with your work and want to make sure that your actual
package passes all tests you may want to look into :doc:`tox <tox:index>`, the
virtualenv test automation tool and its :doc:`pytest support <tox:example/pytest>`.
tox helps you to setup virtualenv environments with pre-defined
virtualenv test automation tool.
``tox`` helps you to setup virtualenv environments with pre-defined
dependencies and then executing a pre-configured test command with
options. It will run tests against the installed package and not
against your source code checkout, helping to detect packaging
@@ -293,3 +294,20 @@ See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`
setuptools intends to
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
Checking with flake8-pytest-style
---------------------------------
In order to ensure that pytest is being used correctly in your project,
it can be helpful to use the `flake8-pytest-style <https://github.com/m-burst/flake8-pytest-style>`_ flake8 plugin.
flake8-pytest-style checks for common mistakes and coding style violations in pytest code,
such as incorrect use of fixtures, test function names, and markers.
By using this plugin, you can catch these errors early in the development process
and ensure that your pytest code is consistent and easy to maintain.
A list of the lints detected by flake8-pytest-style can be found on its `PyPI page <https://pypi.org/project/flake8-pytest-style/>`_.
.. note::
flake8-pytest-style is not an official pytest project. Some of the rules enforce certain style choices, such as using `@pytest.fixture()` over `@pytest.fixture`, but you can configure the plugin to fit your preferred style.
+3 -3
View File
@@ -16,7 +16,7 @@ import process can be controlled through the ``--import-mode`` command-line flag
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:`__import__ <__import__>` builtin.
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
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.
@@ -24,7 +24,7 @@ these values:
This is the classic mechanism, dating back from the time Python 2 was still supported.
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
there, and imported with ``__import__``.
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
This better allows to run test modules against installed versions of a package even if the
package under test has the same import root. For example:
@@ -43,7 +43,7 @@ these values:
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 :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
* ``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`.
For this reason this doesn't require test module names to be unique.
+1 -1
View File
@@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 7.2.0
pytest 7.3.2
.. _`simpletest`:
+30 -4
View File
@@ -109,6 +109,18 @@ When a warning matches more than one option in the list, the action for the last
is performed.
.. note::
The ``-W`` flag and the ``filterwarnings`` ini option use warning filters that are
similar in structure, but each configuration option interprets its filter
differently. For example, *message* in ``filterwarnings`` is a string containing a
regular expression that the start of the warning message must match,
case-insensitively, while *message* in ``-W`` is a literal string that the start of
the warning message must contain (case-insensitively), ignoring any whitespace at
the start or end of message. Consult the `warning filter`_ documentation for more
details.
.. _`filterwarnings`:
``@pytest.mark.filterwarnings``
@@ -270,20 +282,34 @@ which works in a similar manner to :ref:`raises <assertraises>` (except that
warnings.warn("my warning", UserWarning)
The test will fail if the warning in question is not raised. Use the keyword
argument ``match`` to assert that the warning matches a text or regex::
argument ``match`` to assert that the warning matches a text or regex.
To match a literal string that may contain regular expression metacharacters like ``(`` or ``.``, the pattern can
first be escaped with ``re.escape``.
>>> with warns(UserWarning, match='must be 0 or None'):
Some examples:
.. code-block:: pycon
>>> with warns(UserWarning, match="must be 0 or None"):
... warnings.warn("value must be 0 or None", UserWarning)
...
>>> with warns(UserWarning, match=r'must be \d+$'):
>>> with warns(UserWarning, match=r"must be \d+$"):
... warnings.warn("value must be 42", UserWarning)
...
>>> with warns(UserWarning, match=r'must be \d+$'):
>>> with warns(UserWarning, match=r"must be \d+$"):
... warnings.warn("this is not here", UserWarning)
...
Traceback (most recent call last):
...
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
>>> with warns(UserWarning, match=re.escape("issue with foo() func")):
... warnings.warn("issue with foo() func")
...
You can also call :func:`pytest.warns` on a function or code string:
.. code-block:: python
-1
View File
@@ -1237,7 +1237,6 @@ If the data created by the factory requires managing, the fixture can take care
@pytest.fixture
def make_customer_record():
created_records = []
def _make_customer_record(name):
+7
View File
@@ -55,6 +55,13 @@ These options can also be customized through ``pytest.ini`` file:
log_format = %(asctime)s %(levelname)s %(message)s
log_date_format = %Y-%m-%d %H:%M:%S
Specific loggers can be disabled via ``--log-disable={logger_name}``.
This argument can be passed multiple times:
.. code-block:: bash
pytest --log-disable=main --log-disable=testing
Further it is possible to disable reporting of captured content (stdout,
stderr and logs) on failed tests completely with:
+3 -6
View File
@@ -135,10 +135,10 @@ This can be done in our test file by defining a class to represent ``r``.
# this is the previous code block example
import app
# custom class to be the mock return value
# will override the requests.Response returned from requests.get
class MockResponse:
# mock json() method always returns a specific testing dictionary
@staticmethod
def json():
@@ -146,7 +146,6 @@ This can be done in our test file by defining a class to represent ``r``.
def test_get_json(monkeypatch):
# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method.
def mock_get(*args, **kwargs):
@@ -181,6 +180,7 @@ This mock can be shared across tests using a ``fixture``:
# app.py that includes the get_json() function
import app
# custom class to be the mock return value of requests.get()
class MockResponse:
@staticmethod
@@ -358,7 +358,6 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific
def test_connection(monkeypatch):
# Patch the values of DEFAULT_CONFIG to specific
# testing values only for this test.
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
@@ -383,7 +382,6 @@ You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove v
def test_missing_user(monkeypatch):
# patch the DEFAULT_CONFIG t be missing the 'user' key
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
@@ -404,6 +402,7 @@ separate fixtures for each potential mock and reference them in the needed tests
# app.py with the connection string function
import app
# all of the mocks are moved into separated fixtures
@pytest.fixture
def mock_test_user(monkeypatch):
@@ -425,7 +424,6 @@ separate fixtures for each potential mock and reference them in the needed tests
# tests reference only the fixture mocks that are needed
def test_connection(mock_test_user, mock_test_database):
expected = "User Id=test_user; Location=test_db;"
result = app.create_connection_string()
@@ -433,7 +431,6 @@ separate fixtures for each potential mock and reference them in the needed tests
def test_missing_user(mock_missing_default_user):
with pytest.raises(KeyError):
_ = app.create_connection_string()
+3 -3
View File
@@ -167,9 +167,9 @@ Now we can increase pytest's verbosity:
E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40}
E Full diff:
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}...
E
E ...Full output truncated (3 lines hidden), use '-vv' to show
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
E ? - - - - - - - -
E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________
+5 -3
View File
@@ -131,10 +131,12 @@ The default base temporary directory
Temporary directories are by default created as sub-directories of
the system temporary directory. The base name will be ``pytest-NUM`` where
``NUM`` will be incremented with each test run. Moreover, entries older
than 3 temporary directories will be removed.
``NUM`` will be incremented with each test run.
By default, entries older than 3 temporary directories will be removed.
This behavior can be configured with :confval:`tmp_path_retention_count` and
:confval:`tmp_path_retention_policy`.
The number of entries currently cannot be changed, but using the ``--basetemp``
Using the ``--basetemp``
option will remove the directory before every run, effectively meaning the temporary directories
of only the most recent run will be kept.
+2 -1
View File
@@ -35,11 +35,12 @@ Pytest supports several ways to run and select tests from the command-line.
.. code-block:: bash
pytest -k "MyClass and not method"
pytest -k 'MyClass and not method'
This will run tests which contain names that match the given *string expression* (case-insensitive),
which can include Python operators that use filenames, class names and function names as variables.
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
Use ``""`` instead of ``''`` in expression when running this on Windows
.. _nodeids:
+1
View File
@@ -249,6 +249,7 @@ and use pytest_addoption as follows:
# contents of hooks.py
# Use firstresult=True because we only want one plugin to define this
# default value
@hookspec(firstresult=True)
+4 -8
View File
@@ -167,13 +167,8 @@ it in your ``pyproject.toml`` file.
"Framework :: Pytest",
]
[tool.setuptools]
packages = ["myproject"]
[project.entry_points]
pytest11 = [
"myproject = myproject.pluginmodule",
]
[project.entry-points.pytest11]
myproject = "myproject.pluginmodule"
If a package is installed this way, ``pytest`` will load
``myproject.pluginmodule`` as a plugin which can define
@@ -454,7 +449,8 @@ in our ``pytest.ini`` to tell pytest where to look for example files.
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini
rootdir: /home/sweet/project
configfile: pytest.ini
collected 2 items
test_example.py .. [100%]
+1 -2
View File
@@ -2,8 +2,7 @@
.. sidebar:: Next Open Trainings
- Professionelles Testen für Python mit pytest, part of `enterPy <https://www.enterpy.de/>`__ (German), `October 28th <https://www.enterpy.de/veranstaltung-15409-se-0-professionelles-testen-fuer-python-mit-pytest.html>`__ (sold out) and `November 4th <https://www.enterpy.de/veranstaltung-15557-se-0-professionelles-testen-fuer-python-mit-pytest-zusatztermin.html>`__, online
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany
- `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/Remote
Also see :doc:`previous talks and blogposts <talks>`.
+1 -1
View File
@@ -335,7 +335,7 @@ For example:
.. literalinclude:: /example/fixtures/test_fixtures_order_dependencies.py
If we map out what depends on what, we get something that look like this:
If we map out what depends on what, we get something that looks like this:
.. image:: /example/fixtures/test_fixtures_order_dependencies.*
:align: center
File diff suppressed because it is too large Load Diff
+77 -7
View File
@@ -956,6 +956,12 @@ TestReport
:show-inheritance:
:inherited-members:
TestShortLogReport
~~~~~~~~~~~~~~~~~~
.. autoclass:: pytest.TestShortLogReport()
:members:
_Result
~~~~~~~
@@ -1047,6 +1053,14 @@ Environment Variables
Environment variables that can be used to change pytest's behavior.
.. envvar:: CI
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
.. envvar:: BUILD_NUMBER
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
.. envvar:: PYTEST_ADDOPTS
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given
@@ -1212,6 +1226,7 @@ passed multiple times. The expected format is ``name=value``. For example::
* ``classic``: classic pytest output.
* ``progress``: like classic pytest output, but with a progress indicator.
* ``progress-even-when-capture-no``: allows the use of the progress indicator even when ``capture=no``.
* ``count``: like progress, but shows progress as the number of tests completed instead of a percent.
The default is ``progress``, but you can fallback to ``classic`` if you prefer or
@@ -1704,13 +1719,12 @@ passed multiple times. The expected format is ``name=value``. For example::
.. confval:: testpaths
Sets list of directories that should be searched for tests when
no specific directories, files or test ids are given in the command line when
executing pytest from the :ref:`rootdir <rootdir>` directory.
File system paths may use shell-style wildcards, including the recursive
``**`` pattern.
Useful when all project tests are in a known location to speed up
test collection and to avoid picking up undesired tests by accident.
@@ -1719,8 +1733,51 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
testpaths = testing doc
This tells pytest to only look for tests in ``testing`` and ``doc``
directories when executing from the root directory.
This configuration means that executing:
.. code-block:: console
pytest
has the same practical effects as executing:
.. code-block:: console
pytest testing doc
.. confval:: tmp_path_retention_count
How many sessions should we keep the `tmp_path` directories,
according to `tmp_path_retention_policy`.
.. code-block:: ini
[pytest]
tmp_path_retention_count = 3
Default: ``3``
.. confval:: tmp_path_retention_policy
Controls which directories created by the `tmp_path` fixture are kept around,
based on test outcome.
* `all`: retains directories for all tests, regardless of the outcome.
* `failed`: retains directories only for tests with outcome `error` or `failed`.
* `none`: directories are always removed after each test ends, regardless of the outcome.
.. code-block:: ini
[pytest]
tmp_path_retention_policy = "all"
Default: ``all``
.. confval:: usefixtures
@@ -1861,7 +1918,8 @@ All the command-line flags can be obtained by running ``pytest --help``::
--strict-markers Markers not registered in the `markers` section of
the configuration file raise errors
--strict (Deprecated) alias to --strict-markers
-c file Load configuration from `file` instead of trying to
-c, --config-file FILE
Load configuration from `FILE` instead of trying to
locate one of the implicit configuration files
--continue-on-collection-errors
Force test execution even if collection errors occur
@@ -1952,8 +2010,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
--log-auto-indent=LOG_AUTO_INDENT
Auto-indent multiline messages passed to the logging
module. Accepts true|on, false|off or an integer.
--log-disable=LOGGER_DISABLE
Disable a logger by name. Can be passed multiple
times.
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
markers (linelist): Markers for test functions
empty_parameter_set_mark (string):
@@ -1981,9 +2042,18 @@ All the command-line flags can be obtained by running ``pytest --help``::
console_output_style (string):
Console output: "classic", or with additional
progress information ("progress" (percentage) |
"count")
"count" | "progress-even-when-capture-no" (forces
progress even when capture=no)
xfail_strict (bool): Default for the strict parameter of xfail markers
when not given explicitly (default: False)
tmp_path_retention_count (string):
How many sessions should we keep the `tmp_path`
directories, according to
`tmp_path_retention_policy`.
tmp_path_retention_policy (string):
Controls which directories created by the `tmp_path`
fixture are kept around, based on test outcome.
(all/failed/none)
enable_assertion_pass_hook (bool):
Enables the pytest_assertion_pass hook. Make sure to
delete any previously generated pyc cache files.
+5 -1
View File
@@ -1,7 +1,11 @@
pallets-sphinx-themes
pluggy>=1.0
pygments-pytest>=2.2.0
pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0
sphinx>=5,<6
sphinxcontrib-trio
sphinxcontrib-svg2pdfconverter
# Pin packaging because it no longer handles 'latest' version, which
# is the version that is assigned to the docs.
# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045.
packaging <22
+5
View File
@@ -114,3 +114,8 @@ template = "changelog/_template.rst"
[tool.black]
target-version = ['py37']
# 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"
+33 -14
View File
@@ -17,7 +17,9 @@ Plugin List
===========
PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically. Packages classified as inactive are excluded.
automatically together with a manually-maintained list in `the source
code <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
Packages classified as inactive are excluded.
.. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the
@@ -33,6 +35,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
"Development Status :: 6 - Mature",
"Development Status :: 7 - Inactive",
)
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert",
}
def escape_rst(text: str) -> str:
@@ -52,18 +57,18 @@ def iter_plugins():
regex = r">([\d\w-]*)</a>"
response = requests.get("https://pypi.org/simple")
matches = list(
match
for match in re.finditer(regex, response.text)
if match.groups()[0].startswith("pytest-")
)
match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
plugin_names = [
name
for name in match_names
if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
]
for match in tqdm(matches, smoothing=0):
name = match.groups()[0]
for name in tqdm(plugin_names, smoothing=0):
response = requests.get(f"https://pypi.org/pypi/{name}/json")
if response.status_code == 404:
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
# return 404 on the JSON API. Skip.
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
# but return 404 on the JSON API. Skip.
continue
response.raise_for_status()
info = response.json()["info"]
@@ -78,11 +83,23 @@ def iter_plugins():
requires = "N/A"
if info["requires_dist"]:
for requirement in info["requires_dist"]:
if requirement == "pytest" or "pytest " in requirement:
if re.match(r"pytest(?![-.\w])", requirement):
requires = requirement
break
def version_sort_key(version_string):
"""
Return the sort key for the given version string
returned by the API.
"""
try:
return packaging.version.parse(version_string)
except packaging.version.InvalidVersion:
# Use a hard-coded pre-release version.
return packaging.version.Version("0.0.0alpha")
releases = response.json()["releases"]
for release in sorted(releases, key=packaging.version.parse, reverse=True):
for release in sorted(releases, key=version_sort_key, reverse=True):
if releases[release]:
release_date = datetime.date.fromisoformat(
releases[release][-1]["upload_time_iso_8601"].split("T")[0]
@@ -90,7 +107,9 @@ def iter_plugins():
last_release = release_date.strftime("%b %d, %Y")
break
name = f':pypi:`{info["name"]}`'
summary = escape_rst(info["summary"].replace("\n", ""))
summary = ""
if info["summary"]:
summary = escape_rst(info["summary"].replace("\n", ""))
yield {
"name": name,
"summary": summary.strip(),
@@ -122,7 +141,7 @@ def main():
reference_dir = pathlib.Path("doc", "en", "reference")
plugin_list = reference_dir / "plugin_list.rst"
with plugin_list.open("w") as f:
with plugin_list.open("w", encoding="UTF-8") as f:
f.write(FILE_HEAD)
f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(".. only:: not latex\n\n")
+5 -3
View File
@@ -6,7 +6,7 @@ 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_file = LICENSE
license_files = LICENSE
platforms = unix, linux, osx, cygwin, win32
classifiers =
Development Status :: 6 - Mature
@@ -21,6 +21,8 @@ classifiers =
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
@@ -43,7 +45,6 @@ packages =
pytest
py_modules = py
install_requires =
attrs>=19.2.0
iniconfig
packaging
pluggy>=0.12,<2.0
@@ -67,11 +68,13 @@ console_scripts =
[options.extras_require]
testing =
argcomplete
attrs>=19.2.0
hypothesis>=3.56
mock
nose
pygments>=2.7.2
requests
setuptools
xmlschema
[options.package_data]
@@ -95,7 +98,6 @@ mypy_path = src
check_untyped_defs = True
disallow_any_generics = True
ignore_missing_imports = True
no_implicit_optional = True
show_error_codes = True
strict_equality = True
warn_redundant_casts = True
+3 -3
View File
@@ -78,15 +78,15 @@ class FastFilesCompleter:
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
# Only called on non option completions.
if os.path.sep in prefix[1:]:
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
if os.sep in prefix[1:]:
prefix_dir = len(os.path.dirname(prefix) + os.sep)
else:
prefix_dir = 0
completion = []
globbed = []
if "*" not in prefix and "?" not in prefix:
# We are on unix, otherwise no bash.
if not prefix or prefix[-1] == os.path.sep:
if not prefix or prefix[-1] == os.sep:
globbed.extend(glob(prefix + ".*"))
prefix += "*"
globbed.extend(glob(prefix))
+156 -111
View File
@@ -1,4 +1,5 @@
import ast
import dataclasses
import inspect
import os
import re
@@ -30,9 +31,7 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
from weakref import ref
import attr
import pluggy
import _pytest
@@ -50,9 +49,9 @@ from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal
from typing_extensions import SupportsIndex
from weakref import ReferenceType
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
@@ -194,25 +193,25 @@ class Frame:
class TracebackEntry:
"""A single entry in a Traceback."""
__slots__ = ("_rawentry", "_excinfo", "_repr_style")
__slots__ = ("_rawentry", "_repr_style")
def __init__(
self,
rawentry: TracebackType,
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
repr_style: Optional['Literal["short", "long"]'] = None,
) -> None:
self._rawentry = rawentry
self._excinfo = excinfo
self._repr_style: Optional['Literal["short", "long"]'] = None
self._rawentry: "Final" = rawentry
self._repr_style: "Final" = repr_style
def with_repr_style(
self, repr_style: Optional['Literal["short", "long"]']
) -> "TracebackEntry":
return TracebackEntry(self._rawentry, repr_style)
@property
def lineno(self) -> int:
return self._rawentry.tb_lineno - 1
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
assert mode in ("short", "long")
self._repr_style = mode
@property
def frame(self) -> Frame:
return Frame(self._rawentry.tb_frame)
@@ -272,7 +271,7 @@ class TracebackEntry:
source = property(getsource)
def ishidden(self) -> bool:
def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
"""Return True if the current frame has a var __tracebackhide__
resolving to True.
@@ -296,7 +295,7 @@ class TracebackEntry:
else:
break
if tbh and callable(tbh):
return tbh(None if self._excinfo is None else self._excinfo())
return tbh(excinfo)
return tbh
def __str__(self) -> str:
@@ -329,16 +328,14 @@ class Traceback(List[TracebackEntry]):
def __init__(
self,
tb: Union[TracebackType, Iterable[TracebackEntry]],
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
) -> None:
"""Initialize from given python traceback object and ExceptionInfo."""
self._excinfo = excinfo
if isinstance(tb, TracebackType):
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
cur_: Optional[TracebackType] = cur
while cur_ is not None:
yield TracebackEntry(cur_, excinfo=excinfo)
yield TracebackEntry(cur_)
cur_ = cur_.tb_next
super().__init__(f(tb))
@@ -378,7 +375,7 @@ class Traceback(List[TracebackEntry]):
continue
if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
continue
return Traceback(x._rawentry, self._excinfo)
return Traceback(x._rawentry)
return self
@overload
@@ -398,26 +395,27 @@ class Traceback(List[TracebackEntry]):
return super().__getitem__(key)
def filter(
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
self,
# TODO(py38): change to positional only.
_excinfo_or_fn: Union[
"ExceptionInfo[BaseException]",
Callable[[TracebackEntry], bool],
],
) -> "Traceback":
"""Return a Traceback instance with certain items removed
"""Return a Traceback instance with certain items removed.
fn is a function that gets a single argument, a TracebackEntry
instance, and should return True when the item should be added
to the Traceback, False when not.
If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
which are hidden (see ishidden() above).
By default this removes all the TracebackEntries which are hidden
(see ishidden() above).
Otherwise, the filter is a function that gets a single argument, a
``TracebackEntry`` instance, and should return True when the item should
be added to the ``Traceback``, False when not.
"""
return Traceback(filter(fn, self), self._excinfo)
def getcrashentry(self) -> TracebackEntry:
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
for i in range(-1, -len(self) - 1, -1):
entry = self[i]
if not entry.ishidden():
return entry
return self[-1]
if isinstance(_excinfo_or_fn, ExceptionInfo):
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
else:
fn = _excinfo_or_fn
return Traceback(filter(fn, self))
def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if
@@ -445,7 +443,7 @@ E = TypeVar("E", bound=BaseException, covariant=True)
@final
@attr.s(repr=False, init=False, auto_attribs=True)
@dataclasses.dataclass
class ExceptionInfo(Generic[E]):
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
@@ -469,22 +467,41 @@ class ExceptionInfo(Generic[E]):
self._traceback = traceback
@classmethod
def from_exc_info(
def from_exception(
cls,
exc_info: Tuple[Type[E], E, TracebackType],
# Ignoring error: "Cannot use a covariant type variable as a parameter".
# This is OK to ignore because this class is (conceptually) readonly.
# See https://github.com/python/mypy/issues/7049.
exception: E, # type: ignore[misc]
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]":
"""Return an ExceptionInfo for an existing exc_info tuple.
"""Return an ExceptionInfo for an existing exception.
.. warning::
Experimental API
The exception must have a non-``None`` ``__traceback__`` attribute,
otherwise this function fails with an assertion error. This means that
the exception must have been raised, or added a traceback with the
:py:meth:`~BaseException.with_traceback()` method.
:param exprinfo:
A text string helping to determine if we should strip
``AssertionError`` from the output. Defaults to the exception
message/``__str__()``.
.. versionadded:: 7.4
"""
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)
@classmethod
def from_exc_info(
cls,
exc_info: Tuple[Type[E], E, TracebackType],
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]":
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
_striptext = ""
if exprinfo is None and isinstance(exc_info[1], AssertionError):
exprinfo = getattr(exc_info[1], "msg", None)
@@ -563,7 +580,7 @@ class ExceptionInfo(Generic[E]):
def traceback(self) -> Traceback:
"""The traceback."""
if self._traceback is None:
self._traceback = Traceback(self.tb, excinfo=ref(self))
self._traceback = Traceback(self.tb)
return self._traceback
@traceback.setter
@@ -602,18 +619,25 @@ class ExceptionInfo(Generic[E]):
"""
return isinstance(self.value, exc)
def _getreprcrash(self) -> "ReprFileLocation":
exconly = self.exconly(tryshort=True)
entry = self.traceback.getcrashentry()
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
# Find last non-hidden traceback entry that led to the exception of the
# traceback, or None if all hidden.
for i in range(-1, -len(self.traceback) - 1, -1):
entry = self.traceback[i]
if not entry.ishidden(self):
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
exconly = self.exconly(tryshort=True)
return ReprFileLocation(path, lineno + 1, exconly)
return None
def getrepr(
self,
showlocals: bool = False,
style: "_TracebackStyle" = "long",
abspath: bool = False,
tbfilter: bool = True,
tbfilter: Union[
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
] = True,
funcargs: bool = False,
truncate_locals: bool = True,
chain: bool = True,
@@ -625,14 +649,20 @@ class ExceptionInfo(Generic[E]):
Ignored if ``style=="native"``.
:param str style:
long|short|no|native|value traceback style.
long|short|line|no|native|value traceback style.
:param bool abspath:
If paths should be changed to absolute or left unchanged.
:param bool tbfilter:
Hide entries that contain a local variable ``__tracebackhide__==True``.
Ignored if ``style=="native"``.
:param tbfilter:
A filter for traceback entries.
* If false, don't hide any entries.
* If true, hide internal entries and entries that contain a local
variable ``__tracebackhide__ = True``.
* If a callable, delegates the filtering to the callable.
Ignored if ``style`` is ``"native"``.
:param bool funcargs:
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
@@ -649,12 +679,14 @@ class ExceptionInfo(Generic[E]):
"""
if style == "native":
return ReprExceptionInfo(
ReprTracebackNative(
reprtraceback=ReprTracebackNative(
traceback.format_exception(
self.type, self.value, self.traceback[0]._rawentry
self.type,
self.value,
self.traceback[0]._rawentry if self.traceback else None,
)
),
self._getreprcrash(),
reprcrash=self._getreprcrash(),
)
fmt = FormattedExcinfo(
@@ -684,7 +716,7 @@ class ExceptionInfo(Generic[E]):
return True
@attr.s(auto_attribs=True)
@dataclasses.dataclass
class FormattedExcinfo:
"""Presenting information about failing Functions and Generators."""
@@ -695,12 +727,12 @@ class FormattedExcinfo:
showlocals: bool = False
style: "_TracebackStyle" = "long"
abspath: bool = True
tbfilter: bool = True
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
funcargs: bool = False
truncate_locals: bool = True
chain: bool = True
astcache: Dict[Union[str, Path], ast.AST] = attr.ib(
factory=dict, init=False, repr=False
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
default_factory=dict, init=False, repr=False
)
def _getindent(self, source: "Source") -> int:
@@ -741,11 +773,13 @@ class FormattedExcinfo:
) -> List[str]:
"""Return formatted and marked up source lines."""
lines = []
if source is None or line_index >= len(source.lines):
if source is not None and line_index < 0:
line_index += len(source)
if source is None or line_index >= len(source.lines) or line_index < 0:
# `line_index` could still be outside `range(len(source.lines))` if
# we're processing AST with pathological position attributes.
source = Source("???")
line_index = 0
if line_index < 0:
line_index += len(source)
space_prefix = " "
if short:
lines.append(space_prefix + source.lines[line_index].strip())
@@ -805,12 +839,16 @@ class FormattedExcinfo:
def repr_traceback_entry(
self,
entry: TracebackEntry,
entry: Optional[TracebackEntry],
excinfo: Optional[ExceptionInfo[BaseException]] = None,
) -> "ReprEntry":
lines: List[str] = []
style = entry._repr_style if entry._repr_style is not None else self.style
if style in ("short", "long"):
style = (
entry._repr_style
if entry is not None and entry._repr_style is not None
else self.style
)
if style in ("short", "long") and entry is not None:
source = self._getentrysource(entry)
if source is None:
source = Source("???")
@@ -851,25 +889,31 @@ class FormattedExcinfo:
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
traceback = excinfo.traceback
if self.tbfilter:
traceback = traceback.filter()
if callable(self.tbfilter):
traceback = self.tbfilter(excinfo)
elif self.tbfilter:
traceback = traceback.filter(excinfo)
if isinstance(excinfo.value, RecursionError):
traceback, extraline = self._truncate_recursive_traceback(traceback)
else:
extraline = None
if not traceback:
if extraline is None:
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
entries = [self.repr_traceback_entry(None, excinfo)]
return ReprTraceback(entries, extraline, style=self.style)
last = traceback[-1]
entries = []
if self.style == "value":
reprentry = self.repr_traceback_entry(last, excinfo)
entries.append(reprentry)
entries = [self.repr_traceback_entry(last, excinfo)]
return ReprTraceback(entries, None, style=self.style)
for index, entry in enumerate(traceback):
einfo = (last == entry) and excinfo or None
reprentry = self.repr_traceback_entry(entry, einfo)
entries.append(reprentry)
entries = [
self.repr_traceback_entry(entry, excinfo if last == entry else None)
for entry in traceback
]
return ReprTraceback(entries, extraline, style=self.style)
def _truncate_recursive_traceback(
@@ -926,6 +970,7 @@ class FormattedExcinfo:
seen: Set[int] = set()
while e is not None and id(e) not in seen:
seen.add(id(e))
if excinfo_:
# Fall back to native traceback as a temporary workaround until
# full support for exception groups added to ExceptionInfo.
@@ -942,9 +987,7 @@ class FormattedExcinfo:
)
else:
reprtraceback = self.repr_traceback(excinfo_)
reprcrash: Optional[ReprFileLocation] = (
excinfo_._getreprcrash() if self.style != "value" else None
)
reprcrash = excinfo_._getreprcrash()
else:
# Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work.
@@ -952,25 +995,17 @@ class FormattedExcinfo:
traceback.format_exception(type(e), e, None)
)
reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None and self.chain:
e = e.__cause__
excinfo_ = (
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
descr = "The above exception was the direct cause of the following exception:"
elif (
e.__context__ is not None and not e.__suppress_context__ and self.chain
):
e = e.__context__
excinfo_ = (
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
descr = "During handling of the above exception, another exception occurred:"
else:
e = None
@@ -978,7 +1013,7 @@ class FormattedExcinfo:
return ExceptionChainRepr(repr_chain)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class TerminalRepr:
def __str__(self) -> str:
# FYI this is called from pytest-xdist's serialization of exception
@@ -996,14 +1031,14 @@ class TerminalRepr:
# This class is abstract -- only subclasses are instantiated.
@attr.s(eq=False)
@dataclasses.dataclass(eq=False)
class ExceptionRepr(TerminalRepr):
# Provided by subclasses.
reprcrash: Optional["ReprFileLocation"]
reprtraceback: "ReprTraceback"
def __attrs_post_init__(self) -> None:
self.sections: List[Tuple[str, str, str]] = []
reprcrash: Optional["ReprFileLocation"]
sections: List[Tuple[str, str, str]] = dataclasses.field(
init=False, default_factory=list
)
def addsection(self, name: str, content: str, sep: str = "-") -> None:
self.sections.append((name, content, sep))
@@ -1014,16 +1049,23 @@ class ExceptionRepr(TerminalRepr):
tw.line(content)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ExceptionChainRepr(ExceptionRepr):
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
def __attrs_post_init__(self) -> None:
super().__attrs_post_init__()
def __init__(
self,
chain: Sequence[
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
],
) -> None:
# reprcrash and reprtraceback of the outermost (the newest) exception
# in the chain.
self.reprtraceback = self.chain[-1][0]
self.reprcrash = self.chain[-1][1]
super().__init__(
reprtraceback=chain[-1][0],
reprcrash=chain[-1][1],
)
self.chain = chain
def toterminal(self, tw: TerminalWriter) -> None:
for element in self.chain:
@@ -1034,17 +1076,17 @@ class ExceptionChainRepr(ExceptionRepr):
super().toterminal(tw)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback"
reprcrash: "ReprFileLocation"
reprcrash: Optional["ReprFileLocation"]
def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw)
super().toterminal(tw)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprTraceback(TerminalRepr):
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
extraline: Optional[str]
@@ -1073,12 +1115,12 @@ class ReprTraceback(TerminalRepr):
class ReprTracebackNative(ReprTraceback):
def __init__(self, tblines: Sequence[str]) -> None:
self.style = "native"
self.reprentries = [ReprEntryNative(tblines)]
self.extraline = None
self.style = "native"
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprEntryNative(TerminalRepr):
lines: Sequence[str]
@@ -1088,7 +1130,7 @@ class ReprEntryNative(TerminalRepr):
tw.write("".join(self.lines))
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprEntry(TerminalRepr):
lines: Sequence[str]
reprfuncargs: Optional["ReprFuncArgs"]
@@ -1142,8 +1184,8 @@ class ReprEntry(TerminalRepr):
def toterminal(self, tw: TerminalWriter) -> None:
if self.style == "short":
assert self.reprfileloc is not None
self.reprfileloc.toterminal(tw)
if self.reprfileloc:
self.reprfileloc.toterminal(tw)
self._write_entry_lines(tw)
if self.reprlocals:
self.reprlocals.toterminal(tw, indent=" " * 8)
@@ -1168,12 +1210,15 @@ class ReprEntry(TerminalRepr):
)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprFileLocation(TerminalRepr):
path: str = attr.ib(converter=str)
path: str
lineno: int
message: str
def __post_init__(self) -> None:
self.path = str(self.path)
def toterminal(self, tw: TerminalWriter) -> None:
# Filename and lineno output for each entry, using an output format
# that most editors understand.
@@ -1185,7 +1230,7 @@ class ReprFileLocation(TerminalRepr):
tw.line(f":{self.lineno}: {msg}")
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprLocals(TerminalRepr):
lines: Sequence[str]
@@ -1194,7 +1239,7 @@ class ReprLocals(TerminalRepr):
tw.line(indent + line)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprFuncArgs(TerminalRepr):
args: Sequence[Tuple[str, object]]
+9 -8
View File
@@ -24,6 +24,7 @@ from stat import S_ISLNK
from stat import S_ISREG
from typing import Any
from typing import Callable
from typing import cast
from typing import overload
from typing import TYPE_CHECKING
@@ -146,7 +147,7 @@ class Visitor:
self.fil = fil
self.ignore = ignore
self.breadthfirst = bf
self.optsort = sort and sorted or (lambda x: x)
self.optsort = cast(Callable[[Any], Any], sorted) if sort else (lambda x: x)
def gen(self, path):
try:
@@ -224,7 +225,7 @@ class Stat:
raise NotImplementedError("XXX win32")
import pwd
entry = error.checked_call(pwd.getpwuid, self.uid)
entry = error.checked_call(pwd.getpwuid, self.uid) # type:ignore[attr-defined]
return entry[0]
@property
@@ -234,7 +235,7 @@ class Stat:
raise NotImplementedError("XXX win32")
import grp
entry = error.checked_call(grp.getgrgid, self.gid)
entry = error.checked_call(grp.getgrgid, self.gid) # type:ignore[attr-defined]
return entry[0]
def isdir(self):
@@ -252,7 +253,7 @@ def getuserid(user):
import pwd
if not isinstance(user, int):
user = pwd.getpwnam(user)[2]
user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
return user
@@ -260,7 +261,7 @@ def getgroupid(group):
import grp
if not isinstance(group, int):
group = grp.getgrnam(group)[2]
group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
return group
@@ -795,7 +796,7 @@ class LocalPath:
kw = {"exists": 1}
return Checkers(self)._evaluate(kw)
_patternchars = set("*?[" + os.path.sep)
_patternchars = set("*?[" + os.sep)
def listdir(self, fil=None, sort=None):
"""List directory contents, possibly filter by the given fil func
@@ -952,7 +953,7 @@ class LocalPath:
else:
p.dirpath()._ensuredirs()
if not p.check(file=1):
p.open("w").close()
p.open("wb").close()
return p
@overload
@@ -1127,7 +1128,7 @@ class LocalPath:
modfile = modfile[:-1]
elif modfile.endswith("$py.class"):
modfile = modfile[:-9] + ".py"
if modfile.endswith(os.path.sep + "__init__.py"):
if modfile.endswith(os.sep + "__init__.py"):
if self.basename != "__init__.py":
modfile = modfile[:-12]
try:
+102 -24
View File
@@ -44,10 +44,20 @@ from _pytest.stash import StashKey
if TYPE_CHECKING:
from _pytest.assertion import AssertionState
if sys.version_info >= (3, 8):
namedExpr = ast.NamedExpr
astNameConstant = ast.Constant
astStr = ast.Constant
astNum = ast.Constant
else:
namedExpr = ast.Expr
astNameConstant = ast.NameConstant
astStr = ast.Str
astNum = ast.Num
assertstate_key = StashKey["AssertionState"]()
# pytest caches rewritten pycs in pycache dirs
PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
PYC_EXT = ".py" + (__debug__ and "c" or "o")
@@ -180,7 +190,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
for initial_path in self.session._initialpaths:
# Make something as c:/projects/my_project/path.py ->
# ['c:', 'projects', 'my_project', 'path.py']
parts = str(initial_path).split(os.path.sep)
parts = str(initial_path).split(os.sep)
# add 'path' to basenames to be checked.
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
@@ -274,8 +284,12 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
return f.read()
if sys.version_info >= (3, 10):
if sys.version_info >= (3, 12):
from importlib.resources.abc import TraversableResources
else:
from importlib.abc import TraversableResources
def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore
def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore
if sys.version_info < (3, 11):
from importlib.readers import FileReader
else:
@@ -631,8 +645,12 @@ class AssertionRewriter(ast.NodeVisitor):
.push_format_context() and .pop_format_context() which allows
to build another %-formatted string while already building one.
This state is reset on every new assert statement visited and used
by the other visitors.
:variables_overwrite: A dict filled with references to variables
that change value within an assert. This happens when a variable is
reassigned with the walrus operator
This state, except the variables_overwrite, is reset on every new assert
statement visited and used by the other visitors.
"""
def __init__(
@@ -648,6 +666,7 @@ class AssertionRewriter(ast.NodeVisitor):
else:
self.enable_assertion_pass_hook = False
self.source = source
self.variables_overwrite: Dict[str, str] = {}
def run(self, mod: ast.Module) -> None:
"""Find all assert statements in *mod* and rewrite them."""
@@ -662,14 +681,17 @@ class AssertionRewriter(ast.NodeVisitor):
if doc is not None and self.is_rewrite_disabled(doc):
return
pos = 0
lineno = 1
item = None
for item in mod.body:
if (
expect_docstring
and isinstance(item, ast.Expr)
and isinstance(item.value, ast.Str)
and isinstance(item.value, astStr)
):
doc = item.value.s
if sys.version_info >= (3, 8):
doc = item.value.value
else:
doc = item.value.s
if self.is_rewrite_disabled(doc):
return
expect_docstring = False
@@ -801,7 +823,7 @@ class AssertionRewriter(ast.NodeVisitor):
current = self.stack.pop()
if self.stack:
self.explanation_specifiers = self.stack[-1]
keys = [ast.Str(key) for key in current.keys()]
keys = [astStr(key) for key in current.keys()]
format_dict = ast.Dict(keys, list(current.values()))
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
name = "@py_format" + str(next(self.variable_counter))
@@ -855,16 +877,16 @@ class AssertionRewriter(ast.NodeVisitor):
negation = ast.UnaryOp(ast.Not(), top_condition)
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
msg = self.pop_format_context(ast.Str(explanation))
msg = self.pop_format_context(astStr(explanation))
# Failed
if assert_.msg:
assertmsg = self.helper("_format_assertmsg", assert_.msg)
gluestr = "\n>assert "
else:
assertmsg = ast.Str("")
assertmsg = astStr("")
gluestr = "assert "
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
err_name = ast.Name("AssertionError", ast.Load())
fmt = self.helper("_format_explanation", err_msg)
@@ -880,8 +902,8 @@ class AssertionRewriter(ast.NodeVisitor):
hook_call_pass = ast.Expr(
self.helper(
"_call_assertion_pass",
ast.Num(assert_.lineno),
ast.Str(orig),
astNum(assert_.lineno),
astStr(orig),
fmt_pass,
)
)
@@ -900,7 +922,7 @@ class AssertionRewriter(ast.NodeVisitor):
variables = [
ast.Name(name, ast.Store()) for name in self.format_variables
]
clear_format = ast.Assign(variables, ast.NameConstant(None))
clear_format = ast.Assign(variables, astNameConstant(None))
self.statements.append(clear_format)
else: # Original assertion rewriting
@@ -911,9 +933,9 @@ class AssertionRewriter(ast.NodeVisitor):
assertmsg = self.helper("_format_assertmsg", assert_.msg)
explanation = "\n>assert " + explanation
else:
assertmsg = ast.Str("")
assertmsg = astStr("")
explanation = "assert " + explanation
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
msg = self.pop_format_context(template)
fmt = self.helper("_format_explanation", msg)
err_name = ast.Name("AssertionError", ast.Load())
@@ -925,7 +947,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Clear temporary variables by setting them to None.
if self.variables:
variables = [ast.Name(name, ast.Store()) for name in self.variables]
clear = ast.Assign(variables, ast.NameConstant(None))
clear = ast.Assign(variables, astNameConstant(None))
self.statements.append(clear)
# Fix locations (line numbers/column offsets).
for stmt in self.statements:
@@ -933,14 +955,26 @@ class AssertionRewriter(ast.NodeVisitor):
ast.copy_location(node, assert_)
return self.statements
def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
# This method handles the 'walrus operator' repr of the target
# name if it's a local variable or _should_repr_global_name()
# thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
target_id = name.target.id # type: ignore[attr-defined]
inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), astStr(target_id))
return name, self.explanation_param(expr)
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
# Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
expr = ast.IfExp(test, self.display(name), astStr(name.id))
return name, self.explanation_param(expr)
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
@@ -959,10 +993,26 @@ class AssertionRewriter(ast.NodeVisitor):
# cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
self.expl_stmts = fail_inner
# Check if the left operand is a namedExpr and the value has already been visited
if (
isinstance(v, ast.Compare)
and isinstance(v.left, namedExpr)
and v.left.target.id
in [
ast_expr.id
for ast_expr in boolop.values[:i]
if hasattr(ast_expr, "id")
]
):
pytest_temp = self.variable()
self.variables_overwrite[
v.left.target.id
] = v.left # type:ignore[assignment]
v.left.target.id = pytest_temp
self.push_format_context()
res, expl = self.visit(v)
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
expl_format = self.pop_format_context(ast.Str(expl))
expl_format = self.pop_format_context(astStr(expl))
call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call))
if i < levels:
@@ -974,7 +1024,7 @@ class AssertionRewriter(ast.NodeVisitor):
self.statements = body = inner
self.statements = save
self.expl_stmts = fail_save
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
expl = self.pop_format_context(expl_template)
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
@@ -998,10 +1048,19 @@ class AssertionRewriter(ast.NodeVisitor):
new_args = []
new_kwargs = []
for arg in call.args:
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite:
arg = self.variables_overwrite[arg.id] # type:ignore[assignment]
res, expl = self.visit(arg)
arg_expls.append(expl)
new_args.append(res)
for keyword in call.keywords:
if (
isinstance(keyword.value, ast.Name)
and keyword.value.id in self.variables_overwrite
):
keyword.value = self.variables_overwrite[
keyword.value.id
] # type:ignore[assignment]
res, expl = self.visit(keyword.value)
new_kwargs.append(ast.keyword(keyword.arg, res))
if keyword.arg:
@@ -1034,6 +1093,15 @@ class AssertionRewriter(ast.NodeVisitor):
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
self.push_format_context()
# We first check if we have overwritten a variable in the previous assert
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
comp.left = self.variables_overwrite[
comp.left.id
] # type:ignore[assignment]
if isinstance(comp.left, namedExpr):
self.variables_overwrite[
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})"
@@ -1045,14 +1113,23 @@ class AssertionRewriter(ast.NodeVisitor):
syms = []
results = [left_res]
for i, op, next_operand in it:
if (
isinstance(next_operand, namedExpr)
and isinstance(left_res, ast.Name)
and next_operand.target.id == left_res.id
):
next_operand.target.id = self.variable()
self.variables_overwrite[
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})"
results.append(next_res)
sym = BINOP_MAP[op.__class__]
syms.append(ast.Str(sym))
syms.append(astStr(sym))
expl = f"{left_expl} {sym} {next_expl}"
expls.append(ast.Str(expl))
expls.append(astStr(expl))
res_expr = ast.Compare(left_res, [op], [next_res])
self.statements.append(ast.Assign([store_names[i]], res_expr))
left_res, left_expl = next_res, next_expl
@@ -1068,6 +1145,7 @@ class AssertionRewriter(ast.NodeVisitor):
res: ast.expr = ast.BoolOp(ast.And(), load_names)
else:
res = load_names[0]
return res, self.explanation_param(self.pop_format_context(expl_call))
+46 -25
View File
@@ -38,9 +38,9 @@ def _truncate_explanation(
"""Truncate given list of strings that makes up the assertion explanation.
Truncates to either 8 lines, or 640 characters - whichever the input reaches
first. The remaining lines will be replaced by a usage message.
first, taking the truncation explanation into account. The remaining lines
will be replaced by a usage message.
"""
if max_lines is None:
max_lines = DEFAULT_MAX_LINES
if max_chars is None:
@@ -48,35 +48,56 @@ def _truncate_explanation(
# Check if truncation required
input_char_count = len("".join(input_lines))
if len(input_lines) <= max_lines and input_char_count <= max_chars:
# The length of the truncation explanation depends on the number of lines
# removed but is at least 68 characters:
# The real value is
# 64 (for the base message:
# '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
# )
# + 1 (for plural)
# + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
# + 3 for the '...' added to the truncated line
# But if there's more than 100 lines it's very likely that we're going to
# truncate, so we don't need the exact value using log10.
tolerable_max_chars = (
max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
)
# The truncation explanation add two lines to the output
tolerable_max_lines = max_lines + 2
if (
len(input_lines) <= tolerable_max_lines
and input_char_count <= tolerable_max_chars
):
return input_lines
# Truncate first to max_lines, and then truncate to max_chars if max_chars
# is exceeded.
# Truncate first to max_lines, and then truncate to max_chars if necessary
truncated_explanation = input_lines[:max_lines]
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
# Add ellipsis to final line
truncated_explanation[-1] = truncated_explanation[-1] + "..."
# Append useful message to explanation
truncated_line_count = len(input_lines) - len(truncated_explanation)
truncated_line_count += 1 # Account for the part-truncated final line
msg = "...Full output truncated"
if truncated_line_count == 1:
msg += f" ({truncated_line_count} line hidden)"
truncated_char = True
# We reevaluate the need to truncate chars following removal of some lines
if len("".join(truncated_explanation)) > tolerable_max_chars:
truncated_explanation = _truncate_by_char_count(
truncated_explanation, max_chars
)
else:
msg += f" ({truncated_line_count} lines hidden)"
msg += f", {USAGE_MSG}"
truncated_explanation.extend(["", str(msg)])
return truncated_explanation
truncated_char = False
truncated_line_count = len(input_lines) - len(truncated_explanation)
if truncated_explanation[-1]:
# Add ellipsis and take into account part-truncated final line
truncated_explanation[-1] = truncated_explanation[-1] + "..."
if truncated_char:
# It's possible that we did not remove any char from this line
truncated_line_count += 1
else:
# Add proper ellipsis when we were able to fit a full line exactly
truncated_explanation[-1] = "..."
return truncated_explanation + [
"",
f"...Full output truncated ({truncated_line_count} line"
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
]
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
# Check if truncation required
if len("".join(input_lines)) <= max_chars:
return input_lines
# Find point at which input length exceeds total allowed length
iterated_char_count = 0
for iterated_index, input_line in enumerate(input_lines):
+40 -22
View File
@@ -1,6 +1,7 @@
"""Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version.
import dataclasses
import json
import os
from pathlib import Path
@@ -12,8 +13,6 @@ from typing import Optional
from typing import Set
from typing import Union
import attr
from .pathlib import resolve_from_str
from .pathlib import rm_rf
from .reports import CollectReport
@@ -28,11 +27,10 @@ from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.main import Session
from _pytest.python import Module
from _pytest.nodes import File
from _pytest.python import Package
from _pytest.reports import TestReport
README_CONTENT = """\
# pytest cache directory #
@@ -53,10 +51,12 @@ Signature: 8a477f597d28d172789f06886806bc55
@final
@attr.s(init=False, auto_attribs=True)
@dataclasses.dataclass
class Cache:
_cachedir: Path = attr.ib(repr=False)
_config: Config = attr.ib(repr=False)
"""Instance of the `cache` fixture."""
_cachedir: Path = dataclasses.field(repr=False)
_config: Config = dataclasses.field(repr=False)
# Sub-directory under cache-dir for directories created by `mkdir()`.
_CACHE_PREFIX_DIRS = "d"
@@ -179,16 +179,22 @@ class Cache:
else:
cache_dir_exists_already = self._cachedir.exists()
path.parent.mkdir(exist_ok=True, parents=True)
except OSError:
self.warn("could not create cache path {path}", path=path, _ispytest=True)
except OSError as exc:
self.warn(
f"could not create cache path {path}: {exc}",
_ispytest=True,
)
return
if not cache_dir_exists_already:
self._ensure_supporting_files()
data = json.dumps(value, ensure_ascii=False, indent=2)
try:
f = path.open("w", encoding="UTF-8")
except OSError:
self.warn("cache could not write path {path}", path=path, _ispytest=True)
except OSError as exc:
self.warn(
f"cache could not write path {path}: {exc}",
_ispytest=True,
)
else:
with f:
f.write(data)
@@ -213,22 +219,30 @@ class LFPluginCollWrapper:
@hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector):
if isinstance(collector, Session):
if isinstance(collector, (Session, Package)):
out = yield
res: CollectReport = out.get_result()
# Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths
# Use stable sort to priorize last failed.
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
# Package.path is the __init__.py file, we need the directory.
if isinstance(node, Package):
path = node.path.parent
else:
path = node.path
return path in lf_paths
res.result = sorted(
res.result,
# use stable sort to priorize last failed
key=lambda x: x.path in lf_paths,
key=sort_key,
reverse=True,
)
return
elif isinstance(collector, Module):
elif isinstance(collector, File):
if collector.path in self.lfplugin._last_failed_paths:
out = yield
res = out.get_result()
@@ -266,10 +280,9 @@ class LFPluginCollSkipfiles:
def pytest_make_collect_report(
self, collector: nodes.Collector
) -> Optional[CollectReport]:
# Packages are Modules, but _last_failed_paths only contains
# test-bearing paths and doesn't try to include the paths of their
# packages, so don't filter them.
if isinstance(collector, Module) and not isinstance(collector, Package):
# Packages are Files, but we only want to skip test-bearing Files,
# so don't filter Packages.
if isinstance(collector, File) and not isinstance(collector, Package):
if collector.path not in self.lfplugin._last_failed_paths:
self.lfplugin._skipped_files += 1
@@ -299,9 +312,14 @@ class LFPlugin:
)
def get_last_failed_paths(self) -> Set[Path]:
"""Return a set with all Paths()s of the previously failed nodeids."""
"""Return a set with all Paths of the previously failed nodeids and
their parents."""
rootpath = self.config.rootpath
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
result = set()
for nodeid in self.lastfailed:
path = rootpath / nodeid.split("::")[0]
result.add(path)
result.update(path.parents)
return {x for x in result if x.exists()}
def pytest_report_collectionfinish(self) -> Optional[str]:
@@ -492,7 +510,7 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
if config.option.cacheshow:
if config.option.cacheshow and not config.option.help:
from _pytest.main import wrap_session
return wrap_session(config, cacheshow)
+196 -128
View File
@@ -1,19 +1,26 @@
"""Per-test stdout/stderr capturing mechanism."""
import abc
import collections
import contextlib
import functools
import io
import os
import sys
from io import UnsupportedOperation
from tempfile import TemporaryFile
from types import TracebackType
from typing import Any
from typing import AnyStr
from typing import BinaryIO
from typing import Generator
from typing import Generic
from typing import Iterable
from typing import Iterator
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import TextIO
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
@@ -29,6 +36,7 @@ from _pytest.nodes import File
from _pytest.nodes import Item
if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
@@ -185,19 +193,27 @@ class TeeCaptureIO(CaptureIO):
return self._other.write(s)
class DontReadFromInput:
encoding = None
class DontReadFromInput(TextIO):
@property
def encoding(self) -> str:
return sys.__stdin__.encoding
def read(self, *args):
def read(self, size: int = -1) -> str:
raise OSError(
"pytest: reading from stdin while output is captured! Consider using `-s`."
)
readline = read
readlines = read
__next__ = read
def __iter__(self):
def __next__(self) -> str:
return self.readline()
def readlines(self, hint: Optional[int] = -1) -> List[str]:
raise OSError(
"pytest: reading from stdin while output is captured! Consider using `-s`."
)
def __iter__(self) -> Iterator[str]:
return self
def fileno(self) -> int:
@@ -215,7 +231,7 @@ class DontReadFromInput:
def readable(self) -> bool:
return False
def seek(self, offset: int) -> int:
def seek(self, offset: int, whence: int = 0) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
def seekable(self) -> bool:
@@ -224,41 +240,104 @@ class DontReadFromInput:
def tell(self) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
def truncate(self, size: int) -> None:
raise UnsupportedOperation("cannont truncate stdin")
def truncate(self, size: Optional[int] = None) -> int:
raise UnsupportedOperation("cannot truncate stdin")
def write(self, *args) -> None:
def write(self, data: str) -> int:
raise UnsupportedOperation("cannot write to stdin")
def writelines(self, *args) -> None:
def writelines(self, lines: Iterable[str]) -> None:
raise UnsupportedOperation("Cannot write to stdin")
def writable(self) -> bool:
return False
@property
def buffer(self):
def __enter__(self) -> "DontReadFromInput":
return self
def __exit__(
self,
type: Optional[Type[BaseException]],
value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
pass
@property
def buffer(self) -> BinaryIO:
# The str/bytes doesn't actually matter in this type, so OK to fake.
return self # type: ignore[return-value]
# Capture classes.
class CaptureBase(abc.ABC, Generic[AnyStr]):
EMPTY_BUFFER: AnyStr
@abc.abstractmethod
def __init__(self, fd: int) -> None:
raise NotImplementedError()
@abc.abstractmethod
def start(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def done(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def suspend(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def resume(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def writeorg(self, data: AnyStr) -> None:
raise NotImplementedError()
@abc.abstractmethod
def snap(self) -> AnyStr:
raise NotImplementedError()
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
class NoCapture:
EMPTY_BUFFER = None
__init__ = start = done = suspend = resume = lambda *args: None
class NoCapture(CaptureBase[str]):
EMPTY_BUFFER = ""
def __init__(self, fd: int) -> None:
pass
def start(self) -> None:
pass
def done(self) -> None:
pass
def suspend(self) -> None:
pass
def resume(self) -> None:
pass
def snap(self) -> str:
return ""
def writeorg(self, data: str) -> None:
pass
class SysCaptureBinary:
EMPTY_BUFFER = b""
def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
class SysCaptureBase(CaptureBase[AnyStr]):
def __init__(
self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
) -> None:
name = patchsysdict[fd]
self._old = getattr(sys, name)
self._old: TextIO = getattr(sys, name)
self.name = name
if tmpfile is None:
if name == "stdin":
@@ -298,14 +377,6 @@ class SysCaptureBinary:
setattr(sys, self.name, self.tmpfile)
self._state = "started"
def snap(self):
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def done(self) -> None:
self._assert_state("done", ("initialized", "started", "suspended", "done"))
if self._state == "done":
@@ -327,36 +398,43 @@ class SysCaptureBinary:
setattr(sys, self.name, self.tmpfile)
self._state = "started"
def writeorg(self, data) -> None:
class SysCaptureBinary(SysCaptureBase[bytes]):
EMPTY_BUFFER = b""
def snap(self) -> bytes:
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def writeorg(self, data: bytes) -> None:
self._assert_state("writeorg", ("started", "suspended"))
self._old.flush()
self._old.buffer.write(data)
self._old.buffer.flush()
class SysCapture(SysCaptureBinary):
EMPTY_BUFFER = "" # type: ignore[assignment]
class SysCapture(SysCaptureBase[str]):
EMPTY_BUFFER = ""
def snap(self):
def snap(self) -> str:
self._assert_state("snap", ("started", "suspended"))
assert isinstance(self.tmpfile, CaptureIO)
res = self.tmpfile.getvalue()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def writeorg(self, data):
def writeorg(self, data: str) -> None:
self._assert_state("writeorg", ("started", "suspended"))
self._old.write(data)
self._old.flush()
class FDCaptureBinary:
"""Capture IO to/from a given OS-level file descriptor.
snap() produces `bytes`.
"""
EMPTY_BUFFER = b""
class FDCaptureBase(CaptureBase[AnyStr]):
def __init__(self, targetfd: int) -> None:
self.targetfd = targetfd
@@ -382,7 +460,7 @@ class FDCaptureBinary:
if targetfd == 0:
self.tmpfile = open(os.devnull, encoding="utf-8")
self.syscapture = SysCapture(targetfd)
self.syscapture: CaptureBase[str] = SysCapture(targetfd)
else:
self.tmpfile = EncodedFile(
TemporaryFile(buffering=0),
@@ -394,7 +472,7 @@ class FDCaptureBinary:
if targetfd in patchsysdict:
self.syscapture = SysCapture(targetfd, self.tmpfile)
else:
self.syscapture = NoCapture()
self.syscapture = NoCapture(targetfd)
self._state = "initialized"
@@ -421,14 +499,6 @@ class FDCaptureBinary:
self.syscapture.start()
self._state = "started"
def snap(self):
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def done(self) -> None:
"""Stop capturing, restore streams, return original capture file,
seeked to position zero."""
@@ -461,22 +531,38 @@ class FDCaptureBinary:
os.dup2(self.tmpfile.fileno(), self.targetfd)
self._state = "started"
def writeorg(self, data):
class FDCaptureBinary(FDCaptureBase[bytes]):
"""Capture IO to/from a given OS-level file descriptor.
snap() produces `bytes`.
"""
EMPTY_BUFFER = b""
def snap(self) -> bytes:
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def writeorg(self, data: bytes) -> None:
"""Write to original file descriptor."""
self._assert_state("writeorg", ("started", "suspended"))
os.write(self.targetfd_save, data)
class FDCapture(FDCaptureBinary):
class FDCapture(FDCaptureBase[str]):
"""Capture IO to/from a given OS-level file descriptor.
snap() produces text.
"""
# Ignore type because it doesn't match the type in the superclass (bytes).
EMPTY_BUFFER = "" # type: ignore
EMPTY_BUFFER = ""
def snap(self):
def snap(self) -> str:
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.read()
@@ -484,77 +570,49 @@ class FDCapture(FDCaptureBinary):
self.tmpfile.truncate()
return res
def writeorg(self, data):
def writeorg(self, data: str) -> None:
"""Write to original file descriptor."""
super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
self._assert_state("writeorg", ("started", "suspended"))
# XXX use encoding of original stream
os.write(self.targetfd_save, data.encode("utf-8"))
# MultiCapture
# This class was a namedtuple, but due to mypy limitation[0] it could not be
# made generic, so was replaced by a regular class which tries to emulate the
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
# make it a namedtuple again.
# [0]: https://github.com/python/mypy/issues/685
@final
@functools.total_ordering
class CaptureResult(Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`."""
# Generic NamedTuple only supported since Python 3.11.
if sys.version_info >= (3, 11) or TYPE_CHECKING:
__slots__ = ("out", "err")
@final
class CaptureResult(NamedTuple, Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`."""
def __init__(self, out: AnyStr, err: AnyStr) -> None:
self.out: AnyStr = out
self.err: AnyStr = err
out: AnyStr
err: AnyStr
def __len__(self) -> int:
return 2
else:
def __iter__(self) -> Iterator[AnyStr]:
return iter((self.out, self.err))
class CaptureResult(
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
):
"""The result of :method:`CaptureFixture.readouterr`."""
def __getitem__(self, item: int) -> AnyStr:
return tuple(self)[item]
def _replace(
self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None
) -> "CaptureResult[AnyStr]":
return CaptureResult(
out=self.out if out is None else out, err=self.err if err is None else err
)
def count(self, value: AnyStr) -> int:
return tuple(self).count(value)
def index(self, value) -> int:
return tuple(self).index(value)
def __eq__(self, other: object) -> bool:
if not isinstance(other, (CaptureResult, tuple)):
return NotImplemented
return tuple(self) == tuple(other)
def __hash__(self) -> int:
return hash(tuple(self))
def __lt__(self, other: object) -> bool:
if not isinstance(other, (CaptureResult, tuple)):
return NotImplemented
return tuple(self) < tuple(other)
def __repr__(self) -> str:
return f"CaptureResult(out={self.out!r}, err={self.err!r})"
__slots__ = ()
class MultiCapture(Generic[AnyStr]):
_state = None
_in_suspended = False
def __init__(self, in_, out, err) -> None:
self.in_ = in_
self.out = out
self.err = err
def __init__(
self,
in_: Optional[CaptureBase[AnyStr]],
out: Optional[CaptureBase[AnyStr]],
err: Optional[CaptureBase[AnyStr]],
) -> None:
self.in_: Optional[CaptureBase[AnyStr]] = in_
self.out: Optional[CaptureBase[AnyStr]] = out
self.err: Optional[CaptureBase[AnyStr]] = err
def __repr__(self) -> str:
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
@@ -578,8 +636,10 @@ class MultiCapture(Generic[AnyStr]):
"""Pop current snapshot out/err capture and flush to orig streams."""
out, err = self.readouterr()
if out:
assert self.out is not None
self.out.writeorg(out)
if err:
assert self.err is not None
self.err.writeorg(err)
return out, err
@@ -600,6 +660,7 @@ class MultiCapture(Generic[AnyStr]):
if self.err:
self.err.resume()
if self._in_suspended:
assert self.in_ is not None
self.in_.resume()
self._in_suspended = False
@@ -622,7 +683,8 @@ class MultiCapture(Generic[AnyStr]):
def readouterr(self) -> CaptureResult[AnyStr]:
out = self.out.snap() if self.out else ""
err = self.err.snap() if self.err else ""
return CaptureResult(out, err)
# TODO: This type error is real, need to fix.
return CaptureResult(out, err) # type: ignore[arg-type]
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
@@ -662,7 +724,7 @@ class CaptureManager:
"""
def __init__(self, method: "_CaptureMethod") -> None:
self._method = method
self._method: Final = method
self._global_capturing: Optional[MultiCapture[str]] = None
self._capture_fixture: Optional[CaptureFixture[Any]] = None
@@ -831,14 +893,18 @@ class CaptureFixture(Generic[AnyStr]):
:fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
def __init__(
self, captureclass, request: SubRequest, *, _ispytest: bool = False
self,
captureclass: Type[CaptureBase[AnyStr]],
request: SubRequest,
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self.captureclass = captureclass
self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
self.request = request
self._capture: Optional[MultiCapture[AnyStr]] = None
self._captured_out = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER
self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
def _start(self) -> None:
if self._capture is None:
@@ -893,7 +959,9 @@ class CaptureFixture(Generic[AnyStr]):
@contextlib.contextmanager
def disabled(self) -> Generator[None, None, None]:
"""Temporarily disable capturing while inside the ``with`` block."""
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
"capturemanager"
)
with capmanager.global_and_fixture_disabled():
yield
@@ -920,8 +988,8 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
captured = capsys.readouterr()
assert captured.out == "hello\n"
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -948,8 +1016,8 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
captured = capsysbinary.readouterr()
assert captured.out == b"hello\n"
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -976,8 +1044,8 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
captured = capfd.readouterr()
assert captured.out == "hello\n"
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -1005,8 +1073,8 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
assert captured.out == b"hello\n"
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
+27 -18
View File
@@ -1,4 +1,7 @@
"""Python version compatibility code."""
from __future__ import annotations
import dataclasses
import enum
import functools
import inspect
@@ -11,13 +14,8 @@ from typing import Any
from typing import Callable
from typing import Generic
from typing import NoReturn
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
import py
@@ -47,7 +45,7 @@ LEGACY_PATH = py.path. local
# fmt: on
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
return LEGACY_PATH(path)
@@ -57,7 +55,7 @@ def legacy_path(path: Union[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 # noqa: E305
# fmt: on
if sys.version_info >= (3, 8):
@@ -95,7 +93,7 @@ def is_async_function(func: object) -> bool:
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
def getlocation(function, curdir: Optional[str] = None) -> str:
def getlocation(function, curdir: str | None = None) -> str:
function = get_real_func(function)
fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno
@@ -133,8 +131,8 @@ def getfuncargnames(
*,
name: str = "",
is_method: bool = False,
cls: Optional[type] = None,
) -> Tuple[str, ...]:
cls: type | None = None,
) -> tuple[str, ...]:
"""Return the names of a function's mandatory arguments.
Should return the names of all function arguments that:
@@ -198,7 +196,7 @@ def getfuncargnames(
return arg_names
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
# Note: this code intentionally mirrors the code at the beginning of
# getfuncargnames, to get the arguments which were excluded from its result
# because they had default values.
@@ -229,7 +227,7 @@ def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace")
def ascii_escaped(val: Union[bytes, str]) -> str:
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:
@@ -253,7 +251,7 @@ def ascii_escaped(val: Union[bytes, str]) -> str:
return _translate_non_printable(ret)
@attr.s
@dataclasses.dataclass
class _PytestWrapper:
"""Dummy wrapper around a function object for internal use only.
@@ -262,7 +260,7 @@ class _PytestWrapper:
decorator to issue warnings when the fixture function is called directly.
"""
obj = attr.ib()
obj: Any
def get_real_func(obj):
@@ -356,7 +354,6 @@ else:
if sys.version_info >= (3, 8):
from functools import cached_property as cached_property
else:
from typing import Type
class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__")
@@ -367,12 +364,12 @@ else:
@overload
def __get__(
self, instance: None, owner: Optional[Type[_S]] = ...
) -> "cached_property[_S, _T]":
self, instance: None, owner: type[_S] | None = ...
) -> cached_property[_S, _T]:
...
@overload
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T:
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
...
def __get__(self, instance, owner=None):
@@ -382,6 +379,18 @@ else:
return value
def get_user_id() -> int | None:
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
# win32 does not have a getuid() function.
# On Emscripten, getuid() is a stub that always returns 0.
if sys.platform in ("win32", "emscripten"):
return None
# getuid shouldn't fail, but cpython defines such a case.
# Let's hope for the best.
uid = os.getuid()
return uid if uid != -1 else None
# Perform exhaustiveness checking.
#
# Consider this example:
+50 -18
View File
@@ -2,6 +2,7 @@
import argparse
import collections.abc
import copy
import dataclasses
import enum
import glob
import inspect
@@ -34,7 +35,6 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import attr
from pluggy import HookimplMarker
from pluggy import HookspecMarker
from pluggy import PluginManager
@@ -49,7 +49,7 @@ from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.compat import importlib_metadata
from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
from _pytest.outcomes import fail
from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath
@@ -62,7 +62,6 @@ from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter
from .argparsing import Argument
@@ -527,7 +526,10 @@ class PytestPluginManager(PluginManager):
# Internal API for local conftest plugin handling.
#
def _set_initial_conftests(
self, namespace: argparse.Namespace, rootpath: Path
self,
namespace: argparse.Namespace,
rootpath: Path,
testpaths_ini: Sequence[str],
) -> None:
"""Load initial conftest files given a preparsed "namespace".
@@ -544,7 +546,7 @@ class PytestPluginManager(PluginManager):
)
self._noconftest = namespace.noconftest
self._using_pyargs = namespace.pyargs
testpaths = namespace.file_or_dir
testpaths = namespace.file_or_dir + testpaths_ini
foundanchor = False
for testpath in testpaths:
path = str(testpath)
@@ -553,7 +555,14 @@ class PytestPluginManager(PluginManager):
if i != -1:
path = path[:i]
anchor = absolutepath(current / path)
if anchor.exists(): # we found some file object
# Ensure we do not break if what appears to be an anchor
# is in fact a very long option (#10169).
try:
anchor_exists = anchor.exists()
except OSError: # pragma: no cover
anchor_exists = False
if anchor_exists:
self._try_load_conftest(anchor, namespace.importmode, rootpath)
foundanchor = True
if not foundanchor:
@@ -697,6 +706,7 @@ class PytestPluginManager(PluginManager):
parg = opt[2:]
else:
continue
parg = parg.strip()
if exclude_only and not parg.startswith("no:"):
continue
self.consider_pluginarg(parg)
@@ -886,10 +896,6 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
yield from _iter_rewritable_modules(new_package_files)
def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
return tuple(args)
@final
class Config:
"""Access to configuration values, pluginmanager and plugin hooks.
@@ -903,7 +909,7 @@ class Config:
"""
@final
@attr.s(frozen=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class InvocationParams:
"""Holds parameters passed during :func:`pytest.main`.
@@ -919,13 +925,24 @@ class Config:
Plugins accessing ``InvocationParams`` must be aware of that.
"""
args: Tuple[str, ...] = attr.ib(converter=_args_converter)
args: Tuple[str, ...]
"""The command-line arguments as passed to :func:`pytest.main`."""
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
"""Extra plugins, might be `None`."""
dir: Path
"""The directory from which :func:`pytest.main` was invoked."""
def __init__(
self,
*,
args: Iterable[str],
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]],
dir: Path,
) -> None:
object.__setattr__(self, "args", tuple(args))
object.__setattr__(self, "plugins", plugins)
object.__setattr__(self, "dir", dir)
class ArgsSource(enum.Enum):
"""Indicates the source of the test arguments.
@@ -998,6 +1015,8 @@ class Config:
self.hook.pytest_addoption.call_historic(
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
)
self.args_source = Config.ArgsSource.ARGS
self.args: List[str] = []
if TYPE_CHECKING:
from _pytest.cacheprovider import Cache
@@ -1057,7 +1076,6 @@ class Config:
try:
self.parse(args)
except UsageError:
# Handle --version and --help here in a minimal fashion.
# This gets done via helpconfig normally, but its
# pytest_cmdline_main is not called in case of errors.
@@ -1123,7 +1141,9 @@ class Config:
@hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
self.pluginmanager._set_initial_conftests(
early_config.known_args_namespace, rootpath=early_config.rootpath
early_config.known_args_namespace,
rootpath=early_config.rootpath,
testpaths_ini=self.getini("testpaths"),
)
def _initini(self, args: Sequence[str]) -> None:
@@ -1241,8 +1261,11 @@ class Config:
_pytest.deprecated.STRICT_OPTION, stacklevel=2
)
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
confcutdir = str(self.inipath.parent)
if self.known_args_namespace.confcutdir is None:
if self.inipath is not None:
confcutdir = str(self.inipath.parent)
else:
confcutdir = str(self.rootpath)
self.known_args_namespace.confcutdir = confcutdir
try:
self.hook.pytest_load_initial_conftests(
@@ -1337,8 +1360,8 @@ class Config:
def parse(self, args: List[str], addopts: bool = True) -> None:
# Parse given cmdline arguments into this config object.
assert not hasattr(
self, "args"
assert (
self.args == []
), "can only parse cmdline args at most once per Config object"
self.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=self.pluginmanager)
@@ -1362,6 +1385,15 @@ class Config:
args = []
for path in testpaths:
args.extend(sorted(glob.iglob(path, recursive=True)))
if testpaths and not args:
warning_text = (
"No files were found in testpaths; "
"consider removing or adjusting your testpaths configuration. "
"Searching recursively from the current directory instead."
)
self.issue_config_time_warning(
PytestConfigWarning(warning_text), stacklevel=3
)
if not args:
source = Config.ArgsSource.INCOVATION_DIR
args = [str(self.invocation_params.dir)]
-1
View File
@@ -43,7 +43,6 @@ class PathAwareHookProxy:
@_wraps(hook)
def fixed_hook(**kw):
path_value: Optional[Path] = kw.pop(path_var, None)
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
if fspath_value is not None:
+9 -2
View File
@@ -203,8 +203,7 @@ def determine_setup(
else:
cwd = Path.cwd()
rootdir = get_common_ancestor([cwd, ancestor])
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
if is_fs_root:
if is_fs_root(rootdir):
rootdir = ancestor
if rootdir_cmd_arg:
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
@@ -216,3 +215,11 @@ def determine_setup(
)
assert rootdir is not None
return rootdir, inipath, inicfg or {}
def is_fs_root(p: Path) -> bool:
r"""
Return True if the given path is pointing to the root of the
file system ("/" on Unix and "C:\\" on Windows for example).
"""
return os.path.splitdrive(str(p))[1] == os.sep
-1
View File
@@ -531,7 +531,6 @@ class DoctestModule(Module):
if _is_mocked(obj):
return
with _patch_unwrap_mock_aware():
# Type ignored because this is a private function.
super()._find( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen
+8 -10
View File
@@ -2,7 +2,6 @@ import io
import os
import sys
from typing import Generator
from typing import TextIO
import pytest
from _pytest.config import Config
@@ -11,7 +10,7 @@ from _pytest.nodes import Item
from _pytest.stash import StashKey
fault_handler_stderr_key = StashKey[TextIO]()
fault_handler_stderr_fd_key = StashKey[int]()
fault_handler_originally_enabled_key = StashKey[bool]()
@@ -26,10 +25,9 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_configure(config: Config) -> None:
import faulthandler
stderr_fd_copy = os.dup(get_stderr_fileno())
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
faulthandler.enable(file=config.stash[fault_handler_stderr_key])
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
def pytest_unconfigure(config: Config) -> None:
@@ -37,9 +35,9 @@ def pytest_unconfigure(config: Config) -> None:
faulthandler.disable()
# Close the dup file installed during pytest_configure.
if fault_handler_stderr_key in config.stash:
config.stash[fault_handler_stderr_key].close()
del config.stash[fault_handler_stderr_key]
if fault_handler_stderr_fd_key in config.stash:
os.close(config.stash[fault_handler_stderr_fd_key])
del config.stash[fault_handler_stderr_fd_key]
if config.stash.get(fault_handler_originally_enabled_key, False):
# Re-enable the faulthandler if it was originally enabled.
faulthandler.enable(file=get_stderr_fileno())
@@ -67,10 +65,10 @@ def get_timeout_config_value(config: Config) -> float:
@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
timeout = get_timeout_config_value(item.config)
stderr = item.config.stash[fault_handler_stderr_key]
if timeout > 0 and stderr is not None:
if timeout > 0:
import faulthandler
stderr = item.config.stash[fault_handler_stderr_fd_key]
faulthandler.dump_traceback_later(timeout, file=stderr)
try:
yield
+89 -48
View File
@@ -1,3 +1,4 @@
import dataclasses
import functools
import inspect
import os
@@ -28,8 +29,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
import _pytest
from _pytest import nodes
from _pytest._code import getfslineno
@@ -47,6 +46,7 @@ from _pytest.compat import getimfunc
from _pytest.compat import getlocation
from _pytest.compat import is_generator
from _pytest.compat import NOTSET
from _pytest.compat import NotSetType
from _pytest.compat import overload
from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin
@@ -58,6 +58,7 @@ from _pytest.mark import Mark
from _pytest.mark import ParameterSet
from _pytest.mark.structures import MarkDecorator
from _pytest.outcomes import fail
from _pytest.outcomes import skip
from _pytest.outcomes import TEST_OUTCOME
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
@@ -102,7 +103,7 @@ _FixtureCachedResult = Union[
]
@attr.s(frozen=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class PseudoFixtureDef(Generic[FixtureValue]):
cached_result: "_FixtureCachedResult[FixtureValue]"
_scope: Scope
@@ -112,16 +113,18 @@ def pytest_sessionstart(session: "Session") -> None:
session._fixturemanager = FixtureManager(session)
def get_scope_package(node, fixturedef: "FixtureDef[object]"):
import pytest
def get_scope_package(
node: nodes.Item,
fixturedef: "FixtureDef[object]",
) -> Optional[Union[nodes.Item, nodes.Collector]]:
from _pytest.python import Package
cls = pytest.Package
current = node
current: Optional[Union[nodes.Item, nodes.Collector]] = node
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
while current and (
type(current) is not cls or fixture_package_name != current.nodeid
not isinstance(current, Package) or fixture_package_name != current.nodeid
):
current = current.parent
current = current.parent # type: ignore[assignment]
if current is None:
return node.session
return current
@@ -349,8 +352,10 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
return request.param
@attr.s(slots=True, auto_attribs=True)
@dataclasses.dataclass
class FuncFixtureInfo:
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
# Original function argument names.
argnames: Tuple[str, ...]
# Argnames that function immediately requires. These include argnames +
@@ -432,7 +437,23 @@ class FixtureRequest:
@property
def node(self):
"""Underlying collection node (depends on current request scope)."""
return self._getscopeitem(self._scope)
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
elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on FixtureRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
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
)
return node
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None)
@@ -516,11 +537,7 @@ class FixtureRequest:
"""Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution."""
# XXX usually this method is shadowed by fixturedef specific ones.
self._addfinalizer(finalizer, scope=self.scope)
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
node = self._getscopeitem(scope)
node.addfinalizer(finalizer)
self.node.addfinalizer(finalizer)
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
"""Apply a marker to a single test function invocation.
@@ -715,28 +732,6 @@ class FixtureRequest:
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
return lines
def _getscopeitem(
self, scope: Union[Scope, "_ScopeName"]
) -> Union[nodes.Item, nodes.Collector]:
if isinstance(scope, str):
scope = Scope(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
elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on FixtureRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
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
)
return node
def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node)
@@ -1129,6 +1124,10 @@ def pytest_fixture_setup(
except TEST_OUTCOME:
exc_info = sys.exc_info()
assert exc_info[0] is not None
if isinstance(
exc_info[1], skip.Exception
) and not fixturefunc.__name__.startswith("xunit_setup"):
exc_info[1]._use_item_location = True # type: ignore[attr-defined]
fixturedef.cached_result = (None, my_cache_key, exc_info)
raise
fixturedef.cached_result = (result, my_cache_key, None)
@@ -1176,19 +1175,21 @@ def wrap_function_to_error_out_if_called_directly(
@final
@attr.s(frozen=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class FixtureFunctionMarker:
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
params: Optional[Tuple[object, ...]]
autouse: bool = False
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = attr.ib(
default=None,
converter=_ensure_immutable_ids,
)
] = None
name: Optional[str] = None
_ispytest: dataclasses.InitVar[bool] = False
def __post_init__(self, _ispytest: bool) -> None:
check_ispytest(_ispytest)
def __call__(self, function: FixtureFunction) -> FixtureFunction:
if inspect.isclass(function):
raise ValueError("class fixtures not supported (maybe in the future)")
@@ -1308,10 +1309,11 @@ def fixture( # noqa: F811
"""
fixture_marker = FixtureFunctionMarker(
scope=scope,
params=params,
params=tuple(params) if params is not None else None,
autouse=autouse,
ids=ids,
ids=None if ids is None else ids if callable(ids) else tuple(ids),
name=name,
_ispytest=True,
)
# Direct decoration.
@@ -1584,13 +1586,52 @@ class FixtureManager:
# Separate parametrized setups.
items[:] = reorder_items(items)
@overload
def parsefactories(
self, node_or_obj, nodeid=NOTSET, unittest: bool = False
self,
node_or_obj: nodes.Node,
*,
unittest: bool = ...,
) -> None:
raise NotImplementedError()
@overload
def parsefactories( # noqa: F811
self,
node_or_obj: object,
nodeid: Optional[str],
*,
unittest: bool = ...,
) -> None:
raise NotImplementedError()
def parsefactories( # noqa: F811
self,
node_or_obj: Union[nodes.Node, object],
nodeid: Union[str, NotSetType, None] = NOTSET,
*,
unittest: bool = False,
) -> None:
"""Collect fixtures from a collection node or object.
Found fixtures are parsed into `FixtureDef`s and saved.
If `node_or_object` is a collection node (with an underlying Python
object), the node's object is traversed and the node's nodeid is used to
determine the fixtures' visibilty. `nodeid` must not be specified in
this case.
If `node_or_object` is an object (e.g. a plugin), the object is
traversed and the given `nodeid` is used to determine the fixtures'
visibility. `nodeid` must be specified in this case; None and "" mean
total visibility.
"""
if nodeid is not NOTSET:
holderobj = node_or_obj
else:
holderobj = node_or_obj.obj
assert isinstance(node_or_obj, nodes.Node)
holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
assert isinstance(node_or_obj.nodeid, str)
nodeid = node_or_obj.nodeid
if holderobj in self._holderobjseen:
return
+2 -1
View File
@@ -164,7 +164,8 @@ def showhelp(config: Config) -> None:
tw.write(config._parser.optparser.format_help())
tw.line()
tw.line(
"[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:"
"[pytest] ini-options in the first "
"pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:"
)
tw.line()
+15 -8
View File
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
from typing_extensions import Literal
from _pytest._code.code import ExceptionRepr
from _pytest.code import ExceptionInfo
from _pytest._code.code import ExceptionInfo
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PytestPluginManager
@@ -41,6 +41,7 @@ if TYPE_CHECKING:
from _pytest.reports import TestReport
from _pytest.runner import CallInfo
from _pytest.terminal import TerminalReporter
from _pytest.terminal import TestShortLogReport
from _pytest.compat import LEGACY_PATH
@@ -505,7 +506,9 @@ def pytest_runtest_logstart(
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
:param nodeid: Full node ID of the item.
:param location: A tuple of ``(filename, lineno, testname)``.
:param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based.
"""
@@ -517,7 +520,9 @@ def pytest_runtest_logfinish(
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
:param nodeid: Full node ID of the item.
:param location: A tuple of ``(filename, lineno, testname)``.
:param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based.
"""
@@ -738,7 +743,7 @@ def pytest_assertion_pass(item: "Item", lineno: int, orig: str, expl: str) -> No
# -------------------------------------------------------------------------
def pytest_report_header(
def pytest_report_header( # type:ignore[empty-body]
config: "Config", start_path: Path, startdir: "LEGACY_PATH"
) -> Union[str, List[str]]:
"""Return a string or list of strings to be displayed as header info for terminal reporting.
@@ -767,7 +772,7 @@ def pytest_report_header(
"""
def pytest_report_collectionfinish(
def pytest_report_collectionfinish( # type:ignore[empty-body]
config: "Config",
start_path: Path,
startdir: "LEGACY_PATH",
@@ -800,9 +805,9 @@ def pytest_report_collectionfinish(
@hookspec(firstresult=True)
def pytest_report_teststatus(
def pytest_report_teststatus( # type:ignore[empty-body]
report: Union["CollectReport", "TestReport"], config: "Config"
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]":
"""Return result-category, shortletter and verbose word for status
reporting.
@@ -880,7 +885,9 @@ def pytest_warning_recorded(
# -------------------------------------------------------------------------
def pytest_markeval_namespace(config: "Config") -> Dict[str, Any]:
def pytest_markeval_namespace( # type:ignore[empty-body]
config: "Config",
) -> Dict[str, Any]:
"""Called when constructing the globals dictionary used for
evaluating string conditions in xfail/skipif markers.
+2 -2
View File
@@ -645,8 +645,8 @@ class LogXML:
def pytest_sessionfinish(self) -> None:
dirname = os.path.dirname(os.path.abspath(self.logfile))
if not os.path.isdir(dirname):
os.makedirs(dirname)
# exist_ok avoids filesystem race conditions between checking path existence and requesting creation
os.makedirs(dirname, exist_ok=True)
with open(self.logfile, "w", encoding="utf-8") as logfile:
suite_stop_time = timing.time()
+2 -2
View File
@@ -1,4 +1,5 @@
"""Add backward compatibility support for the legacy py path type."""
import dataclasses
import shlex
import subprocess
from pathlib import Path
@@ -7,7 +8,6 @@ from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
import attr
from iniconfig import SectionWrapper
from _pytest.cacheprovider import Cache
@@ -268,7 +268,7 @@ class LegacyTestdirPlugin:
@final
@attr.s(init=False, auto_attribs=True)
@dataclasses.dataclass
class TempdirFactory:
"""Backward compatibility wrapper that implements :class:`py.path.local`
for :class:`TempPathFactory`.
+90 -4
View File
@@ -5,7 +5,11 @@ import os
import re
from contextlib import contextmanager
from contextlib import nullcontext
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from io import StringIO
from logging import LogRecord
from pathlib import Path
from typing import AbstractSet
from typing import Dict
@@ -53,7 +57,25 @@ def _remove_ansi_escape_sequences(text: str) -> str:
return _ANSI_ESCAPE_SEQ.sub("", text)
class ColoredLevelFormatter(logging.Formatter):
class DatetimeFormatter(logging.Formatter):
"""A logging formatter which formats record with
:func:`datetime.datetime.strftime` formatter instead of
:func:`time.strftime` in case of microseconds in format string.
"""
def formatTime(self, record: LogRecord, datefmt=None) -> str:
if datefmt and "%f" in datefmt:
ct = self.converter(record.created)
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
# Construct `datetime.datetime` object from `struct_time`
# and msecs information from `record`
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
return dt.strftime(datefmt)
# Use `logging.Formatter` for non-microsecond formats
return super().formatTime(record, datefmt)
class ColoredLevelFormatter(DatetimeFormatter):
"""A logging formatter which colorizes the %(levelname)..s part of the
log format passed to __init__."""
@@ -297,6 +319,13 @@ def pytest_addoption(parser: Parser) -> None:
default=None,
help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.",
)
group.addoption(
"--log-disable",
action="append",
default=[],
dest="logger_disable",
help="Disable a logger by name. Can be passed multiple times.",
)
_HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
@@ -369,11 +398,12 @@ class LogCaptureFixture:
self._initial_handler_level: Optional[int] = None
# Dict of log name -> log level.
self._initial_logger_levels: Dict[Optional[str], int] = {}
self._initial_disabled_logging_level: Optional[int] = None
def _finalize(self) -> None:
"""Finalize the fixture.
This restores the log levels changed by :meth:`set_level`.
This restores the log levels and the disabled logging levels changed by :meth:`set_level`.
"""
# Restore log levels.
if self._initial_handler_level is not None:
@@ -381,6 +411,10 @@ class LogCaptureFixture:
for logger_name, level in self._initial_logger_levels.items():
logger = logging.getLogger(logger_name)
logger.setLevel(level)
# Disable logging at the original disabled logging level.
if self._initial_disabled_logging_level is not None:
logging.disable(self._initial_disabled_logging_level)
self._initial_disabled_logging_level = None
@property
def handler(self) -> LogCaptureHandler:
@@ -446,6 +480,40 @@ class LogCaptureFixture:
"""Reset the list of log records and the captured log text."""
self.handler.clear()
def _force_enable_logging(
self, level: Union[int, str], logger_obj: logging.Logger
) -> int:
"""Enable the desired logging level if the global level was disabled via ``logging.disabled``.
Only enables logging levels greater than or equal to the requested ``level``.
Does nothing if the desired ``level`` wasn't disabled.
:param level:
The logger level caplog should capture.
All logging is enabled if a non-standard logging level string is supplied.
Valid level strings are in :data:`logging._nameToLevel`.
:param logger_obj: The logger object to check.
:return: The original disabled logging level.
"""
original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
if isinstance(level, str):
# Try to translate the level string to an int for `logging.disable()`
level = logging.getLevelName(level)
if not isinstance(level, int):
# The level provided was not valid, so just un-disable all logging.
logging.disable(logging.NOTSET)
elif not logger_obj.isEnabledFor(level):
# Each level is `10` away from other levels.
# https://docs.python.org/3/library/logging.html#logging-levels
disable_level = max(level - 10, logging.NOTSET)
logging.disable(disable_level)
return original_disable_level
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
"""Set the level of a logger for the duration of a test.
@@ -453,6 +521,8 @@ class LogCaptureFixture:
The levels of the loggers changed by this function will be
restored to their initial values at the end of the test.
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
:param level: The level.
:param logger: The logger to update. If not given, the root logger.
"""
@@ -463,6 +533,9 @@ class LogCaptureFixture:
if self._initial_handler_level is None:
self._initial_handler_level = self.handler.level
self.handler.setLevel(level)
initial_disabled_logging_level = self._force_enable_logging(level, logger_obj)
if self._initial_disabled_logging_level is None:
self._initial_disabled_logging_level = initial_disabled_logging_level
@contextmanager
def at_level(
@@ -472,6 +545,8 @@ class LogCaptureFixture:
the end of the 'with' statement the level is restored to its original
value.
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
:param level: The level.
:param logger: The logger to update. If not given, the root logger.
"""
@@ -480,11 +555,13 @@ class LogCaptureFixture:
logger_obj.setLevel(level)
handler_orig_level = self.handler.level
self.handler.setLevel(level)
original_disable_level = self._force_enable_logging(level, logger_obj)
try:
yield
finally:
logger_obj.setLevel(orig_level)
self.handler.setLevel(handler_orig_level)
logging.disable(original_disable_level)
@fixture
@@ -570,7 +647,7 @@ class LoggingPlugin:
config, "log_file_date_format", "log_date_format"
)
log_file_formatter = logging.Formatter(
log_file_formatter = DatetimeFormatter(
log_file_format, datefmt=log_file_date_format
)
self.log_file_handler.setFormatter(log_file_formatter)
@@ -594,6 +671,15 @@ class LoggingPlugin:
get_option_ini(config, "log_auto_indent"),
)
self.log_cli_handler.setFormatter(log_cli_formatter)
self._disable_loggers(loggers_to_disable=config.option.logger_disable)
def _disable_loggers(self, loggers_to_disable: List[str]) -> None:
if not loggers_to_disable:
return
for name in loggers_to_disable:
logger = logging.getLogger(name)
logger.disabled = True
def _create_formatter(self, log_format, log_date_format, auto_indent):
# Color option doesn't exist if terminal plugin is disabled.
@@ -605,7 +691,7 @@ class LoggingPlugin:
create_terminal_writer(self._config), log_format, log_date_format
)
else:
formatter = logging.Formatter(log_format, log_date_format)
formatter = DatetimeFormatter(log_format, log_date_format)
formatter._style = PercentStyleMultiline(
formatter._style._fmt, auto_indent=auto_indent
+21 -16
View File
@@ -1,5 +1,6 @@
"""Core implementation of the testing process: init, session, runtest loop."""
import argparse
import dataclasses
import fnmatch
import functools
import importlib
@@ -19,8 +20,6 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import attr
import _pytest._code
from _pytest import nodes
from _pytest.compat import final
@@ -123,11 +122,12 @@ def pytest_addoption(parser: Parser) -> None:
)
group._addoption(
"-c",
metavar="file",
"--config-file",
metavar="FILE",
type=str,
dest="inifilename",
help="Load configuration from `file` instead of trying to locate one of the "
"implicit configuration files",
help="Load configuration from `FILE` instead of trying to locate one of the "
"implicit configuration files.",
)
group._addoption(
"--continue-on-collection-errors",
@@ -400,6 +400,12 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
allow_in_venv = config.getoption("collect_in_virtualenv")
if not allow_in_venv and _in_venv(collection_path):
return True
if collection_path.is_dir():
norecursepatterns = config.getini("norecursedirs")
if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
return True
return None
@@ -442,8 +448,10 @@ class Failed(Exception):
"""Signals a stop as failed test run."""
@attr.s(slots=True, auto_attribs=True)
@dataclasses.dataclass
class _bestrelpath_cache(Dict[Path, str]):
__slots__ = ("path",)
path: Path
def __missing__(self, path: Path) -> str:
@@ -561,9 +569,6 @@ class Session(nodes.FSCollector):
ihook = self.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True
def _collectfile(
@@ -684,8 +689,8 @@ class Session(nodes.FSCollector):
# are not collected more than once.
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
# Dirnames of pkgs with dunder-init files.
pkg_roots: Dict[str, Package] = {}
# Directories of pkgs with dunder-init files.
pkg_roots: Dict[Path, Package] = {}
for argpath, names in self._initial_parts:
self.trace("processing argument", (argpath, names))
@@ -706,7 +711,7 @@ class Session(nodes.FSCollector):
col = self._collectfile(pkginit, handle_dupes=False)
if col:
if isinstance(col[0], Package):
pkg_roots[str(parent)] = col[0]
pkg_roots[parent] = col[0]
node_cache1[col[0].path] = [col[0]]
# If it's a directory argument, recurse and look for any Subpackages.
@@ -715,7 +720,7 @@ class Session(nodes.FSCollector):
assert not names, f"invalid arg {(argpath, names)!r}"
seen_dirs: Set[Path] = set()
for direntry in visit(str(argpath), self._recurse):
for direntry in visit(argpath, self._recurse):
if not direntry.is_file():
continue
@@ -730,8 +735,8 @@ class Session(nodes.FSCollector):
for x in self._collectfile(pkginit):
yield x
if isinstance(x, Package):
pkg_roots[str(dirpath)] = x
if str(dirpath) in pkg_roots:
pkg_roots[dirpath] = x
if dirpath in pkg_roots:
# Do not collect packages here.
continue
@@ -748,7 +753,7 @@ class Session(nodes.FSCollector):
if argpath in node_cache1:
col = node_cache1[argpath]
else:
collect_root = pkg_roots.get(str(argpath.parent), self)
collect_root = pkg_roots.get(argpath.parent, self)
col = collect_root._collectfile(argpath, handle_dupes=False)
if col:
node_cache1[argpath] = col
+7 -4
View File
@@ -1,4 +1,5 @@
"""Generic mechanism for marking and selecting python functions."""
import dataclasses
from typing import AbstractSet
from typing import Collection
from typing import List
@@ -6,8 +7,6 @@ from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
import attr
from .expression import Expression
from .expression import ParseError
from .structures import EMPTY_PARAMETERSET_OPTION
@@ -130,7 +129,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
return None
@attr.s(slots=True, auto_attribs=True)
@dataclasses.dataclass
class KeywordMatcher:
"""A matcher for keywords.
@@ -145,6 +144,8 @@ class KeywordMatcher:
any item, as well as names directly assigned to test functions.
"""
__slots__ = ("_names",)
_names: AbstractSet[str]
@classmethod
@@ -201,13 +202,15 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
items[:] = remaining
@attr.s(slots=True, auto_attribs=True)
@dataclasses.dataclass
class MarkMatcher:
"""A matcher for markers which are present.
Tries to match on any marker names, attached to the given colitem.
"""
__slots__ = ("own_mark_names",)
own_mark_names: AbstractSet[str]
@classmethod
+9 -3
View File
@@ -15,8 +15,10 @@ The semantics are:
- or/and/not evaluate according to the usual boolean semantics.
"""
import ast
import dataclasses
import enum
import re
import sys
import types
from typing import Callable
from typing import Iterator
@@ -25,7 +27,10 @@ from typing import NoReturn
from typing import Optional
from typing import Sequence
import attr
if sys.version_info >= (3, 8):
astNameConstant = ast.Constant
else:
astNameConstant = ast.NameConstant
__all__ = [
@@ -44,8 +49,9 @@ class TokenType(enum.Enum):
EOF = "end of input"
@attr.s(frozen=True, slots=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class Token:
__slots__ = ("type", "value", "pos")
type: TokenType
value: str
pos: int
@@ -132,7 +138,7 @@ IDENT_PREFIX = "$"
def expression(s: Scanner) -> ast.Expression:
if s.accept(TokenType.EOF):
ret: ast.expr = ast.NameConstant(False)
ret: ast.expr = astNameConstant(False)
else:
ret = expr(s)
s.accept(TokenType.EOF, reject=True)
+9 -6
View File
@@ -1,4 +1,5 @@
import collections.abc
import dataclasses
import inspect
import warnings
from typing import Any
@@ -20,8 +21,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
from .._code import getfslineno
from ..compat import ascii_escaped
from ..compat import final
@@ -191,8 +190,10 @@ class ParameterSet(NamedTuple):
@final
@attr.s(frozen=True, init=False, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class Mark:
"""A pytest mark."""
#: Name of the mark.
name: str
#: Positional arguments of the mark decorator.
@@ -201,9 +202,11 @@ class Mark:
kwargs: Mapping[str, Any]
#: Source Mark for ids with parametrize Marks.
_param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False)
_param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False)
#: Resolved/generated ids with parametrize Marks.
_param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)
_param_ids_generated: Optional[Sequence[str]] = dataclasses.field(
default=None, repr=False
)
def __init__(
self,
@@ -261,7 +264,7 @@ class Mark:
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
@attr.s(init=False, auto_attribs=True)
@dataclasses.dataclass
class MarkDecorator:
"""A decorator for applying a mark on test functions and classes.
+12 -7
View File
@@ -7,6 +7,7 @@ from contextlib import contextmanager
from typing import Any
from typing import Generator
from typing import List
from typing import Mapping
from typing import MutableMapping
from typing import Optional
from typing import overload
@@ -129,7 +130,7 @@ class MonkeyPatch:
def __init__(self) -> None:
self._setattr: List[Tuple[object, str, object]] = []
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = []
self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
self._cwd: Optional[str] = None
self._savesyspath: Optional[List[str]] = None
@@ -290,12 +291,13 @@ class MonkeyPatch:
self._setattr.append((target, name, oldval))
delattr(target, name)
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
"""Set dictionary entry ``name`` to value."""
self._setitem.append((dic, name, dic.get(name, notset)))
dic[name] = value
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
dic[name] = value # type: ignore[index]
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
"""Delete ``name`` from dict.
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
@@ -306,7 +308,8 @@ class MonkeyPatch:
raise KeyError(name)
else:
self._setitem.append((dic, name, dic.get(name, notset)))
del dic[name]
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
del dic[name] # type: ignore[attr-defined]
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
"""Set environment variable ``name`` to ``value``.
@@ -401,11 +404,13 @@ class MonkeyPatch:
for dictionary, key, value in reversed(self._setitem):
if value is notset:
try:
del dictionary[key]
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
del dictionary[key] # type: ignore[attr-defined]
except KeyError:
pass # Was already deleted, so we have the desired state.
else:
dictionary[key] = value
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
dictionary[key] = value # type: ignore[index]
self._setitem[:] = []
if self._savesyspath is not None:
sys.path[:] = self._savesyspath
+18 -11
View File
@@ -22,6 +22,7 @@ import _pytest._code
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 cached_property
from _pytest.compat import LEGACY_PATH
from _pytest.config import Config
@@ -432,8 +433,8 @@ class Node(metaclass=NodeMeta):
assert current is None or isinstance(current, cls)
return current
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
pass
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
return excinfo.traceback
def _repr_failure_py(
self,
@@ -449,13 +450,13 @@ class Node(metaclass=NodeMeta):
style = "value"
if isinstance(excinfo.value, FixtureLookupError):
return excinfo.value.formatrepr()
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]]
if self.config.getoption("fulltrace", False):
style = "long"
tbfilter = False
else:
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
self._prunetraceback(excinfo)
if len(excinfo.traceback) == 0:
excinfo.traceback = tb
tbfilter = self._traceback_filter
if style == "auto":
style = "long"
# XXX should excinfo.getrepr record all data and toterminal() process it?
@@ -486,7 +487,7 @@ class Node(metaclass=NodeMeta):
abspath=abspath,
showlocals=self.config.getoption("showlocals", False),
style=style,
tbfilter=False, # pruned already, or in --fulltrace mode.
tbfilter=tbfilter,
truncate_locals=truncate_locals,
)
@@ -511,7 +512,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
* "obj": a Python object that the node wraps.
* "fspath": just a path
:rtype: A tuple of (str|Path, int) with filename and line number.
:rtype: A tuple of (str|Path, int) with filename and 0-based line number.
"""
# See Item.location.
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
@@ -557,13 +558,14 @@ class Collector(Node):
return self._repr_failure_py(excinfo, style=tbstyle)
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
if hasattr(self, "path"):
traceback = excinfo.traceback
ntraceback = traceback.cut(path=self.path)
if ntraceback == traceback:
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
excinfo.traceback = ntraceback.filter()
return excinfo.traceback.filter(excinfo)
return excinfo.traceback
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
@@ -755,7 +757,7 @@ class Item(Node):
Returns a tuple with three elements:
- The path of the test (default ``self.path``)
- The line number of the test (default ``None``)
- The 0-based line number of the test (default ``None``)
- A name of the test to be shown (default ``""``)
.. seealso:: :ref:`non-python tests`
@@ -764,6 +766,11 @@ class Item(Node):
@cached_property
def location(self) -> Tuple[str, Optional[int], str]:
"""
Returns a tuple of ``(relfspath, lineno, testname)`` for this item
where ``relfspath`` is file path relative to ``config.rootpath``
and lineno is a 0-based line number.
"""
location = self.reportinfo()
path = absolutepath(os.fspath(location[0]))
relfspath = self.session._node_location_to_relpath(path)
+6 -3
View File
@@ -157,8 +157,12 @@ def skip(
The message to show the user as reason for the skip.
:param allow_module_level:
Allows this function to be called at module level, skipping the rest
of the module. Defaults to False.
Allows this function to be called at module level.
Raising the skip exception at module level will stop
the execution of the module and prevent the collection of all tests in the module,
even those defined before the `skip` call.
Defaults to False.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
@@ -219,7 +223,6 @@ def _resolve_msg_to_reason(
"""
__tracebackhide__ = True
if msg is not None:
if reason:
from pytest import UsageError
+70 -30
View File
@@ -6,6 +6,7 @@ import itertools
import os
import shutil
import sys
import types
import uuid
import warnings
from enum import Enum
@@ -26,8 +27,11 @@ from typing import Callable
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
@@ -63,21 +67,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
return path.joinpath(".lock")
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
def on_rm_rf_error(
func,
path: str,
excinfo: Union[
BaseException,
Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
],
*,
start_path: Path,
) -> bool:
"""Handle known read-only errors during rmtree.
The returned value is used only by our own tests.
"""
exctype, excvalue = exc[:2]
if isinstance(excinfo, BaseException):
exc = excinfo
else:
exc = excinfo[1]
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
if isinstance(excvalue, FileNotFoundError):
if isinstance(exc, FileNotFoundError):
return False
if not isinstance(excvalue, PermissionError):
if not isinstance(exc, PermissionError):
warnings.warn(
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
)
return False
@@ -86,7 +102,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
warnings.warn(
PytestWarning(
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
func, path, exctype, excvalue
func, path, type(exc), exc
)
)
)
@@ -149,7 +165,10 @@ def rm_rf(path: Path) -> None:
are read-only."""
path = ensure_extended_length_path(path)
onerror = partial(on_rm_rf_error, start_path=path)
shutil.rmtree(str(path), onerror=onerror)
if sys.version_info >= (3, 12):
shutil.rmtree(str(path), onexc=onerror)
else:
shutil.rmtree(str(path), onerror=onerror)
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
@@ -335,15 +354,26 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
yield path
def cleanup_dead_symlinks(root: Path):
for left_dir in root.iterdir():
if left_dir.is_symlink():
if not left_dir.resolve().exists():
left_dir.unlink()
def cleanup_numbered_dir(
root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
) -> None:
"""Cleanup for lock driven numbered directories."""
if not root.exists():
return
for path in cleanup_candidates(root, prefix, keep):
try_cleanup(path, consider_lock_dead_if_created_before)
for path in root.glob("garbage-*"):
try_cleanup(path, consider_lock_dead_if_created_before)
cleanup_dead_symlinks(root)
def make_numbered_dir_with_cleanup(
root: Path,
@@ -357,8 +387,10 @@ def make_numbered_dir_with_cleanup(
for i in range(10):
try:
p = make_numbered_dir(root, prefix, mode)
lock_path = create_cleanup_lock(p)
register_cleanup_lock_removal(lock_path)
# Only lock the current dir when keep is not 0
if keep != 0:
lock_path = create_cleanup_lock(p)
register_cleanup_lock_removal(lock_path)
except Exception as exc:
e = exc
else:
@@ -464,14 +496,14 @@ def import_path(
* `mode == ImportMode.prepend`: the directory containing the module (or package, taking
`__init__.py` files into account) will be put at the *start* of `sys.path` before
being imported with `__import__.
being imported with `importlib.import_module`.
* `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
to the end of `sys.path`, if not already in `sys.path`.
* `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib`
to import the module, which avoids having to use `__import__` and muck with `sys.path`
at all. It effectively allows having same-named test modules in different places.
to import the module, which avoids having to muck with `sys.path` at all. It effectively
allows having same-named test modules in different places.
:param root:
Used as an anchor when mode == ImportMode.importlib to obtain
@@ -544,8 +576,8 @@ def import_path(
if module_file.endswith((".pyc", ".pyo")):
module_file = module_file[:-1]
if module_file.endswith(os.path.sep + "__init__.py"):
module_file = module_file[: -(len(os.path.sep + "__init__.py"))]
if module_file.endswith(os.sep + "__init__.py"):
module_file = module_file[: -(len(os.sep + "__init__.py"))]
try:
is_same = _is_same(str(path), module_file)
@@ -638,30 +670,38 @@ def resolve_package_path(path: Path) -> Optional[Path]:
return result
def scandir(path: Union[str, "os.PathLike[str]"]) -> List["os.DirEntry[str]"]:
"""Scan a directory recursively, in breadth-first order.
The returned entries are sorted.
"""
entries = []
with os.scandir(path) as s:
# Skip entries with symlink loops and other brokenness, so the caller
# doesn't have to deal with it.
for entry in s:
try:
entry.is_file()
except OSError as err:
if _ignore_error(err):
continue
raise
entries.append(entry)
entries.sort(key=lambda entry: entry.name)
return entries
def visit(
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
) -> Iterator["os.DirEntry[str]"]:
"""Walk a directory recursively, in breadth-first order.
The `recurse` predicate determines whether a directory is recursed.
Entries at each directory level are sorted.
"""
# Skip entries with symlink loops and other brokenness, so the caller doesn't
# have to deal with it.
entries = []
for entry in os.scandir(path):
try:
entry.is_file()
except OSError as err:
if _ignore_error(err):
continue
raise
entries.append(entry)
entries.sort(key=lambda entry: entry.name)
entries = scandir(path)
yield from entries
for entry in entries:
if entry.is_dir() and recurse(entry):
yield from visit(entry.path, recurse)
+47 -41
View File
@@ -1,4 +1,5 @@
"""Python test discovery, setup and run of test functions."""
import dataclasses
import enum
import fnmatch
import inspect
@@ -27,8 +28,6 @@ from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import attr
import _pytest
from _pytest import fixtures
from _pytest import nodes
@@ -36,6 +35,7 @@ from _pytest._code import filter_traceback
from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback
from _pytest._io import TerminalWriter
from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped
@@ -57,7 +57,6 @@ from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import INSTANCE_COLLECTOR
from _pytest.deprecated import NOSE_SUPPORT_METHOD
from _pytest.fixtures import FuncFixtureInfo
@@ -403,8 +402,8 @@ class PyCollector(PyobjMixin, nodes.Collector):
def istestfunction(self, obj: object, name: str) -> bool:
if self.funcnamefilter(name) or self.isnosetest(obj):
if isinstance(obj, staticmethod):
# staticmethods need to be unwrapped.
if isinstance(obj, (staticmethod, classmethod)):
# staticmethods and classmethods need to be unwrapped.
obj = safe_getattr(obj, "__func__", False)
return callable(obj) and fixtures.getfixturemarker(obj) is None
else:
@@ -668,7 +667,7 @@ class Package(Module):
config=None,
session=None,
nodeid=None,
path=Optional[Path],
path: Optional[Path] = None,
) -> None:
# NOTE: Could be just the following, but kept as-is for compat.
# nodes.FSCollector.__init__(self, fspath, parent=parent)
@@ -700,14 +699,6 @@ class Package(Module):
func = partial(_call_with_optional_argument, teardown_module, self.obj)
self.addfinalizer(func)
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)
def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
if direntry.name == "__pycache__":
return False
@@ -715,9 +706,6 @@ class Package(Module):
ihook = self.session.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True
def _collectfile(
@@ -746,11 +734,11 @@ class Package(Module):
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.path.parent
init_module = this_path / "__init__.py"
if init_module.is_file() and path_matches_patterns(
init_module, self.config.getini("python_files")
):
yield Module.from_parent(self, path=init_module)
# Always collect the __init__ first.
if path_matches_patterns(self.path, self.config.getini("python_files")):
yield Module.from_parent(self, path=self.path)
pkg_prefixes: Set[Path] = set()
for direntry in visit(str(this_path), recurse=self._recurse):
path = Path(direntry.path)
@@ -790,7 +778,8 @@ def _call_with_optional_argument(func, arg) -> None:
def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]:
"""Return the attribute from the given object to be used as a setup/teardown
xunit-style function, but only if not marked as a fixture to avoid calling it twice."""
xunit-style function, but only if not marked as a fixture to avoid calling it twice.
"""
for name in names:
meth: Optional[object] = getattr(obj, name, None)
if meth is not None and fixtures.getfixturemarker(meth) is None:
@@ -848,7 +837,7 @@ class Class(PyCollector):
other fixtures (#517).
"""
setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
teardown_class = getattr(self.obj, "teardown_class", None)
teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",))
if setup_class is None and teardown_class is None:
return
@@ -885,12 +874,12 @@ class Class(PyCollector):
emit_nose_setup_warning = True
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
teardown_name = "teardown_method"
teardown_method = getattr(self.obj, teardown_name, None)
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
emit_nose_teardown_warning = False
if teardown_method is None and has_nose:
teardown_name = "teardown"
emit_nose_teardown_warning = True
teardown_method = getattr(self.obj, teardown_name, None)
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
if setup_method is None and teardown_method is None:
return
@@ -956,10 +945,20 @@ def hasnew(obj: object) -> bool:
@final
@attr.s(frozen=True, auto_attribs=True, slots=True)
@dataclasses.dataclass(frozen=True)
class IdMaker:
"""Make IDs for a parametrization."""
__slots__ = (
"argnames",
"parametersets",
"idfn",
"ids",
"config",
"nodeid",
"func_name",
)
# The argnames of the parametrization.
argnames: Sequence[str]
# The ParameterSets of the parametrization.
@@ -1109,7 +1108,7 @@ class IdMaker:
@final
@attr.s(frozen=True, slots=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class CallSpec2:
"""A planned parameterized invocation of a test function.
@@ -1120,18 +1119,18 @@ class CallSpec2:
# arg name -> arg value which will be passed to the parametrized test
# function (direct parameterization).
funcargs: Dict[str, object] = attr.Factory(dict)
funcargs: Dict[str, object] = dataclasses.field(default_factory=dict)
# arg name -> arg value which will be passed to a fixture of the same name
# (indirect parametrization).
params: Dict[str, object] = attr.Factory(dict)
params: Dict[str, object] = dataclasses.field(default_factory=dict)
# arg name -> arg index.
indices: Dict[str, int] = attr.Factory(dict)
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
# Used for sorting parametrized resources.
_arg2scope: Dict[str, Scope] = attr.Factory(dict)
_arg2scope: Dict[str, Scope] = dataclasses.field(default_factory=dict)
# Parts which will be added to the item's name in `[..]` separated by "-".
_idlist: List[str] = attr.Factory(list)
_idlist: List[str] = dataclasses.field(default_factory=list)
# Marks which will be applied to the item.
marks: List[Mark] = attr.Factory(list)
marks: List[Mark] = dataclasses.field(default_factory=list)
def setmulti(
self,
@@ -1163,9 +1162,9 @@ class CallSpec2:
return CallSpec2(
funcargs=funcargs,
params=params,
arg2scope=arg2scope,
indices=indices,
idlist=[*self._idlist, id],
_arg2scope=arg2scope,
_idlist=[*self._idlist, id],
marks=[*self.marks, *normalize_mark_list(marks)],
)
@@ -1791,7 +1790,7 @@ class Function(PyobjMixin, nodes.Item):
def setup(self) -> None:
self._request._fillfixtures()
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
code = _pytest._code.Code.from_function(get_real_func(self.obj))
path, firstlineno = code.path, code.firstlineno
@@ -1803,14 +1802,21 @@ class Function(PyobjMixin, nodes.Item):
ntraceback = ntraceback.filter(filter_traceback)
if not ntraceback:
ntraceback = traceback
ntraceback = ntraceback.filter(excinfo)
excinfo.traceback = ntraceback.filter()
# issue364: mark all but first and last frames to
# only show a single-line message for each frame.
if self.config.getoption("tbstyle", "auto") == "auto":
if len(excinfo.traceback) > 2:
for entry in excinfo.traceback[1:-1]:
entry.set_repr_style("short")
if len(ntraceback) > 2:
ntraceback = Traceback(
entry
if i == 0 or i == len(ntraceback) - 1
else entry.with_repr_style("short")
for i, entry in enumerate(ntraceback)
)
return ntraceback
return excinfo.traceback
# TODO: Type ignored -- breaks Liskov Substitution.
def repr_failure( # type: ignore[override]
+18 -16
View File
@@ -8,7 +8,7 @@ from types import TracebackType
from typing import Any
from typing import Callable
from typing import cast
from typing import Generic
from typing import ContextManager
from typing import List
from typing import Mapping
from typing import Optional
@@ -269,10 +269,16 @@ class ApproxMapping(ApproxBase):
max_abs_diff = max(
max_abs_diff, abs(approx_value.expected - other_value)
)
max_rel_diff = max(
max_rel_diff,
abs((approx_value.expected - other_value) / approx_value.expected),
)
if approx_value.expected == 0.0:
max_rel_diff = math.inf
else:
max_rel_diff = max(
max_rel_diff,
abs(
(approx_value.expected - other_value)
/ approx_value.expected
),
)
different_ids.append(approx_key)
message_data = [
@@ -801,8 +807,8 @@ def raises( # noqa: F811
r"""Assert that a code block/function call raises an exception.
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
The excpected exception type, or a tuple if one of multiple possible
exception types are excepted.
The expected exception type, or a tuple if one of multiple possible
exception types are expected.
:kwparam str | typing.Pattern[str] | None match:
If specified, a string containing a regular expression,
or a regular expression object, that is tested against the string
@@ -918,10 +924,10 @@ def raises( # noqa: F811
f"any special code to say 'this should never raise an exception'."
)
if isinstance(expected_exception, type):
excepted_exceptions: Tuple[Type[E], ...] = (expected_exception,)
expected_exceptions: Tuple[Type[E], ...] = (expected_exception,)
else:
excepted_exceptions = expected_exception
for exc in excepted_exceptions:
expected_exceptions = expected_exception
for exc in expected_exceptions:
if not isinstance(exc, type) or not issubclass(exc, BaseException):
msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
@@ -944,11 +950,7 @@ def raises( # noqa: F811
try:
func(*args[1:], **kwargs)
except expected_exception as e:
# We just caught the exception - there is a traceback.
assert e.__traceback__ is not None
return _pytest._code.ExceptionInfo.from_exc_info(
(type(e), e, e.__traceback__)
)
return _pytest._code.ExceptionInfo.from_exception(e)
fail(message)
@@ -957,7 +959,7 @@ raises.Exception = fail.Exception # type: ignore
@final
class RaisesContext(Generic[E]):
class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
def __init__(
self,
expected_exception: Union[Type[E], Tuple[Type[E], ...]],
+28 -9
View File
@@ -1,3 +1,4 @@
import dataclasses
import os
from io import StringIO
from pprint import pprint
@@ -16,8 +17,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ExceptionRepr
@@ -263,6 +262,8 @@ class TestReport(BaseReport):
when: "Literal['setup', 'call', 'teardown']",
sections: Iterable[Tuple[str, str]] = (),
duration: float = 0,
start: float = 0,
stop: float = 0,
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
**extra,
) -> None:
@@ -272,6 +273,8 @@ class TestReport(BaseReport):
#: A (filesystempath, lineno, domaininfo) tuple indicating the
#: actual location of a test item - it might be different from the
#: collected one e.g. if a method is inherited from a different module.
#: The filesystempath may be relative to ``config.rootdir``.
#: The line number is 0-based.
self.location: Tuple[str, Optional[int], str] = location
#: A name -> value dictionary containing all keywords and
@@ -300,6 +303,11 @@ class TestReport(BaseReport):
#: Time it took to run just the test.
self.duration: float = duration
#: The system time when the call started, in seconds since the epoch.
self.start: float = start
#: The system time when the call ended, in seconds since the epoch.
self.stop: float = stop
self.__dict__.update(extra)
def __repr__(self) -> str:
@@ -318,6 +326,8 @@ class TestReport(BaseReport):
# Remove "collect" from the Literal type -- only for collection calls.
assert when != "collect"
duration = call.duration
start = call.start
stop = call.stop
keywords = {x: 1 for x in item.keywords}
excinfo = call.excinfo
sections = []
@@ -337,6 +347,9 @@ class TestReport(BaseReport):
elif isinstance(excinfo.value, skip.Exception):
outcome = "skipped"
r = excinfo._getreprcrash()
assert (
r is not None
), "There should always be a traceback entry for skipping a test."
if excinfo.value._use_item_location:
path, line = item.reportinfo()[:2]
assert line is not None
@@ -362,6 +375,8 @@ class TestReport(BaseReport):
when,
sections,
duration,
start,
stop,
user_properties=item.user_properties,
)
@@ -407,7 +422,9 @@ class CollectReport(BaseReport):
self.__dict__.update(extra)
@property
def location(self):
def location( # type:ignore[override]
self,
) -> Optional[Tuple[str, Optional[int], str]]:
return (self.fspath, None, self.fspath)
def __repr__(self) -> str:
@@ -459,15 +476,15 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
def serialize_repr_entry(
entry: Union[ReprEntry, ReprEntryNative]
) -> Dict[str, Any]:
data = attr.asdict(entry)
data = dataclasses.asdict(entry)
for key, value in data.items():
if hasattr(value, "__dict__"):
data[key] = attr.asdict(value)
data[key] = dataclasses.asdict(value)
entry_data = {"type": type(entry).__name__, "data": data}
return entry_data
def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
result = attr.asdict(reprtraceback)
result = dataclasses.asdict(reprtraceback)
result["reprentries"] = [
serialize_repr_entry(x) for x in reprtraceback.reprentries
]
@@ -477,7 +494,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
reprcrash: Optional[ReprFileLocation],
) -> Optional[Dict[str, Any]]:
if reprcrash is not None:
return attr.asdict(reprcrash)
return dataclasses.asdict(reprcrash)
else:
return None
@@ -573,7 +590,6 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
and "reprcrash" in reportdict["longrepr"]
and "reprtraceback" in reportdict["longrepr"]
):
reprtraceback = deserialize_repr_traceback(
reportdict["longrepr"]["reprtraceback"]
)
@@ -594,7 +610,10 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
ExceptionChainRepr, ReprExceptionInfo
] = ExceptionChainRepr(chain)
else:
exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
exception_info = ReprExceptionInfo(
reprtraceback=reprtraceback,
reprcrash=reprcrash,
)
for section in reportdict["longrepr"]["sections"]:
exception_info.addsection(*section)
+19 -10
View File
@@ -1,5 +1,6 @@
"""Basic collect and runtest protocol implementations."""
import bdb
import dataclasses
import os
import sys
from typing import Callable
@@ -14,8 +15,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
from .reports import BaseReport
from .reports import CollectErrorRepr
from .reports import CollectReport
@@ -35,6 +34,9 @@ from _pytest.outcomes import OutcomeException
from _pytest.outcomes import Skipped
from _pytest.outcomes import TEST_OUTCOME
if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup
if TYPE_CHECKING:
from typing_extensions import Literal
@@ -265,7 +267,7 @@ TResult = TypeVar("TResult", covariant=True)
@final
@attr.s(repr=False, init=False, auto_attribs=True)
@dataclasses.dataclass
class CallInfo(Generic[TResult]):
"""Result/Exception info of a function invocation."""
@@ -512,22 +514,29 @@ class SetupState:
stack is torn down.
"""
needed_collectors = nextitem and nextitem.listchain() or []
exc = None
exceptions: List[BaseException] = []
while self.stack:
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
break
node, (finalizers, _) = self.stack.popitem()
these_exceptions = []
while finalizers:
fin = finalizers.pop()
try:
fin()
except TEST_OUTCOME 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
these_exceptions.append(e)
if len(these_exceptions) == 1:
exceptions.extend(these_exceptions)
elif these_exceptions:
msg = f"errors while tearing down {node!r}"
exceptions.append(BaseExceptionGroup(msg, these_exceptions[::-1]))
if len(exceptions) == 1:
raise exceptions[0]
elif exceptions:
raise BaseExceptionGroup("errors during test teardown", exceptions[::-1])
if nextitem is None:
assert not self.stack
+5 -4
View File
@@ -1,4 +1,5 @@
"""Support for skip/xfail functions and markers."""
import dataclasses
import os
import platform
import sys
@@ -9,8 +10,6 @@ from typing import Optional
from typing import Tuple
from typing import Type
import attr
from _pytest.config import Config
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
@@ -157,7 +156,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
return result, reason
@attr.s(slots=True, frozen=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class Skip:
"""The result of evaluate_skip_marks()."""
@@ -192,10 +191,12 @@ def evaluate_skip_marks(item: Item) -> Optional[Skip]:
return None
@attr.s(slots=True, frozen=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class Xfail:
"""The result of evaluate_xfail_marks()."""
__slots__ = ("reason", "run", "strict", "raises")
reason: str
run: bool
strict: bool
+8
View File
@@ -48,6 +48,10 @@ def pytest_configure(config: Config) -> None:
def pytest_sessionfinish(session: Session) -> None:
if not session.config.getoption("stepwise"):
assert session.config.cache is not None
if hasattr(session.config, "workerinput"):
# Do not update cache if this process is a xdist worker to prevent
# race conditions (#10641).
return
# Clear the list of failing tests if the plugin is not active.
session.config.cache.set(STEPWISE_CACHE_DIR, [])
@@ -119,4 +123,8 @@ class StepwisePlugin:
return None
def pytest_sessionfinish(self) -> None:
if hasattr(self.config, "workerinput"):
# Do not update cache if this process is a xdist worker to prevent
# race conditions (#10641).
return
self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)
+65 -16
View File
@@ -3,10 +3,12 @@
This is a good source for looking at the various reporting hooks.
"""
import argparse
import dataclasses
import datetime
import inspect
import platform
import sys
import textwrap
import warnings
from collections import Counter
from functools import partial
@@ -19,6 +21,7 @@ from typing import Dict
from typing import Generator
from typing import List
from typing import Mapping
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Set
@@ -27,7 +30,6 @@ from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import attr
import pluggy
import _pytest._version
@@ -111,6 +113,26 @@ class MoreQuietAction(argparse.Action):
namespace.quiet = getattr(namespace, "quiet", 0) + 1
class TestShortLogReport(NamedTuple):
"""Used to store the test status result category, shortletter and verbose word.
For example ``"rerun", "R", ("RERUN", {"yellow": True})``.
:ivar category:
The class of result, for example ``passed``, ``skipped``, ``error``, or the empty string.
:ivar letter:
The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string.
:ivar word:
Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``,
``"ERROR"``, or the empty string.
"""
category: str
letter: str
word: Union[str, Tuple[str, Mapping[str, bool]]]
def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("terminal reporting", "Reporting", after="general")
group._addoption(
@@ -229,7 +251,8 @@ def pytest_addoption(parser: Parser) -> None:
parser.addini(
"console_output_style",
help='Console output: "classic", or with additional progress information '
'("progress" (percentage) | "count")',
'("progress" (percentage) | "count" | "progress-even-when-capture-no" (forces '
"progress even when capture=no)",
default="progress",
)
@@ -287,7 +310,7 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
return outcome, letter, outcome.upper()
@attr.s(auto_attribs=True)
@dataclasses.dataclass
class WarningReport:
"""Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
@@ -346,14 +369,19 @@ class TerminalReporter:
def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
"""Return whether we should display progress information based on the current config."""
# do not show progress if we are not capturing output (#3038)
if self.config.getoption("capture", "no") == "no":
# do not show progress if we are not capturing output (#3038) unless explicitly
# overridden by progress-even-when-capture-no
if (
self.config.getoption("capture", "no") == "no"
and self.config.getini("console_output_style")
!= "progress-even-when-capture-no"
):
return False
# do not show progress if we are showing fixture setup/teardown
if self.config.getoption("setupshow", False):
return False
cfg: str = self.config.getini("console_output_style")
if cfg == "progress":
if cfg == "progress" or cfg == "progress-even-when-capture-no":
return "progress"
elif cfg == "count":
return "count"
@@ -420,6 +448,28 @@ class TerminalReporter:
self._tw.line()
self.currentfspath = None
def wrap_write(
self,
content: str,
*,
flush: bool = False,
margin: int = 8,
line_sep: str = "\n",
**markup: bool,
) -> None:
"""Wrap message with margin for progress info."""
width_of_current_line = self._tw.width_of_current_line
wrapped = line_sep.join(
textwrap.wrap(
" " * width_of_current_line + content,
width=self._screen_width - margin,
drop_whitespace=True,
replace_whitespace=False,
),
)
wrapped = wrapped[width_of_current_line:]
self._tw.write(wrapped, flush=flush, **markup)
def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
self._tw.write(content, flush=flush, **markup)
@@ -519,10 +569,11 @@ class TerminalReporter:
def pytest_runtest_logreport(self, report: TestReport) -> None:
self._tests_ran = True
rep = report
res: Tuple[
str, str, Union[str, Tuple[str, Mapping[str, bool]]]
] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
category, letter, word = res
res = TestShortLogReport(
*self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
)
category, letter, word = res.category, res.letter, res.word
if not isinstance(word, tuple):
markup = None
else:
@@ -566,7 +617,7 @@ class TerminalReporter:
formatted_reason = f" ({reason})"
if reason and formatted_reason is not None:
self._tw.write(formatted_reason)
self.wrap_write(formatted_reason)
if self._show_progress_info:
self._write_progress_information_filling_space()
else:
@@ -733,16 +784,14 @@ class TerminalReporter:
self.write_line(line)
def pytest_report_header(self, config: Config) -> List[str]:
line = "rootdir: %s" % config.rootpath
result = [f"rootdir: {config.rootpath}"]
if config.inipath:
line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
result.append("configfile: " + bestrelpath(config.rootpath, config.inipath))
if config.args_source == Config.ArgsSource.TESTPATHS:
testpaths: List[str] = config.getini("testpaths")
line += ", testpaths: {}".format(", ".join(testpaths))
result = [line]
result.append("testpaths: {}".format(", ".join(testpaths)))
plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:
+132 -24
View File
@@ -1,40 +1,66 @@
"""Support for providing temporary directories to test functions."""
import dataclasses
import os
import re
import sys
import tempfile
from pathlib import Path
from shutil import rmtree
from typing import Any
from typing import Dict
from typing import Generator
from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
import attr
from _pytest.nodes import Item
from _pytest.reports import CollectReport
from _pytest.stash import StashKey
if TYPE_CHECKING:
from typing_extensions import Literal
RetentionType = Literal["all", "failed", "none"]
from _pytest.config.argparsing import Parser
from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import rm_rf
from _pytest.compat import final
from .pathlib import cleanup_dead_symlinks
from _pytest.compat import final, get_user_id
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch
tmppath_result_key = StashKey[Dict[str, bool]]()
@final
@attr.s(init=False)
@dataclasses.dataclass
class TempPathFactory:
"""Factory for temporary directories under the common base temp directory.
The base directory can be configured using the ``--basetemp`` option.
"""
_given_basetemp = attr.ib(type=Optional[Path])
_trace = attr.ib()
_basetemp = attr.ib(type=Optional[Path])
_given_basetemp: Optional[Path]
# pluggy TagTracerSub, not currently exposed, so Any.
_trace: Any
_basetemp: Optional[Path]
_retention_count: int
_retention_policy: "RetentionType"
def __init__(
self,
given_basetemp: Optional[Path],
retention_count: int,
retention_policy: "RetentionType",
trace,
basetemp: Optional[Path] = None,
*,
@@ -49,6 +75,8 @@ class TempPathFactory:
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
self._trace = trace
self._retention_count = retention_count
self._retention_policy = retention_policy
self._basetemp = basetemp
@classmethod
@@ -63,9 +91,23 @@ class TempPathFactory:
:meta private:
"""
check_ispytest(_ispytest)
count = int(config.getini("tmp_path_retention_count"))
if count < 0:
raise ValueError(
f"tmp_path_retention_count must be >= 0. Current input: {count}."
)
policy = config.getini("tmp_path_retention_policy")
if policy not in ("all", "failed", "none"):
raise ValueError(
f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
)
return cls(
given_basetemp=config.option.basetemp,
trace=config.trace.get("tmpdir"),
retention_count=count,
retention_policy=policy,
_ispytest=True,
)
@@ -133,23 +175,23 @@ class TempPathFactory:
# Also, to keep things private, fixup any world-readable temp
# rootdir's permissions. Historically 0o755 was used, so we can't
# just error out on this, at least for a while.
if sys.platform != "win32":
uid = os.getuid()
uid = get_user_id()
if uid is not None:
rootdir_stat = rootdir.stat()
# getuid shouldn't fail, but cpython defines such a case.
# Let's hope for the best.
if uid != -1:
if rootdir_stat.st_uid != uid:
raise OSError(
f"The temporary directory {rootdir} is not owned by the current user. "
"Fix this and try again."
)
if (rootdir_stat.st_mode & 0o077) != 0:
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
if rootdir_stat.st_uid != uid:
raise OSError(
f"The temporary directory {rootdir} is not owned by the current user. "
"Fix this and try again."
)
if (rootdir_stat.st_mode & 0o077) != 0:
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
keep = self._retention_count
if self._retention_policy == "none":
keep = 0
basetemp = make_numbered_dir_with_cleanup(
prefix="pytest-",
root=rootdir,
keep=3,
keep=keep,
lock_timeout=LOCK_TIMEOUT,
mode=0o700,
)
@@ -184,6 +226,21 @@ def pytest_configure(config: Config) -> None:
mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
def pytest_addoption(parser: Parser) -> None:
parser.addini(
"tmp_path_retention_count",
help="How many sessions should we keep the `tmp_path` directories, according to `tmp_path_retention_policy`.",
default=3,
)
parser.addini(
"tmp_path_retention_policy",
help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. "
"(all/failed/none)",
default="all",
)
@fixture(scope="session")
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
"""Return a :class:`pytest.TempPathFactory` instance for the test session."""
@@ -200,17 +257,68 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
@fixture
def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
def tmp_path(
request: FixtureRequest, tmp_path_factory: TempPathFactory
) -> Generator[Path, None, None]:
"""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
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`.
The returned object is a :class:`pathlib.Path` object.
"""
return _mk_tmp(request, tmp_path_factory)
path = _mk_tmp(request, tmp_path_factory)
yield path
# Remove the tmpdir if the policy is "failed" and the test passed.
tmp_path_factory: TempPathFactory = request.session.config._tmp_path_factory # type: ignore
policy = tmp_path_factory._retention_policy
result_dict = request.node.stash[tmppath_result_key]
if policy == "failed" and result_dict.get("call", True):
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
# permissions, etc, in which case we ignore it.
rmtree(path, ignore_errors=True)
del request.node.stash[tmppath_result_key]
def pytest_sessionfinish(session, exitstatus: Union[int, ExitCode]):
"""After each session, remove base directory if all the tests passed,
the policy is "failed", and the basetemp is not specified by a user.
"""
tmp_path_factory: TempPathFactory = session.config._tmp_path_factory
basetemp = tmp_path_factory._basetemp
if basetemp is None:
return
policy = tmp_path_factory._retention_policy
if (
exitstatus == 0
and policy == "failed"
and tmp_path_factory._given_basetemp is None
):
if basetemp.is_dir():
# We do a "best effort" to remove files, but it might not be possible due to some leaked resource,
# permissions, etc, in which case we ignore it.
rmtree(basetemp, ignore_errors=True)
# Remove dead symlinks.
if basetemp.is_dir():
cleanup_dead_symlinks(basetemp)
@hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item: Item, call):
outcome = yield
result: CollectReport = outcome.get_result()
empty: Dict[str, bool] = {}
item.stash.setdefault(tmppath_result_key, empty)[result.when] = result.passed
+11 -7
View File
@@ -298,6 +298,9 @@ class TestCaseFunction(Function):
def stopTest(self, testcase: "unittest.TestCase") -> None:
pass
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
pass
def runtest(self) -> None:
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
@@ -331,15 +334,16 @@ class TestCaseFunction(Function):
finally:
delattr(self._testcase, self.name)
def _prunetraceback(
def _traceback_filter(
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
) -> None:
super()._prunetraceback(excinfo)
traceback = excinfo.traceback.filter(
lambda x: not x.frame.f_globals.get("__unittest")
) -> _pytest._code.Traceback:
traceback = super()._traceback_filter(excinfo)
ntraceback = traceback.filter(
lambda x: not x.frame.f_globals.get("__unittest"),
)
if traceback:
excinfo.traceback = traceback
if not ntraceback:
ntraceback = traceback
return ntraceback
@hookimpl(tryfirst=True)
+3 -4
View File
@@ -1,3 +1,4 @@
import dataclasses
import inspect
import warnings
from types import FunctionType
@@ -6,8 +7,6 @@ from typing import Generic
from typing import Type
from typing import TypeVar
import attr
from _pytest.compat import final
@@ -130,7 +129,7 @@ _W = TypeVar("_W", bound=PytestWarning)
@final
@attr.s(auto_attribs=True)
@dataclasses.dataclass
class UnformattedWarning(Generic[_W]):
"""A warning meant to be formatted during runtime.
@@ -150,7 +149,7 @@ def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
"""
Issue the warning :param:`message` for the definition of the given :param:`method`
this helps to log warnigns for functions defined prior to finding an issue with them
this helps to log warnings for functions defined prior to finding an issue with them
(like hook wrappers being marked in a legacy mechanism)
"""
lineno = method.__code__.co_firstlineno
+2
View File
@@ -62,6 +62,7 @@ from _pytest.reports import TestReport
from _pytest.runner import CallInfo
from _pytest.stash import Stash
from _pytest.stash import StashKey
from _pytest.terminal import TestShortLogReport
from _pytest.tmpdir import TempPathFactory
from _pytest.warning_types import PytestAssertRewriteWarning
from _pytest.warning_types import PytestCacheWarning
@@ -152,6 +153,7 @@ __all__ = [
"TempPathFactory",
"Testdir",
"TestReport",
"TestShortLogReport",
"UsageError",
"WarningsRecorder",
"warns",
+3 -3
View File
@@ -803,7 +803,7 @@ class TestLocalPath(CommonFSTests):
# depending on how the paths are used), but > 4096 (which is the
# Linux' limitation) - the behaviour of paths with names > 4096 chars
# is undetermined
newfilename = "/test" * 60
newfilename = "/test" * 60 # type:ignore[unreachable]
l1 = tmpdir.join(newfilename)
l1.ensure(file=True)
l1.write("foo")
@@ -1344,8 +1344,8 @@ class TestPOSIXLocalPath:
assert realpath.basename == "file"
def test_owner(self, path1, tmpdir):
from pwd import getpwuid
from grp import getgrgid
from pwd import getpwuid # type:ignore[attr-defined]
from grp import getgrgid # type:ignore[attr-defined]
stat = path1.stat()
assert stat.path == path1

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