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: allow:
- dependency-type: direct - dependency-type: direct
- dependency-type: indirect - 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 pull-requests: write
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: true persist-credentials: true
+18 -14
View File
@@ -23,30 +23,34 @@ jobs:
contents: write contents: write
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false 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 - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: "3.7" python-version: "3.7"
- name: Install dependencies - name: Install tox
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install --upgrade build tox pip install --upgrade 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 }}
- name: Publish GitHub release notes - name: Publish GitHub release notes
env: env:
+2 -2
View File
@@ -27,12 +27,12 @@ jobs:
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: "3.8" 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: env:
PYTEST_ADDOPTS: "--color=yes" 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. # Set permissions at the job level.
permissions: {} permissions: {}
@@ -38,6 +43,7 @@ jobs:
"windows-py39", "windows-py39",
"windows-py310", "windows-py310",
"windows-py311", "windows-py311",
"windows-py312",
"ubuntu-py37", "ubuntu-py37",
"ubuntu-py37-pluggy", "ubuntu-py37-pluggy",
@@ -46,12 +52,13 @@ jobs:
"ubuntu-py39", "ubuntu-py39",
"ubuntu-py310", "ubuntu-py310",
"ubuntu-py311", "ubuntu-py311",
"ubuntu-py312",
"ubuntu-pypy3", "ubuntu-pypy3",
"macos-py37", "macos-py37",
"macos-py38",
"macos-py39", "macos-py39",
"macos-py310", "macos-py310",
"macos-py312",
"docs", "docs",
"doctesting", "doctesting",
@@ -81,9 +88,13 @@ jobs:
os: windows-latest os: windows-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "windows-py311" - name: "windows-py311"
python: "3.11-dev" python: "3.11"
os: windows-latest os: windows-latest
tox_env: "py311" tox_env: "py311"
- name: "windows-py312"
python: "3.12-dev"
os: windows-latest
tox_env: "py312"
- name: "ubuntu-py37" - name: "ubuntu-py37"
python: "3.7" python: "3.7"
@@ -111,10 +122,15 @@ jobs:
os: ubuntu-latest os: ubuntu-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "ubuntu-py311" - name: "ubuntu-py311"
python: "3.11-dev" python: "3.11"
os: ubuntu-latest os: ubuntu-latest
tox_env: "py311" tox_env: "py311"
use_coverage: true use_coverage: true
- name: "ubuntu-py312"
python: "3.12-dev"
os: ubuntu-latest
tox_env: "py312"
use_coverage: true
- name: "ubuntu-pypy3" - name: "ubuntu-pypy3"
python: "pypy-3.7" python: "pypy-3.7"
os: ubuntu-latest os: ubuntu-latest
@@ -124,19 +140,19 @@ jobs:
python: "3.7" python: "3.7"
os: macos-latest os: macos-latest
tox_env: "py37-xdist" tox_env: "py37-xdist"
- name: "macos-py38"
python: "3.8"
os: macos-latest
tox_env: "py38-xdist"
use_coverage: true
- name: "macos-py39" - name: "macos-py39"
python: "3.9" python: "3.9"
os: macos-latest os: macos-latest
tox_env: "py39-xdist" tox_env: "py39-xdist"
use_coverage: true
- name: "macos-py310" - name: "macos-py310"
python: "3.10" python: "3.10"
os: macos-latest os: macos-latest
tox_env: "py310-xdist" tox_env: "py310-xdist"
- name: "macos-py312"
python: "3.12-dev"
os: macos-latest
tox_env: "py312-xdist"
- name: "plugins" - name: "plugins"
python: "3.9" python: "3.9"
@@ -163,6 +179,7 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
check-latest: ${{ endsWith(matrix.python, '-dev') }}
- name: Install dependencies - name: Install dependencies
run: | run: |
@@ -189,3 +206,10 @@ jobs:
fail_ci_if_error: true fail_ci_if_error: true
files: ./coverage.xml files: ./coverage.xml
verbose: true 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: {} permissions: {}
jobs: jobs:
createPullRequest: update-plugin-list:
if: github.repository_owner == 'pytest-dev' if: github.repository_owner == 'pytest-dev'
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
@@ -20,12 +20,12 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v2 uses: actions/setup-python@v4
with: with:
python-version: 3.8 python-version: 3.8
@@ -38,7 +38,7 @@ jobs:
run: python scripts/update-plugin-list.py run: python scripts/update-plugin-list.py
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6 uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5
with: with:
commit-message: '[automated] Update plugin list' commit-message: '[automated] Update plugin list'
author: 'pytest bot <pytestbot@users.noreply.github.com>' author: 'pytest bot <pytestbot@users.noreply.github.com>'
+13 -15
View File
@@ -1,18 +1,16 @@
default_language_version:
python: "3.10"
repos: repos:
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.10.0 rev: 23.3.0
hooks: hooks:
- id: black - id: black
args: [--safe, --quiet] args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs - repo: https://github.com/asottile/blacken-docs
rev: v1.12.1 rev: 1.13.0
hooks: hooks:
- id: blacken-docs - id: blacken-docs
additional_dependencies: [black==20.8b1] additional_dependencies: [black==23.1.0]
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0 rev: v4.4.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
@@ -23,7 +21,7 @@ repos:
exclude: _pytest/(debugging|hookspec).py exclude: _pytest/(debugging|hookspec).py
language_version: python3 language_version: python3
- repo: https://github.com/PyCQA/autoflake - repo: https://github.com/PyCQA/autoflake
rev: v1.7.6 rev: v2.1.1
hooks: hooks:
- id: autoflake - id: autoflake
name: autoflake name: autoflake
@@ -31,34 +29,34 @@ repos:
language: python language: python
files: \.py$ files: \.py$
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 5.0.4 rev: 6.0.0
hooks: hooks:
- id: flake8 - id: flake8
language_version: python3 language_version: python3
additional_dependencies: additional_dependencies:
- flake8-typing-imports==1.12.0 - flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0 - flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder_python_imports - repo: https://github.com/asottile/reorder-python-imports
rev: v3.8.5 rev: v3.9.0
hooks: hooks:
- id: reorder-python-imports - id: reorder-python-imports
args: ['--application-directories=.:src', --py37-plus] args: ['--application-directories=.:src', --py37-plus]
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.1.0 rev: v3.4.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py37-plus] args: [--py37-plus]
- repo: https://github.com/asottile/setup-cfg-fmt - repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.1.0 rev: v2.3.0
hooks: hooks:
- id: setup-cfg-fmt - 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 - repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0 rev: v1.10.0
hooks: hooks:
- id: python-use-type-annotations - id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.982 rev: v1.3.0
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/)
+6 -3
View File
@@ -2,9 +2,12 @@ version: 2
python: python:
install: install:
- requirements: doc/en/requirements.txt # Install pytest first, then doc/en/requirements.txt.
- method: pip # This order is important to honor any pins in doc/en/requirements.txt
path: . # when the pinned library is also a dependency of pytest.
- method: pip
path: .
- requirements: doc/en/requirements.txt
build: build:
os: ubuntu-20.04 os: ubuntu-20.04
+28 -4
View File
@@ -8,10 +8,14 @@ Abdeali JK
Abdelrahman Elbehery Abdelrahman Elbehery
Abhijeet Kasurde Abhijeet Kasurde
Adam Johnson Adam Johnson
Adam Stewart
Adam Uhlir Adam Uhlir
Ahn Ki-Wook Ahn Ki-Wook
Akiomi Kamakura Akiomi Kamakura
Alan Velasco Alan Velasco
Alessio Izzo
Alex Jones
Alex Lambson
Alexander Johnson Alexander Johnson
Alexander King Alexander King
Alexei Kozlenok Alexei Kozlenok
@@ -43,6 +47,7 @@ Ariel Pillemer
Armin Rigo Armin Rigo
Aron Coyle Aron Coyle
Aron Curzon Aron Curzon
Ashish Kurmi
Aviral Verma Aviral Verma
Aviv Palivoda Aviv Palivoda
Babak Keyvani Babak Keyvani
@@ -53,10 +58,12 @@ Benjamin Peterson
Bernard Pratz Bernard Pratz
Bob Ippolito Bob Ippolito
Brian Dorsey Brian Dorsey
Brian Larsen
Brian Maissy Brian Maissy
Brian Okken Brian Okken
Brianna Laugher Brianna Laugher
Bruno Oliveira Bruno Oliveira
Cal Jacobson
Cal Leeming Cal Leeming
Carl Friedrich Bolz Carl Friedrich Bolz
Carlos Jenkins Carlos Jenkins
@@ -65,6 +72,7 @@ Charles Cloud
Charles Machalow Charles Machalow
Charnjit SiNGH (CCSJ) Charnjit SiNGH (CCSJ)
Cheuk Ting Ho Cheuk Ting Ho
Chris Mahoney
Chris Lamb Chris Lamb
Chris NeJame Chris NeJame
Chris Rose Chris Rose
@@ -88,6 +96,7 @@ Daniel Grana
Daniel Hahler Daniel Hahler
Daniel Nuri Daniel Nuri
Daniel Sánchez Castelló Daniel Sánchez Castelló
Daniel Valenzuela Zenteno
Daniel Wandschneider Daniel Wandschneider
Daniele Procida Daniele Procida
Danielle Jenkins Danielle Jenkins
@@ -122,8 +131,10 @@ Eric Siegerman
Erik Aronesty Erik Aronesty
Erik M. Bray Erik M. Bray
Evan Kepner Evan Kepner
Evgeny Seliverstov
Fabien Zarifian Fabien Zarifian
Fabio Zadrozny Fabio Zadrozny
Felix Hofstätter
Felix Nieuwenhuizen Felix Nieuwenhuizen
Feng Ma Feng Ma
Florian Bruhin Florian Bruhin
@@ -158,6 +169,7 @@ Ionuț Turturică
Itxaso Aizpurua Itxaso Aizpurua
Iwan Briquemont Iwan Briquemont
Jaap Broekhuizen Jaap Broekhuizen
Jake VanderPlas
Jakob van Santen Jakob van Santen
Jakub Mitoraj Jakub Mitoraj
James Bourbeau James Bourbeau
@@ -182,10 +194,11 @@ Joseph Hunkeler
Josh Karpel Josh Karpel
Joshua Bronson Joshua Bronson
Jurko Gospodnetić Jurko Gospodnetić
Justyna Janczyszyn
Justice Ndou Justice Ndou
Justyna Janczyszyn
Kale Kundert Kale Kundert
Kamran Ahmad Kamran Ahmad
Kenny Y
Karl O. Pinc Karl O. Pinc
Karthikeyan Singaravelan Karthikeyan Singaravelan
Katarzyna Jachim Katarzyna Jachim
@@ -222,6 +235,7 @@ Marcin Bachry
Marco Gorelli Marco Gorelli
Mark Abramowitz Mark Abramowitz
Mark Dickinson Mark Dickinson
Marko Pacak
Markus Unterwaditzer Markus Unterwaditzer
Martijn Faassen Martijn Faassen
Martin Altmayer Martin Altmayer
@@ -235,7 +249,6 @@ Matthias Hafner
Maxim Filipenko Maxim Filipenko
Maximilian Cosmo Sitter Maximilian Cosmo Sitter
mbyt mbyt
Mickey Pashov
Michael Aquilina Michael Aquilina
Michael Birtwell Michael Birtwell
Michael Droettboom Michael Droettboom
@@ -244,6 +257,7 @@ Michael Krebs
Michael Seifert Michael Seifert
Michal Wajszczuk Michal Wajszczuk
Michał Zięba Michał Zięba
Mickey Pashov
Mihai Capotă Mihai Capotă
Mike Hoyle (hoylemd) Mike Hoyle (hoylemd)
Mike Lundy Mike Lundy
@@ -258,9 +272,9 @@ Niclas Olofsson
Nicolas Delaby Nicolas Delaby
Nikolay Kondratyev Nikolay Kondratyev
Nipunn Koorapati Nipunn Koorapati
Olga Matoula
Oleg Pidsadnyi Oleg Pidsadnyi
Oleg Sushchenko Oleg Sushchenko
Olga Matoula
Oliver Bestwalter Oliver Bestwalter
Omar Kohl Omar Kohl
Omer Hadari Omer Hadari
@@ -276,6 +290,7 @@ Paweł Adamczak
Pedro Algarvio Pedro Algarvio
Petter Strandmark Petter Strandmark
Philipp Loose Philipp Loose
Pierre Sassoulas
Pieter Mulder Pieter Mulder
Piotr Banaszkiewicz Piotr Banaszkiewicz
Piotr Helm Piotr Helm
@@ -285,15 +300,18 @@ Prashant Sharma
Pulkit Goyal Pulkit Goyal
Punyashloka Biswal Punyashloka Biswal
Quentin Pradet Quentin Pradet
q0w
Ralf Schmitt Ralf Schmitt
Ram Rachum
Ralph Giles Ralph Giles
Ram Rachum
Ran Benita Ran Benita
Raphael Castaneda Raphael Castaneda
Raphael Pierzina Raphael Pierzina
Rafal Semik
Raquel Alegre Raquel Alegre
Ravi Chandra Ravi Chandra
Robert Holt Robert Holt
Roberto Aldera
Roberto Polli Roberto Polli
Roland Puntaier Roland Puntaier
Romain Dorgueil Romain Dorgueil
@@ -310,6 +328,7 @@ Samuel Searles-Bryant
Samuele Pedroni Samuele Pedroni
Sanket Duthade Sanket Duthade
Sankt Petersbug Sankt Petersbug
Saravanan Padmanaban
Segev Finer Segev Finer
Serhii Mozghovyi Serhii Mozghovyi
Seth Junot Seth Junot
@@ -323,6 +342,7 @@ Srinivas Reddy Thatiparthy
Stefan Farmbauer Stefan Farmbauer
Stefan Scherfke Stefan Scherfke
Stefan Zimmermann Stefan Zimmermann
Stefanie Molin
Stefano Taschini Stefano Taschini
Steffen Allner Steffen Allner
Stephan Obermann Stephan Obermann
@@ -341,6 +361,7 @@ Thomas Grainger
Thomas Hisch Thomas Hisch
Tim Hoffmann Tim Hoffmann
Tim Strazny Tim Strazny
TJ Bruno
Tobias Diez Tobias Diez
Tom Dalton Tom Dalton
Tom Viner Tom Viner
@@ -371,7 +392,10 @@ Wouter van Ackooy
Xixi Zhao Xixi Zhao
Xuan Luong Xuan Luong
Xuecong Liao Xuecong Liao
Yannick Péroux
Yoav Caspi Yoav Caspi
Yuliang Shao
Yusuke Kadowaki
Yuval Shimon Yuval Shimon
Zac Hatfield-Dodds Zac Hatfield-Dodds
Zachary Kneupper 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 :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.2.0
release-7.1.3 release-7.1.3
release-7.1.2 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+ 5.0 - 6.1 3.5+
3.3 - 4.6 2.7, 3.4+ 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. Values can be any object handled by the json stdlib module.
capsys -- .../_pytest/capture.py:905 capsysbinary -- .../_pytest/capture.py:1001
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
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``. Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsysbinary.readouterr()`` 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() captured = capsysbinary.readouterr()
assert captured.out == b"hello\n" 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``. Enable text capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method 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() captured = capfd.readouterr()
assert captured.out == "hello\n" 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``. Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method 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() captured = capfdbinary.readouterr()
assert captured.out == b"hello\n" 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 Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests. 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`. 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` Session-scoped fixture that returns the session's :class:`pytest.Config`
object. 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 .. _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. Access and control log capturing.
Captured logs are available through the following properties/methods:: 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.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string * 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. A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or 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 See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
on warning categories. 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. 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 Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary function invocation, created as a sub directory of the base temporary
directory. directory.
By default, a new base temporary directory is created each test session, 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 and old bases are removed after 3 sessions, to aid in debugging.
``--basetemp`` is used then it is cleared each session. See :ref:`base 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`. temporary directory`.
The returned object is a :class:`pathlib.Path` object. 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 .. 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) 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 .. _`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 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``. - `#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. - `#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.
- `#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`.
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and - `#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. - `#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``. - `#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. - `#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``. - `#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 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 - `#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 type check and runtime purposes. Made `_pytest.doctest` use internal APIs
to avoid circular imports. 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 - `#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. torn down.
Add an additional sentence to the docstring explaining when it's not a good 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) 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`. - `#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. 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)`. 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. 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. 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. 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. # The scheme of the identifier. Typical schemes are ISBN or URL.
# epub_scheme = '' # 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. # or the project homepage.
# epub_identifier = '' # epub_identifier = ''
+2 -2
View File
@@ -1052,7 +1052,7 @@ that are then turned into proper test methods. Example:
.. code-block:: python .. code-block:: python
def check(x, y): def check(x, y):
assert x ** x == y assert x**x == y
def test_squared(): 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)]) @pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
def test_squared(x, y): def test_squared(x, y):
assert x ** x == y assert x**x == y
.. _internal classes accessed through node deprecated: .. _internal classes accessed through node deprecated:
@@ -17,7 +17,7 @@ def b(a, order):
@pytest.fixture @pytest.fixture
def c(a, b, order): def c(b, order):
order.append("c") order.append("c")
+1
View File
@@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
" no further details known at this point.", " no further details known at this point.",
] ]
) )
return super().repr_failure(excinfo)
def reportinfo(self): def reportinfo(self):
return self.path, 0, f"usecase: {self.name}" 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 . $ pytest -rs -q multipython.py
sssssssssssssssssssssssssss [100%] sssssssssssssssssssssssssss [100%]
========================= short test summary info ========================== ========================= short test summary info ==========================
SKIPPED [9] multipython.py:29: 'python3.5' not found SKIPPED [9] multipython.py:69: 'python3.5' not found
SKIPPED [9] multipython.py:29: 'python3.6' not found SKIPPED [9] multipython.py:69: 'python3.6' not found
SKIPPED [9] multipython.py:29: 'python3.7' not found SKIPPED [9] multipython.py:69: 'python3.7' not found
27 skipped in 0.12s 27 skipped in 0.12s
Indirect parametrization of optional implementations/imports Indirect parametrization of optional implementations/imports
@@ -574,7 +574,7 @@ If you run this with reporting for skips enabled:
test_module.py .s [100%] test_module.py .s [100%]
========================= short test summary info ========================== ========================= 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 ======================= ======================= 1 passed, 1 skipped in 0.12s =======================
You'll see that we don't have an ``opt2`` module and thus the second test run 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 $ pytest --collect-only
=========================== test session starts ============================ =========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y 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 collected 2 items
<Module check_myapp.py> <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 . $ pytest --collect-only pythoncollection.py
=========================== test session starts ============================ =========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y 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 collected 3 items
<Module CWD/pythoncollection.py> <Module CWD/pythoncollection.py>
@@ -290,7 +292,8 @@ file will be left out:
$ pytest --collect-only $ pytest --collect-only
=========================== test session starts ============================ =========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y 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 collected 0 items
======================= no tests collected in 0.12s ======================== ======================= 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 1... E 1...
E 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 failure_demo.py:60: AssertionError
_________________ TestSpecialisedExplanations.test_eq_list _________________ _________________ 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 Left contains 1 more item:
E {'c': 0} E {'c': 0}
E Right contains 1 more item: E Right contains 1 more item:
E {'d': 0}... E {'d': 0}
E E Use -v to get more diff
E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:71: AssertionError failure_demo.py:71: AssertionError
_________________ TestSpecialisedExplanations.test_eq_set __________________ _________________ 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): def test_eq_set(self):
> assert {0, 10, 11, 12} == {0, 20, 21} > 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 Extra items in the left set:
E 10 E 10
E 11 E 11
E 12 E 12
E Extra items in the right set: E Extra items in the right set:
E 20 E 20
E 21... E 21
E E Use -v to get more diff
E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:74: AssertionError failure_demo.py:74: AssertionError
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________ _____________ 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 which
E includes foo E includes foo
E ? +++ E ? +++
E and a... E and a
E E tail
E ...Full output truncated (2 lines hidden), use '-vv' to show
failure_demo.py:84: AssertionError failure_demo.py:84: AssertionError
___________ TestSpecialisedExplanations.test_not_in_text_single ____________ ___________ 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 ['b']
E E
E Drill down into differing attribute b: E Drill down into differing attribute b:
E b: 'b' != 'c'... E b: 'b' != 'c'
E E - c
E ...Full output truncated (3 lines hidden), use '-vv' to show E + b
failure_demo.py:108: AssertionError failure_demo.py:108: AssertionError
________________ TestSpecialisedExplanations.test_eq_attrs _________________ ________________ TestSpecialisedExplanations.test_eq_attrs _________________
@@ -334,9 +331,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E ['b'] E ['b']
E E
E Drill down into differing attribute b: E Drill down into differing attribute b:
E b: 'b' != 'c'... E b: 'b' != 'c'
E E - c
E ...Full output truncated (3 lines hidden), use '-vv' to show E + b
failure_demo.py:120: AssertionError failure_demo.py:120: AssertionError
______________________________ test_attribute ______________________________ ______________________________ 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 - asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_long - ... 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_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_eq_longer_list
FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser... FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline 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 pass
@pytest.fixture(scope="session") @pytest.fixture(scope="package")
def db(): def db():
return DB() return DB()
@@ -892,8 +892,11 @@ here is a little example implemented via a local plugin:
.. code-block:: python .. code-block:: python
# content of conftest.py # content of conftest.py
from typing import Dict
import pytest import pytest
from pytest import StashKey, CollectReport
phase_report_key = StashKey[Dict[str, CollectReport]]()
@pytest.hookimpl(tryfirst=True, hookwrapper=True) @pytest.hookimpl(tryfirst=True, hookwrapper=True)
@@ -902,10 +905,9 @@ here is a little example implemented via a local plugin:
outcome = yield outcome = yield
rep = outcome.get_result() 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" # be "setup", "call", "teardown"
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
setattr(item, "rep_" + rep.when, rep)
@pytest.fixture @pytest.fixture
@@ -913,11 +915,11 @@ here is a little example implemented via a local plugin:
yield yield
# request.node is an "item" because we use the default # request.node is an "item" because we use the default
# "function" scope # "function" scope
if request.node.rep_setup.failed: report = request.node.stash[phase_report_key]
print("setting up a test failed!", request.node.nodeid) if report["setup"].failed:
elif request.node.rep_setup.passed: print("setting up a test failed or skipped", request.node.nodeid)
if request.node.rep_call.failed: elif ("call" not in report) or report["call"].failed:
print("executing test failed", request.node.nodeid) print("executing test failed or skipped", request.node.nodeid)
if you then have failing tests: if you then have failing tests:
@@ -955,8 +957,8 @@ and run it:
rootdir: /home/sweet/project rootdir: /home/sweet/project
collected 3 items collected 3 items
test_module.py Esetting up a test failed! test_module.py::test_setup_fails test_module.py Esetting up a test failed or skipped test_module.py::test_setup_fails
Fexecuting test failed test_module.py::test_call_fails Fexecuting test failed or skipped test_module.py::test_call_fails
F F
================================== ERRORS ================================== ================================== ERRORS ==================================
+23 -5
View File
@@ -24,8 +24,9 @@ The first few lines should look like this:
[project] [project]
name = "PACKAGENAME" 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: 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`_. * In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_.
* From those files, collect test items: * From those files, collect test items:
* ``test`` prefixed test functions or methods outside of class * ``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 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`. 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 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 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>`. virtualenv test automation tool.
tox helps you to setup virtualenv environments with pre-defined ``tox`` helps you to setup virtualenv environments with pre-defined
dependencies and then executing a pre-configured test command with dependencies and then executing a pre-configured test command with
options. It will run tests against the installed package and not options. It will run tests against the installed package and not
against your source code checkout, helping to detect packaging 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 setuptools intends to
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_. `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: these values:
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning* * ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
of :py:data:`sys.path` if not already there, and then imported with the :func:`__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 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. 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. 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 * ``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 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: 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 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. 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. 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 .. code-block:: bash
$ pytest --version $ pytest --version
pytest 7.2.0 pytest 7.3.2
.. _`simpletest`: .. _`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. 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`: .. _`filterwarnings`:
``@pytest.mark.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) warnings.warn("my warning", UserWarning)
The test will fail if the warning in question is not raised. Use the keyword 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) ... 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) ... 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) ... warnings.warn("this is not here", UserWarning)
...
Traceback (most recent call last): Traceback (most recent call last):
... ...
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted... 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: You can also call :func:`pytest.warns` on a function or code string:
.. code-block:: python .. 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 @pytest.fixture
def make_customer_record(): def make_customer_record():
created_records = [] created_records = []
def _make_customer_record(name): 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_format = %(asctime)s %(levelname)s %(message)s
log_date_format = %Y-%m-%d %H:%M:%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, Further it is possible to disable reporting of captured content (stdout,
stderr and logs) on failed tests completely with: 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 # this is the previous code block example
import app import app
# custom class to be the mock return value # custom class to be the mock return value
# will override the requests.Response returned from requests.get # will override the requests.Response returned from requests.get
class MockResponse: class MockResponse:
# mock json() method always returns a specific testing dictionary # mock json() method always returns a specific testing dictionary
@staticmethod @staticmethod
def json(): 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): def test_get_json(monkeypatch):
# Any arguments may be passed and mock_get() will always return our # Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method. # mocked object, which only has the .json() method.
def mock_get(*args, **kwargs): 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 # app.py that includes the get_json() function
import app import app
# custom class to be the mock return value of requests.get() # custom class to be the mock return value of requests.get()
class MockResponse: class MockResponse:
@staticmethod @staticmethod
@@ -358,7 +358,6 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific
def test_connection(monkeypatch): def test_connection(monkeypatch):
# Patch the values of DEFAULT_CONFIG to specific # Patch the values of DEFAULT_CONFIG to specific
# testing values only for this test. # testing values only for this test.
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user") 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): def test_missing_user(monkeypatch):
# patch the DEFAULT_CONFIG t be missing the 'user' key # patch the DEFAULT_CONFIG t be missing the 'user' key
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False) 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 # app.py with the connection string function
import app import app
# all of the mocks are moved into separated fixtures # all of the mocks are moved into separated fixtures
@pytest.fixture @pytest.fixture
def mock_test_user(monkeypatch): 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 # tests reference only the fixture mocks that are needed
def test_connection(mock_test_user, mock_test_database): def test_connection(mock_test_user, mock_test_database):
expected = "User Id=test_user; Location=test_db;" expected = "User Id=test_user; Location=test_db;"
result = app.create_connection_string() 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): def test_missing_user(mock_missing_default_user):
with pytest.raises(KeyError): with pytest.raises(KeyError):
_ = app.create_connection_string() _ = 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 Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40} E {'10': 10, '20': 20, '30': 30, '40': 40}
E Full diff: E Full diff:
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}... E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
E E ? - - - - - - - -
E ...Full output truncated (3 lines hidden), use '-vv' to show E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
test_verbosity_example.py:14: AssertionError test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________ ___________________________ 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 Temporary directories are by default created as sub-directories of
the system temporary directory. The base name will be ``pytest-NUM`` where the system temporary directory. The base name will be ``pytest-NUM`` where
``NUM`` will be incremented with each test run. Moreover, entries older ``NUM`` will be incremented with each test run.
than 3 temporary directories will be removed. 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 option will remove the directory before every run, effectively meaning the temporary directories
of only the most recent run will be kept. 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 .. 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), 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. 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``. 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: .. _nodeids:
+1
View File
@@ -249,6 +249,7 @@ and use pytest_addoption as follows:
# contents of hooks.py # contents of hooks.py
# Use firstresult=True because we only want one plugin to define this # Use firstresult=True because we only want one plugin to define this
# default value # default value
@hookspec(firstresult=True) @hookspec(firstresult=True)
+4 -8
View File
@@ -167,13 +167,8 @@ it in your ``pyproject.toml`` file.
"Framework :: Pytest", "Framework :: Pytest",
] ]
[tool.setuptools] [project.entry-points.pytest11]
packages = ["myproject"] myproject = "myproject.pluginmodule"
[project.entry_points]
pytest11 = [
"myproject = myproject.pluginmodule",
]
If a package is installed this way, ``pytest`` will load If a package is installed this way, ``pytest`` will load
``myproject.pluginmodule`` as a plugin which can define ``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 $ pytest
=========================== test session starts ============================ =========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y 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 collected 2 items
test_example.py .. [100%] test_example.py .. [100%]
+1 -2
View File
@@ -2,8 +2,7 @@
.. sidebar:: Next Open Trainings .. 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 5th to 7th 2024 (3 day in-depth training), Leipzig/Remote
- `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
Also see :doc:`previous talks and blogposts <talks>`. 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 .. 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.* .. image:: /example/fixtures/test_fixtures_order_dependencies.*
:align: center :align: center
File diff suppressed because it is too large Load Diff
+77 -7
View File
@@ -956,6 +956,12 @@ TestReport
:show-inheritance: :show-inheritance:
:inherited-members: :inherited-members:
TestShortLogReport
~~~~~~~~~~~~~~~~~~
.. autoclass:: pytest.TestShortLogReport()
:members:
_Result _Result
~~~~~~~ ~~~~~~~
@@ -1047,6 +1053,14 @@ Environment Variables
Environment variables that can be used to change pytest's behavior. 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 .. envvar:: PYTEST_ADDOPTS
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given 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. * ``classic``: classic pytest output.
* ``progress``: like classic pytest output, but with a progress indicator. * ``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. * ``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 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 .. confval:: testpaths
Sets list of directories that should be searched for tests when 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 no specific directories, files or test ids are given in the command line when
executing pytest from the :ref:`rootdir <rootdir>` directory. executing pytest from the :ref:`rootdir <rootdir>` directory.
File system paths may use shell-style wildcards, including the recursive File system paths may use shell-style wildcards, including the recursive
``**`` pattern. ``**`` pattern.
Useful when all project tests are in a known location to speed up Useful when all project tests are in a known location to speed up
test collection and to avoid picking up undesired tests by accident. 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] [pytest]
testpaths = testing doc testpaths = testing doc
This tells pytest to only look for tests in ``testing`` and ``doc`` This configuration means that executing:
directories when executing from the root directory.
.. 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 .. 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 --strict-markers Markers not registered in the `markers` section of
the configuration file raise errors the configuration file raise errors
--strict (Deprecated) alias to --strict-markers --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 locate one of the implicit configuration files
--continue-on-collection-errors --continue-on-collection-errors
Force test execution even if collection errors occur 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 --log-auto-indent=LOG_AUTO_INDENT
Auto-indent multiline messages passed to the logging Auto-indent multiline messages passed to the logging
module. Accepts true|on, false|off or an integer. 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 markers (linelist): Markers for test functions
empty_parameter_set_mark (string): 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_style (string):
Console output: "classic", or with additional Console output: "classic", or with additional
progress information ("progress" (percentage) | 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 xfail_strict (bool): Default for the strict parameter of xfail markers
when not given explicitly (default: False) 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): enable_assertion_pass_hook (bool):
Enables the pytest_assertion_pass hook. Make sure to Enables the pytest_assertion_pass hook. Make sure to
delete any previously generated pyc cache files. delete any previously generated pyc cache files.
+5 -1
View File
@@ -1,7 +1,11 @@
pallets-sphinx-themes pallets-sphinx-themes
pluggy>=1.0 pluggy>=1.0
pygments-pytest>=2.2.0 pygments-pytest>=2.3.0
sphinx-removed-in>=0.2.0 sphinx-removed-in>=0.2.0
sphinx>=5,<6 sphinx>=5,<6
sphinxcontrib-trio sphinxcontrib-trio
sphinxcontrib-svg2pdfconverter 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] [tool.black]
target-version = ['py37'] 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 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 .. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the 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 :: 6 - Mature",
"Development Status :: 7 - Inactive", "Development Status :: 7 - Inactive",
) )
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert",
}
def escape_rst(text: str) -> str: def escape_rst(text: str) -> str:
@@ -52,18 +57,18 @@ def iter_plugins():
regex = r">([\d\w-]*)</a>" regex = r">([\d\w-]*)</a>"
response = requests.get("https://pypi.org/simple") response = requests.get("https://pypi.org/simple")
matches = list( match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
match plugin_names = [
for match in re.finditer(regex, response.text) name
if match.groups()[0].startswith("pytest-") for name in match_names
) if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
]
for match in tqdm(matches, smoothing=0): for name in tqdm(plugin_names, smoothing=0):
name = match.groups()[0]
response = requests.get(f"https://pypi.org/pypi/{name}/json") response = requests.get(f"https://pypi.org/pypi/{name}/json")
if response.status_code == 404: if response.status_code == 404:
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but # Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
# return 404 on the JSON API. Skip. # but return 404 on the JSON API. Skip.
continue continue
response.raise_for_status() response.raise_for_status()
info = response.json()["info"] info = response.json()["info"]
@@ -78,11 +83,23 @@ def iter_plugins():
requires = "N/A" requires = "N/A"
if info["requires_dist"]: if info["requires_dist"]:
for requirement in 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 requires = requirement
break 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"] 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]: if releases[release]:
release_date = datetime.date.fromisoformat( release_date = datetime.date.fromisoformat(
releases[release][-1]["upload_time_iso_8601"].split("T")[0] 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") last_release = release_date.strftime("%b %d, %Y")
break break
name = f':pypi:`{info["name"]}`' name = f':pypi:`{info["name"]}`'
summary = escape_rst(info["summary"].replace("\n", "")) summary = ""
if info["summary"]:
summary = escape_rst(info["summary"].replace("\n", ""))
yield { yield {
"name": name, "name": name,
"summary": summary.strip(), "summary": summary.strip(),
@@ -122,7 +141,7 @@ def main():
reference_dir = pathlib.Path("doc", "en", "reference") reference_dir = pathlib.Path("doc", "en", "reference")
plugin_list = reference_dir / "plugin_list.rst" 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(FILE_HEAD)
f.write(f"This list contains {len(plugins)} plugins.\n\n") f.write(f"This list contains {len(plugins)} plugins.\n\n")
f.write(".. only:: not latex\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/ url = https://docs.pytest.org/en/latest/
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
license = MIT license = MIT
license_file = LICENSE license_files = LICENSE
platforms = unix, linux, osx, cygwin, win32 platforms = unix, linux, osx, cygwin, win32
classifiers = classifiers =
Development Status :: 6 - Mature Development Status :: 6 - Mature
@@ -21,6 +21,8 @@ classifiers =
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Software Development :: Libraries Topic :: Software Development :: Libraries
Topic :: Software Development :: Testing Topic :: Software Development :: Testing
Topic :: Utilities Topic :: Utilities
@@ -43,7 +45,6 @@ packages =
pytest pytest
py_modules = py py_modules = py
install_requires = install_requires =
attrs>=19.2.0
iniconfig iniconfig
packaging packaging
pluggy>=0.12,<2.0 pluggy>=0.12,<2.0
@@ -67,11 +68,13 @@ console_scripts =
[options.extras_require] [options.extras_require]
testing = testing =
argcomplete argcomplete
attrs>=19.2.0
hypothesis>=3.56 hypothesis>=3.56
mock mock
nose nose
pygments>=2.7.2 pygments>=2.7.2
requests requests
setuptools
xmlschema xmlschema
[options.package_data] [options.package_data]
@@ -95,7 +98,6 @@ mypy_path = src
check_untyped_defs = True check_untyped_defs = True
disallow_any_generics = True disallow_any_generics = True
ignore_missing_imports = True ignore_missing_imports = True
no_implicit_optional = True
show_error_codes = True show_error_codes = True
strict_equality = True strict_equality = True
warn_redundant_casts = True warn_redundant_casts = True
+3 -3
View File
@@ -78,15 +78,15 @@ class FastFilesCompleter:
def __call__(self, prefix: str, **kwargs: Any) -> List[str]: def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
# Only called on non option completions. # Only called on non option completions.
if os.path.sep in prefix[1:]: if os.sep in prefix[1:]:
prefix_dir = len(os.path.dirname(prefix) + os.path.sep) prefix_dir = len(os.path.dirname(prefix) + os.sep)
else: else:
prefix_dir = 0 prefix_dir = 0
completion = [] completion = []
globbed = [] globbed = []
if "*" not in prefix and "?" not in prefix: if "*" not in prefix and "?" not in prefix:
# We are on unix, otherwise no bash. # 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 + ".*")) globbed.extend(glob(prefix + ".*"))
prefix += "*" prefix += "*"
globbed.extend(glob(prefix)) globbed.extend(glob(prefix))
+156 -111
View File
@@ -1,4 +1,5 @@
import ast import ast
import dataclasses
import inspect import inspect
import os import os
import re import re
@@ -30,9 +31,7 @@ from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
from weakref import ref
import attr
import pluggy import pluggy
import _pytest import _pytest
@@ -50,9 +49,9 @@ from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath from _pytest.pathlib import bestrelpath
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal from typing_extensions import Literal
from typing_extensions import SupportsIndex from typing_extensions import SupportsIndex
from weakref import ReferenceType
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"] _TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
@@ -194,25 +193,25 @@ class Frame:
class TracebackEntry: class TracebackEntry:
"""A single entry in a Traceback.""" """A single entry in a Traceback."""
__slots__ = ("_rawentry", "_excinfo", "_repr_style") __slots__ = ("_rawentry", "_repr_style")
def __init__( def __init__(
self, self,
rawentry: TracebackType, rawentry: TracebackType,
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None, repr_style: Optional['Literal["short", "long"]'] = None,
) -> None: ) -> None:
self._rawentry = rawentry self._rawentry: "Final" = rawentry
self._excinfo = excinfo self._repr_style: "Final" = repr_style
self._repr_style: Optional['Literal["short", "long"]'] = None
def with_repr_style(
self, repr_style: Optional['Literal["short", "long"]']
) -> "TracebackEntry":
return TracebackEntry(self._rawentry, repr_style)
@property @property
def lineno(self) -> int: def lineno(self) -> int:
return self._rawentry.tb_lineno - 1 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 @property
def frame(self) -> Frame: def frame(self) -> Frame:
return Frame(self._rawentry.tb_frame) return Frame(self._rawentry.tb_frame)
@@ -272,7 +271,7 @@ class TracebackEntry:
source = property(getsource) 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__ """Return True if the current frame has a var __tracebackhide__
resolving to True. resolving to True.
@@ -296,7 +295,7 @@ class TracebackEntry:
else: else:
break break
if tbh and callable(tbh): if tbh and callable(tbh):
return tbh(None if self._excinfo is None else self._excinfo()) return tbh(excinfo)
return tbh return tbh
def __str__(self) -> str: def __str__(self) -> str:
@@ -329,16 +328,14 @@ class Traceback(List[TracebackEntry]):
def __init__( def __init__(
self, self,
tb: Union[TracebackType, Iterable[TracebackEntry]], tb: Union[TracebackType, Iterable[TracebackEntry]],
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
) -> None: ) -> None:
"""Initialize from given python traceback object and ExceptionInfo.""" """Initialize from given python traceback object and ExceptionInfo."""
self._excinfo = excinfo
if isinstance(tb, TracebackType): if isinstance(tb, TracebackType):
def f(cur: TracebackType) -> Iterable[TracebackEntry]: def f(cur: TracebackType) -> Iterable[TracebackEntry]:
cur_: Optional[TracebackType] = cur cur_: Optional[TracebackType] = cur
while cur_ is not None: while cur_ is not None:
yield TracebackEntry(cur_, excinfo=excinfo) yield TracebackEntry(cur_)
cur_ = cur_.tb_next cur_ = cur_.tb_next
super().__init__(f(tb)) super().__init__(f(tb))
@@ -378,7 +375,7 @@ class Traceback(List[TracebackEntry]):
continue continue
if firstlineno is not None and x.frame.code.firstlineno != firstlineno: if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
continue continue
return Traceback(x._rawentry, self._excinfo) return Traceback(x._rawentry)
return self return self
@overload @overload
@@ -398,26 +395,27 @@ class Traceback(List[TracebackEntry]):
return super().__getitem__(key) return super().__getitem__(key)
def filter( 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": ) -> "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 If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
instance, and should return True when the item should be added which are hidden (see ishidden() above).
to the Traceback, False when not.
By default this removes all the TracebackEntries which are hidden Otherwise, the filter is a function that gets a single argument, a
(see ishidden() above). ``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) if isinstance(_excinfo_or_fn, ExceptionInfo):
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
def getcrashentry(self) -> TracebackEntry: else:
"""Return last non-hidden traceback entry that lead to the exception of a traceback.""" fn = _excinfo_or_fn
for i in range(-1, -len(self) - 1, -1): return Traceback(filter(fn, self))
entry = self[i]
if not entry.ishidden():
return entry
return self[-1]
def recursionindex(self) -> Optional[int]: def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if """Return the index of the frame/TracebackEntry where recursion originates if
@@ -445,7 +443,7 @@ E = TypeVar("E", bound=BaseException, covariant=True)
@final @final
@attr.s(repr=False, init=False, auto_attribs=True) @dataclasses.dataclass
class ExceptionInfo(Generic[E]): class ExceptionInfo(Generic[E]):
"""Wraps sys.exc_info() objects and offers help for navigating the traceback.""" """Wraps sys.exc_info() objects and offers help for navigating the traceback."""
@@ -469,22 +467,41 @@ class ExceptionInfo(Generic[E]):
self._traceback = traceback self._traceback = traceback
@classmethod @classmethod
def from_exc_info( def from_exception(
cls, 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, exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]": ) -> "ExceptionInfo[E]":
"""Return an ExceptionInfo for an existing exc_info tuple. """Return an ExceptionInfo for an existing exception.
.. warning:: The exception must have a non-``None`` ``__traceback__`` attribute,
otherwise this function fails with an assertion error. This means that
Experimental API the exception must have been raised, or added a traceback with the
:py:meth:`~BaseException.with_traceback()` method.
:param exprinfo: :param exprinfo:
A text string helping to determine if we should strip A text string helping to determine if we should strip
``AssertionError`` from the output. Defaults to the exception ``AssertionError`` from the output. Defaults to the exception
message/``__str__()``. 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 = "" _striptext = ""
if exprinfo is None and isinstance(exc_info[1], AssertionError): if exprinfo is None and isinstance(exc_info[1], AssertionError):
exprinfo = getattr(exc_info[1], "msg", None) exprinfo = getattr(exc_info[1], "msg", None)
@@ -563,7 +580,7 @@ class ExceptionInfo(Generic[E]):
def traceback(self) -> Traceback: def traceback(self) -> Traceback:
"""The traceback.""" """The traceback."""
if self._traceback is None: if self._traceback is None:
self._traceback = Traceback(self.tb, excinfo=ref(self)) self._traceback = Traceback(self.tb)
return self._traceback return self._traceback
@traceback.setter @traceback.setter
@@ -602,18 +619,25 @@ class ExceptionInfo(Generic[E]):
""" """
return isinstance(self.value, exc) return isinstance(self.value, exc)
def _getreprcrash(self) -> "ReprFileLocation": def _getreprcrash(self) -> Optional["ReprFileLocation"]:
exconly = self.exconly(tryshort=True) # Find last non-hidden traceback entry that led to the exception of the
entry = self.traceback.getcrashentry() # traceback, or None if all hidden.
path, lineno = entry.frame.code.raw.co_filename, entry.lineno for i in range(-1, -len(self.traceback) - 1, -1):
return ReprFileLocation(path, lineno + 1, exconly) 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( def getrepr(
self, self,
showlocals: bool = False, showlocals: bool = False,
style: "_TracebackStyle" = "long", style: "_TracebackStyle" = "long",
abspath: bool = False, abspath: bool = False,
tbfilter: bool = True, tbfilter: Union[
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
] = True,
funcargs: bool = False, funcargs: bool = False,
truncate_locals: bool = True, truncate_locals: bool = True,
chain: bool = True, chain: bool = True,
@@ -625,14 +649,20 @@ class ExceptionInfo(Generic[E]):
Ignored if ``style=="native"``. Ignored if ``style=="native"``.
:param str style: :param str style:
long|short|no|native|value traceback style. long|short|line|no|native|value traceback style.
:param bool abspath: :param bool abspath:
If paths should be changed to absolute or left unchanged. If paths should be changed to absolute or left unchanged.
:param bool tbfilter: :param tbfilter:
Hide entries that contain a local variable ``__tracebackhide__==True``. A filter for traceback entries.
Ignored if ``style=="native"``.
* 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: :param bool funcargs:
Show fixtures ("funcargs" for legacy purposes) per traceback entry. Show fixtures ("funcargs" for legacy purposes) per traceback entry.
@@ -649,12 +679,14 @@ class ExceptionInfo(Generic[E]):
""" """
if style == "native": if style == "native":
return ReprExceptionInfo( return ReprExceptionInfo(
ReprTracebackNative( reprtraceback=ReprTracebackNative(
traceback.format_exception( 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( fmt = FormattedExcinfo(
@@ -684,7 +716,7 @@ class ExceptionInfo(Generic[E]):
return True return True
@attr.s(auto_attribs=True) @dataclasses.dataclass
class FormattedExcinfo: class FormattedExcinfo:
"""Presenting information about failing Functions and Generators.""" """Presenting information about failing Functions and Generators."""
@@ -695,12 +727,12 @@ class FormattedExcinfo:
showlocals: bool = False showlocals: bool = False
style: "_TracebackStyle" = "long" style: "_TracebackStyle" = "long"
abspath: bool = True abspath: bool = True
tbfilter: bool = True tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
funcargs: bool = False funcargs: bool = False
truncate_locals: bool = True truncate_locals: bool = True
chain: bool = True chain: bool = True
astcache: Dict[Union[str, Path], ast.AST] = attr.ib( astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
factory=dict, init=False, repr=False default_factory=dict, init=False, repr=False
) )
def _getindent(self, source: "Source") -> int: def _getindent(self, source: "Source") -> int:
@@ -741,11 +773,13 @@ class FormattedExcinfo:
) -> List[str]: ) -> List[str]:
"""Return formatted and marked up source lines.""" """Return formatted and marked up source lines."""
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("???") source = Source("???")
line_index = 0 line_index = 0
if line_index < 0:
line_index += len(source)
space_prefix = " " space_prefix = " "
if short: if short:
lines.append(space_prefix + source.lines[line_index].strip()) lines.append(space_prefix + source.lines[line_index].strip())
@@ -805,12 +839,16 @@ class FormattedExcinfo:
def repr_traceback_entry( def repr_traceback_entry(
self, self,
entry: TracebackEntry, entry: Optional[TracebackEntry],
excinfo: Optional[ExceptionInfo[BaseException]] = None, excinfo: Optional[ExceptionInfo[BaseException]] = None,
) -> "ReprEntry": ) -> "ReprEntry":
lines: List[str] = [] lines: List[str] = []
style = entry._repr_style if entry._repr_style is not None else self.style style = (
if style in ("short", "long"): 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) source = self._getentrysource(entry)
if source is None: if source is None:
source = Source("???") source = Source("???")
@@ -851,25 +889,31 @@ class FormattedExcinfo:
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback": def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
traceback = excinfo.traceback traceback = excinfo.traceback
if self.tbfilter: if callable(self.tbfilter):
traceback = traceback.filter() traceback = self.tbfilter(excinfo)
elif self.tbfilter:
traceback = traceback.filter(excinfo)
if isinstance(excinfo.value, RecursionError): if isinstance(excinfo.value, RecursionError):
traceback, extraline = self._truncate_recursive_traceback(traceback) traceback, extraline = self._truncate_recursive_traceback(traceback)
else: else:
extraline = None 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] last = traceback[-1]
entries = []
if self.style == "value": if self.style == "value":
reprentry = self.repr_traceback_entry(last, excinfo) entries = [self.repr_traceback_entry(last, excinfo)]
entries.append(reprentry)
return ReprTraceback(entries, None, style=self.style) return ReprTraceback(entries, None, style=self.style)
for index, entry in enumerate(traceback): entries = [
einfo = (last == entry) and excinfo or None self.repr_traceback_entry(entry, excinfo if last == entry else None)
reprentry = self.repr_traceback_entry(entry, einfo) for entry in traceback
entries.append(reprentry) ]
return ReprTraceback(entries, extraline, style=self.style) return ReprTraceback(entries, extraline, style=self.style)
def _truncate_recursive_traceback( def _truncate_recursive_traceback(
@@ -926,6 +970,7 @@ class FormattedExcinfo:
seen: Set[int] = set() seen: Set[int] = set()
while e is not None and id(e) not in seen: while e is not None and id(e) not in seen:
seen.add(id(e)) seen.add(id(e))
if excinfo_: if excinfo_:
# Fall back to native traceback as a temporary workaround until # Fall back to native traceback as a temporary workaround until
# full support for exception groups added to ExceptionInfo. # full support for exception groups added to ExceptionInfo.
@@ -942,9 +987,7 @@ class FormattedExcinfo:
) )
else: else:
reprtraceback = self.repr_traceback(excinfo_) reprtraceback = self.repr_traceback(excinfo_)
reprcrash: Optional[ReprFileLocation] = ( reprcrash = excinfo_._getreprcrash()
excinfo_._getreprcrash() if self.style != "value" else None
)
else: else:
# Fallback to native repr if the exception doesn't have a traceback: # Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work. # ExceptionInfo objects require a full traceback to work.
@@ -952,25 +995,17 @@ class FormattedExcinfo:
traceback.format_exception(type(e), e, None) traceback.format_exception(type(e), e, None)
) )
reprcrash = None reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)] repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None and self.chain: if e.__cause__ is not None and self.chain:
e = e.__cause__ e = e.__cause__
excinfo_ = ( excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
descr = "The above exception was the direct cause of the following exception:" descr = "The above exception was the direct cause of the following exception:"
elif ( elif (
e.__context__ is not None and not e.__suppress_context__ and self.chain e.__context__ is not None and not e.__suppress_context__ and self.chain
): ):
e = e.__context__ e = e.__context__
excinfo_ = ( excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
descr = "During handling of the above exception, another exception occurred:" descr = "During handling of the above exception, another exception occurred:"
else: else:
e = None e = None
@@ -978,7 +1013,7 @@ class FormattedExcinfo:
return ExceptionChainRepr(repr_chain) return ExceptionChainRepr(repr_chain)
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class TerminalRepr: class TerminalRepr:
def __str__(self) -> str: def __str__(self) -> str:
# FYI this is called from pytest-xdist's serialization of exception # 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. # This class is abstract -- only subclasses are instantiated.
@attr.s(eq=False) @dataclasses.dataclass(eq=False)
class ExceptionRepr(TerminalRepr): class ExceptionRepr(TerminalRepr):
# Provided by subclasses. # Provided by subclasses.
reprcrash: Optional["ReprFileLocation"]
reprtraceback: "ReprTraceback" reprtraceback: "ReprTraceback"
reprcrash: Optional["ReprFileLocation"]
def __attrs_post_init__(self) -> None: sections: List[Tuple[str, str, str]] = dataclasses.field(
self.sections: List[Tuple[str, str, str]] = [] init=False, default_factory=list
)
def addsection(self, name: str, content: str, sep: str = "-") -> None: def addsection(self, name: str, content: str, sep: str = "-") -> None:
self.sections.append((name, content, sep)) self.sections.append((name, content, sep))
@@ -1014,16 +1049,23 @@ class ExceptionRepr(TerminalRepr):
tw.line(content) tw.line(content)
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ExceptionChainRepr(ExceptionRepr): class ExceptionChainRepr(ExceptionRepr):
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]] chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
def __attrs_post_init__(self) -> None: def __init__(
super().__attrs_post_init__() self,
chain: Sequence[
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
],
) -> None:
# reprcrash and reprtraceback of the outermost (the newest) exception # reprcrash and reprtraceback of the outermost (the newest) exception
# in the chain. # in the chain.
self.reprtraceback = self.chain[-1][0] super().__init__(
self.reprcrash = self.chain[-1][1] reprtraceback=chain[-1][0],
reprcrash=chain[-1][1],
)
self.chain = chain
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
for element in self.chain: for element in self.chain:
@@ -1034,17 +1076,17 @@ class ExceptionChainRepr(ExceptionRepr):
super().toterminal(tw) super().toterminal(tw)
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprExceptionInfo(ExceptionRepr): class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback" reprtraceback: "ReprTraceback"
reprcrash: "ReprFileLocation" reprcrash: Optional["ReprFileLocation"]
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw) self.reprtraceback.toterminal(tw)
super().toterminal(tw) super().toterminal(tw)
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprTraceback(TerminalRepr): class ReprTraceback(TerminalRepr):
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]] reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
extraline: Optional[str] extraline: Optional[str]
@@ -1073,12 +1115,12 @@ class ReprTraceback(TerminalRepr):
class ReprTracebackNative(ReprTraceback): class ReprTracebackNative(ReprTraceback):
def __init__(self, tblines: Sequence[str]) -> None: def __init__(self, tblines: Sequence[str]) -> None:
self.style = "native"
self.reprentries = [ReprEntryNative(tblines)] self.reprentries = [ReprEntryNative(tblines)]
self.extraline = None self.extraline = None
self.style = "native"
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprEntryNative(TerminalRepr): class ReprEntryNative(TerminalRepr):
lines: Sequence[str] lines: Sequence[str]
@@ -1088,7 +1130,7 @@ class ReprEntryNative(TerminalRepr):
tw.write("".join(self.lines)) tw.write("".join(self.lines))
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprEntry(TerminalRepr): class ReprEntry(TerminalRepr):
lines: Sequence[str] lines: Sequence[str]
reprfuncargs: Optional["ReprFuncArgs"] reprfuncargs: Optional["ReprFuncArgs"]
@@ -1142,8 +1184,8 @@ class ReprEntry(TerminalRepr):
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
if self.style == "short": if self.style == "short":
assert self.reprfileloc is not None if self.reprfileloc:
self.reprfileloc.toterminal(tw) self.reprfileloc.toterminal(tw)
self._write_entry_lines(tw) self._write_entry_lines(tw)
if self.reprlocals: if self.reprlocals:
self.reprlocals.toterminal(tw, indent=" " * 8) 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): class ReprFileLocation(TerminalRepr):
path: str = attr.ib(converter=str) path: str
lineno: int lineno: int
message: str message: str
def __post_init__(self) -> None:
self.path = str(self.path)
def toterminal(self, tw: TerminalWriter) -> None: def toterminal(self, tw: TerminalWriter) -> None:
# Filename and lineno output for each entry, using an output format # Filename and lineno output for each entry, using an output format
# that most editors understand. # that most editors understand.
@@ -1185,7 +1230,7 @@ class ReprFileLocation(TerminalRepr):
tw.line(f":{self.lineno}: {msg}") tw.line(f":{self.lineno}: {msg}")
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprLocals(TerminalRepr): class ReprLocals(TerminalRepr):
lines: Sequence[str] lines: Sequence[str]
@@ -1194,7 +1239,7 @@ class ReprLocals(TerminalRepr):
tw.line(indent + line) tw.line(indent + line)
@attr.s(eq=False, auto_attribs=True) @dataclasses.dataclass(eq=False)
class ReprFuncArgs(TerminalRepr): class ReprFuncArgs(TerminalRepr):
args: Sequence[Tuple[str, object]] args: Sequence[Tuple[str, object]]
+9 -8
View File
@@ -24,6 +24,7 @@ from stat import S_ISLNK
from stat import S_ISREG from stat import S_ISREG
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import cast
from typing import overload from typing import overload
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@@ -146,7 +147,7 @@ class Visitor:
self.fil = fil self.fil = fil
self.ignore = ignore self.ignore = ignore
self.breadthfirst = bf 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): def gen(self, path):
try: try:
@@ -224,7 +225,7 @@ class Stat:
raise NotImplementedError("XXX win32") raise NotImplementedError("XXX win32")
import pwd 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] return entry[0]
@property @property
@@ -234,7 +235,7 @@ class Stat:
raise NotImplementedError("XXX win32") raise NotImplementedError("XXX win32")
import grp 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] return entry[0]
def isdir(self): def isdir(self):
@@ -252,7 +253,7 @@ def getuserid(user):
import pwd import pwd
if not isinstance(user, int): if not isinstance(user, int):
user = pwd.getpwnam(user)[2] user = pwd.getpwnam(user)[2] # type:ignore[attr-defined]
return user return user
@@ -260,7 +261,7 @@ def getgroupid(group):
import grp import grp
if not isinstance(group, int): if not isinstance(group, int):
group = grp.getgrnam(group)[2] group = grp.getgrnam(group)[2] # type:ignore[attr-defined]
return group return group
@@ -795,7 +796,7 @@ class LocalPath:
kw = {"exists": 1} kw = {"exists": 1}
return Checkers(self)._evaluate(kw) return Checkers(self)._evaluate(kw)
_patternchars = set("*?[" + os.path.sep) _patternchars = set("*?[" + os.sep)
def listdir(self, fil=None, sort=None): def listdir(self, fil=None, sort=None):
"""List directory contents, possibly filter by the given fil func """List directory contents, possibly filter by the given fil func
@@ -952,7 +953,7 @@ class LocalPath:
else: else:
p.dirpath()._ensuredirs() p.dirpath()._ensuredirs()
if not p.check(file=1): if not p.check(file=1):
p.open("w").close() p.open("wb").close()
return p return p
@overload @overload
@@ -1127,7 +1128,7 @@ class LocalPath:
modfile = modfile[:-1] modfile = modfile[:-1]
elif modfile.endswith("$py.class"): elif modfile.endswith("$py.class"):
modfile = modfile[:-9] + ".py" modfile = modfile[:-9] + ".py"
if modfile.endswith(os.path.sep + "__init__.py"): if modfile.endswith(os.sep + "__init__.py"):
if self.basename != "__init__.py": if self.basename != "__init__.py":
modfile = modfile[:-12] modfile = modfile[:-12]
try: try:
+102 -24
View File
@@ -44,10 +44,20 @@ from _pytest.stash import StashKey
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.assertion import AssertionState 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"]() assertstate_key = StashKey["AssertionState"]()
# pytest caches rewritten pycs in pycache dirs # pytest caches rewritten pycs in pycache dirs
PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}" PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
PYC_EXT = ".py" + (__debug__ and "c" or "o") 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: for initial_path in self.session._initialpaths:
# Make something as c:/projects/my_project/path.py -> # Make something as c:/projects/my_project/path.py ->
# ['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. # add 'path' to basenames to be checked.
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0]) 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() return f.read()
if sys.version_info >= (3, 10): 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): if sys.version_info < (3, 11):
from importlib.readers import FileReader from importlib.readers import FileReader
else: else:
@@ -631,8 +645,12 @@ class AssertionRewriter(ast.NodeVisitor):
.push_format_context() and .pop_format_context() which allows .push_format_context() and .pop_format_context() which allows
to build another %-formatted string while already building one. to build another %-formatted string while already building one.
This state is reset on every new assert statement visited and used :variables_overwrite: A dict filled with references to variables
by the other visitors. 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__( def __init__(
@@ -648,6 +666,7 @@ class AssertionRewriter(ast.NodeVisitor):
else: else:
self.enable_assertion_pass_hook = False self.enable_assertion_pass_hook = False
self.source = source self.source = source
self.variables_overwrite: Dict[str, str] = {}
def run(self, mod: ast.Module) -> None: def run(self, mod: ast.Module) -> None:
"""Find all assert statements in *mod* and rewrite them.""" """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): if doc is not None and self.is_rewrite_disabled(doc):
return return
pos = 0 pos = 0
lineno = 1 item = None
for item in mod.body: for item in mod.body:
if ( if (
expect_docstring expect_docstring
and isinstance(item, ast.Expr) 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): if self.is_rewrite_disabled(doc):
return return
expect_docstring = False expect_docstring = False
@@ -801,7 +823,7 @@ class AssertionRewriter(ast.NodeVisitor):
current = self.stack.pop() current = self.stack.pop()
if self.stack: if self.stack:
self.explanation_specifiers = self.stack[-1] 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())) format_dict = ast.Dict(keys, list(current.values()))
form = ast.BinOp(expl_expr, ast.Mod(), format_dict) form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
name = "@py_format" + str(next(self.variable_counter)) name = "@py_format" + str(next(self.variable_counter))
@@ -855,16 +877,16 @@ class AssertionRewriter(ast.NodeVisitor):
negation = ast.UnaryOp(ast.Not(), top_condition) negation = ast.UnaryOp(ast.Not(), top_condition)
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook 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 # Failed
if assert_.msg: if assert_.msg:
assertmsg = self.helper("_format_assertmsg", assert_.msg) assertmsg = self.helper("_format_assertmsg", assert_.msg)
gluestr = "\n>assert " gluestr = "\n>assert "
else: else:
assertmsg = ast.Str("") assertmsg = astStr("")
gluestr = "assert " 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_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
err_name = ast.Name("AssertionError", ast.Load()) err_name = ast.Name("AssertionError", ast.Load())
fmt = self.helper("_format_explanation", err_msg) fmt = self.helper("_format_explanation", err_msg)
@@ -880,8 +902,8 @@ class AssertionRewriter(ast.NodeVisitor):
hook_call_pass = ast.Expr( hook_call_pass = ast.Expr(
self.helper( self.helper(
"_call_assertion_pass", "_call_assertion_pass",
ast.Num(assert_.lineno), astNum(assert_.lineno),
ast.Str(orig), astStr(orig),
fmt_pass, fmt_pass,
) )
) )
@@ -900,7 +922,7 @@ class AssertionRewriter(ast.NodeVisitor):
variables = [ variables = [
ast.Name(name, ast.Store()) for name in self.format_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) self.statements.append(clear_format)
else: # Original assertion rewriting else: # Original assertion rewriting
@@ -911,9 +933,9 @@ class AssertionRewriter(ast.NodeVisitor):
assertmsg = self.helper("_format_assertmsg", assert_.msg) assertmsg = self.helper("_format_assertmsg", assert_.msg)
explanation = "\n>assert " + explanation explanation = "\n>assert " + explanation
else: else:
assertmsg = ast.Str("") assertmsg = astStr("")
explanation = "assert " + explanation 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) msg = self.pop_format_context(template)
fmt = self.helper("_format_explanation", msg) fmt = self.helper("_format_explanation", msg)
err_name = ast.Name("AssertionError", ast.Load()) err_name = ast.Name("AssertionError", ast.Load())
@@ -925,7 +947,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Clear temporary variables by setting them to None. # Clear temporary variables by setting them to None.
if self.variables: if self.variables:
variables = [ast.Name(name, ast.Store()) for name in 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) self.statements.append(clear)
# Fix locations (line numbers/column offsets). # Fix locations (line numbers/column offsets).
for stmt in self.statements: for stmt in self.statements:
@@ -933,14 +955,26 @@ class AssertionRewriter(ast.NodeVisitor):
ast.copy_location(node, assert_) ast.copy_location(node, assert_)
return self.statements 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]: def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
# Display the repr of the name if it's a local variable or # Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable. # _should_repr_global_name() thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], []) 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) dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr]) 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) return name, self.explanation_param(expr)
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]: def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
@@ -959,10 +993,26 @@ class AssertionRewriter(ast.NodeVisitor):
# cond is set in a prior loop iteration below # cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
self.expl_stmts = fail_inner 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() self.push_format_context()
res, expl = self.visit(v) res, expl = self.visit(v)
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res)) 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], []) call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call)) self.expl_stmts.append(ast.Expr(call))
if i < levels: if i < levels:
@@ -974,7 +1024,7 @@ class AssertionRewriter(ast.NodeVisitor):
self.statements = body = inner self.statements = body = inner
self.statements = save self.statements = save
self.expl_stmts = fail_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) expl = self.pop_format_context(expl_template)
return ast.Name(res_var, ast.Load()), self.explanation_param(expl) return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
@@ -998,10 +1048,19 @@ class AssertionRewriter(ast.NodeVisitor):
new_args = [] new_args = []
new_kwargs = [] new_kwargs = []
for arg in call.args: 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) res, expl = self.visit(arg)
arg_expls.append(expl) arg_expls.append(expl)
new_args.append(res) new_args.append(res)
for keyword in call.keywords: 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) res, expl = self.visit(keyword.value)
new_kwargs.append(ast.keyword(keyword.arg, res)) new_kwargs.append(ast.keyword(keyword.arg, res))
if keyword.arg: if keyword.arg:
@@ -1034,6 +1093,15 @@ class AssertionRewriter(ast.NodeVisitor):
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]: def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
self.push_format_context() 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) left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)): if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})" left_expl = f"({left_expl})"
@@ -1045,14 +1113,23 @@ class AssertionRewriter(ast.NodeVisitor):
syms = [] syms = []
results = [left_res] results = [left_res]
for i, op, next_operand in it: 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) next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)): if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = f"({next_expl})" next_expl = f"({next_expl})"
results.append(next_res) results.append(next_res)
sym = BINOP_MAP[op.__class__] sym = BINOP_MAP[op.__class__]
syms.append(ast.Str(sym)) syms.append(astStr(sym))
expl = f"{left_expl} {sym} {next_expl}" 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]) res_expr = ast.Compare(left_res, [op], [next_res])
self.statements.append(ast.Assign([store_names[i]], res_expr)) self.statements.append(ast.Assign([store_names[i]], res_expr))
left_res, left_expl = next_res, next_expl 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) res: ast.expr = ast.BoolOp(ast.And(), load_names)
else: else:
res = load_names[0] res = load_names[0]
return res, self.explanation_param(self.pop_format_context(expl_call)) 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. """Truncate given list of strings that makes up the assertion explanation.
Truncates to either 8 lines, or 640 characters - whichever the input reaches 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: if max_lines is None:
max_lines = DEFAULT_MAX_LINES max_lines = DEFAULT_MAX_LINES
if max_chars is None: if max_chars is None:
@@ -48,35 +48,56 @@ def _truncate_explanation(
# Check if truncation required # Check if truncation required
input_char_count = len("".join(input_lines)) 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 return input_lines
# Truncate first to max_lines, and then truncate to max_chars if necessary
# Truncate first to max_lines, and then truncate to max_chars if max_chars
# is exceeded.
truncated_explanation = input_lines[:max_lines] truncated_explanation = input_lines[:max_lines]
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars) truncated_char = True
# We reevaluate the need to truncate chars following removal of some lines
# Add ellipsis to final line if len("".join(truncated_explanation)) > tolerable_max_chars:
truncated_explanation[-1] = truncated_explanation[-1] + "..." truncated_explanation = _truncate_by_char_count(
truncated_explanation, max_chars
# 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)"
else: else:
msg += f" ({truncated_line_count} lines hidden)" truncated_char = False
msg += f", {USAGE_MSG}"
truncated_explanation.extend(["", str(msg)]) truncated_line_count = len(input_lines) - len(truncated_explanation)
return 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]: 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 # Find point at which input length exceeds total allowed length
iterated_char_count = 0 iterated_char_count = 0
for iterated_index, input_line in enumerate(input_lines): for iterated_index, input_line in enumerate(input_lines):
+40 -22
View File
@@ -1,6 +1,7 @@
"""Implementation of the cache provider.""" """Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external # This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version. # pytest-cache version.
import dataclasses
import json import json
import os import os
from pathlib import Path from pathlib import Path
@@ -12,8 +13,6 @@ from typing import Optional
from typing import Set from typing import Set
from typing import Union from typing import Union
import attr
from .pathlib import resolve_from_str from .pathlib import resolve_from_str
from .pathlib import rm_rf from .pathlib import rm_rf
from .reports import CollectReport from .reports import CollectReport
@@ -28,11 +27,10 @@ from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.main import Session from _pytest.main import Session
from _pytest.python import Module from _pytest.nodes import File
from _pytest.python import Package from _pytest.python import Package
from _pytest.reports import TestReport from _pytest.reports import TestReport
README_CONTENT = """\ README_CONTENT = """\
# pytest cache directory # # pytest cache directory #
@@ -53,10 +51,12 @@ Signature: 8a477f597d28d172789f06886806bc55
@final @final
@attr.s(init=False, auto_attribs=True) @dataclasses.dataclass
class Cache: class Cache:
_cachedir: Path = attr.ib(repr=False) """Instance of the `cache` fixture."""
_config: Config = attr.ib(repr=False)
_cachedir: Path = dataclasses.field(repr=False)
_config: Config = dataclasses.field(repr=False)
# Sub-directory under cache-dir for directories created by `mkdir()`. # Sub-directory under cache-dir for directories created by `mkdir()`.
_CACHE_PREFIX_DIRS = "d" _CACHE_PREFIX_DIRS = "d"
@@ -179,16 +179,22 @@ class Cache:
else: else:
cache_dir_exists_already = self._cachedir.exists() cache_dir_exists_already = self._cachedir.exists()
path.parent.mkdir(exist_ok=True, parents=True) path.parent.mkdir(exist_ok=True, parents=True)
except OSError: except OSError as exc:
self.warn("could not create cache path {path}", path=path, _ispytest=True) self.warn(
f"could not create cache path {path}: {exc}",
_ispytest=True,
)
return return
if not cache_dir_exists_already: if not cache_dir_exists_already:
self._ensure_supporting_files() self._ensure_supporting_files()
data = json.dumps(value, ensure_ascii=False, indent=2) data = json.dumps(value, ensure_ascii=False, indent=2)
try: try:
f = path.open("w", encoding="UTF-8") f = path.open("w", encoding="UTF-8")
except OSError: except OSError as exc:
self.warn("cache could not write path {path}", path=path, _ispytest=True) self.warn(
f"cache could not write path {path}: {exc}",
_ispytest=True,
)
else: else:
with f: with f:
f.write(data) f.write(data)
@@ -213,22 +219,30 @@ class LFPluginCollWrapper:
@hookimpl(hookwrapper=True) @hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector): def pytest_make_collect_report(self, collector: nodes.Collector):
if isinstance(collector, Session): if isinstance(collector, (Session, Package)):
out = yield out = yield
res: CollectReport = out.get_result() res: CollectReport = out.get_result()
# Sort any lf-paths to the beginning. # Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths 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 = sorted(
res.result, res.result,
# use stable sort to priorize last failed key=sort_key,
key=lambda x: x.path in lf_paths,
reverse=True, reverse=True,
) )
return return
elif isinstance(collector, Module): elif isinstance(collector, File):
if collector.path in self.lfplugin._last_failed_paths: if collector.path in self.lfplugin._last_failed_paths:
out = yield out = yield
res = out.get_result() res = out.get_result()
@@ -266,10 +280,9 @@ class LFPluginCollSkipfiles:
def pytest_make_collect_report( def pytest_make_collect_report(
self, collector: nodes.Collector self, collector: nodes.Collector
) -> Optional[CollectReport]: ) -> Optional[CollectReport]:
# Packages are Modules, but _last_failed_paths only contains # Packages are Files, but we only want to skip test-bearing Files,
# test-bearing paths and doesn't try to include the paths of their # so don't filter Packages.
# packages, so don't filter them. if isinstance(collector, File) and not isinstance(collector, Package):
if isinstance(collector, Module) and not isinstance(collector, Package):
if collector.path not in self.lfplugin._last_failed_paths: if collector.path not in self.lfplugin._last_failed_paths:
self.lfplugin._skipped_files += 1 self.lfplugin._skipped_files += 1
@@ -299,9 +312,14 @@ class LFPlugin:
) )
def get_last_failed_paths(self) -> Set[Path]: 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 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()} return {x for x in result if x.exists()}
def pytest_report_collectionfinish(self) -> Optional[str]: 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]]: 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 from _pytest.main import wrap_session
return wrap_session(config, cacheshow) return wrap_session(config, cacheshow)
+196 -128
View File
@@ -1,19 +1,26 @@
"""Per-test stdout/stderr capturing mechanism.""" """Per-test stdout/stderr capturing mechanism."""
import abc
import collections
import contextlib import contextlib
import functools
import io import io
import os import os
import sys import sys
from io import UnsupportedOperation from io import UnsupportedOperation
from tempfile import TemporaryFile from tempfile import TemporaryFile
from types import TracebackType
from typing import Any from typing import Any
from typing import AnyStr from typing import AnyStr
from typing import BinaryIO
from typing import Generator from typing import Generator
from typing import Generic from typing import Generic
from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List
from typing import NamedTuple
from typing import Optional from typing import Optional
from typing import TextIO from typing import TextIO
from typing import Tuple from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
@@ -29,6 +36,7 @@ from _pytest.nodes import File
from _pytest.nodes import Item from _pytest.nodes import Item
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal from typing_extensions import Literal
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"] _CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
@@ -185,19 +193,27 @@ class TeeCaptureIO(CaptureIO):
return self._other.write(s) return self._other.write(s)
class DontReadFromInput: class DontReadFromInput(TextIO):
encoding = None @property
def encoding(self) -> str:
return sys.__stdin__.encoding
def read(self, *args): def read(self, size: int = -1) -> str:
raise OSError( raise OSError(
"pytest: reading from stdin while output is captured! Consider using `-s`." "pytest: reading from stdin while output is captured! Consider using `-s`."
) )
readline = read 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 return self
def fileno(self) -> int: def fileno(self) -> int:
@@ -215,7 +231,7 @@ class DontReadFromInput:
def readable(self) -> bool: def readable(self) -> bool:
return False 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)") raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
def seekable(self) -> bool: def seekable(self) -> bool:
@@ -224,41 +240,104 @@ class DontReadFromInput:
def tell(self) -> int: def tell(self) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()") raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
def truncate(self, size: int) -> None: def truncate(self, size: Optional[int] = None) -> int:
raise UnsupportedOperation("cannont truncate stdin") raise UnsupportedOperation("cannot truncate stdin")
def write(self, *args) -> None: def write(self, data: str) -> int:
raise UnsupportedOperation("cannot write to stdin") raise UnsupportedOperation("cannot write to stdin")
def writelines(self, *args) -> None: def writelines(self, lines: Iterable[str]) -> None:
raise UnsupportedOperation("Cannot write to stdin") raise UnsupportedOperation("Cannot write to stdin")
def writable(self) -> bool: def writable(self) -> bool:
return False return False
@property def __enter__(self) -> "DontReadFromInput":
def buffer(self):
return self 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. # 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"} patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
class NoCapture: class NoCapture(CaptureBase[str]):
EMPTY_BUFFER = None EMPTY_BUFFER = ""
__init__ = start = done = suspend = resume = lambda *args: None
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: class SysCaptureBase(CaptureBase[AnyStr]):
def __init__(
EMPTY_BUFFER = b"" self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
) -> None:
def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
name = patchsysdict[fd] name = patchsysdict[fd]
self._old = getattr(sys, name) self._old: TextIO = getattr(sys, name)
self.name = name self.name = name
if tmpfile is None: if tmpfile is None:
if name == "stdin": if name == "stdin":
@@ -298,14 +377,6 @@ class SysCaptureBinary:
setattr(sys, self.name, self.tmpfile) setattr(sys, self.name, self.tmpfile)
self._state = "started" 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: def done(self) -> None:
self._assert_state("done", ("initialized", "started", "suspended", "done")) self._assert_state("done", ("initialized", "started", "suspended", "done"))
if self._state == "done": if self._state == "done":
@@ -327,36 +398,43 @@ class SysCaptureBinary:
setattr(sys, self.name, self.tmpfile) setattr(sys, self.name, self.tmpfile)
self._state = "started" 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._assert_state("writeorg", ("started", "suspended"))
self._old.flush() self._old.flush()
self._old.buffer.write(data) self._old.buffer.write(data)
self._old.buffer.flush() self._old.buffer.flush()
class SysCapture(SysCaptureBinary): class SysCapture(SysCaptureBase[str]):
EMPTY_BUFFER = "" # type: ignore[assignment] EMPTY_BUFFER = ""
def snap(self): def snap(self) -> str:
self._assert_state("snap", ("started", "suspended"))
assert isinstance(self.tmpfile, CaptureIO)
res = self.tmpfile.getvalue() res = self.tmpfile.getvalue()
self.tmpfile.seek(0) self.tmpfile.seek(0)
self.tmpfile.truncate() self.tmpfile.truncate()
return res return res
def writeorg(self, data): def writeorg(self, data: str) -> None:
self._assert_state("writeorg", ("started", "suspended")) self._assert_state("writeorg", ("started", "suspended"))
self._old.write(data) self._old.write(data)
self._old.flush() self._old.flush()
class FDCaptureBinary: class FDCaptureBase(CaptureBase[AnyStr]):
"""Capture IO to/from a given OS-level file descriptor.
snap() produces `bytes`.
"""
EMPTY_BUFFER = b""
def __init__(self, targetfd: int) -> None: def __init__(self, targetfd: int) -> None:
self.targetfd = targetfd self.targetfd = targetfd
@@ -382,7 +460,7 @@ class FDCaptureBinary:
if targetfd == 0: if targetfd == 0:
self.tmpfile = open(os.devnull, encoding="utf-8") self.tmpfile = open(os.devnull, encoding="utf-8")
self.syscapture = SysCapture(targetfd) self.syscapture: CaptureBase[str] = SysCapture(targetfd)
else: else:
self.tmpfile = EncodedFile( self.tmpfile = EncodedFile(
TemporaryFile(buffering=0), TemporaryFile(buffering=0),
@@ -394,7 +472,7 @@ class FDCaptureBinary:
if targetfd in patchsysdict: if targetfd in patchsysdict:
self.syscapture = SysCapture(targetfd, self.tmpfile) self.syscapture = SysCapture(targetfd, self.tmpfile)
else: else:
self.syscapture = NoCapture() self.syscapture = NoCapture(targetfd)
self._state = "initialized" self._state = "initialized"
@@ -421,14 +499,6 @@ class FDCaptureBinary:
self.syscapture.start() self.syscapture.start()
self._state = "started" 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: def done(self) -> None:
"""Stop capturing, restore streams, return original capture file, """Stop capturing, restore streams, return original capture file,
seeked to position zero.""" seeked to position zero."""
@@ -461,22 +531,38 @@ class FDCaptureBinary:
os.dup2(self.tmpfile.fileno(), self.targetfd) os.dup2(self.tmpfile.fileno(), self.targetfd)
self._state = "started" 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.""" """Write to original file descriptor."""
self._assert_state("writeorg", ("started", "suspended")) self._assert_state("writeorg", ("started", "suspended"))
os.write(self.targetfd_save, data) os.write(self.targetfd_save, data)
class FDCapture(FDCaptureBinary): class FDCapture(FDCaptureBase[str]):
"""Capture IO to/from a given OS-level file descriptor. """Capture IO to/from a given OS-level file descriptor.
snap() produces text. snap() produces text.
""" """
# Ignore type because it doesn't match the type in the superclass (bytes). EMPTY_BUFFER = ""
EMPTY_BUFFER = "" # type: ignore
def snap(self): def snap(self) -> str:
self._assert_state("snap", ("started", "suspended")) self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0) self.tmpfile.seek(0)
res = self.tmpfile.read() res = self.tmpfile.read()
@@ -484,77 +570,49 @@ class FDCapture(FDCaptureBinary):
self.tmpfile.truncate() self.tmpfile.truncate()
return res return res
def writeorg(self, data): def writeorg(self, data: str) -> None:
"""Write to original file descriptor.""" """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 # MultiCapture
# This class was a namedtuple, but due to mypy limitation[0] it could not be # Generic NamedTuple only supported since Python 3.11.
# made generic, so was replaced by a regular class which tries to emulate the if sys.version_info >= (3, 11) or TYPE_CHECKING:
# 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`."""
__slots__ = ("out", "err") @final
class CaptureResult(NamedTuple, Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`."""
def __init__(self, out: AnyStr, err: AnyStr) -> None: out: AnyStr
self.out: AnyStr = out err: AnyStr
self.err: AnyStr = err
def __len__(self) -> int: else:
return 2
def __iter__(self) -> Iterator[AnyStr]: class CaptureResult(
return iter((self.out, self.err)) collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
):
"""The result of :method:`CaptureFixture.readouterr`."""
def __getitem__(self, item: int) -> AnyStr: __slots__ = ()
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})"
class MultiCapture(Generic[AnyStr]): class MultiCapture(Generic[AnyStr]):
_state = None _state = None
_in_suspended = False _in_suspended = False
def __init__(self, in_, out, err) -> None: def __init__(
self.in_ = in_ self,
self.out = out in_: Optional[CaptureBase[AnyStr]],
self.err = err 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: def __repr__(self) -> str:
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format( 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.""" """Pop current snapshot out/err capture and flush to orig streams."""
out, err = self.readouterr() out, err = self.readouterr()
if out: if out:
assert self.out is not None
self.out.writeorg(out) self.out.writeorg(out)
if err: if err:
assert self.err is not None
self.err.writeorg(err) self.err.writeorg(err)
return out, err return out, err
@@ -600,6 +660,7 @@ class MultiCapture(Generic[AnyStr]):
if self.err: if self.err:
self.err.resume() self.err.resume()
if self._in_suspended: if self._in_suspended:
assert self.in_ is not None
self.in_.resume() self.in_.resume()
self._in_suspended = False self._in_suspended = False
@@ -622,7 +683,8 @@ class MultiCapture(Generic[AnyStr]):
def readouterr(self) -> CaptureResult[AnyStr]: def readouterr(self) -> CaptureResult[AnyStr]:
out = self.out.snap() if self.out else "" out = self.out.snap() if self.out else ""
err = self.err.snap() if self.err 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]: def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
@@ -662,7 +724,7 @@ class CaptureManager:
""" """
def __init__(self, method: "_CaptureMethod") -> None: def __init__(self, method: "_CaptureMethod") -> None:
self._method = method self._method: Final = method
self._global_capturing: Optional[MultiCapture[str]] = None self._global_capturing: Optional[MultiCapture[str]] = None
self._capture_fixture: Optional[CaptureFixture[Any]] = None self._capture_fixture: Optional[CaptureFixture[Any]] = None
@@ -831,14 +893,18 @@ class CaptureFixture(Generic[AnyStr]):
:fixture:`capfd` and :fixture:`capfdbinary` fixtures.""" :fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
def __init__( def __init__(
self, captureclass, request: SubRequest, *, _ispytest: bool = False self,
captureclass: Type[CaptureBase[AnyStr]],
request: SubRequest,
*,
_ispytest: bool = False,
) -> None: ) -> None:
check_ispytest(_ispytest) check_ispytest(_ispytest)
self.captureclass = captureclass self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
self.request = request self.request = request
self._capture: Optional[MultiCapture[AnyStr]] = None self._capture: Optional[MultiCapture[AnyStr]] = None
self._captured_out = self.captureclass.EMPTY_BUFFER self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
def _start(self) -> None: def _start(self) -> None:
if self._capture is None: if self._capture is None:
@@ -893,7 +959,9 @@ class CaptureFixture(Generic[AnyStr]):
@contextlib.contextmanager @contextlib.contextmanager
def disabled(self) -> Generator[None, None, None]: def disabled(self) -> Generator[None, None, None]:
"""Temporarily disable capturing while inside the ``with`` block.""" """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(): with capmanager.global_and_fixture_disabled():
yield yield
@@ -920,8 +988,8 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
captured = capsys.readouterr() captured = capsys.readouterr()
assert captured.out == "hello\n" assert captured.out == "hello\n"
""" """
capman = request.config.pluginmanager.getplugin("capturemanager") capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True) capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture) capman.set_fixture(capture_fixture)
capture_fixture._start() capture_fixture._start()
yield capture_fixture yield capture_fixture
@@ -948,8 +1016,8 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
captured = capsysbinary.readouterr() captured = capsysbinary.readouterr()
assert captured.out == b"hello\n" assert captured.out == b"hello\n"
""" """
capman = request.config.pluginmanager.getplugin("capturemanager") capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True) capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture) capman.set_fixture(capture_fixture)
capture_fixture._start() capture_fixture._start()
yield capture_fixture yield capture_fixture
@@ -976,8 +1044,8 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
captured = capfd.readouterr() captured = capfd.readouterr()
assert captured.out == "hello\n" assert captured.out == "hello\n"
""" """
capman = request.config.pluginmanager.getplugin("capturemanager") capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True) capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture) capman.set_fixture(capture_fixture)
capture_fixture._start() capture_fixture._start()
yield capture_fixture yield capture_fixture
@@ -1005,8 +1073,8 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
assert captured.out == b"hello\n" assert captured.out == b"hello\n"
""" """
capman = request.config.pluginmanager.getplugin("capturemanager") capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True) capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture) capman.set_fixture(capture_fixture)
capture_fixture._start() capture_fixture._start()
yield capture_fixture yield capture_fixture
+27 -18
View File
@@ -1,4 +1,7 @@
"""Python version compatibility code.""" """Python version compatibility code."""
from __future__ import annotations
import dataclasses
import enum import enum
import functools import functools
import inspect import inspect
@@ -11,13 +14,8 @@ from typing import Any
from typing import Callable from typing import Callable
from typing import Generic from typing import Generic
from typing import NoReturn from typing import NoReturn
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union
import attr
import py import py
@@ -47,7 +45,7 @@ LEGACY_PATH = py.path. local
# fmt: on # 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""" """Internal wrapper to prepare lazy proxies for legacy_path instances"""
return LEGACY_PATH(path) 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 # https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum): class NotSetType(enum.Enum):
token = 0 token = 0
NOTSET: "Final" = NotSetType.token # noqa: E305 NOTSET: Final = NotSetType.token # noqa: E305
# fmt: on # fmt: on
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
@@ -95,7 +93,7 @@ def is_async_function(func: object) -> bool:
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func) 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) function = get_real_func(function)
fn = Path(inspect.getfile(function)) fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno lineno = function.__code__.co_firstlineno
@@ -133,8 +131,8 @@ def getfuncargnames(
*, *,
name: str = "", name: str = "",
is_method: bool = False, is_method: bool = False,
cls: Optional[type] = None, cls: type | None = None,
) -> Tuple[str, ...]: ) -> tuple[str, ...]:
"""Return the names of a function's mandatory arguments. """Return the names of a function's mandatory arguments.
Should return the names of all function arguments that: Should return the names of all function arguments that:
@@ -198,7 +196,7 @@ def getfuncargnames(
return arg_names 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 # Note: this code intentionally mirrors the code at the beginning of
# getfuncargnames, to get the arguments which were excluded from its result # getfuncargnames, to get the arguments which were excluded from its result
# because they had default values. # because they had default values.
@@ -229,7 +227,7 @@ def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace") 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 r"""If val is pure ASCII, return it as an str, otherwise, escape
bytes objects into a sequence of escaped bytes: 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) return _translate_non_printable(ret)
@attr.s @dataclasses.dataclass
class _PytestWrapper: class _PytestWrapper:
"""Dummy wrapper around a function object for internal use only. """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. decorator to issue warnings when the fixture function is called directly.
""" """
obj = attr.ib() obj: Any
def get_real_func(obj): def get_real_func(obj):
@@ -356,7 +354,6 @@ else:
if sys.version_info >= (3, 8): if sys.version_info >= (3, 8):
from functools import cached_property as cached_property from functools import cached_property as cached_property
else: else:
from typing import Type
class cached_property(Generic[_S, _T]): class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__") __slots__ = ("func", "__doc__")
@@ -367,12 +364,12 @@ else:
@overload @overload
def __get__( def __get__(
self, instance: None, owner: Optional[Type[_S]] = ... self, instance: None, owner: type[_S] | None = ...
) -> "cached_property[_S, _T]": ) -> cached_property[_S, _T]:
... ...
@overload @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): def __get__(self, instance, owner=None):
@@ -382,6 +379,18 @@ else:
return value 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. # Perform exhaustiveness checking.
# #
# Consider this example: # Consider this example:
+50 -18
View File
@@ -2,6 +2,7 @@
import argparse import argparse
import collections.abc import collections.abc
import copy import copy
import dataclasses
import enum import enum
import glob import glob
import inspect import inspect
@@ -34,7 +35,6 @@ from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import attr
from pluggy import HookimplMarker from pluggy import HookimplMarker
from pluggy import HookspecMarker from pluggy import HookspecMarker
from pluggy import PluginManager from pluggy import PluginManager
@@ -49,7 +49,7 @@ from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest.compat import final 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 fail
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
@@ -62,7 +62,6 @@ from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
from .argparsing import Argument from .argparsing import Argument
@@ -527,7 +526,10 @@ class PytestPluginManager(PluginManager):
# Internal API for local conftest plugin handling. # Internal API for local conftest plugin handling.
# #
def _set_initial_conftests( def _set_initial_conftests(
self, namespace: argparse.Namespace, rootpath: Path self,
namespace: argparse.Namespace,
rootpath: Path,
testpaths_ini: Sequence[str],
) -> None: ) -> None:
"""Load initial conftest files given a preparsed "namespace". """Load initial conftest files given a preparsed "namespace".
@@ -544,7 +546,7 @@ class PytestPluginManager(PluginManager):
) )
self._noconftest = namespace.noconftest self._noconftest = namespace.noconftest
self._using_pyargs = namespace.pyargs self._using_pyargs = namespace.pyargs
testpaths = namespace.file_or_dir testpaths = namespace.file_or_dir + testpaths_ini
foundanchor = False foundanchor = False
for testpath in testpaths: for testpath in testpaths:
path = str(testpath) path = str(testpath)
@@ -553,7 +555,14 @@ class PytestPluginManager(PluginManager):
if i != -1: if i != -1:
path = path[:i] path = path[:i]
anchor = absolutepath(current / path) 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) self._try_load_conftest(anchor, namespace.importmode, rootpath)
foundanchor = True foundanchor = True
if not foundanchor: if not foundanchor:
@@ -697,6 +706,7 @@ class PytestPluginManager(PluginManager):
parg = opt[2:] parg = opt[2:]
else: else:
continue continue
parg = parg.strip()
if exclude_only and not parg.startswith("no:"): if exclude_only and not parg.startswith("no:"):
continue continue
self.consider_pluginarg(parg) 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) yield from _iter_rewritable_modules(new_package_files)
def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
return tuple(args)
@final @final
class Config: class Config:
"""Access to configuration values, pluginmanager and plugin hooks. """Access to configuration values, pluginmanager and plugin hooks.
@@ -903,7 +909,7 @@ class Config:
""" """
@final @final
@attr.s(frozen=True, auto_attribs=True) @dataclasses.dataclass(frozen=True)
class InvocationParams: class InvocationParams:
"""Holds parameters passed during :func:`pytest.main`. """Holds parameters passed during :func:`pytest.main`.
@@ -919,13 +925,24 @@ class Config:
Plugins accessing ``InvocationParams`` must be aware of that. 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`.""" """The command-line arguments as passed to :func:`pytest.main`."""
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]] plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
"""Extra plugins, might be `None`.""" """Extra plugins, might be `None`."""
dir: Path dir: Path
"""The directory from which :func:`pytest.main` was invoked.""" """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): class ArgsSource(enum.Enum):
"""Indicates the source of the test arguments. """Indicates the source of the test arguments.
@@ -998,6 +1015,8 @@ class Config:
self.hook.pytest_addoption.call_historic( self.hook.pytest_addoption.call_historic(
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
) )
self.args_source = Config.ArgsSource.ARGS
self.args: List[str] = []
if TYPE_CHECKING: if TYPE_CHECKING:
from _pytest.cacheprovider import Cache from _pytest.cacheprovider import Cache
@@ -1057,7 +1076,6 @@ class Config:
try: try:
self.parse(args) self.parse(args)
except UsageError: except UsageError:
# Handle --version and --help here in a minimal fashion. # Handle --version and --help here in a minimal fashion.
# This gets done via helpconfig normally, but its # This gets done via helpconfig normally, but its
# pytest_cmdline_main is not called in case of errors. # pytest_cmdline_main is not called in case of errors.
@@ -1123,7 +1141,9 @@ class Config:
@hookimpl(trylast=True) @hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config: "Config") -> None: def pytest_load_initial_conftests(self, early_config: "Config") -> None:
self.pluginmanager._set_initial_conftests( 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: def _initini(self, args: Sequence[str]) -> None:
@@ -1241,8 +1261,11 @@ class Config:
_pytest.deprecated.STRICT_OPTION, stacklevel=2 _pytest.deprecated.STRICT_OPTION, stacklevel=2
) )
if self.known_args_namespace.confcutdir is None and self.inipath is not None: if self.known_args_namespace.confcutdir is None:
confcutdir = str(self.inipath.parent) if self.inipath is not None:
confcutdir = str(self.inipath.parent)
else:
confcutdir = str(self.rootpath)
self.known_args_namespace.confcutdir = confcutdir self.known_args_namespace.confcutdir = confcutdir
try: try:
self.hook.pytest_load_initial_conftests( self.hook.pytest_load_initial_conftests(
@@ -1337,8 +1360,8 @@ class Config:
def parse(self, args: List[str], addopts: bool = True) -> None: def parse(self, args: List[str], addopts: bool = True) -> None:
# Parse given cmdline arguments into this config object. # Parse given cmdline arguments into this config object.
assert not hasattr( assert (
self, "args" self.args == []
), "can only parse cmdline args at most once per Config object" ), "can only parse cmdline args at most once per Config object"
self.hook.pytest_addhooks.call_historic( self.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=self.pluginmanager) kwargs=dict(pluginmanager=self.pluginmanager)
@@ -1362,6 +1385,15 @@ class Config:
args = [] args = []
for path in testpaths: for path in testpaths:
args.extend(sorted(glob.iglob(path, recursive=True))) 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: if not args:
source = Config.ArgsSource.INCOVATION_DIR source = Config.ArgsSource.INCOVATION_DIR
args = [str(self.invocation_params.dir)] args = [str(self.invocation_params.dir)]
-1
View File
@@ -43,7 +43,6 @@ class PathAwareHookProxy:
@_wraps(hook) @_wraps(hook)
def fixed_hook(**kw): def fixed_hook(**kw):
path_value: Optional[Path] = kw.pop(path_var, None) path_value: Optional[Path] = kw.pop(path_var, None)
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None) fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
if fspath_value is not None: if fspath_value is not None:
+9 -2
View File
@@ -203,8 +203,7 @@ def determine_setup(
else: else:
cwd = Path.cwd() cwd = Path.cwd()
rootdir = get_common_ancestor([cwd, ancestor]) rootdir = get_common_ancestor([cwd, ancestor])
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/" if is_fs_root(rootdir):
if is_fs_root:
rootdir = ancestor rootdir = ancestor
if rootdir_cmd_arg: if rootdir_cmd_arg:
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg)) rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
@@ -216,3 +215,11 @@ def determine_setup(
) )
assert rootdir is not None assert rootdir is not None
return rootdir, inipath, inicfg or {} 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): if _is_mocked(obj):
return return
with _patch_unwrap_mock_aware(): with _patch_unwrap_mock_aware():
# Type ignored because this is a private function. # Type ignored because this is a private function.
super()._find( # type:ignore[misc] super()._find( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen tests, obj, name, module, source_lines, globs, seen
+8 -10
View File
@@ -2,7 +2,6 @@ import io
import os import os
import sys import sys
from typing import Generator from typing import Generator
from typing import TextIO
import pytest import pytest
from _pytest.config import Config from _pytest.config import Config
@@ -11,7 +10,7 @@ from _pytest.nodes import Item
from _pytest.stash import StashKey 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]() fault_handler_originally_enabled_key = StashKey[bool]()
@@ -26,10 +25,9 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_configure(config: Config) -> None: def pytest_configure(config: Config) -> None:
import faulthandler import faulthandler
stderr_fd_copy = os.dup(get_stderr_fileno()) config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled() 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: def pytest_unconfigure(config: Config) -> None:
@@ -37,9 +35,9 @@ def pytest_unconfigure(config: Config) -> None:
faulthandler.disable() faulthandler.disable()
# Close the dup file installed during pytest_configure. # Close the dup file installed during pytest_configure.
if fault_handler_stderr_key in config.stash: if fault_handler_stderr_fd_key in config.stash:
config.stash[fault_handler_stderr_key].close() os.close(config.stash[fault_handler_stderr_fd_key])
del config.stash[fault_handler_stderr_key] del config.stash[fault_handler_stderr_fd_key]
if config.stash.get(fault_handler_originally_enabled_key, False): if config.stash.get(fault_handler_originally_enabled_key, False):
# Re-enable the faulthandler if it was originally enabled. # Re-enable the faulthandler if it was originally enabled.
faulthandler.enable(file=get_stderr_fileno()) faulthandler.enable(file=get_stderr_fileno())
@@ -67,10 +65,10 @@ def get_timeout_config_value(config: Config) -> float:
@pytest.hookimpl(hookwrapper=True, trylast=True) @pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]: def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
timeout = get_timeout_config_value(item.config) timeout = get_timeout_config_value(item.config)
stderr = item.config.stash[fault_handler_stderr_key] if timeout > 0:
if timeout > 0 and stderr is not None:
import faulthandler import faulthandler
stderr = item.config.stash[fault_handler_stderr_fd_key]
faulthandler.dump_traceback_later(timeout, file=stderr) faulthandler.dump_traceback_later(timeout, file=stderr)
try: try:
yield yield
+89 -48
View File
@@ -1,3 +1,4 @@
import dataclasses
import functools import functools
import inspect import inspect
import os import os
@@ -28,8 +29,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import attr
import _pytest import _pytest
from _pytest import nodes from _pytest import nodes
from _pytest._code import getfslineno from _pytest._code import getfslineno
@@ -47,6 +46,7 @@ from _pytest.compat import getimfunc
from _pytest.compat import getlocation from _pytest.compat import getlocation
from _pytest.compat import is_generator from _pytest.compat import is_generator
from _pytest.compat import NOTSET from _pytest.compat import NOTSET
from _pytest.compat import NotSetType
from _pytest.compat import overload from _pytest.compat import overload
from _pytest.compat import safe_getattr from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin from _pytest.config import _PluggyPlugin
@@ -58,6 +58,7 @@ from _pytest.mark import Mark
from _pytest.mark import ParameterSet from _pytest.mark import ParameterSet
from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import MarkDecorator
from _pytest.outcomes import fail from _pytest.outcomes import fail
from _pytest.outcomes import skip
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
from _pytest.pathlib import absolutepath from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath 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]): class PseudoFixtureDef(Generic[FixtureValue]):
cached_result: "_FixtureCachedResult[FixtureValue]" cached_result: "_FixtureCachedResult[FixtureValue]"
_scope: Scope _scope: Scope
@@ -112,16 +113,18 @@ def pytest_sessionstart(session: "Session") -> None:
session._fixturemanager = FixtureManager(session) session._fixturemanager = FixtureManager(session)
def get_scope_package(node, fixturedef: "FixtureDef[object]"): def get_scope_package(
import pytest node: nodes.Item,
fixturedef: "FixtureDef[object]",
) -> Optional[Union[nodes.Item, nodes.Collector]]:
from _pytest.python import Package
cls = pytest.Package current: Optional[Union[nodes.Item, nodes.Collector]] = node
current = node
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py") fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
while current and ( 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: if current is None:
return node.session return node.session
return current return current
@@ -349,8 +352,10 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
return request.param return request.param
@attr.s(slots=True, auto_attribs=True) @dataclasses.dataclass
class FuncFixtureInfo: class FuncFixtureInfo:
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
# Original function argument names. # Original function argument names.
argnames: Tuple[str, ...] argnames: Tuple[str, ...]
# Argnames that function immediately requires. These include argnames + # Argnames that function immediately requires. These include argnames +
@@ -432,7 +437,23 @@ class FixtureRequest:
@property @property
def node(self): def node(self):
"""Underlying collection node (depends on current request scope).""" """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]": def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None) fixturedefs = self._arg2fixturedefs.get(argname, None)
@@ -516,11 +537,7 @@ class FixtureRequest:
"""Add finalizer/teardown function to be called without arguments after """Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution.""" the last test within the requesting test context finished execution."""
# XXX usually this method is shadowed by fixturedef specific ones. # XXX usually this method is shadowed by fixturedef specific ones.
self._addfinalizer(finalizer, scope=self.scope) self.node.addfinalizer(finalizer)
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
node = self._getscopeitem(scope)
node.addfinalizer(finalizer)
def applymarker(self, marker: Union[str, MarkDecorator]) -> None: def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
"""Apply a marker to a single test function invocation. """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)) lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
return lines 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: def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node) return "<FixtureRequest for %r>" % (self.node)
@@ -1129,6 +1124,10 @@ def pytest_fixture_setup(
except TEST_OUTCOME: except TEST_OUTCOME:
exc_info = sys.exc_info() exc_info = sys.exc_info()
assert exc_info[0] is not None 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) fixturedef.cached_result = (None, my_cache_key, exc_info)
raise raise
fixturedef.cached_result = (result, my_cache_key, None) fixturedef.cached_result = (result, my_cache_key, None)
@@ -1176,19 +1175,21 @@ def wrap_function_to_error_out_if_called_directly(
@final @final
@attr.s(frozen=True, auto_attribs=True) @dataclasses.dataclass(frozen=True)
class FixtureFunctionMarker: class FixtureFunctionMarker:
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]" scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter) params: Optional[Tuple[object, ...]]
autouse: bool = False autouse: bool = False
ids: Optional[ ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]] Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = attr.ib( ] = None
default=None,
converter=_ensure_immutable_ids,
)
name: Optional[str] = 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: def __call__(self, function: FixtureFunction) -> FixtureFunction:
if inspect.isclass(function): if inspect.isclass(function):
raise ValueError("class fixtures not supported (maybe in the future)") raise ValueError("class fixtures not supported (maybe in the future)")
@@ -1308,10 +1309,11 @@ def fixture( # noqa: F811
""" """
fixture_marker = FixtureFunctionMarker( fixture_marker = FixtureFunctionMarker(
scope=scope, scope=scope,
params=params, params=tuple(params) if params is not None else None,
autouse=autouse, autouse=autouse,
ids=ids, ids=None if ids is None else ids if callable(ids) else tuple(ids),
name=name, name=name,
_ispytest=True,
) )
# Direct decoration. # Direct decoration.
@@ -1584,13 +1586,52 @@ class FixtureManager:
# Separate parametrized setups. # Separate parametrized setups.
items[:] = reorder_items(items) items[:] = reorder_items(items)
@overload
def parsefactories( def parsefactories(
self, node_or_obj, nodeid=NOTSET, unittest: bool = False self,
node_or_obj: nodes.Node,
*,
unittest: bool = ...,
) -> None: ) -> 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: if nodeid is not NOTSET:
holderobj = node_or_obj holderobj = node_or_obj
else: 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 nodeid = node_or_obj.nodeid
if holderobj in self._holderobjseen: if holderobj in self._holderobjseen:
return return
+2 -1
View File
@@ -164,7 +164,8 @@ def showhelp(config: Config) -> None:
tw.write(config._parser.optparser.format_help()) tw.write(config._parser.optparser.format_help())
tw.line() tw.line()
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() tw.line()
+15 -8
View File
@@ -21,7 +21,7 @@ if TYPE_CHECKING:
from typing_extensions import Literal from typing_extensions import Literal
from _pytest._code.code import ExceptionRepr 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 Config
from _pytest.config import ExitCode from _pytest.config import ExitCode
from _pytest.config import PytestPluginManager from _pytest.config import PytestPluginManager
@@ -41,6 +41,7 @@ if TYPE_CHECKING:
from _pytest.reports import TestReport from _pytest.reports import TestReport
from _pytest.runner import CallInfo from _pytest.runner import CallInfo
from _pytest.terminal import TerminalReporter from _pytest.terminal import TerminalReporter
from _pytest.terminal import TestShortLogReport
from _pytest.compat import LEGACY_PATH 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. See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
:param nodeid: Full node ID of the item. :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. See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
:param nodeid: Full node ID of the item. :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" config: "Config", start_path: Path, startdir: "LEGACY_PATH"
) -> Union[str, List[str]]: ) -> Union[str, List[str]]:
"""Return a string or list of strings to be displayed as header info for terminal reporting. """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", config: "Config",
start_path: Path, start_path: Path,
startdir: "LEGACY_PATH", startdir: "LEGACY_PATH",
@@ -800,9 +805,9 @@ def pytest_report_collectionfinish(
@hookspec(firstresult=True) @hookspec(firstresult=True)
def pytest_report_teststatus( def pytest_report_teststatus( # type:ignore[empty-body]
report: Union["CollectReport", "TestReport"], config: "Config" 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 """Return result-category, shortletter and verbose word for status
reporting. 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 """Called when constructing the globals dictionary used for
evaluating string conditions in xfail/skipif markers. evaluating string conditions in xfail/skipif markers.
+2 -2
View File
@@ -645,8 +645,8 @@ class LogXML:
def pytest_sessionfinish(self) -> None: def pytest_sessionfinish(self) -> None:
dirname = os.path.dirname(os.path.abspath(self.logfile)) dirname = os.path.dirname(os.path.abspath(self.logfile))
if not os.path.isdir(dirname): # exist_ok avoids filesystem race conditions between checking path existence and requesting creation
os.makedirs(dirname) os.makedirs(dirname, exist_ok=True)
with open(self.logfile, "w", encoding="utf-8") as logfile: with open(self.logfile, "w", encoding="utf-8") as logfile:
suite_stop_time = timing.time() suite_stop_time = timing.time()
+2 -2
View File
@@ -1,4 +1,5 @@
"""Add backward compatibility support for the legacy py path type.""" """Add backward compatibility support for the legacy py path type."""
import dataclasses
import shlex import shlex
import subprocess import subprocess
from pathlib import Path from pathlib import Path
@@ -7,7 +8,6 @@ from typing import Optional
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import attr
from iniconfig import SectionWrapper from iniconfig import SectionWrapper
from _pytest.cacheprovider import Cache from _pytest.cacheprovider import Cache
@@ -268,7 +268,7 @@ class LegacyTestdirPlugin:
@final @final
@attr.s(init=False, auto_attribs=True) @dataclasses.dataclass
class TempdirFactory: class TempdirFactory:
"""Backward compatibility wrapper that implements :class:`py.path.local` """Backward compatibility wrapper that implements :class:`py.path.local`
for :class:`TempPathFactory`. for :class:`TempPathFactory`.
+90 -4
View File
@@ -5,7 +5,11 @@ import os
import re import re
from contextlib import contextmanager from contextlib import contextmanager
from contextlib import nullcontext from contextlib import nullcontext
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from io import StringIO from io import StringIO
from logging import LogRecord
from pathlib import Path from pathlib import Path
from typing import AbstractSet from typing import AbstractSet
from typing import Dict from typing import Dict
@@ -53,7 +57,25 @@ def _remove_ansi_escape_sequences(text: str) -> str:
return _ANSI_ESCAPE_SEQ.sub("", text) 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 """A logging formatter which colorizes the %(levelname)..s part of the
log format passed to __init__.""" log format passed to __init__."""
@@ -297,6 +319,13 @@ def pytest_addoption(parser: Parser) -> None:
default=None, default=None,
help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.", 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) _HandlerType = TypeVar("_HandlerType", bound=logging.Handler)
@@ -369,11 +398,12 @@ class LogCaptureFixture:
self._initial_handler_level: Optional[int] = None self._initial_handler_level: Optional[int] = None
# Dict of log name -> log level. # Dict of log name -> log level.
self._initial_logger_levels: Dict[Optional[str], int] = {} self._initial_logger_levels: Dict[Optional[str], int] = {}
self._initial_disabled_logging_level: Optional[int] = None
def _finalize(self) -> None: def _finalize(self) -> None:
"""Finalize the fixture. """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. # Restore log levels.
if self._initial_handler_level is not None: if self._initial_handler_level is not None:
@@ -381,6 +411,10 @@ class LogCaptureFixture:
for logger_name, level in self._initial_logger_levels.items(): for logger_name, level in self._initial_logger_levels.items():
logger = logging.getLogger(logger_name) logger = logging.getLogger(logger_name)
logger.setLevel(level) 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 @property
def handler(self) -> LogCaptureHandler: def handler(self) -> LogCaptureHandler:
@@ -446,6 +480,40 @@ class LogCaptureFixture:
"""Reset the list of log records and the captured log text.""" """Reset the list of log records and the captured log text."""
self.handler.clear() 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: 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. """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 The levels of the loggers changed by this function will be
restored to their initial values at the end of the test. 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 level: The level.
:param logger: The logger to update. If not given, the root logger. :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: if self._initial_handler_level is None:
self._initial_handler_level = self.handler.level self._initial_handler_level = self.handler.level
self.handler.setLevel(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 @contextmanager
def at_level( def at_level(
@@ -472,6 +545,8 @@ class LogCaptureFixture:
the end of the 'with' statement the level is restored to its original the end of the 'with' statement the level is restored to its original
value. value.
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
:param level: The level. :param level: The level.
:param logger: The logger to update. If not given, the root logger. :param logger: The logger to update. If not given, the root logger.
""" """
@@ -480,11 +555,13 @@ class LogCaptureFixture:
logger_obj.setLevel(level) logger_obj.setLevel(level)
handler_orig_level = self.handler.level handler_orig_level = self.handler.level
self.handler.setLevel(level) self.handler.setLevel(level)
original_disable_level = self._force_enable_logging(level, logger_obj)
try: try:
yield yield
finally: finally:
logger_obj.setLevel(orig_level) logger_obj.setLevel(orig_level)
self.handler.setLevel(handler_orig_level) self.handler.setLevel(handler_orig_level)
logging.disable(original_disable_level)
@fixture @fixture
@@ -570,7 +647,7 @@ class LoggingPlugin:
config, "log_file_date_format", "log_date_format" 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 log_file_format, datefmt=log_file_date_format
) )
self.log_file_handler.setFormatter(log_file_formatter) self.log_file_handler.setFormatter(log_file_formatter)
@@ -594,6 +671,15 @@ class LoggingPlugin:
get_option_ini(config, "log_auto_indent"), get_option_ini(config, "log_auto_indent"),
) )
self.log_cli_handler.setFormatter(log_cli_formatter) 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): def _create_formatter(self, log_format, log_date_format, auto_indent):
# Color option doesn't exist if terminal plugin is disabled. # 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 create_terminal_writer(self._config), log_format, log_date_format
) )
else: else:
formatter = logging.Formatter(log_format, log_date_format) formatter = DatetimeFormatter(log_format, log_date_format)
formatter._style = PercentStyleMultiline( formatter._style = PercentStyleMultiline(
formatter._style._fmt, auto_indent=auto_indent 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.""" """Core implementation of the testing process: init, session, runtest loop."""
import argparse import argparse
import dataclasses
import fnmatch import fnmatch
import functools import functools
import importlib import importlib
@@ -19,8 +20,6 @@ from typing import Type
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import attr
import _pytest._code import _pytest._code
from _pytest import nodes from _pytest import nodes
from _pytest.compat import final from _pytest.compat import final
@@ -123,11 +122,12 @@ def pytest_addoption(parser: Parser) -> None:
) )
group._addoption( group._addoption(
"-c", "-c",
metavar="file", "--config-file",
metavar="FILE",
type=str, type=str,
dest="inifilename", dest="inifilename",
help="Load configuration from `file` instead of trying to locate one of the " help="Load configuration from `FILE` instead of trying to locate one of the "
"implicit configuration files", "implicit configuration files.",
) )
group._addoption( group._addoption(
"--continue-on-collection-errors", "--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") allow_in_venv = config.getoption("collect_in_virtualenv")
if not allow_in_venv and _in_venv(collection_path): if not allow_in_venv and _in_venv(collection_path):
return True 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 return None
@@ -442,8 +448,10 @@ class Failed(Exception):
"""Signals a stop as failed test run.""" """Signals a stop as failed test run."""
@attr.s(slots=True, auto_attribs=True) @dataclasses.dataclass
class _bestrelpath_cache(Dict[Path, str]): class _bestrelpath_cache(Dict[Path, str]):
__slots__ = ("path",)
path: Path path: Path
def __missing__(self, path: Path) -> str: def __missing__(self, path: Path) -> str:
@@ -561,9 +569,6 @@ class Session(nodes.FSCollector):
ihook = self.gethookproxy(fspath.parent) ihook = self.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True return True
def _collectfile( def _collectfile(
@@ -684,8 +689,8 @@ class Session(nodes.FSCollector):
# are not collected more than once. # are not collected more than once.
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {} matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
# Dirnames of pkgs with dunder-init files. # Directories of pkgs with dunder-init files.
pkg_roots: Dict[str, Package] = {} pkg_roots: Dict[Path, Package] = {}
for argpath, names in self._initial_parts: for argpath, names in self._initial_parts:
self.trace("processing argument", (argpath, names)) self.trace("processing argument", (argpath, names))
@@ -706,7 +711,7 @@ class Session(nodes.FSCollector):
col = self._collectfile(pkginit, handle_dupes=False) col = self._collectfile(pkginit, handle_dupes=False)
if col: if col:
if isinstance(col[0], Package): if isinstance(col[0], Package):
pkg_roots[str(parent)] = col[0] pkg_roots[parent] = col[0]
node_cache1[col[0].path] = [col[0]] node_cache1[col[0].path] = [col[0]]
# If it's a directory argument, recurse and look for any Subpackages. # 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}" assert not names, f"invalid arg {(argpath, names)!r}"
seen_dirs: Set[Path] = set() 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(): if not direntry.is_file():
continue continue
@@ -730,8 +735,8 @@ class Session(nodes.FSCollector):
for x in self._collectfile(pkginit): for x in self._collectfile(pkginit):
yield x yield x
if isinstance(x, Package): if isinstance(x, Package):
pkg_roots[str(dirpath)] = x pkg_roots[dirpath] = x
if str(dirpath) in pkg_roots: if dirpath in pkg_roots:
# Do not collect packages here. # Do not collect packages here.
continue continue
@@ -748,7 +753,7 @@ class Session(nodes.FSCollector):
if argpath in node_cache1: if argpath in node_cache1:
col = node_cache1[argpath] col = node_cache1[argpath]
else: 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) col = collect_root._collectfile(argpath, handle_dupes=False)
if col: if col:
node_cache1[argpath] = col node_cache1[argpath] = col
+7 -4
View File
@@ -1,4 +1,5 @@
"""Generic mechanism for marking and selecting python functions.""" """Generic mechanism for marking and selecting python functions."""
import dataclasses
from typing import AbstractSet from typing import AbstractSet
from typing import Collection from typing import Collection
from typing import List from typing import List
@@ -6,8 +7,6 @@ from typing import Optional
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import attr
from .expression import Expression from .expression import Expression
from .expression import ParseError from .expression import ParseError
from .structures import EMPTY_PARAMETERSET_OPTION from .structures import EMPTY_PARAMETERSET_OPTION
@@ -130,7 +129,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
return None return None
@attr.s(slots=True, auto_attribs=True) @dataclasses.dataclass
class KeywordMatcher: class KeywordMatcher:
"""A matcher for keywords. """A matcher for keywords.
@@ -145,6 +144,8 @@ class KeywordMatcher:
any item, as well as names directly assigned to test functions. any item, as well as names directly assigned to test functions.
""" """
__slots__ = ("_names",)
_names: AbstractSet[str] _names: AbstractSet[str]
@classmethod @classmethod
@@ -201,13 +202,15 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
items[:] = remaining items[:] = remaining
@attr.s(slots=True, auto_attribs=True) @dataclasses.dataclass
class MarkMatcher: class MarkMatcher:
"""A matcher for markers which are present. """A matcher for markers which are present.
Tries to match on any marker names, attached to the given colitem. Tries to match on any marker names, attached to the given colitem.
""" """
__slots__ = ("own_mark_names",)
own_mark_names: AbstractSet[str] own_mark_names: AbstractSet[str]
@classmethod @classmethod
+9 -3
View File
@@ -15,8 +15,10 @@ The semantics are:
- or/and/not evaluate according to the usual boolean semantics. - or/and/not evaluate according to the usual boolean semantics.
""" """
import ast import ast
import dataclasses
import enum import enum
import re import re
import sys
import types import types
from typing import Callable from typing import Callable
from typing import Iterator from typing import Iterator
@@ -25,7 +27,10 @@ from typing import NoReturn
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
import attr if sys.version_info >= (3, 8):
astNameConstant = ast.Constant
else:
astNameConstant = ast.NameConstant
__all__ = [ __all__ = [
@@ -44,8 +49,9 @@ class TokenType(enum.Enum):
EOF = "end of input" EOF = "end of input"
@attr.s(frozen=True, slots=True, auto_attribs=True) @dataclasses.dataclass(frozen=True)
class Token: class Token:
__slots__ = ("type", "value", "pos")
type: TokenType type: TokenType
value: str value: str
pos: int pos: int
@@ -132,7 +138,7 @@ IDENT_PREFIX = "$"
def expression(s: Scanner) -> ast.Expression: def expression(s: Scanner) -> ast.Expression:
if s.accept(TokenType.EOF): if s.accept(TokenType.EOF):
ret: ast.expr = ast.NameConstant(False) ret: ast.expr = astNameConstant(False)
else: else:
ret = expr(s) ret = expr(s)
s.accept(TokenType.EOF, reject=True) s.accept(TokenType.EOF, reject=True)
+9 -6
View File
@@ -1,4 +1,5 @@
import collections.abc import collections.abc
import dataclasses
import inspect import inspect
import warnings import warnings
from typing import Any from typing import Any
@@ -20,8 +21,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import attr
from .._code import getfslineno from .._code import getfslineno
from ..compat import ascii_escaped from ..compat import ascii_escaped
from ..compat import final from ..compat import final
@@ -191,8 +190,10 @@ class ParameterSet(NamedTuple):
@final @final
@attr.s(frozen=True, init=False, auto_attribs=True) @dataclasses.dataclass(frozen=True)
class Mark: class Mark:
"""A pytest mark."""
#: Name of the mark. #: Name of the mark.
name: str name: str
#: Positional arguments of the mark decorator. #: Positional arguments of the mark decorator.
@@ -201,9 +202,11 @@ class Mark:
kwargs: Mapping[str, Any] kwargs: Mapping[str, Any]
#: Source Mark for ids with parametrize Marks. #: 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. #: 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__( def __init__(
self, self,
@@ -261,7 +264,7 @@ class Mark:
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type]) Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
@attr.s(init=False, auto_attribs=True) @dataclasses.dataclass
class MarkDecorator: class MarkDecorator:
"""A decorator for applying a mark on test functions and classes. """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 Any
from typing import Generator from typing import Generator
from typing import List from typing import List
from typing import Mapping
from typing import MutableMapping from typing import MutableMapping
from typing import Optional from typing import Optional
from typing import overload from typing import overload
@@ -129,7 +130,7 @@ class MonkeyPatch:
def __init__(self) -> None: def __init__(self) -> None:
self._setattr: List[Tuple[object, str, object]] = [] 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._cwd: Optional[str] = None
self._savesyspath: Optional[List[str]] = None self._savesyspath: Optional[List[str]] = None
@@ -290,12 +291,13 @@ class MonkeyPatch:
self._setattr.append((target, name, oldval)) self._setattr.append((target, name, oldval))
delattr(target, name) 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.""" """Set dictionary entry ``name`` to value."""
self._setitem.append((dic, name, dic.get(name, notset))) 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. """Delete ``name`` from dict.
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
@@ -306,7 +308,8 @@ class MonkeyPatch:
raise KeyError(name) raise KeyError(name)
else: else:
self._setitem.append((dic, name, dic.get(name, notset))) 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: def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
"""Set environment variable ``name`` to ``value``. """Set environment variable ``name`` to ``value``.
@@ -401,11 +404,13 @@ class MonkeyPatch:
for dictionary, key, value in reversed(self._setitem): for dictionary, key, value in reversed(self._setitem):
if value is notset: if value is notset:
try: 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: except KeyError:
pass # Was already deleted, so we have the desired state. pass # Was already deleted, so we have the desired state.
else: else:
dictionary[key] = value # Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
dictionary[key] = value # type: ignore[index]
self._setitem[:] = [] self._setitem[:] = []
if self._savesyspath is not None: if self._savesyspath is not None:
sys.path[:] = self._savesyspath sys.path[:] = self._savesyspath
+18 -11
View File
@@ -22,6 +22,7 @@ import _pytest._code
from _pytest._code import getfslineno from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback
from _pytest.compat import cached_property from _pytest.compat import cached_property
from _pytest.compat import LEGACY_PATH from _pytest.compat import LEGACY_PATH
from _pytest.config import Config from _pytest.config import Config
@@ -432,8 +433,8 @@ class Node(metaclass=NodeMeta):
assert current is None or isinstance(current, cls) assert current is None or isinstance(current, cls)
return current return current
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None: def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
pass return excinfo.traceback
def _repr_failure_py( def _repr_failure_py(
self, self,
@@ -449,13 +450,13 @@ class Node(metaclass=NodeMeta):
style = "value" style = "value"
if isinstance(excinfo.value, FixtureLookupError): if isinstance(excinfo.value, FixtureLookupError):
return excinfo.value.formatrepr() return excinfo.value.formatrepr()
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]]
if self.config.getoption("fulltrace", False): if self.config.getoption("fulltrace", False):
style = "long" style = "long"
tbfilter = False
else: else:
tb = _pytest._code.Traceback([excinfo.traceback[-1]]) tbfilter = self._traceback_filter
self._prunetraceback(excinfo)
if len(excinfo.traceback) == 0:
excinfo.traceback = tb
if style == "auto": if style == "auto":
style = "long" style = "long"
# XXX should excinfo.getrepr record all data and toterminal() process it? # XXX should excinfo.getrepr record all data and toterminal() process it?
@@ -486,7 +487,7 @@ class Node(metaclass=NodeMeta):
abspath=abspath, abspath=abspath,
showlocals=self.config.getoption("showlocals", False), showlocals=self.config.getoption("showlocals", False),
style=style, style=style,
tbfilter=False, # pruned already, or in --fulltrace mode. tbfilter=tbfilter,
truncate_locals=truncate_locals, 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. * "obj": a Python object that the node wraps.
* "fspath": just a path * "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. # See Item.location.
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None) 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) 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"): if hasattr(self, "path"):
traceback = excinfo.traceback traceback = excinfo.traceback
ntraceback = traceback.cut(path=self.path) ntraceback = traceback.cut(path=self.path)
if ntraceback == traceback: if ntraceback == traceback:
ntraceback = ntraceback.cut(excludepath=tracebackcutdir) 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]: def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
@@ -755,7 +757,7 @@ class Item(Node):
Returns a tuple with three elements: Returns a tuple with three elements:
- The path of the test (default ``self.path``) - 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 ``""``) - A name of the test to be shown (default ``""``)
.. seealso:: :ref:`non-python tests` .. seealso:: :ref:`non-python tests`
@@ -764,6 +766,11 @@ class Item(Node):
@cached_property @cached_property
def location(self) -> Tuple[str, Optional[int], str]: 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() location = self.reportinfo()
path = absolutepath(os.fspath(location[0])) path = absolutepath(os.fspath(location[0]))
relfspath = self.session._node_location_to_relpath(path) 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. The message to show the user as reason for the skip.
:param allow_module_level: :param allow_module_level:
Allows this function to be called at module level, skipping the rest Allows this function to be called at module level.
of the module. Defaults to False. 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: :param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead. 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 __tracebackhide__ = True
if msg is not None: if msg is not None:
if reason: if reason:
from pytest import UsageError from pytest import UsageError
+70 -30
View File
@@ -6,6 +6,7 @@ import itertools
import os import os
import shutil import shutil
import sys import sys
import types
import uuid import uuid
import warnings import warnings
from enum import Enum from enum import Enum
@@ -26,8 +27,11 @@ from typing import Callable
from typing import Dict from typing import Dict
from typing import Iterable from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import List
from typing import Optional from typing import Optional
from typing import Set from typing import Set
from typing import Tuple
from typing import Type
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
@@ -63,21 +67,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
return path.joinpath(".lock") 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. """Handle known read-only errors during rmtree.
The returned value is used only by our own tests. 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). # 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 # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
if isinstance(excvalue, FileNotFoundError): if isinstance(exc, FileNotFoundError):
return False return False
if not isinstance(excvalue, PermissionError): if not isinstance(exc, PermissionError):
warnings.warn( 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 return False
@@ -86,7 +102,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
warnings.warn( warnings.warn(
PytestWarning( PytestWarning(
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format( "(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.""" are read-only."""
path = ensure_extended_length_path(path) path = ensure_extended_length_path(path)
onerror = partial(on_rm_rf_error, start_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]: 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 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( def cleanup_numbered_dir(
root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float
) -> None: ) -> None:
"""Cleanup for lock driven numbered directories.""" """Cleanup for lock driven numbered directories."""
if not root.exists():
return
for path in cleanup_candidates(root, prefix, keep): for path in cleanup_candidates(root, prefix, keep):
try_cleanup(path, consider_lock_dead_if_created_before) try_cleanup(path, consider_lock_dead_if_created_before)
for path in root.glob("garbage-*"): for path in root.glob("garbage-*"):
try_cleanup(path, consider_lock_dead_if_created_before) try_cleanup(path, consider_lock_dead_if_created_before)
cleanup_dead_symlinks(root)
def make_numbered_dir_with_cleanup( def make_numbered_dir_with_cleanup(
root: Path, root: Path,
@@ -357,8 +387,10 @@ def make_numbered_dir_with_cleanup(
for i in range(10): for i in range(10):
try: try:
p = make_numbered_dir(root, prefix, mode) p = make_numbered_dir(root, prefix, mode)
lock_path = create_cleanup_lock(p) # Only lock the current dir when keep is not 0
register_cleanup_lock_removal(lock_path) if keep != 0:
lock_path = create_cleanup_lock(p)
register_cleanup_lock_removal(lock_path)
except Exception as exc: except Exception as exc:
e = exc e = exc
else: else:
@@ -464,14 +496,14 @@ def import_path(
* `mode == ImportMode.prepend`: the directory containing the module (or package, taking * `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 `__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 * `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
to the end of `sys.path`, if not already in `sys.path`. to the end of `sys.path`, if not already in `sys.path`.
* `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib` * `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` to import the module, which avoids having to muck with `sys.path` at all. It effectively
at all. It effectively allows having same-named test modules in different places. allows having same-named test modules in different places.
:param root: :param root:
Used as an anchor when mode == ImportMode.importlib to obtain Used as an anchor when mode == ImportMode.importlib to obtain
@@ -544,8 +576,8 @@ def import_path(
if module_file.endswith((".pyc", ".pyo")): if module_file.endswith((".pyc", ".pyo")):
module_file = module_file[:-1] module_file = module_file[:-1]
if module_file.endswith(os.path.sep + "__init__.py"): if module_file.endswith(os.sep + "__init__.py"):
module_file = module_file[: -(len(os.path.sep + "__init__.py"))] module_file = module_file[: -(len(os.sep + "__init__.py"))]
try: try:
is_same = _is_same(str(path), module_file) is_same = _is_same(str(path), module_file)
@@ -638,30 +670,38 @@ def resolve_package_path(path: Path) -> Optional[Path]:
return result 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( def visit(
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool] path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
) -> Iterator["os.DirEntry[str]"]: ) -> Iterator["os.DirEntry[str]"]:
"""Walk a directory recursively, in breadth-first order. """Walk a directory recursively, in breadth-first order.
The `recurse` predicate determines whether a directory is recursed.
Entries at each directory level are sorted. Entries at each directory level are sorted.
""" """
entries = scandir(path)
# 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)
yield from entries yield from entries
for entry in entries: for entry in entries:
if entry.is_dir() and recurse(entry): if entry.is_dir() and recurse(entry):
yield from visit(entry.path, recurse) yield from visit(entry.path, recurse)
+47 -41
View File
@@ -1,4 +1,5 @@
"""Python test discovery, setup and run of test functions.""" """Python test discovery, setup and run of test functions."""
import dataclasses
import enum import enum
import fnmatch import fnmatch
import inspect import inspect
@@ -27,8 +28,6 @@ from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import attr
import _pytest import _pytest
from _pytest import fixtures from _pytest import fixtures
from _pytest import nodes from _pytest import nodes
@@ -36,6 +35,7 @@ from _pytest._code import filter_traceback
from _pytest._code import getfslineno from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback
from _pytest._io import TerminalWriter from _pytest._io import TerminalWriter
from _pytest._io.saferepr import saferepr from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped from _pytest.compat import ascii_escaped
@@ -57,7 +57,6 @@ from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import INSTANCE_COLLECTOR from _pytest.deprecated import INSTANCE_COLLECTOR
from _pytest.deprecated import NOSE_SUPPORT_METHOD from _pytest.deprecated import NOSE_SUPPORT_METHOD
from _pytest.fixtures import FuncFixtureInfo from _pytest.fixtures import FuncFixtureInfo
@@ -403,8 +402,8 @@ class PyCollector(PyobjMixin, nodes.Collector):
def istestfunction(self, obj: object, name: str) -> bool: def istestfunction(self, obj: object, name: str) -> bool:
if self.funcnamefilter(name) or self.isnosetest(obj): if self.funcnamefilter(name) or self.isnosetest(obj):
if isinstance(obj, staticmethod): if isinstance(obj, (staticmethod, classmethod)):
# staticmethods need to be unwrapped. # staticmethods and classmethods need to be unwrapped.
obj = safe_getattr(obj, "__func__", False) obj = safe_getattr(obj, "__func__", False)
return callable(obj) and fixtures.getfixturemarker(obj) is None return callable(obj) and fixtures.getfixturemarker(obj) is None
else: else:
@@ -668,7 +667,7 @@ class Package(Module):
config=None, config=None,
session=None, session=None,
nodeid=None, nodeid=None,
path=Optional[Path], path: Optional[Path] = None,
) -> None: ) -> None:
# NOTE: Could be just the following, but kept as-is for compat. # NOTE: Could be just the following, but kept as-is for compat.
# nodes.FSCollector.__init__(self, fspath, parent=parent) # nodes.FSCollector.__init__(self, fspath, parent=parent)
@@ -700,14 +699,6 @@ class Package(Module):
func = partial(_call_with_optional_argument, teardown_module, self.obj) func = partial(_call_with_optional_argument, teardown_module, self.obj)
self.addfinalizer(func) 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: def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
if direntry.name == "__pycache__": if direntry.name == "__pycache__":
return False return False
@@ -715,9 +706,6 @@ class Package(Module):
ihook = self.session.gethookproxy(fspath.parent) ihook = self.session.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config): if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True return True
def _collectfile( def _collectfile(
@@ -746,11 +734,11 @@ class Package(Module):
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]: def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.path.parent this_path = self.path.parent
init_module = this_path / "__init__.py"
if init_module.is_file() and path_matches_patterns( # Always collect the __init__ first.
init_module, self.config.getini("python_files") if path_matches_patterns(self.path, self.config.getini("python_files")):
): yield Module.from_parent(self, path=self.path)
yield Module.from_parent(self, path=init_module)
pkg_prefixes: Set[Path] = set() pkg_prefixes: Set[Path] = set()
for direntry in visit(str(this_path), recurse=self._recurse): for direntry in visit(str(this_path), recurse=self._recurse):
path = Path(direntry.path) 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]: 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 """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: for name in names:
meth: Optional[object] = getattr(obj, name, None) meth: Optional[object] = getattr(obj, name, None)
if meth is not None and fixtures.getfixturemarker(meth) is None: if meth is not None and fixtures.getfixturemarker(meth) is None:
@@ -848,7 +837,7 @@ class Class(PyCollector):
other fixtures (#517). other fixtures (#517).
""" """
setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",)) 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: if setup_class is None and teardown_class is None:
return return
@@ -885,12 +874,12 @@ class Class(PyCollector):
emit_nose_setup_warning = True emit_nose_setup_warning = True
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,)) setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
teardown_name = "teardown_method" 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 emit_nose_teardown_warning = False
if teardown_method is None and has_nose: if teardown_method is None and has_nose:
teardown_name = "teardown" teardown_name = "teardown"
emit_nose_teardown_warning = True 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: if setup_method is None and teardown_method is None:
return return
@@ -956,10 +945,20 @@ def hasnew(obj: object) -> bool:
@final @final
@attr.s(frozen=True, auto_attribs=True, slots=True) @dataclasses.dataclass(frozen=True)
class IdMaker: class IdMaker:
"""Make IDs for a parametrization.""" """Make IDs for a parametrization."""
__slots__ = (
"argnames",
"parametersets",
"idfn",
"ids",
"config",
"nodeid",
"func_name",
)
# The argnames of the parametrization. # The argnames of the parametrization.
argnames: Sequence[str] argnames: Sequence[str]
# The ParameterSets of the parametrization. # The ParameterSets of the parametrization.
@@ -1109,7 +1108,7 @@ class IdMaker:
@final @final
@attr.s(frozen=True, slots=True, auto_attribs=True) @dataclasses.dataclass(frozen=True)
class CallSpec2: class CallSpec2:
"""A planned parameterized invocation of a test function. """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 # arg name -> arg value which will be passed to the parametrized test
# function (direct parameterization). # 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 # arg name -> arg value which will be passed to a fixture of the same name
# (indirect parametrization). # (indirect parametrization).
params: Dict[str, object] = attr.Factory(dict) params: Dict[str, object] = dataclasses.field(default_factory=dict)
# arg name -> arg index. # 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. # 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 "-". # 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 which will be applied to the item.
marks: List[Mark] = attr.Factory(list) marks: List[Mark] = dataclasses.field(default_factory=list)
def setmulti( def setmulti(
self, self,
@@ -1163,9 +1162,9 @@ class CallSpec2:
return CallSpec2( return CallSpec2(
funcargs=funcargs, funcargs=funcargs,
params=params, params=params,
arg2scope=arg2scope,
indices=indices, indices=indices,
idlist=[*self._idlist, id], _arg2scope=arg2scope,
_idlist=[*self._idlist, id],
marks=[*self.marks, *normalize_mark_list(marks)], marks=[*self.marks, *normalize_mark_list(marks)],
) )
@@ -1791,7 +1790,7 @@ class Function(PyobjMixin, nodes.Item):
def setup(self) -> None: def setup(self) -> None:
self._request._fillfixtures() 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): if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
code = _pytest._code.Code.from_function(get_real_func(self.obj)) code = _pytest._code.Code.from_function(get_real_func(self.obj))
path, firstlineno = code.path, code.firstlineno path, firstlineno = code.path, code.firstlineno
@@ -1803,14 +1802,21 @@ class Function(PyobjMixin, nodes.Item):
ntraceback = ntraceback.filter(filter_traceback) ntraceback = ntraceback.filter(filter_traceback)
if not ntraceback: if not ntraceback:
ntraceback = traceback ntraceback = traceback
ntraceback = ntraceback.filter(excinfo)
excinfo.traceback = ntraceback.filter()
# issue364: mark all but first and last frames to # issue364: mark all but first and last frames to
# only show a single-line message for each frame. # only show a single-line message for each frame.
if self.config.getoption("tbstyle", "auto") == "auto": if self.config.getoption("tbstyle", "auto") == "auto":
if len(excinfo.traceback) > 2: if len(ntraceback) > 2:
for entry in excinfo.traceback[1:-1]: ntraceback = Traceback(
entry.set_repr_style("short") 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. # TODO: Type ignored -- breaks Liskov Substitution.
def repr_failure( # type: ignore[override] 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 Any
from typing import Callable from typing import Callable
from typing import cast from typing import cast
from typing import Generic from typing import ContextManager
from typing import List from typing import List
from typing import Mapping from typing import Mapping
from typing import Optional from typing import Optional
@@ -269,10 +269,16 @@ class ApproxMapping(ApproxBase):
max_abs_diff = max( max_abs_diff = max(
max_abs_diff, abs(approx_value.expected - other_value) max_abs_diff, abs(approx_value.expected - other_value)
) )
max_rel_diff = max( if approx_value.expected == 0.0:
max_rel_diff, max_rel_diff = math.inf
abs((approx_value.expected - other_value) / approx_value.expected), else:
) max_rel_diff = max(
max_rel_diff,
abs(
(approx_value.expected - other_value)
/ approx_value.expected
),
)
different_ids.append(approx_key) different_ids.append(approx_key)
message_data = [ message_data = [
@@ -801,8 +807,8 @@ def raises( # noqa: F811
r"""Assert that a code block/function call raises an exception. r"""Assert that a code block/function call raises an exception.
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception: :param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
The excpected exception type, or a tuple if one of multiple possible The expected exception type, or a tuple if one of multiple possible
exception types are excepted. exception types are expected.
:kwparam str | typing.Pattern[str] | None match: :kwparam str | typing.Pattern[str] | None match:
If specified, a string containing a regular expression, If specified, a string containing a regular expression,
or a regular expression object, that is tested against the string 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'." f"any special code to say 'this should never raise an exception'."
) )
if isinstance(expected_exception, type): if isinstance(expected_exception, type):
excepted_exceptions: Tuple[Type[E], ...] = (expected_exception,) expected_exceptions: Tuple[Type[E], ...] = (expected_exception,)
else: else:
excepted_exceptions = expected_exception expected_exceptions = expected_exception
for exc in excepted_exceptions: for exc in expected_exceptions:
if not isinstance(exc, type) or not issubclass(exc, BaseException): if not isinstance(exc, type) or not issubclass(exc, BaseException):
msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable] msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__ not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
@@ -944,11 +950,7 @@ def raises( # noqa: F811
try: try:
func(*args[1:], **kwargs) func(*args[1:], **kwargs)
except expected_exception as e: except expected_exception as e:
# We just caught the exception - there is a traceback. return _pytest._code.ExceptionInfo.from_exception(e)
assert e.__traceback__ is not None
return _pytest._code.ExceptionInfo.from_exc_info(
(type(e), e, e.__traceback__)
)
fail(message) fail(message)
@@ -957,7 +959,7 @@ raises.Exception = fail.Exception # type: ignore
@final @final
class RaisesContext(Generic[E]): class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
def __init__( def __init__(
self, self,
expected_exception: Union[Type[E], Tuple[Type[E], ...]], expected_exception: Union[Type[E], Tuple[Type[E], ...]],
+28 -9
View File
@@ -1,3 +1,4 @@
import dataclasses
import os import os
from io import StringIO from io import StringIO
from pprint import pprint from pprint import pprint
@@ -16,8 +17,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import attr
from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ExceptionRepr from _pytest._code.code import ExceptionRepr
@@ -263,6 +262,8 @@ class TestReport(BaseReport):
when: "Literal['setup', 'call', 'teardown']", when: "Literal['setup', 'call', 'teardown']",
sections: Iterable[Tuple[str, str]] = (), sections: Iterable[Tuple[str, str]] = (),
duration: float = 0, duration: float = 0,
start: float = 0,
stop: float = 0,
user_properties: Optional[Iterable[Tuple[str, object]]] = None, user_properties: Optional[Iterable[Tuple[str, object]]] = None,
**extra, **extra,
) -> None: ) -> None:
@@ -272,6 +273,8 @@ class TestReport(BaseReport):
#: A (filesystempath, lineno, domaininfo) tuple indicating the #: A (filesystempath, lineno, domaininfo) tuple indicating the
#: actual location of a test item - it might be different from 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. #: collected one e.g. if a method is inherited from a different module.
#: The filesystempath may be relative to ``config.rootdir``.
#: The line number is 0-based.
self.location: Tuple[str, Optional[int], str] = location self.location: Tuple[str, Optional[int], str] = location
#: A name -> value dictionary containing all keywords and #: A name -> value dictionary containing all keywords and
@@ -300,6 +303,11 @@ class TestReport(BaseReport):
#: Time it took to run just the test. #: Time it took to run just the test.
self.duration: float = duration 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) self.__dict__.update(extra)
def __repr__(self) -> str: def __repr__(self) -> str:
@@ -318,6 +326,8 @@ class TestReport(BaseReport):
# Remove "collect" from the Literal type -- only for collection calls. # Remove "collect" from the Literal type -- only for collection calls.
assert when != "collect" assert when != "collect"
duration = call.duration duration = call.duration
start = call.start
stop = call.stop
keywords = {x: 1 for x in item.keywords} keywords = {x: 1 for x in item.keywords}
excinfo = call.excinfo excinfo = call.excinfo
sections = [] sections = []
@@ -337,6 +347,9 @@ class TestReport(BaseReport):
elif isinstance(excinfo.value, skip.Exception): elif isinstance(excinfo.value, skip.Exception):
outcome = "skipped" outcome = "skipped"
r = excinfo._getreprcrash() 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: if excinfo.value._use_item_location:
path, line = item.reportinfo()[:2] path, line = item.reportinfo()[:2]
assert line is not None assert line is not None
@@ -362,6 +375,8 @@ class TestReport(BaseReport):
when, when,
sections, sections,
duration, duration,
start,
stop,
user_properties=item.user_properties, user_properties=item.user_properties,
) )
@@ -407,7 +422,9 @@ class CollectReport(BaseReport):
self.__dict__.update(extra) self.__dict__.update(extra)
@property @property
def location(self): def location( # type:ignore[override]
self,
) -> Optional[Tuple[str, Optional[int], str]]:
return (self.fspath, None, self.fspath) return (self.fspath, None, self.fspath)
def __repr__(self) -> str: def __repr__(self) -> str:
@@ -459,15 +476,15 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
def serialize_repr_entry( def serialize_repr_entry(
entry: Union[ReprEntry, ReprEntryNative] entry: Union[ReprEntry, ReprEntryNative]
) -> Dict[str, Any]: ) -> Dict[str, Any]:
data = attr.asdict(entry) data = dataclasses.asdict(entry)
for key, value in data.items(): for key, value in data.items():
if hasattr(value, "__dict__"): if hasattr(value, "__dict__"):
data[key] = attr.asdict(value) data[key] = dataclasses.asdict(value)
entry_data = {"type": type(entry).__name__, "data": data} entry_data = {"type": type(entry).__name__, "data": data}
return entry_data return entry_data
def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]: def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
result = attr.asdict(reprtraceback) result = dataclasses.asdict(reprtraceback)
result["reprentries"] = [ result["reprentries"] = [
serialize_repr_entry(x) for x in reprtraceback.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], reprcrash: Optional[ReprFileLocation],
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
if reprcrash is not None: if reprcrash is not None:
return attr.asdict(reprcrash) return dataclasses.asdict(reprcrash)
else: else:
return None return None
@@ -573,7 +590,6 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
and "reprcrash" in reportdict["longrepr"] and "reprcrash" in reportdict["longrepr"]
and "reprtraceback" in reportdict["longrepr"] and "reprtraceback" in reportdict["longrepr"]
): ):
reprtraceback = deserialize_repr_traceback( reprtraceback = deserialize_repr_traceback(
reportdict["longrepr"]["reprtraceback"] reportdict["longrepr"]["reprtraceback"]
) )
@@ -594,7 +610,10 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
ExceptionChainRepr, ReprExceptionInfo ExceptionChainRepr, ReprExceptionInfo
] = ExceptionChainRepr(chain) ] = ExceptionChainRepr(chain)
else: else:
exception_info = ReprExceptionInfo(reprtraceback, reprcrash) exception_info = ReprExceptionInfo(
reprtraceback=reprtraceback,
reprcrash=reprcrash,
)
for section in reportdict["longrepr"]["sections"]: for section in reportdict["longrepr"]["sections"]:
exception_info.addsection(*section) exception_info.addsection(*section)
+19 -10
View File
@@ -1,5 +1,6 @@
"""Basic collect and runtest protocol implementations.""" """Basic collect and runtest protocol implementations."""
import bdb import bdb
import dataclasses
import os import os
import sys import sys
from typing import Callable from typing import Callable
@@ -14,8 +15,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
import attr
from .reports import BaseReport from .reports import BaseReport
from .reports import CollectErrorRepr from .reports import CollectErrorRepr
from .reports import CollectReport from .reports import CollectReport
@@ -35,6 +34,9 @@ from _pytest.outcomes import OutcomeException
from _pytest.outcomes import Skipped from _pytest.outcomes import Skipped
from _pytest.outcomes import TEST_OUTCOME from _pytest.outcomes import TEST_OUTCOME
if sys.version_info[:2] < (3, 11):
from exceptiongroup import BaseExceptionGroup
if TYPE_CHECKING: if TYPE_CHECKING:
from typing_extensions import Literal from typing_extensions import Literal
@@ -265,7 +267,7 @@ TResult = TypeVar("TResult", covariant=True)
@final @final
@attr.s(repr=False, init=False, auto_attribs=True) @dataclasses.dataclass
class CallInfo(Generic[TResult]): class CallInfo(Generic[TResult]):
"""Result/Exception info of a function invocation.""" """Result/Exception info of a function invocation."""
@@ -512,22 +514,29 @@ class SetupState:
stack is torn down. stack is torn down.
""" """
needed_collectors = nextitem and nextitem.listchain() or [] needed_collectors = nextitem and nextitem.listchain() or []
exc = None exceptions: List[BaseException] = []
while self.stack: while self.stack:
if list(self.stack.keys()) == needed_collectors[: len(self.stack)]: if list(self.stack.keys()) == needed_collectors[: len(self.stack)]:
break break
node, (finalizers, _) = self.stack.popitem() node, (finalizers, _) = self.stack.popitem()
these_exceptions = []
while finalizers: while finalizers:
fin = finalizers.pop() fin = finalizers.pop()
try: try:
fin() fin()
except TEST_OUTCOME as e: except TEST_OUTCOME as e:
# XXX Only first exception will be seen by user, these_exceptions.append(e)
# ideally all should be reported.
if exc is None: if len(these_exceptions) == 1:
exc = e exceptions.extend(these_exceptions)
if exc: elif these_exceptions:
raise exc 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: if nextitem is None:
assert not self.stack assert not self.stack
+5 -4
View File
@@ -1,4 +1,5 @@
"""Support for skip/xfail functions and markers.""" """Support for skip/xfail functions and markers."""
import dataclasses
import os import os
import platform import platform
import sys import sys
@@ -9,8 +10,6 @@ from typing import Optional
from typing import Tuple from typing import Tuple
from typing import Type from typing import Type
import attr
from _pytest.config import Config from _pytest.config import Config
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
@@ -157,7 +156,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
return result, reason return result, reason
@attr.s(slots=True, frozen=True, auto_attribs=True) @dataclasses.dataclass(frozen=True)
class Skip: class Skip:
"""The result of evaluate_skip_marks().""" """The result of evaluate_skip_marks()."""
@@ -192,10 +191,12 @@ def evaluate_skip_marks(item: Item) -> Optional[Skip]:
return None return None
@attr.s(slots=True, frozen=True, auto_attribs=True) @dataclasses.dataclass(frozen=True)
class Xfail: class Xfail:
"""The result of evaluate_xfail_marks().""" """The result of evaluate_xfail_marks()."""
__slots__ = ("reason", "run", "strict", "raises")
reason: str reason: str
run: bool run: bool
strict: bool strict: bool
+8
View File
@@ -48,6 +48,10 @@ def pytest_configure(config: Config) -> None:
def pytest_sessionfinish(session: Session) -> None: def pytest_sessionfinish(session: Session) -> None:
if not session.config.getoption("stepwise"): if not session.config.getoption("stepwise"):
assert session.config.cache is not None 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. # Clear the list of failing tests if the plugin is not active.
session.config.cache.set(STEPWISE_CACHE_DIR, []) session.config.cache.set(STEPWISE_CACHE_DIR, [])
@@ -119,4 +123,8 @@ class StepwisePlugin:
return None return None
def pytest_sessionfinish(self) -> 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) 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. This is a good source for looking at the various reporting hooks.
""" """
import argparse import argparse
import dataclasses
import datetime import datetime
import inspect import inspect
import platform import platform
import sys import sys
import textwrap
import warnings import warnings
from collections import Counter from collections import Counter
from functools import partial from functools import partial
@@ -19,6 +21,7 @@ from typing import Dict
from typing import Generator from typing import Generator
from typing import List from typing import List
from typing import Mapping from typing import Mapping
from typing import NamedTuple
from typing import Optional from typing import Optional
from typing import Sequence from typing import Sequence
from typing import Set from typing import Set
@@ -27,7 +30,6 @@ from typing import Tuple
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from typing import Union from typing import Union
import attr
import pluggy import pluggy
import _pytest._version import _pytest._version
@@ -111,6 +113,26 @@ class MoreQuietAction(argparse.Action):
namespace.quiet = getattr(namespace, "quiet", 0) + 1 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: def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("terminal reporting", "Reporting", after="general") group = parser.getgroup("terminal reporting", "Reporting", after="general")
group._addoption( group._addoption(
@@ -229,7 +251,8 @@ def pytest_addoption(parser: Parser) -> None:
parser.addini( parser.addini(
"console_output_style", "console_output_style",
help='Console output: "classic", or with additional progress information ' 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", default="progress",
) )
@@ -287,7 +310,7 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
return outcome, letter, outcome.upper() return outcome, letter, outcome.upper()
@attr.s(auto_attribs=True) @dataclasses.dataclass
class WarningReport: class WarningReport:
"""Simple structure to hold warnings information captured by ``pytest_warning_recorded``. """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]": def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
"""Return whether we should display progress information based on the current config.""" """Return whether we should display progress information based on the current config."""
# do not show progress if we are not capturing output (#3038) # do not show progress if we are not capturing output (#3038) unless explicitly
if self.config.getoption("capture", "no") == "no": # 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 return False
# do not show progress if we are showing fixture setup/teardown # do not show progress if we are showing fixture setup/teardown
if self.config.getoption("setupshow", False): if self.config.getoption("setupshow", False):
return False return False
cfg: str = self.config.getini("console_output_style") cfg: str = self.config.getini("console_output_style")
if cfg == "progress": if cfg == "progress" or cfg == "progress-even-when-capture-no":
return "progress" return "progress"
elif cfg == "count": elif cfg == "count":
return "count" return "count"
@@ -420,6 +448,28 @@ class TerminalReporter:
self._tw.line() self._tw.line()
self.currentfspath = None 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: def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
self._tw.write(content, flush=flush, **markup) self._tw.write(content, flush=flush, **markup)
@@ -519,10 +569,11 @@ class TerminalReporter:
def pytest_runtest_logreport(self, report: TestReport) -> None: def pytest_runtest_logreport(self, report: TestReport) -> None:
self._tests_ran = True self._tests_ran = True
rep = report rep = report
res: Tuple[
str, str, Union[str, Tuple[str, Mapping[str, bool]]] res = TestShortLogReport(
] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) *self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
category, letter, word = res )
category, letter, word = res.category, res.letter, res.word
if not isinstance(word, tuple): if not isinstance(word, tuple):
markup = None markup = None
else: else:
@@ -566,7 +617,7 @@ class TerminalReporter:
formatted_reason = f" ({reason})" formatted_reason = f" ({reason})"
if reason and formatted_reason is not None: if reason and formatted_reason is not None:
self._tw.write(formatted_reason) self.wrap_write(formatted_reason)
if self._show_progress_info: if self._show_progress_info:
self._write_progress_information_filling_space() self._write_progress_information_filling_space()
else: else:
@@ -733,16 +784,14 @@ class TerminalReporter:
self.write_line(line) self.write_line(line)
def pytest_report_header(self, config: Config) -> List[str]: def pytest_report_header(self, config: Config) -> List[str]:
line = "rootdir: %s" % config.rootpath result = [f"rootdir: {config.rootpath}"]
if config.inipath: 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: if config.args_source == Config.ArgsSource.TESTPATHS:
testpaths: List[str] = config.getini("testpaths") testpaths: List[str] = config.getini("testpaths")
line += ", testpaths: {}".format(", ".join(testpaths)) result.append("testpaths: {}".format(", ".join(testpaths)))
result = [line]
plugininfo = config.pluginmanager.list_plugin_distinfo() plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo: if plugininfo:
+132 -24
View File
@@ -1,40 +1,66 @@
"""Support for providing temporary directories to test functions.""" """Support for providing temporary directories to test functions."""
import dataclasses
import os import os
import re import re
import sys
import tempfile import tempfile
from pathlib import Path 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 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 LOCK_TIMEOUT
from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import rm_rf 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 Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.monkeypatch import MonkeyPatch from _pytest.monkeypatch import MonkeyPatch
tmppath_result_key = StashKey[Dict[str, bool]]()
@final @final
@attr.s(init=False) @dataclasses.dataclass
class TempPathFactory: class TempPathFactory:
"""Factory for temporary directories under the common base temp directory. """Factory for temporary directories under the common base temp directory.
The base directory can be configured using the ``--basetemp`` option. The base directory can be configured using the ``--basetemp`` option.
""" """
_given_basetemp = attr.ib(type=Optional[Path]) _given_basetemp: Optional[Path]
_trace = attr.ib() # pluggy TagTracerSub, not currently exposed, so Any.
_basetemp = attr.ib(type=Optional[Path]) _trace: Any
_basetemp: Optional[Path]
_retention_count: int
_retention_policy: "RetentionType"
def __init__( def __init__(
self, self,
given_basetemp: Optional[Path], given_basetemp: Optional[Path],
retention_count: int,
retention_policy: "RetentionType",
trace, trace,
basetemp: Optional[Path] = None, basetemp: Optional[Path] = None,
*, *,
@@ -49,6 +75,8 @@ class TempPathFactory:
# Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012). # 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._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
self._trace = trace self._trace = trace
self._retention_count = retention_count
self._retention_policy = retention_policy
self._basetemp = basetemp self._basetemp = basetemp
@classmethod @classmethod
@@ -63,9 +91,23 @@ class TempPathFactory:
:meta private: :meta private:
""" """
check_ispytest(_ispytest) 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( return cls(
given_basetemp=config.option.basetemp, given_basetemp=config.option.basetemp,
trace=config.trace.get("tmpdir"), trace=config.trace.get("tmpdir"),
retention_count=count,
retention_policy=policy,
_ispytest=True, _ispytest=True,
) )
@@ -133,23 +175,23 @@ class TempPathFactory:
# Also, to keep things private, fixup any world-readable temp # Also, to keep things private, fixup any world-readable temp
# rootdir's permissions. Historically 0o755 was used, so we can't # rootdir's permissions. Historically 0o755 was used, so we can't
# just error out on this, at least for a while. # just error out on this, at least for a while.
if sys.platform != "win32": uid = get_user_id()
uid = os.getuid() if uid is not None:
rootdir_stat = rootdir.stat() rootdir_stat = rootdir.stat()
# getuid shouldn't fail, but cpython defines such a case. if rootdir_stat.st_uid != uid:
# Let's hope for the best. raise OSError(
if uid != -1: f"The temporary directory {rootdir} is not owned by the current user. "
if rootdir_stat.st_uid != uid: "Fix this and try again."
raise OSError( )
f"The temporary directory {rootdir} is not owned by the current user. " if (rootdir_stat.st_mode & 0o077) != 0:
"Fix this and try again." os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
) keep = self._retention_count
if (rootdir_stat.st_mode & 0o077) != 0: if self._retention_policy == "none":
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077) keep = 0
basetemp = make_numbered_dir_with_cleanup( basetemp = make_numbered_dir_with_cleanup(
prefix="pytest-", prefix="pytest-",
root=rootdir, root=rootdir,
keep=3, keep=keep,
lock_timeout=LOCK_TIMEOUT, lock_timeout=LOCK_TIMEOUT,
mode=0o700, mode=0o700,
) )
@@ -184,6 +226,21 @@ def pytest_configure(config: Config) -> None:
mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False) 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") @fixture(scope="session")
def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
"""Return a :class:`pytest.TempPathFactory` instance for the test session.""" """Return a :class:`pytest.TempPathFactory` instance for the test session."""
@@ -200,17 +257,68 @@ def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
@fixture @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 """Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary function invocation, created as a sub directory of the base temporary
directory. directory.
By default, a new base temporary directory is created each test session, 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 and old bases are removed after 3 sessions, to aid in debugging.
``--basetemp`` is used then it is cleared each session. See :ref:`base 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`. temporary directory`.
The returned object is a :class:`pathlib.Path` object. 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: def stopTest(self, testcase: "unittest.TestCase") -> None:
pass pass
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
pass
def runtest(self) -> None: def runtest(self) -> None:
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
@@ -331,15 +334,16 @@ class TestCaseFunction(Function):
finally: finally:
delattr(self._testcase, self.name) delattr(self._testcase, self.name)
def _prunetraceback( def _traceback_filter(
self, excinfo: _pytest._code.ExceptionInfo[BaseException] self, excinfo: _pytest._code.ExceptionInfo[BaseException]
) -> None: ) -> _pytest._code.Traceback:
super()._prunetraceback(excinfo) traceback = super()._traceback_filter(excinfo)
traceback = excinfo.traceback.filter( ntraceback = traceback.filter(
lambda x: not x.frame.f_globals.get("__unittest") lambda x: not x.frame.f_globals.get("__unittest"),
) )
if traceback: if not ntraceback:
excinfo.traceback = traceback ntraceback = traceback
return ntraceback
@hookimpl(tryfirst=True) @hookimpl(tryfirst=True)
+3 -4
View File
@@ -1,3 +1,4 @@
import dataclasses
import inspect import inspect
import warnings import warnings
from types import FunctionType from types import FunctionType
@@ -6,8 +7,6 @@ from typing import Generic
from typing import Type from typing import Type
from typing import TypeVar from typing import TypeVar
import attr
from _pytest.compat import final from _pytest.compat import final
@@ -130,7 +129,7 @@ _W = TypeVar("_W", bound=PytestWarning)
@final @final
@attr.s(auto_attribs=True) @dataclasses.dataclass
class UnformattedWarning(Generic[_W]): class UnformattedWarning(Generic[_W]):
"""A warning meant to be formatted during runtime. """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` 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) (like hook wrappers being marked in a legacy mechanism)
""" """
lineno = method.__code__.co_firstlineno lineno = method.__code__.co_firstlineno
+2
View File
@@ -62,6 +62,7 @@ from _pytest.reports import TestReport
from _pytest.runner import CallInfo from _pytest.runner import CallInfo
from _pytest.stash import Stash from _pytest.stash import Stash
from _pytest.stash import StashKey from _pytest.stash import StashKey
from _pytest.terminal import TestShortLogReport
from _pytest.tmpdir import TempPathFactory from _pytest.tmpdir import TempPathFactory
from _pytest.warning_types import PytestAssertRewriteWarning from _pytest.warning_types import PytestAssertRewriteWarning
from _pytest.warning_types import PytestCacheWarning from _pytest.warning_types import PytestCacheWarning
@@ -152,6 +153,7 @@ __all__ = [
"TempPathFactory", "TempPathFactory",
"Testdir", "Testdir",
"TestReport", "TestReport",
"TestShortLogReport",
"UsageError", "UsageError",
"WarningsRecorder", "WarningsRecorder",
"warns", "warns",
+3 -3
View File
@@ -803,7 +803,7 @@ class TestLocalPath(CommonFSTests):
# depending on how the paths are used), but > 4096 (which is the # depending on how the paths are used), but > 4096 (which is the
# Linux' limitation) - the behaviour of paths with names > 4096 chars # Linux' limitation) - the behaviour of paths with names > 4096 chars
# is undetermined # is undetermined
newfilename = "/test" * 60 newfilename = "/test" * 60 # type:ignore[unreachable]
l1 = tmpdir.join(newfilename) l1 = tmpdir.join(newfilename)
l1.ensure(file=True) l1.ensure(file=True)
l1.write("foo") l1.write("foo")
@@ -1344,8 +1344,8 @@ class TestPOSIXLocalPath:
assert realpath.basename == "file" assert realpath.basename == "file"
def test_owner(self, path1, tmpdir): def test_owner(self, path1, tmpdir):
from pwd import getpwuid from pwd import getpwuid # type:ignore[attr-defined]
from grp import getgrgid from grp import getgrgid # type:ignore[attr-defined]
stat = path1.stat() stat = path1.stat()
assert stat.path == path1 assert stat.path == path1

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