Compare commits

...

206 Commits
7.3.0 ... 7.4.3

Author SHA1 Message Date
Bruno Oliveira
2390610696 Tweak changelog.rst 2023-10-24 15:45:08 -03:00
pytest bot
a0714aa007 Prepare release version 7.4.3 2023-10-24 18:43:16 +00:00
github-actions[bot]
44ad1c9811 [7.4.x] fix #10447 - consider marks in reverse mro order to give base classes priority (#11545)
Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
2023-10-24 15:04:13 +00:00
github-actions[bot]
5dc77253d4 [7.4.x] Ensure logging tests always cleanup after themselves (#11541)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-10-23 14:28:04 +00:00
github-actions[bot]
a517827318 [7.4.x] Configure ReadTheDocs to fail on warnings (#11540)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-10-23 13:23:18 +00:00
github-actions[bot]
21fe071d79 [7.4.x] fix for ValueError raised in faulthandler teardown code (#11455)
Co-authored-by: Simon Blanchard <bnomis@gmail.com>
2023-09-20 12:41:01 +00:00
Bruno Oliveira
f8bb8572fe Force terminal width when running tests (#11425) (#11432)
Related to #11423

(cherry picked from commit 241f2a890e)
2023-09-11 09:48:22 -03:00
github-actions[bot]
1944dc06d3 [7.4.x] Fix --import-mode=importlib when root contains __init__.py file (#11426)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-09-10 13:27:53 +00:00
Bruno Oliveira
946634c84c Merge pull request #11419 from nicoddemus/backport-11414-to-7.4.x
[7.4.x] Fix assert rewriting with assignment expressions (#11414)
2023-09-09 10:08:41 -03:00
github-actions[bot]
d849a3ed64 [7.4.x] fix: closes #11343's [attr-defined] type errors (#11421)
Co-authored-by: Warren Markham <rabbitsinwarrens@gmail.com>
2023-09-09 13:02:31 +00:00
Bruno Oliveira
721a0881fb Skip test_assertion_walrus_different_test_cases on Python 3.7 2023-09-09 09:43:15 -03:00
Marc Mueller
5341b9cd67 Fix assert rewriting with assignment expressions (#11414)
Fixes #11239

(cherry picked from commit 7259e8db98)
2023-09-09 09:13:10 -03:00
Bruno Oliveira
c39bdf6190 Adjustments to the release process (#11410) (#11415)
As discussed in #11408:

* Improve documentation for the release process.
* Fix the description for the PRs created by the `prepare release pr` workflow.
* Fix pushing tag in the `deploy` workflow.

(cherry picked from commit e5c81fa41a)
2023-09-08 08:42:55 -03:00
Bruno Oliveira
b0c4775a28 Merge pull request #11408 from pytest-dev/release-7.4.2
Prepare release 7.4.2
2023-09-07 15:47:56 -03:00
pytest bot
45f34dfb8d Prepare release version 7.4.2 2023-09-07 17:21:49 +00:00
Bruno Oliveira
e4f022f0d8 Merge pull request #11406 from nicoddemus/backport-11404-to-7.4.x
[7.4.x] Fix crash when passing a very long cmdline argument (#11404)
2023-09-07 14:14:40 -03:00
Bruno Oliveira
63b0c6f75f Use _pytest.pathlib.safe_exists in get_dirs_from_args
Related to #11394
2023-09-07 13:50:02 -03:00
Bruno Oliveira
884b911a9c Fix crash when passing a very long cmdline argument (#11404)
Fixes #11394

(cherry picked from commit 28ccf476b9)
2023-09-07 12:54:41 -03:00
github-actions[bot]
6e49a74089 [7.4.x] Fix doctest collection of functools.cached_property objects. (#11403)
Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
2023-09-07 13:33:12 +00:00
github-actions[bot]
79c2012d40 [7.4.x] doc: Remove done training (#11400)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2023-09-06 13:50:00 +00:00
github-actions[bot]
de69883e3a [7.4.x] improve plugin list disclaimer (#11398)
Co-authored-by: Stefaan Lippens <soxofaan@users.noreply.github.com>
2023-09-06 11:02:29 +00:00
github-actions[bot]
1de00e9830 [7.4.x] Fix import_path for packages (#11395)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-09-05 23:07:48 +00:00
Bruno Oliveira
7f5d9b9df4 Fix user_properties not saved to XML if fixture errors during teardown (#11382)
Move handling of user_properties to `finalize()`.

Previously if a fixture failed during teardown, `pytest_runtest_logreport` would not be called with "teardown", resulting in the user properties not being saved on the JUnit XML file.

Fixes: #11367
(cherry picked from commit 917ce9aa01)

Co-authored-by: Israel Fruchter <israel.fruchter@gmail.com>
2023-09-03 15:01:56 -03:00
Bruno Oliveira
82eb86f707 Merge pull request #11377 from pytest-dev/release-7.4.1
Prepare release 7.4.1
2023-09-02 12:41:32 -03:00
Bruno Oliveira
0319a0d4fd Checkout source code during deploy
We need the checked out repository in order to push the tag.
2023-09-02 12:38:39 -03:00
Bruno Oliveira
7855a72d2c Improve CI workflow
* Build the package only once, and test on all platforms.
* Deploy is now triggered manually via an Action, which is then responsible for tagging the repository after the package has been uploaded successfully.
* Drop 'docs': we nowadays rely on readthedocs preview PR builds.
2023-09-02 08:46:22 -03:00
pytest bot
7a0a0e8b08 Prepare release version 7.4.1 2023-09-02 11:03:06 +00:00
github-actions[bot]
fbcfd3a52e [7.4.x] Update CONTRIBUTING.rst (#11371)
Co-authored-by: Sourabh Beniwal <sourabhbeniwal@outlook.com>
2023-08-30 08:57:49 -03:00
github-actions[bot]
b170081788 [7.4.x] Issue 11354 fixing docs for lfnf (#11364)
Co-authored-by: Sean Patrick Malloy <spmalloy@ucdavis.edu>
2023-08-29 00:40:49 +00:00
Ran Benita
7a5f2feefb [7.4.x] Fixes for typed pluggy (#11355)
Since version 1.3 pluggy added typing, which requires some fixes to
please mypy.
2023-08-26 22:15:32 +00:00
github-actions[bot]
69140717d4 [7.4.x] Improve duplicate values documentation (#11296)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-08-08 14:44:03 +03:00
Bruno Oliveira
5c7c3f6329 Merge pull request #11294 from The-Compiler/pluggy-py38
ci: Use Python 3.8 to test latest pluggy
2023-08-07 08:13:03 -03:00
Florian Bruhin
ba40975bb7 ci: Use Python 3.8 to test latest pluggy
Pluggy dropped Python 3.7 support.
Also see 165fbbd12a

Fixes #11293
2023-08-07 12:08:56 +02:00
github-actions[bot]
e3fe7286f8 [7.4.x] doc: Link pytest.main to how-to guide (#11290)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2023-08-07 10:47:37 +02:00
github-actions[bot]
34c73944e1 [7.4.x] doc: update information about assertion messages (#11286)
Co-authored-by: Christoph Anton Mitterer <mail@christoph.anton.mitterer.name>
2023-08-07 10:47:12 +02:00
github-actions[bot]
350122abb2 [7.4.x] Remove ep2023 training (#11242)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2023-07-22 18:40:56 +00:00
github-actions[bot]
06ff7ca13b [7.4.x] Clarify docs for pytest.main default behavior (#11188)
Co-authored-by: antosikv <79337398+antosikv@users.noreply.github.com>
2023-07-09 15:54:59 +00:00
github-actions[bot]
6dfe498c77 [7.4.x] doc: fix EncodingWarnings in examples (#11182)
Co-authored-by: Ran Benita <ran@unusedvar.com>
2023-07-08 19:17:21 +00:00
github-actions[bot]
a566b78730 [7.4.x] reference: improve the node types docs a bit (#11181)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-07-08 19:07:55 +00:00
github-actions[bot]
511adf85be [7.4.x] Fix error assertion handling in approx when None in dict comparison (#11180)
Co-authored-by: Zac Hatfield-Dodds <zac.hatfield.dodds@gmail.com>
2023-07-08 18:37:35 +00:00
github-actions[bot]
c71b5df734 [7.4.x] Add child modules as attributes of parent modules. (#11163)
* [7.4.x] Add child modules as attributes of parent modules.

* Update 10337.bugfix.rst

---------

Co-authored-by: akhilramkee <31619526+akhilramkee@users.noreply.github.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-07-08 15:11:26 -03:00
github-actions[bot]
d53951836d [7.4.x] Update open trainings (#11172)
Co-authored-by: Florian Bruhin <me@the-compiler.org>
2023-07-04 21:49:24 +00:00
github-actions[bot]
a4d7254d18 [7.4.x] Fix duplicated imports with importlib mode and doctest-modules (#11164)
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-07-03 16:33:47 +00:00
Bruno Oliveira
b6c55787fe Switch to deploy environment and configure for pypi oidc (#10925) (#11162)
Closes #10871
Closes #10870

Co-authored-by: Ronny Pfannschmidt <opensource@ronnypfannschmidt.de>
2023-07-03 16:17:02 +00:00
Ran Benita
fb03d1388b Merge pull request #11131 from pytest-dev/release-7.4.0
Prepare release 7.4.0
2023-06-23 14:18:43 +03:00
pytest bot
d9bf9dbec1 Prepare release version 7.4.0
[ran: made some fixups]
2023-06-23 14:03:58 +03:00
Ran Benita
64319dbc01 Merge pull request #11128 from bluetech/pythonpath-note
reference: add note that `pythonpath` does not affect `-p`
2023-06-22 18:12:37 +03:00
Ran Benita
1e8135df16 reference: add note that pythonpath does not affect -p
Fix #11118.
2023-06-22 15:45:20 +03:00
Zac Hatfield-Dodds
1e32a4b570 Merge pull request #10935 from nondescryptid/10328 2023-06-21 11:04:10 -07:00
Ran Benita
faa1f9d2ad Merge pull request #11125 from bluetech/initial-conftests-testpaths
config: fix the paths considered for initial conftest discovery
2023-06-21 09:21:27 +03:00
Ran Benita
14890329dc config: fix the paths considered for initial conftest discovery
Fixes #11104.

See the issue for a description of the problem.

Now, we use the same logic for initial conftest paths as we do for
deciding the initial args, which was the idea behind checking
`namespace.file_or_dir` and `testpaths` previously.

This fixes the issue of `testpaths` being considered for initial
conftests even when it's not used for the args.

(Another issue in faeb16146b was that the
`testpaths` were not glob-expanded, this is also fixed.)
2023-06-21 09:01:42 +03:00
Ran Benita
d97d44a97a config: extract initial paths/nodeids args logic to a function
Will be reused in the next commit.
2023-06-20 21:28:48 +03:00
Zac Hatfield-Dodds
f6b995e9d5 Use utf-8 debug file 2023-06-20 04:55:40 -07:00
Zac Hatfield-Dodds
661b938fca Add encoding in more tests 2023-06-20 04:55:40 -07:00
Zac Hatfield-Dodds
7e510769b4 Encoding for subprocess.run 2023-06-20 04:55:39 -07:00
nondescryptid
a704605cf1 Fix encoding warnings 2023-06-20 04:55:39 -07:00
pre-commit-ci[bot]
797b924fc4 [pre-commit.ci] pre-commit autoupdate (#11124)
updates:
- [github.com/asottile/blacken-docs: 1.13.0 → 1.14.0](https://github.com/asottile/blacken-docs/compare/1.13.0...1.14.0)
- [github.com/asottile/reorder-python-imports: v3.9.0 → v3.10.0](https://github.com/asottile/reorder-python-imports/compare/v3.9.0...v3.10.0)
- [github.com/asottile/pyupgrade: v3.6.0 → v3.7.0](https://github.com/asottile/pyupgrade/compare/v3.6.0...v3.7.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-20 06:38:36 +00:00
Zac Hatfield-Dodds
b5ec092525 Merge pull request #9149 from Vijay-Arora/main 2023-06-19 20:35:01 -07:00
Zac Hatfield-Dodds
5b35518389 Apply suggestions from code review
Co-authored-by: Ronnie Dutta <61982285+MetRonnie@users.noreply.github.com>
2023-06-19 20:06:21 -07:00
dependabot[bot]
8528052a95 build(deps): Bump peter-evans/create-pull-request from 5.0.0 to 5.0.2 (#11120)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 5.0.0 to 5.0.2.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](5b4a9f6a9e...153407881e)

---
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-06-19 19:22:49 -03:00
dependabot[bot]
1eb83706b6 build(deps): Bump pytest-mock in /testing/plugins_integration (#11119)
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.10.0 to 3.11.1.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.10.0...v3.11.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-19 04:15:50 +00:00
github-actions[bot]
fa09f6dcc6 [automated] Update plugin list (#11117)
Co-authored-by: pytest bot <pytestbot@users.noreply.github.com>
2023-06-18 09:40:52 -03:00
Florian Bruhin
b55c02c3c9 doc: Add ep2023 training (#11113) 2023-06-15 14:17:14 +02:00
pre-commit-ci[bot]
b7a142f4ba [pre-commit.ci] pre-commit autoupdate (#11103)
updates:
- [github.com/asottile/pyupgrade: v3.4.0 → v3.6.0](https://github.com/asottile/pyupgrade/compare/v3.4.0...v3.6.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2023-06-13 05:36:28 +00:00
dependabot[bot]
2d824329eb build(deps): Bump django in /testing/plugins_integration (#11102)
Bumps [django](https://github.com/django/django) from 4.2.1 to 4.2.2.
- [Commits](https://github.com/django/django/compare/4.2.1...4.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-12 04:19:01 +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](38e0b6e68b...5b4a9f6a9e)

---
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
Stefanie Molin
f1c7585184 Update fixture scope in package/directory fixture example. 2023-03-31 10:00:45 -07:00
pre-commit-ci[bot]
bb6155adfa [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-10-01 16:07:39 +00:00
Vijay Arora
5fefd7de96 Updated indentation and spaces in logging.py for #9146 2021-10-01 21:36:35 +05:30
Vijay Arora
750ce30392 Update 9146.doc.rst 2021-10-01 21:33:32 +05:30
pre-commit-ci[bot]
de1f378b60 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2021-10-01 14:53:26 +00:00
Vijay Arora
14d5b4ca6c Merge pull request #2 from Vijay-Arora/Vijay-Arora-patch-1
Updated AUTHORS and added changelog file
2021-10-01 20:19:23 +05:30
Vijay Arora
307dbf15c4 Add Myself as Authors 2021-10-01 20:16:10 +05:30
Vijay Arora
a8697601ad Create 9146.doc.rst
Create 9146.doc.rst
2021-10-01 20:10:46 +05:30
Vijay Arora
0ed1b0ac12 Merge pull request #1 from Vijay-Arora/Vijay-Arora-patch-1
Updated logging.py for #9146
2021-10-01 20:04:51 +05:30
Vijay Arora
26b0702b98 Updated logging.py
Updated logging.py for #9146
2021-10-01 19:34:59 +05:30
126 changed files with 3859 additions and 1346 deletions

View File

@@ -1,26 +1,23 @@
name: deploy
on:
push:
tags:
# These tags are protected, see:
# https://github.com/pytest-dev/pytest/settings/tag_protection
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
workflow_dispatch:
inputs:
version:
description: 'Release version'
required: true
default: '1.2.3'
# Set permissions at the job level.
permissions: {}
jobs:
deploy:
if: github.repository == 'pytest-dev/pytest'
package:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
env:
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
@@ -31,6 +28,18 @@ jobs:
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5
deploy:
if: github.repository == 'pytest-dev/pytest'
needs: [package]
runs-on: ubuntu-latest
environment: deploy
timeout-minutes: 30
permissions:
id-token: write
contents: write
steps:
- uses: actions/checkout@v3
- name: Download Package
uses: actions/download-artifact@v3
with:
@@ -38,14 +47,35 @@ jobs:
path: dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@v1.8.5
- name: Push tag
run: |
git config user.name "pytest bot"
git config user.email "pytestbot@gmail.com"
git tag --annotate --message=v${{ github.event.inputs.version }} v${{ github.event.inputs.version }} ${{ github.sha }}
git push origin v${{ github.event.inputs.version }}
release-notes:
# todo: generate the content in the build job
# the goal being of using a github action script to push the release data
# after success instead of creating a complete python/tox env
needs: [deploy]
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
steps:
- uses: actions/checkout@v3
with:
password: ${{ secrets.pypi_token }}
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.7"
python-version: "3.10"
- name: Install tox
run: |

23
.github/workflows/stale.yml vendored Normal file
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

View File

@@ -27,7 +27,19 @@ concurrency:
permissions: {}
jobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
persist-credentials: false
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5
build:
needs: [package]
runs-on: ${{ matrix.os }}
timeout-minutes: 45
permissions:
@@ -38,27 +50,28 @@ jobs:
matrix:
name: [
"windows-py37",
"windows-py37-pluggy",
"windows-py38",
"windows-py38-pluggy",
"windows-py39",
"windows-py310",
"windows-py311",
"windows-py312",
"ubuntu-py37",
"ubuntu-py37-pluggy",
"ubuntu-py37-freeze",
"ubuntu-py38",
"ubuntu-py38-pluggy",
"ubuntu-py39",
"ubuntu-py310",
"ubuntu-py311",
"ubuntu-py312",
"ubuntu-pypy3",
"macos-py37",
"macos-py38",
"macos-py39",
"macos-py310",
"macos-py312",
"docs",
"doctesting",
"plugins",
]
@@ -68,15 +81,15 @@ jobs:
python: "3.7"
os: windows-latest
tox_env: "py37-numpy"
- name: "windows-py37-pluggy"
python: "3.7"
os: windows-latest
tox_env: "py37-pluggymain-pylib-xdist"
- name: "windows-py38"
python: "3.8"
os: windows-latest
tox_env: "py38-unittestextras"
use_coverage: true
- name: "windows-py38-pluggy"
python: "3.8"
os: windows-latest
tox_env: "py38-pluggymain-pylib-xdist"
- name: "windows-py39"
python: "3.9"
os: windows-latest
@@ -86,19 +99,19 @@ jobs:
os: windows-latest
tox_env: "py310-xdist"
- name: "windows-py311"
python: "3.11-dev"
python: "3.11"
os: windows-latest
tox_env: "py311"
- name: "windows-py312"
python: "3.12-dev"
os: windows-latest
tox_env: "py312"
- name: "ubuntu-py37"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-lsof-numpy-pexpect"
use_coverage: true
- name: "ubuntu-py37-pluggy"
python: "3.7"
os: ubuntu-latest
tox_env: "py37-pluggymain-pylib-xdist"
- name: "ubuntu-py37-freeze"
python: "3.7"
os: ubuntu-latest
@@ -107,6 +120,10 @@ jobs:
python: "3.8"
os: ubuntu-latest
tox_env: "py38-xdist"
- name: "ubuntu-py38-pluggy"
python: "3.8"
os: ubuntu-latest
tox_env: "py38-pluggymain-pylib-xdist"
- name: "ubuntu-py39"
python: "3.9"
os: ubuntu-latest
@@ -116,10 +133,15 @@ jobs:
os: ubuntu-latest
tox_env: "py310-xdist"
- name: "ubuntu-py311"
python: "3.11-dev"
python: "3.11"
os: ubuntu-latest
tox_env: "py311"
use_coverage: true
- name: "ubuntu-py312"
python: "3.12-dev"
os: ubuntu-latest
tox_env: "py312"
use_coverage: true
- name: "ubuntu-pypy3"
python: "pypy-3.7"
os: ubuntu-latest
@@ -129,29 +151,25 @@ jobs:
python: "3.7"
os: macos-latest
tox_env: "py37-xdist"
- name: "macos-py38"
python: "3.8"
os: macos-latest
tox_env: "py38-xdist"
use_coverage: true
- name: "macos-py39"
python: "3.9"
os: macos-latest
tox_env: "py39-xdist"
use_coverage: true
- name: "macos-py310"
python: "3.10"
os: macos-latest
tox_env: "py310-xdist"
- name: "macos-py312"
python: "3.12-dev"
os: macos-latest
tox_env: "py312-xdist"
- name: "plugins"
python: "3.9"
os: ubuntu-latest
tox_env: "plugins"
- name: "docs"
python: "3.7"
os: ubuntu-latest
tox_env: "docs"
- name: "doctesting"
python: "3.7"
os: ubuntu-latest
@@ -164,10 +182,17 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Download Package
uses: actions/download-artifact@v3
with:
name: Packages
path: dist
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
check-latest: ${{ endsWith(matrix.python, '-dev') }}
- name: Install dependencies
run: |
@@ -176,11 +201,13 @@ jobs:
- name: Test without coverage
if: "! matrix.use_coverage"
run: "tox -e ${{ matrix.tox_env }}"
shell: bash
run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
- name: Test with coverage
if: "matrix.use_coverage"
run: "tox -e ${{ matrix.tox_env }}-coverage"
shell: bash
run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz`
- name: Generate coverage report
if: "matrix.use_coverage"
@@ -194,10 +221,3 @@ jobs:
fail_ci_if_error: true
files: ./coverage.xml
verbose: true
check-package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5

View File

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

View File

@@ -1,5 +1,3 @@
default_language_version:
python: "3.10"
repos:
- repo: https://github.com/psf/black
rev: 23.3.0
@@ -7,7 +5,7 @@ repos:
- id: black
args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs
rev: 1.13.0
rev: 1.14.0
hooks:
- id: blacken-docs
additional_dependencies: [black==23.1.0]
@@ -23,7 +21,7 @@ repos:
exclude: _pytest/(debugging|hookspec).py
language_version: python3
- repo: https://github.com/PyCQA/autoflake
rev: v2.0.2
rev: v2.1.1
hooks:
- id: autoflake
name: autoflake
@@ -38,27 +36,27 @@ repos:
additional_dependencies:
- flake8-typing-imports==1.12.0
- flake8-docstrings==1.5.0
- repo: https://github.com/asottile/reorder_python_imports
rev: v3.9.0
- repo: https://github.com/asottile/reorder-python-imports
rev: v3.10.0
hooks:
- id: reorder-python-imports
args: ['--application-directories=.:src', --py37-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v3.7.0
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.2.0
rev: v2.3.0
hooks:
- id: setup-cfg-fmt
args: ["--max-py-version=3.11", "--include-version-classifiers"]
args: ["--max-py-version=3.12", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.10.0
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.1.1
rev: v1.3.0
hooks:
- id: mypy
files: ^(src/|testing/)

View File

@@ -9,6 +9,10 @@ python:
path: .
- requirements: doc/en/requirements.txt
sphinx:
configuration: doc/en/conf.py
fail_on_warning: true
build:
os: ubuntu-20.04
tools:

15
AUTHORS
View File

@@ -8,11 +8,14 @@ Abdeali JK
Abdelrahman Elbehery
Abhijeet Kasurde
Adam Johnson
Adam Stewart
Adam Uhlir
Ahn Ki-Wook
Akiomi Kamakura
Alan Velasco
Alessio Izzo
Alex Jones
Alex Lambson
Alexander Johnson
Alexander King
Alexei Kozlenok
@@ -55,6 +58,7 @@ Benjamin Peterson
Bernard Pratz
Bob Ippolito
Brian Dorsey
Brian Larsen
Brian Maissy
Brian Okken
Brianna Laugher
@@ -68,6 +72,7 @@ Charles Cloud
Charles Machalow
Charnjit SiNGH (CCSJ)
Cheuk Ting Ho
Chris Mahoney
Chris Lamb
Chris NeJame
Chris Rose
@@ -126,6 +131,7 @@ Eric Siegerman
Erik Aronesty
Erik M. Bray
Evan Kepner
Evgeny Seliverstov
Fabien Zarifian
Fabio Zadrozny
Felix Hofstätter
@@ -160,6 +166,8 @@ Ian Bicking
Ian Lesperance
Ilya Konstantinov
Ionuț Turturică
Isaac Virshup
Israel Fruchter
Itxaso Aizpurua
Iwan Briquemont
Jaap Broekhuizen
@@ -192,6 +200,7 @@ Justice Ndou
Justyna Janczyszyn
Kale Kundert
Kamran Ahmad
Kenny Y
Karl O. Pinc
Karthikeyan Singaravelan
Katarzyna Jachim
@@ -222,6 +231,7 @@ Maho
Maik Figura
Mandeep Bhutani
Manuel Krebber
Marc Mueller
Marc Schlaich
Marcelo Duarte Trevisani
Marcin Bachry
@@ -304,6 +314,7 @@ Rafal Semik
Raquel Alegre
Ravi Chandra
Robert Holt
Roberto Aldera
Roberto Polli
Roland Puntaier
Romain Dorgueil
@@ -326,11 +337,13 @@ Serhii Mozghovyi
Seth Junot
Shantanu Jain
Shubham Adep
Simon Blanchard
Simon Gomizelj
Simon Holesch
Simon Kerr
Skylar Downes
Srinivas Reddy Thatiparthy
Stefaan Lippens
Stefan Farmbauer
Stefan Scherfke
Stefan Zimmermann
@@ -363,12 +376,14 @@ Tony Narlock
Tor Colvin
Trevor Bekolay
Tyler Goodlet
Tyler Smart
Tzu-ping Chung
Vasily Kuznetsov
Victor Maryama
Victor Rodriguez
Victor Uriarte
Vidar T. Fauske
Vijay Arora
Virgil Dupras
Vitaly Lashmanov
Vivaan Verma

View File

@@ -50,7 +50,7 @@ Fix bugs
--------
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
See also the `"status: easy" issues <https://github.com/pytest-dev/pytest/labels/status%3A%20easy>`_
See also the `"good first issue" issues <https://github.com/pytest-dev/pytest/labels/good%20first%20issue>`_
that are friendly to new contributors.
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going

View File

@@ -133,14 +133,12 @@ Releasing
Both automatic and manual processes described above follow the same steps from this point onward.
#. After all tests pass and the PR has been approved, tag the release commit
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
as source.
git fetch upstream
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
git push upstream MAJOR.MINOR.PATCH
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
and tag the repository.
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.

View File

@@ -1 +0,0 @@
If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.

View File

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

View File

@@ -1,3 +0,0 @@
Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of
``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration
file.

View File

@@ -1 +0,0 @@
pytest no longer depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported).

View File

@@ -1 +0,0 @@
Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.

View File

@@ -1 +0,0 @@
Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.

View File

@@ -1 +0,0 @@
The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.

View File

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

View File

@@ -1 +0,0 @@
Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.

View File

@@ -1 +0,0 @@
Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.

View File

@@ -1 +0,0 @@
pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`.

View File

@@ -1 +0,0 @@
Correctly handle ``__tracebackhide__`` for chained exceptions.

View File

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

View File

@@ -1 +0,0 @@
``--log-disable`` CLI option added to disable individual loggers.

View File

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

View File

@@ -6,6 +6,13 @@ Release announcements
:maxdepth: 2
release-7.4.3
release-7.4.2
release-7.4.1
release-7.4.0
release-7.3.2
release-7.3.1
release-7.3.0
release-7.2.2
release-7.2.1
release-7.2.0

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

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

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

View File

@@ -0,0 +1,49 @@
pytest-7.4.0
=======================================
The pytest team is proud to announce the 7.4.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:
* Adam J. Stewart
* Alessio Izzo
* Alex
* Alex Lambson
* Brian Larsen
* Bruno Oliveira
* Bryan Ricker
* Chris Mahoney
* Facundo Batista
* Florian Bruhin
* Jarrett Keifer
* Kenny Y
* Miro Hrončok
* Ran Benita
* Roberto Aldera
* Ronny Pfannschmidt
* Sergey Kim
* Stefanie Molin
* Vijay Arora
* Ville Skyttä
* Zac Hatfield-Dodds
* bzoracler
* leeyueh
* nondescryptid
* theirix
Happy testing,
The pytest Development Team

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
pytest-7.4.3
=======================================
pytest 7.4.3 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
* Marc Mueller
Happy testing,
The pytest Development Team

View File

@@ -92,3 +92,5 @@ pytest version min. Python version
5.0 - 6.1 3.5+
3.3 - 4.6 2.7, 3.4+
============== ===================
`Status of Python Versions <https://devguide.python.org/versions/>`__.

View File

@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
cachedir: .pytest_cache
rootdir: /home/sweet/project
collected 0 items
cache -- .../_pytest/cacheprovider.py:510
cache -- .../_pytest/cacheprovider.py:532
Return a cache object that can persist state between testing sessions.
cache.get(key, default)
@@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsys.readouterr()
assert captured.out == "hello\n"
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
doctest_namespace [session scope] -- .../_pytest/doctest.py:757
Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.
@@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
pytestconfig [session scope] -- .../_pytest/fixtures.py:1353
Session-scoped fixture that returns the session's :class:`pytest.Config`
object.
@@ -196,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:498
caplog -- .../_pytest/logging.py:570
Access and control log capturing.
Captured logs are available through the following properties/methods::
@@ -207,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
* caplog.record_tuples -> list of (logger_name, level, message) tuples
* caplog.clear() -> clear captured records and formatted log output string
monkeypatch -- .../_pytest/monkeypatch.py:29
monkeypatch -- .../_pytest/monkeypatch.py:30
A convenient fixture for monkey-patching.
The fixture provides these methods to modify objects, dictionaries, or

View File

@@ -28,6 +28,278 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
pytest 7.4.3 (2023-10-24)
=========================
Bug Fixes
---------
- `#10447 <https://github.com/pytest-dev/pytest/issues/10447>`_: Markers are now considered in the reverse mro order to ensure base class markers are considered first -- this resolves a regression.
- `#11239 <https://github.com/pytest-dev/pytest/issues/11239>`_: Fixed ``:=`` in asserts impacting unrelated test cases.
- `#11439 <https://github.com/pytest-dev/pytest/issues/11439>`_: Handled an edge case where :data:`sys.stderr` might already be closed when :ref:`faulthandler` is tearing down.
pytest 7.4.2 (2023-09-07)
=========================
Bug Fixes
---------
- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects.
- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.
- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files.
Improved Documentation
----------------------
- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.
pytest 7.4.1 (2023-09-02)
=========================
Bug Fixes
---------
- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug where fake intermediate modules generated by ``--import-mode=importlib`` would not include the
child modules as attributes of the parent modules.
- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
to be imported more than once, causing problems with modules that have import side effects.
pytest 7.4.0 (2023-06-23)
=========================
Features
--------
- `#10901 <https://github.com/pytest-dev/pytest/issues/10901>`_: 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.
Improvements
------------
- `#10872 <https://github.com/pytest-dev/pytest/issues/10872>`_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
- `#10907 <https://github.com/pytest-dev/pytest/issues/10907>`_: 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.
- `#10940 <https://github.com/pytest-dev/pytest/issues/10940>`_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
Added ``TerminalReporter.wrap_write()`` as a helper for that.
- `#10991 <https://github.com/pytest-dev/pytest/issues/10991>`_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
- `#11005 <https://github.com/pytest-dev/pytest/issues/11005>`_: Added the underlying exception to the cache provider's path creation and write warning messages.
- `#11013 <https://github.com/pytest-dev/pytest/issues/11013>`_: 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.
- `#11043 <https://github.com/pytest-dev/pytest/issues/11043>`_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir <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`.
- `#11081 <https://github.com/pytest-dev/pytest/issues/11081>`_: The :confval:`norecursedirs` 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 `norecursedirs` 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.
- `#8711 <https://github.com/pytest-dev/pytest/issues/8711>`_: :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :func:`caplog.at_level() <pytest.LogCaptureFixture.at_level>`
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
``logging.disable(LEVEL)``.
Bug Fixes
---------
- `#10831 <https://github.com/pytest-dev/pytest/issues/10831>`_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
- `#11068 <https://github.com/pytest-dev/pytest/issues/11068>`_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
- `#11104 <https://github.com/pytest-dev/pytest/issues/11104>`_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
even when it was not utilized (e.g. when explicit paths were given on the command line).
Now the ``testpaths`` are only considered when they are in use.
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
- `#7781 <https://github.com/pytest-dev/pytest/issues/7781>`_: Fix writing non-encodable text to log file when using ``--debug``.
Improved Documentation
----------------------
- `#9146 <https://github.com/pytest-dev/pytest/issues/9146>`_: Improved documentation for :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>`.
Trivial/Internal Changes
------------------------
- `#11031 <https://github.com/pytest-dev/pytest/issues/11031>`_: 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.
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)
=========================
@@ -468,7 +740,7 @@ Breaking Changes
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
@@ -3968,7 +4240,7 @@ Removals
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
Use ``Node.get_closest_marker(name)`` as a replacement.

View File

@@ -341,7 +341,7 @@ epub_copyright = "2013, holger krekel et alii"
# The scheme of the identifier. Typical schemes are ISBN or URL.
# epub_scheme = ''
# The unique identifier of the text. This can be a ISBN number
# The unique identifier of the text. This can be an ISBN number
# or the project homepage.
# epub_identifier = ''

View File

@@ -12,7 +12,7 @@ class YamlFile(pytest.File):
# We need a yaml parser, e.g. PyYAML.
import yaml
raw = yaml.safe_load(self.path.open())
raw = yaml.safe_load(self.path.open(encoding="utf-8"))
for name, spec in sorted(raw.items()):
yield YamlItem.from_parent(self, name=name, spec=spec)
@@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
" no further details known at this point.",
]
)
return super().repr_failure(excinfo)
def reportinfo(self):
return self.path, 0, f"usecase: {self.name}"

View File

@@ -502,8 +502,12 @@ Running it results in some skips if we don't have all the python interpreters in
.. code-block:: pytest
. $ pytest -rs -q multipython.py
........................... [100%]
27 passed in 0.12s
sssssssssssssssssssssssssss [100%]
========================= short test summary info ==========================
SKIPPED [9] multipython.py:69: 'python3.5' not found
SKIPPED [9] multipython.py:69: 'python3.6' not found
SKIPPED [9] multipython.py:69: 'python3.7' not found
27 skipped in 0.12s
Indirect parametrization of optional implementations/imports
--------------------------------------------------------------------

View File

@@ -70,12 +70,12 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert not f()
E assert not 42
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef0002>()
E + where 42 = <function TestFailing.test_not.<locals>.f at 0xdeadbeef0006>()
failure_demo.py:39: AssertionError
_________________ TestSpecialisedExplanations.test_eq_text _________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0006>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0007>
def test_eq_text(self):
> assert "spam" == "eggs"
@@ -86,7 +86,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:44: AssertionError
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0007>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0008>
def test_eq_similar_text(self):
> assert "foo 1 bar" == "foo 2 bar"
@@ -99,7 +99,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:47: AssertionError
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0008>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0009>
def test_eq_multiline_text(self):
> assert "foo\nspam\nbar" == "foo\neggs\nbar"
@@ -112,7 +112,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:50: AssertionError
______________ TestSpecialisedExplanations.test_eq_long_text _______________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0009>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000a>
def test_eq_long_text(self):
a = "1" * 100 + "a" + "2" * 100
@@ -129,7 +129,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:55: AssertionError
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000a>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000b>
def test_eq_long_text_multiline(self):
a = "1\n" * 100 + "a" + "2\n" * 100
@@ -149,7 +149,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:60: AssertionError
_________________ TestSpecialisedExplanations.test_eq_list _________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000b>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000c>
def test_eq_list(self):
> assert [0, 1, 2] == [0, 1, 3]
@@ -160,7 +160,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:63: AssertionError
______________ TestSpecialisedExplanations.test_eq_list_long _______________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000c>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000d>
def test_eq_list_long(self):
a = [0] * 100 + [1] + [3] * 100
@@ -173,7 +173,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:68: AssertionError
_________________ TestSpecialisedExplanations.test_eq_dict _________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000d>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
def test_eq_dict(self):
> assert {"a": 0, "b": 1, "c": 0} == {"a": 0, "b": 2, "d": 0}
@@ -190,7 +190,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:71: AssertionError
_________________ TestSpecialisedExplanations.test_eq_set __________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000e>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000f>
def test_eq_set(self):
> assert {0, 10, 11, 12} == {0, 20, 21}
@@ -207,7 +207,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:74: AssertionError
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef000f>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0010>
def test_eq_longer_list(self):
> assert [1, 2] == [1, 2, 3]
@@ -218,7 +218,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:77: AssertionError
_________________ TestSpecialisedExplanations.test_in_list _________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0010>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0011>
def test_in_list(self):
> assert 1 in [0, 2, 3, 4, 5]
@@ -227,7 +227,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:80: AssertionError
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0011>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0012>
def test_not_in_text_multiline(self):
text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail"
@@ -245,7 +245,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:84: AssertionError
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0012>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0013>
def test_not_in_text_single(self):
text = "single foo line"
@@ -258,7 +258,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:88: AssertionError
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0013>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0014>
def test_not_in_text_single_long(self):
text = "head " * 50 + "foo " + "tail " * 20
@@ -271,7 +271,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:92: AssertionError
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0014>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0015>
def test_not_in_text_single_long_term(self):
text = "head " * 50 + "f" * 70 + "tail " * 20
@@ -284,7 +284,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:96: AssertionError
______________ TestSpecialisedExplanations.test_eq_dataclass _______________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0015>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
def test_eq_dataclass(self):
from dataclasses import dataclass
@@ -311,7 +311,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:108: AssertionError
________________ TestSpecialisedExplanations.test_eq_attrs _________________
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0016>
self = <failure_demo.TestSpecialisedExplanations object at 0xdeadbeef0017>
def test_eq_attrs(self):
import attr
@@ -345,7 +345,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
i = Foo()
> assert i.b == 2
E assert 1 == 2
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef0017>.b
E + where 1 = <failure_demo.test_attribute.<locals>.Foo object at 0xdeadbeef0018>.b
failure_demo.py:128: AssertionError
_________________________ test_attribute_instance __________________________
@@ -356,8 +356,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert Foo().b == 2
E AssertionError: assert 1 == 2
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0018>.b
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0018> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
E + where 1 = <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0019>.b
E + where <failure_demo.test_attribute_instance.<locals>.Foo object at 0xdeadbeef0019> = <class 'failure_demo.test_attribute_instance.<locals>.Foo'>()
failure_demo.py:135: AssertionError
__________________________ test_attribute_failure __________________________
@@ -375,7 +375,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:146:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef0019>
self = <failure_demo.test_attribute_failure.<locals>.Foo object at 0xdeadbeef001a>
def _get_b(self):
> raise Exception("Failed to get attrib")
@@ -393,15 +393,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert Foo().b == Bar().b
E AssertionError: assert 1 == 2
E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001a>.b
E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001a> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001b>.b
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
E + where 1 = <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001b>.b
E + where <failure_demo.test_attribute_multiple.<locals>.Foo object at 0xdeadbeef001b> = <class 'failure_demo.test_attribute_multiple.<locals>.Foo'>()
E + and 2 = <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001c>.b
E + where <failure_demo.test_attribute_multiple.<locals>.Bar object at 0xdeadbeef001c> = <class 'failure_demo.test_attribute_multiple.<locals>.Bar'>()
failure_demo.py:156: AssertionError
__________________________ TestRaises.test_raises __________________________
self = <failure_demo.TestRaises object at 0xdeadbeef001c>
self = <failure_demo.TestRaises object at 0xdeadbeef001d>
def test_raises(self):
s = "qwe"
@@ -411,7 +411,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:166: ValueError
______________________ TestRaises.test_raises_doesnt _______________________
self = <failure_demo.TestRaises object at 0xdeadbeef001d>
self = <failure_demo.TestRaises object at 0xdeadbeef001e>
def test_raises_doesnt(self):
> raises(OSError, int, "3")
@@ -420,7 +420,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:169: Failed
__________________________ TestRaises.test_raise ___________________________
self = <failure_demo.TestRaises object at 0xdeadbeef001e>
self = <failure_demo.TestRaises object at 0xdeadbeef001f>
def test_raise(self):
> raise ValueError("demo error")
@@ -429,7 +429,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:172: ValueError
________________________ TestRaises.test_tupleerror ________________________
self = <failure_demo.TestRaises object at 0xdeadbeef001f>
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
def test_tupleerror(self):
> a, b = [1] # NOQA
@@ -438,7 +438,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:175: ValueError
______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
self = <failure_demo.TestRaises object at 0xdeadbeef0020>
self = <failure_demo.TestRaises object at 0xdeadbeef0021>
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
items = [1, 2, 3]
@@ -451,7 +451,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
items is [1, 2, 3]
________________________ TestRaises.test_some_error ________________________
self = <failure_demo.TestRaises object at 0xdeadbeef0021>
self = <failure_demo.TestRaises object at 0xdeadbeef0022>
def test_some_error(self):
> if namenotexi: # NOQA
@@ -482,7 +482,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
abc-123:2: AssertionError
____________________ TestMoreErrors.test_complex_error _____________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0022>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0023>
def test_complex_error(self):
def f():
@@ -508,7 +508,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:6: AssertionError
___________________ TestMoreErrors.test_z1_unpack_error ____________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0023>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0024>
def test_z1_unpack_error(self):
items = []
@@ -518,7 +518,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:217: ValueError
____________________ TestMoreErrors.test_z2_type_error _____________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0024>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0025>
def test_z2_type_error(self):
items = 3
@@ -528,20 +528,20 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:221: TypeError
______________________ TestMoreErrors.test_startswith ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0025>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0026>
def test_startswith(self):
s = "123"
g = "456"
> assert s.startswith(g)
E AssertionError: assert False
E + where False = <built-in method startswith of str object at 0xdeadbeef0026>('456')
E + where <built-in method startswith of str object at 0xdeadbeef0026> = '123'.startswith
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
failure_demo.py:226: AssertionError
__________________ TestMoreErrors.test_startswith_nested ___________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0027>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef0028>
def test_startswith_nested(self):
def f():
@@ -552,15 +552,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
> assert f().startswith(g())
E AssertionError: assert False
E + where False = <built-in method startswith of str object at 0xdeadbeef0026>('456')
E + where <built-in method startswith of str object at 0xdeadbeef0026> = '123'.startswith
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0028>()
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef0029>()
E + where False = <built-in method startswith of str object at 0xdeadbeef0027>('456')
E + where <built-in method startswith of str object at 0xdeadbeef0027> = '123'.startswith
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef0029>()
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef002a>()
failure_demo.py:235: AssertionError
_____________________ TestMoreErrors.test_global_func ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002a>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
def test_global_func(self):
> assert isinstance(globf(42), float)
@@ -571,18 +571,18 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:238: AssertionError
_______________________ TestMoreErrors.test_instance _______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
def test_instance(self):
self.x = 6 * 7
> assert self.x != 42
E assert 42 != 42
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002b>.x
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>.x
failure_demo.py:242: AssertionError
_______________________ TestMoreErrors.test_compare ________________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002c>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
def test_compare(self):
> assert globf(10) < 5
@@ -592,7 +592,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:245: AssertionError
_____________________ TestMoreErrors.test_try_finally ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002d>
self = <failure_demo.TestMoreErrors object at 0xdeadbeef002e>
def test_try_finally(self):
x = 1
@@ -603,7 +603,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:250: AssertionError
___________________ TestCustomAssertMsg.test_single_line ___________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002e>
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
def test_single_line(self):
class A:
@@ -618,7 +618,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:261: AssertionError
____________________ TestCustomAssertMsg.test_multiline ____________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef002f>
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
def test_multiline(self):
class A:
@@ -637,7 +637,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
failure_demo.py:268: AssertionError
___________________ TestCustomAssertMsg.test_custom_repr ___________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0030>
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef0031>
def test_custom_repr(self):
class JSON:

View File

@@ -691,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory:
pass
@pytest.fixture(scope="session")
@pytest.fixture(scope="package")
def db():
return DB()
@@ -817,7 +817,7 @@ case we just write some information out to a ``failures`` file:
# we only look at actual failing test calls, not setup/teardown
if rep.when == "call" and rep.failed:
mode = "a" if os.path.exists("failures") else "w"
with open("failures", mode) as f:
with open("failures", mode, encoding="utf-8") as f:
# let's also access a fixture for the fun of it
if "tmp_path" in item.fixturenames:
extra = " ({})".format(item.funcargs["tmp_path"])

View File

@@ -294,3 +294,20 @@ See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`
setuptools intends to
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
Checking with flake8-pytest-style
---------------------------------
In order to ensure that pytest is being used correctly in your project,
it can be helpful to use the `flake8-pytest-style <https://github.com/m-burst/flake8-pytest-style>`_ flake8 plugin.
flake8-pytest-style checks for common mistakes and coding style violations in pytest code,
such as incorrect use of fixtures, test function names, and markers.
By using this plugin, you can catch these errors early in the development process
and ensure that your pytest code is consistent and easy to maintain.
A list of the lints detected by flake8-pytest-style can be found on its `PyPI page <https://pypi.org/project/flake8-pytest-style/>`_.
.. note::
flake8-pytest-style is not an official pytest project. Some of the rules enforce certain style choices, such as using `@pytest.fixture()` over `@pytest.fixture`, but you can configure the plugin to fit your preferred style.

View File

@@ -22,7 +22,7 @@ Install ``pytest``
.. code-block:: bash
$ pytest --version
pytest 7.2.0.dev534+ga2c84caaa.d20230317
pytest 7.4.3
.. _`simpletest`:

View File

@@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the
idiomatic python constructs without boilerplate code while not losing
introspection information.
However, if you specify a message with the assertion like this:
If a message is specified with the assertion like this:
.. code-block:: python
assert a % 2 == 0, "value was odd, should be even"
then no assertion introspection takes places at all and the message
will be simply shown in the traceback.
it is printed alongside the assertion introspection in the traceback.
See :ref:`assert-details` for more information on assertion introspection.

View File

@@ -176,14 +176,21 @@ with more recent files coming first.
Behavior when no tests failed in the last run
---------------------------------------------
When no tests failed in the last run, or when no cached ``lastfailed`` data was
found, ``pytest`` can be configured either to run all of the tests or no tests,
using the ``--last-failed-no-failures`` option, which takes one of the following values:
The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
Determines whether to execute tests when there are no previously (known)
failures or when no cached ``lastfailed`` data was found.
There are two options:
* ``all``: when there are no known test failures, runs all tests (the full test suite). This is the default.
* ``none``: when there are no known test failures, just emits a message stating this and exit successfully.
Example:
.. code-block:: bash
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior)
pytest --last-failed --last-failed-no-failures none # run no tests and exit
pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
The new config.cache object
--------------------------------

View File

@@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker:
class TestDirectoryInit:
def test_cwd_starts_empty(self):
assert os.listdir(os.getcwd()) == []
with open("myfile", "w") as f:
with open("myfile", "w", encoding="utf-8") as f:
f.write("hello")
def test_cwd_again_starts_empty(self):

View File

@@ -24,8 +24,8 @@ created in the `base temporary directory`_.
d = tmp_path / "sub"
d.mkdir()
p = d / "hello.txt"
p.write_text(CONTENT)
assert p.read_text() == CONTENT
p.write_text(CONTENT, encoding="utf-8")
assert p.read_text(encoding="utf-8") == CONTENT
assert len(list(tmp_path.iterdir())) == 1
assert 0
@@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
d = tmp_path / "sub"
d.mkdir()
p = d / "hello.txt"
p.write_text(CONTENT)
assert p.read_text() == CONTENT
p.write_text(CONTENT, encoding="utf-8")
assert p.read_text(encoding="utf-8") == CONTENT
assert len(list(tmp_path.iterdir())) == 1
> assert 0
E assert 0

View File

@@ -207,10 +207,10 @@ creation of a per-test temporary directory:
@pytest.fixture(autouse=True)
def initdir(self, tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory
tmp_path.joinpath("samplefile.ini").write_text("# testdata")
tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8")
def test_method(self):
with open("samplefile.ini") as f:
with open("samplefile.ini", encoding="utf-8") as f:
s = f.read()
assert "testdata" in s

View File

@@ -35,11 +35,12 @@ Pytest supports several ways to run and select tests from the command-line.
.. code-block:: bash
pytest -k "MyClass and not method"
pytest -k 'MyClass and not method'
This will run tests which contain names that match the given *string expression* (case-insensitive),
which can include Python operators that use filenames, class names and function names as variables.
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
Use ``""`` instead of ``''`` in expression when running this on Windows
.. _nodeids:
@@ -172,7 +173,8 @@ You can invoke ``pytest`` from Python code directly:
this acts as if you would call "pytest" from the command line.
It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead.
You can pass in options and arguments:
If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable.
You can pass in options and arguments explicitly:
.. code-block:: python

View File

@@ -1,11 +1,10 @@
:orphan:
..
.. sidebar:: Next Open Trainings
.. sidebar:: Next Open Trainings
- `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
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
Also see :doc:`previous talks and blogposts <talks>`.
Also see :doc:`previous talks and blogposts <talks>`.
.. _features:

View File

@@ -90,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
setup.cfg
~~~~~~~~~
``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <python:distutils/configfile>`, and can also be used to hold pytest configuration
``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration
if they have a ``[tool:pytest]`` section.
.. code-block:: ini

File diff suppressed because it is too large Load Diff

View File

@@ -82,6 +82,8 @@ pytest.exit
pytest.main
~~~~~~~~~~~
**Tutorial**: :ref:`pytest.main-usage`
.. autofunction:: pytest.main
pytest.param
@@ -783,18 +785,66 @@ reporting or interaction with exceptions:
.. autofunction:: pytest_leave_pdb
Objects
-------
Collection tree objects
-----------------------
Full reference to objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`.
These are the collector and item classes (collectively called "nodes") which
make up the collection tree.
Node
~~~~
CallInfo
~~~~~~~~
.. autoclass:: pytest.CallInfo()
.. autoclass:: _pytest.nodes.Node()
:members:
Collector
~~~~~~~~~
.. autoclass:: pytest.Collector()
:members:
:show-inheritance:
Item
~~~~
.. autoclass:: pytest.Item()
:members:
:show-inheritance:
File
~~~~
.. autoclass:: pytest.File()
:members:
:show-inheritance:
FSCollector
~~~~~~~~~~~
.. autoclass:: _pytest.nodes.FSCollector()
:members:
:show-inheritance:
Session
~~~~~~~
.. autoclass:: pytest.Session()
:members:
:show-inheritance:
Package
~~~~~~~
.. autoclass:: pytest.Package()
:members:
:show-inheritance:
Module
~~~~~~
.. autoclass:: pytest.Module()
:members:
:show-inheritance:
Class
~~~~~
@@ -803,13 +853,34 @@ Class
:members:
:show-inheritance:
Collector
~~~~~~~~~
Function
~~~~~~~~
.. autoclass:: pytest.Collector()
.. autoclass:: pytest.Function()
:members:
:show-inheritance:
FunctionDefinition
~~~~~~~~~~~~~~~~~~
.. autoclass:: _pytest.python.FunctionDefinition()
:members:
:show-inheritance:
Objects
-------
Objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`
or importable from ``pytest``.
CallInfo
~~~~~~~~
.. autoclass:: pytest.CallInfo()
:members:
CollectReport
~~~~~~~~~~~~~
@@ -837,13 +908,6 @@ ExitCode
.. autoclass:: pytest.ExitCode
:members:
File
~~~~
.. autoclass:: pytest.File()
:members:
:show-inheritance:
FixtureDef
~~~~~~~~~~
@@ -852,34 +916,6 @@ FixtureDef
:members:
:show-inheritance:
FSCollector
~~~~~~~~~~~
.. autoclass:: _pytest.nodes.FSCollector()
:members:
:show-inheritance:
Function
~~~~~~~~
.. autoclass:: pytest.Function()
:members:
:show-inheritance:
FunctionDefinition
~~~~~~~~~~~~~~~~~~
.. autoclass:: _pytest.python.FunctionDefinition()
:members:
:show-inheritance:
Item
~~~~
.. autoclass:: pytest.Item()
:members:
:show-inheritance:
MarkDecorator
~~~~~~~~~~~~~
@@ -907,19 +943,6 @@ Metafunc
.. autoclass:: pytest.Metafunc()
:members:
Module
~~~~~~
.. autoclass:: pytest.Module()
:members:
:show-inheritance:
Node
~~~~
.. autoclass:: _pytest.nodes.Node()
:members:
Parser
~~~~~~
@@ -941,13 +964,6 @@ PytestPluginManager
:inherited-members:
:show-inheritance:
Session
~~~~~~~
.. autoclass:: pytest.Session()
:members:
:show-inheritance:
TestReport
~~~~~~~~~~
@@ -956,10 +972,16 @@ TestReport
:show-inheritance:
:inherited-members:
_Result
TestShortLogReport
~~~~~~~~~~~~~~~~~~
.. autoclass:: pytest.TestShortLogReport()
:members:
Result
~~~~~~~
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information.
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`Result in the pluggy documentation <pluggy.Result>` for more information.
Stash
~~~~~
@@ -1049,11 +1071,11 @@ 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. Alterative to ``BUILD_NUMBER`` variable.
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. Alterative to CI variable.
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
.. envvar:: PYTEST_ADDOPTS
@@ -1697,6 +1719,11 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
pythonpath = src1 src2
.. note::
``pythonpath`` does not affect some imports that happen very early,
most notably plugins loaded using the ``-p`` command line option.
.. confval:: required_plugins
@@ -1713,13 +1740,12 @@ passed multiple times. The expected format is ``name=value``. For example::
.. confval:: testpaths
Sets list of directories that should be searched for tests when
no specific directories, files or test ids are given in the command line when
executing pytest from the :ref:`rootdir <rootdir>` directory.
File system paths may use shell-style wildcards, including the recursive
``**`` pattern.
Useful when all project tests are in a known location to speed up
test collection and to avoid picking up undesired tests by accident.
@@ -1728,8 +1754,17 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
testpaths = testing doc
This tells pytest to only look for tests in ``testing`` and ``doc``
directories when executing from the root directory.
This configuration means that executing:
.. code-block:: console
pytest
has the same practical effects as executing:
.. code-block:: console
pytest testing doc
.. confval:: tmp_path_retention_count
@@ -1744,7 +1779,7 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
tmp_path_retention_count = 3
Default: 3
Default: ``3``
.. confval:: tmp_path_retention_policy
@@ -1763,7 +1798,7 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
tmp_path_retention_policy = "all"
Default: all
Default: ``all``
.. confval:: usefixtures
@@ -1852,8 +1887,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
tests. Optional argument: glob (default: '*').
--cache-clear Remove all cache contents at start of test run
--lfnf={all,none}, --last-failed-no-failures={all,none}
Which tests to run with no previously (known)
failures
With ``--lf``, determines whether to execute tests
when there are no previously (known) failures or
when no cached ``lastfailed`` data was found.
``all`` (the default) runs the full test suite
again. ``none`` just emits a message about no known
failures and exits successfully.
--sw, --stepwise Exit on test failure and continue from last failing
test next time
--sw-skip, --stepwise-skip
@@ -1904,8 +1943,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
--strict-markers Markers not registered in the `markers` section of
the configuration file raise errors
--strict (Deprecated) alias to --strict-markers
-c file Load configuration from `file` instead of trying to
locate one of the implicit configuration files
-c FILE, --config-file=FILE
Load configuration from `FILE` instead of trying to
locate one of the implicit configuration files.
--continue-on-collection-errors
Force test execution even if collection errors occur
--rootdir=ROOTDIR Define root directory for tests. Can be relative
@@ -1996,7 +2036,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
Auto-indent multiline messages passed to the logging
module. Accepts true|on, false|off or an integer.
--log-disable=LOGGER_DISABLE
Disable a logger by name. Can be passed multipe
Disable a logger by name. Can be passed multiple
times.
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:

View File

@@ -31,10 +31,16 @@ class InvalidFeatureRelease(Exception):
SLUG = "pytest-dev/pytest"
PR_BODY = """\
Created automatically from manual trigger.
Created by the [prepare release pr](https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml)
workflow.
Once all builds pass and it has been **approved** by one or more maintainers, the build
can be released by pushing a tag `{version}` to this repository.
Once all builds pass and it has been **approved** by one or more maintainers,
start the [deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
* `Use workflow from`: `release-{version}`.
* `Release version`: `{version}`.
After the `deploy` workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
"""

View File

@@ -7,7 +7,9 @@ def main():
Platform agnostic wrapper script for towncrier.
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
"""
with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file:
with open(
"doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8"
) as draft_file:
return call(("towncrier", "--draft"), stdout=draft_file)

View File

@@ -13,11 +13,25 @@ from tqdm import tqdm
FILE_HEAD = r"""
.. _plugin-list:
Plugin List
===========
Pytest Plugin List
==================
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
Packages classified as inactive are excluded.
For detailed insights into how this list is generated,
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
.. warning::
Please be aware that this list is not a curated collection of projects
and does not undergo a systematic review process.
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
Do not presume any endorsement from the ``pytest`` project or its developers,
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
PyPI projects that match "pytest-\*" are considered plugins and are listed
automatically. Packages classified as inactive are excluded.
.. The following conditional uses a different format for this list when
creating a PDF, because otherwise the table gets far too wide for the
@@ -33,6 +47,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
"Development Status :: 6 - Mature",
"Development Status :: 7 - Inactive",
)
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
"logassert",
}
def escape_rst(text: str) -> str:
@@ -52,18 +69,18 @@ def iter_plugins():
regex = r">([\d\w-]*)</a>"
response = requests.get("https://pypi.org/simple")
matches = list(
match
for match in re.finditer(regex, response.text)
if match.groups()[0].startswith("pytest-")
)
match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
plugin_names = [
name
for name in match_names
if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
]
for match in tqdm(matches, smoothing=0):
name = match.groups()[0]
for name in tqdm(plugin_names, smoothing=0):
response = requests.get(f"https://pypi.org/pypi/{name}/json")
if response.status_code == 404:
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
# return 404 on the JSON API. Skip.
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
# but return 404 on the JSON API. Skip.
continue
response.raise_for_status()
info = response.json()["info"]

View File

@@ -6,7 +6,7 @@ long_description_content_type = text/x-rst
url = https://docs.pytest.org/en/latest/
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
license = MIT
license_file = LICENSE
license_files = LICENSE
platforms = unix, linux, osx, cygwin, win32
classifiers =
Development Status :: 6 - Mature
@@ -22,6 +22,7 @@ classifiers =
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Software Development :: Libraries
Topic :: Software Development :: Testing
Topic :: Utilities
@@ -73,6 +74,7 @@ testing =
nose
pygments>=2.7.2
requests
setuptools
xmlschema
[options.package_data]

View File

@@ -31,7 +31,6 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
from weakref import ref
import pluggy
@@ -50,9 +49,9 @@ from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal
from typing_extensions import SupportsIndex
from weakref import ReferenceType
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
@@ -194,25 +193,25 @@ class Frame:
class TracebackEntry:
"""A single entry in a Traceback."""
__slots__ = ("_rawentry", "_excinfo", "_repr_style")
__slots__ = ("_rawentry", "_repr_style")
def __init__(
self,
rawentry: TracebackType,
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
repr_style: Optional['Literal["short", "long"]'] = None,
) -> None:
self._rawentry = rawentry
self._excinfo = excinfo
self._repr_style: Optional['Literal["short", "long"]'] = None
self._rawentry: "Final" = rawentry
self._repr_style: "Final" = repr_style
def with_repr_style(
self, repr_style: Optional['Literal["short", "long"]']
) -> "TracebackEntry":
return TracebackEntry(self._rawentry, repr_style)
@property
def lineno(self) -> int:
return self._rawentry.tb_lineno - 1
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
assert mode in ("short", "long")
self._repr_style = mode
@property
def frame(self) -> Frame:
return Frame(self._rawentry.tb_frame)
@@ -272,7 +271,7 @@ class TracebackEntry:
source = property(getsource)
def ishidden(self) -> bool:
def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
"""Return True if the current frame has a var __tracebackhide__
resolving to True.
@@ -296,7 +295,7 @@ class TracebackEntry:
else:
break
if tbh and callable(tbh):
return tbh(None if self._excinfo is None else self._excinfo())
return tbh(excinfo)
return tbh
def __str__(self) -> str:
@@ -329,16 +328,14 @@ class Traceback(List[TracebackEntry]):
def __init__(
self,
tb: Union[TracebackType, Iterable[TracebackEntry]],
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
) -> None:
"""Initialize from given python traceback object and ExceptionInfo."""
self._excinfo = excinfo
if isinstance(tb, TracebackType):
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
cur_: Optional[TracebackType] = cur
while cur_ is not None:
yield TracebackEntry(cur_, excinfo=excinfo)
yield TracebackEntry(cur_)
cur_ = cur_.tb_next
super().__init__(f(tb))
@@ -378,7 +375,7 @@ class Traceback(List[TracebackEntry]):
continue
if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
continue
return Traceback(x._rawentry, self._excinfo)
return Traceback(x._rawentry)
return self
@overload
@@ -398,26 +395,27 @@ class Traceback(List[TracebackEntry]):
return super().__getitem__(key)
def filter(
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
self,
# TODO(py38): change to positional only.
_excinfo_or_fn: Union[
"ExceptionInfo[BaseException]",
Callable[[TracebackEntry], bool],
],
) -> "Traceback":
"""Return a Traceback instance with certain items removed
"""Return a Traceback instance with certain items removed.
fn is a function that gets a single argument, a TracebackEntry
instance, and should return True when the item should be added
to the Traceback, False when not.
If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
which are hidden (see ishidden() above).
By default this removes all the TracebackEntries which are hidden
(see ishidden() above).
Otherwise, the filter is a function that gets a single argument, a
``TracebackEntry`` instance, and should return True when the item should
be added to the ``Traceback``, False when not.
"""
return Traceback(filter(fn, self), self._excinfo)
def getcrashentry(self) -> Optional[TracebackEntry]:
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
for i in range(-1, -len(self) - 1, -1):
entry = self[i]
if not entry.ishidden():
return entry
return None
if isinstance(_excinfo_or_fn, ExceptionInfo):
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
else:
fn = _excinfo_or_fn
return Traceback(filter(fn, self))
def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if
@@ -469,22 +467,41 @@ class ExceptionInfo(Generic[E]):
self._traceback = traceback
@classmethod
def from_exc_info(
def from_exception(
cls,
exc_info: Tuple[Type[E], E, TracebackType],
# Ignoring error: "Cannot use a covariant type variable as a parameter".
# This is OK to ignore because this class is (conceptually) readonly.
# See https://github.com/python/mypy/issues/7049.
exception: E, # type: ignore[misc]
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]":
"""Return an ExceptionInfo for an existing exc_info tuple.
"""Return an ExceptionInfo for an existing exception.
.. warning::
Experimental API
The exception must have a non-``None`` ``__traceback__`` attribute,
otherwise this function fails with an assertion error. This means that
the exception must have been raised, or added a traceback with the
:py:meth:`~BaseException.with_traceback()` method.
:param exprinfo:
A text string helping to determine if we should strip
``AssertionError`` from the output. Defaults to the exception
message/``__str__()``.
.. versionadded:: 7.4
"""
assert (
exception.__traceback__
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
exc_info = (type(exception), exception, exception.__traceback__)
return cls.from_exc_info(exc_info, exprinfo)
@classmethod
def from_exc_info(
cls,
exc_info: Tuple[Type[E], E, TracebackType],
exprinfo: Optional[str] = None,
) -> "ExceptionInfo[E]":
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
_striptext = ""
if exprinfo is None and isinstance(exc_info[1], AssertionError):
exprinfo = getattr(exc_info[1], "msg", None)
@@ -563,7 +580,7 @@ class ExceptionInfo(Generic[E]):
def traceback(self) -> Traceback:
"""The traceback."""
if self._traceback is None:
self._traceback = Traceback(self.tb, excinfo=ref(self))
self._traceback = Traceback(self.tb)
return self._traceback
@traceback.setter
@@ -603,11 +620,14 @@ class ExceptionInfo(Generic[E]):
return isinstance(self.value, exc)
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
exconly = self.exconly(tryshort=True)
entry = self.traceback.getcrashentry()
if entry:
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
# Find last non-hidden traceback entry that led to the exception of the
# traceback, or None if all hidden.
for i in range(-1, -len(self.traceback) - 1, -1):
entry = self.traceback[i]
if not entry.ishidden(self):
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
exconly = self.exconly(tryshort=True)
return ReprFileLocation(path, lineno + 1, exconly)
return None
def getrepr(
@@ -615,7 +635,9 @@ class ExceptionInfo(Generic[E]):
showlocals: bool = False,
style: "_TracebackStyle" = "long",
abspath: bool = False,
tbfilter: bool = True,
tbfilter: Union[
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
] = True,
funcargs: bool = False,
truncate_locals: bool = True,
chain: bool = True,
@@ -627,14 +649,20 @@ class ExceptionInfo(Generic[E]):
Ignored if ``style=="native"``.
:param str style:
long|short|no|native|value traceback style.
long|short|line|no|native|value traceback style.
:param bool abspath:
If paths should be changed to absolute or left unchanged.
:param bool tbfilter:
Hide entries that contain a local variable ``__tracebackhide__==True``.
Ignored if ``style=="native"``.
:param tbfilter:
A filter for traceback entries.
* If false, don't hide any entries.
* If true, hide internal entries and entries that contain a local
variable ``__tracebackhide__ = True``.
* If a callable, delegates the filtering to the callable.
Ignored if ``style`` is ``"native"``.
:param bool funcargs:
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
@@ -653,7 +681,9 @@ class ExceptionInfo(Generic[E]):
return ReprExceptionInfo(
reprtraceback=ReprTracebackNative(
traceback.format_exception(
self.type, self.value, self.traceback[0]._rawentry
self.type,
self.value,
self.traceback[0]._rawentry if self.traceback else None,
)
),
reprcrash=self._getreprcrash(),
@@ -697,7 +727,7 @@ class FormattedExcinfo:
showlocals: bool = False
style: "_TracebackStyle" = "long"
abspath: bool = True
tbfilter: bool = True
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
funcargs: bool = False
truncate_locals: bool = True
chain: bool = True
@@ -809,12 +839,16 @@ class FormattedExcinfo:
def repr_traceback_entry(
self,
entry: TracebackEntry,
entry: Optional[TracebackEntry],
excinfo: Optional[ExceptionInfo[BaseException]] = None,
) -> "ReprEntry":
lines: List[str] = []
style = entry._repr_style if entry._repr_style is not None else self.style
if style in ("short", "long"):
style = (
entry._repr_style
if entry is not None and entry._repr_style is not None
else self.style
)
if style in ("short", "long") and entry is not None:
source = self._getentrysource(entry)
if source is None:
source = Source("???")
@@ -855,25 +889,31 @@ class FormattedExcinfo:
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
traceback = excinfo.traceback
if self.tbfilter:
traceback = traceback.filter()
if callable(self.tbfilter):
traceback = self.tbfilter(excinfo)
elif self.tbfilter:
traceback = traceback.filter(excinfo)
if isinstance(excinfo.value, RecursionError):
traceback, extraline = self._truncate_recursive_traceback(traceback)
else:
extraline = None
if not traceback:
if extraline is None:
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
entries = [self.repr_traceback_entry(None, excinfo)]
return ReprTraceback(entries, extraline, style=self.style)
last = traceback[-1]
entries = []
if self.style == "value":
reprentry = self.repr_traceback_entry(last, excinfo)
entries.append(reprentry)
entries = [self.repr_traceback_entry(last, excinfo)]
return ReprTraceback(entries, None, style=self.style)
for index, entry in enumerate(traceback):
einfo = (last == entry) and excinfo or None
reprentry = self.repr_traceback_entry(entry, einfo)
entries.append(reprentry)
entries = [
self.repr_traceback_entry(entry, excinfo if last == entry else None)
for entry in traceback
]
return ReprTraceback(entries, extraline, style=self.style)
def _truncate_recursive_traceback(
@@ -930,6 +970,7 @@ class FormattedExcinfo:
seen: Set[int] = set()
while e is not None and id(e) not in seen:
seen.add(id(e))
if excinfo_:
# Fall back to native traceback as a temporary workaround until
# full support for exception groups added to ExceptionInfo.
@@ -946,14 +987,7 @@ class FormattedExcinfo:
)
else:
reprtraceback = self.repr_traceback(excinfo_)
# will be None if all traceback entries are hidden
reprcrash: Optional[ReprFileLocation] = excinfo_._getreprcrash()
if reprcrash:
if self.style == "value":
repr_chain += [(reprtraceback, None, descr)]
else:
repr_chain += [(reprtraceback, reprcrash, descr)]
reprcrash = excinfo_._getreprcrash()
else:
# Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work.
@@ -961,25 +995,17 @@ class FormattedExcinfo:
traceback.format_exception(type(e), e, None)
)
reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)]
repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None and self.chain:
e = e.__cause__
excinfo_ = (
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
descr = "The above exception was the direct cause of the following exception:"
elif (
e.__context__ is not None and not e.__suppress_context__ and self.chain
):
e = e.__context__
excinfo_ = (
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
if e.__traceback__
else None
)
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
descr = "During handling of the above exception, another exception occurred:"
else:
e = None
@@ -1158,8 +1184,8 @@ class ReprEntry(TerminalRepr):
def toterminal(self, tw: TerminalWriter) -> None:
if self.style == "short":
assert self.reprfileloc is not None
self.reprfileloc.toterminal(tw)
if self.reprfileloc:
self.reprfileloc.toterminal(tw)
self._write_entry_lines(tw)
if self.reprlocals:
self.reprlocals.toterminal(tw, indent=" " * 8)

View File

@@ -953,7 +953,7 @@ class LocalPath:
else:
p.dirpath()._ensuredirs()
if not p.check(file=1):
p.open("w").close()
p.open("wb").close()
return p
@overload

View File

@@ -13,6 +13,7 @@ import struct
import sys
import tokenize
import types
from collections import defaultdict
from pathlib import Path
from pathlib import PurePath
from typing import Callable
@@ -46,8 +47,18 @@ if TYPE_CHECKING:
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
class Sentinel:
pass
assertstate_key = StashKey["AssertionState"]()
@@ -57,6 +68,9 @@ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
PYC_EXT = ".py" + (__debug__ and "c" or "o")
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
# Special marker that denotes we have just left a scope definition
_SCOPE_END_MARKER = Sentinel()
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
"""PEP302/PEP451 import hook which rewrites asserts."""
@@ -639,6 +653,8 @@ class AssertionRewriter(ast.NodeVisitor):
.push_format_context() and .pop_format_context() which allows
to build another %-formatted string while already building one.
:scope: A tuple containing the current scope used for variables_overwrite.
:variables_overwrite: A dict filled with references to variables
that change value within an assert. This happens when a variable is
reassigned with the walrus operator
@@ -660,7 +676,10 @@ class AssertionRewriter(ast.NodeVisitor):
else:
self.enable_assertion_pass_hook = False
self.source = source
self.variables_overwrite: Dict[str, str] = {}
self.scope: tuple[ast.AST, ...] = ()
self.variables_overwrite: defaultdict[
tuple[ast.AST, ...], Dict[str, str]
] = defaultdict(dict)
def run(self, mod: ast.Module) -> None:
"""Find all assert statements in *mod* and rewrite them."""
@@ -680,9 +699,12 @@ class AssertionRewriter(ast.NodeVisitor):
if (
expect_docstring
and isinstance(item, ast.Expr)
and isinstance(item.value, ast.Str)
and isinstance(item.value, astStr)
):
doc = item.value.s
if sys.version_info >= (3, 8):
doc = item.value.value
else:
doc = item.value.s
if self.is_rewrite_disabled(doc):
return
expect_docstring = False
@@ -723,9 +745,17 @@ class AssertionRewriter(ast.NodeVisitor):
mod.body[pos:pos] = imports
# Collect asserts.
nodes: List[ast.AST] = [mod]
self.scope = (mod,)
nodes: List[Union[ast.AST, Sentinel]] = [mod]
while nodes:
node = nodes.pop()
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
self.scope = tuple((*self.scope, node))
nodes.append(_SCOPE_END_MARKER)
if node == _SCOPE_END_MARKER:
self.scope = self.scope[:-1]
continue
assert isinstance(node, ast.AST)
for name, field in ast.iter_fields(node):
if isinstance(field, list):
new: List[ast.AST] = []
@@ -814,7 +844,7 @@ class AssertionRewriter(ast.NodeVisitor):
current = self.stack.pop()
if self.stack:
self.explanation_specifiers = self.stack[-1]
keys = [ast.Str(key) for key in current.keys()]
keys = [astStr(key) for key in current.keys()]
format_dict = ast.Dict(keys, list(current.values()))
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
name = "@py_format" + str(next(self.variable_counter))
@@ -868,16 +898,16 @@ class AssertionRewriter(ast.NodeVisitor):
negation = ast.UnaryOp(ast.Not(), top_condition)
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
msg = self.pop_format_context(ast.Str(explanation))
msg = self.pop_format_context(astStr(explanation))
# Failed
if assert_.msg:
assertmsg = self.helper("_format_assertmsg", assert_.msg)
gluestr = "\n>assert "
else:
assertmsg = ast.Str("")
assertmsg = astStr("")
gluestr = "assert "
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
err_name = ast.Name("AssertionError", ast.Load())
fmt = self.helper("_format_explanation", err_msg)
@@ -893,8 +923,8 @@ class AssertionRewriter(ast.NodeVisitor):
hook_call_pass = ast.Expr(
self.helper(
"_call_assertion_pass",
ast.Num(assert_.lineno),
ast.Str(orig),
astNum(assert_.lineno),
astStr(orig),
fmt_pass,
)
)
@@ -913,7 +943,7 @@ class AssertionRewriter(ast.NodeVisitor):
variables = [
ast.Name(name, ast.Store()) for name in self.format_variables
]
clear_format = ast.Assign(variables, ast.NameConstant(None))
clear_format = ast.Assign(variables, astNameConstant(None))
self.statements.append(clear_format)
else: # Original assertion rewriting
@@ -924,9 +954,9 @@ class AssertionRewriter(ast.NodeVisitor):
assertmsg = self.helper("_format_assertmsg", assert_.msg)
explanation = "\n>assert " + explanation
else:
assertmsg = ast.Str("")
assertmsg = astStr("")
explanation = "assert " + explanation
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
msg = self.pop_format_context(template)
fmt = self.helper("_format_explanation", msg)
err_name = ast.Name("AssertionError", ast.Load())
@@ -938,7 +968,7 @@ class AssertionRewriter(ast.NodeVisitor):
# Clear temporary variables by setting them to None.
if self.variables:
variables = [ast.Name(name, ast.Store()) for name in self.variables]
clear = ast.Assign(variables, ast.NameConstant(None))
clear = ast.Assign(variables, astNameConstant(None))
self.statements.append(clear)
# Fix locations (line numbers/column offsets).
for stmt in self.statements:
@@ -952,20 +982,20 @@ class AssertionRewriter(ast.NodeVisitor):
# thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
target_id = name.target.id # type: ignore[attr-defined]
inlocs = ast.Compare(ast.Str(target_id), [ast.In()], [locs])
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), ast.Str(target_id))
expr = ast.IfExp(test, self.display(name), astStr(target_id))
return name, self.explanation_param(expr)
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
# Display the repr of the name if it's a local variable or
# _should_repr_global_name() thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
dorepr = self.helper("_should_repr_global_name", name)
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
expr = ast.IfExp(test, self.display(name), astStr(name.id))
return name, self.explanation_param(expr)
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
@@ -996,12 +1026,14 @@ class AssertionRewriter(ast.NodeVisitor):
]
):
pytest_temp = self.variable()
self.variables_overwrite[v.left.target.id] = pytest_temp
self.variables_overwrite[self.scope][
v.left.target.id
] = v.left # type:ignore[assignment]
v.left.target.id = pytest_temp
self.push_format_context()
res, expl = self.visit(v)
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
expl_format = self.pop_format_context(ast.Str(expl))
expl_format = self.pop_format_context(astStr(expl))
call = ast.Call(app, [expl_format], [])
self.expl_stmts.append(ast.Expr(call))
if i < levels:
@@ -1013,7 +1045,7 @@ class AssertionRewriter(ast.NodeVisitor):
self.statements = body = inner
self.statements = save
self.expl_stmts = fail_save
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
expl = self.pop_format_context(expl_template)
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
@@ -1037,10 +1069,22 @@ class AssertionRewriter(ast.NodeVisitor):
new_args = []
new_kwargs = []
for arg in call.args:
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
self.scope, {}
):
arg = self.variables_overwrite[self.scope][
arg.id
] # type:ignore[assignment]
res, expl = self.visit(arg)
arg_expls.append(expl)
new_args.append(res)
for keyword in call.keywords:
if isinstance(
keyword.value, ast.Name
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
keyword.value = self.variables_overwrite[self.scope][
keyword.value.id
] # type:ignore[assignment]
res, expl = self.visit(keyword.value)
new_kwargs.append(ast.keyword(keyword.arg, res))
if keyword.arg:
@@ -1074,8 +1118,16 @@ class AssertionRewriter(ast.NodeVisitor):
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
self.push_format_context()
# We first check if we have overwritten a variable in the previous assert
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
comp.left.id = self.variables_overwrite[comp.left.id]
if isinstance(
comp.left, ast.Name
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
comp.left = self.variables_overwrite[self.scope][
comp.left.id
] # type:ignore[assignment]
if isinstance(comp.left, namedExpr):
self.variables_overwrite[self.scope][
comp.left.target.id
] = comp.left # type:ignore[assignment]
left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})"
@@ -1093,15 +1145,17 @@ class AssertionRewriter(ast.NodeVisitor):
and next_operand.target.id == left_res.id
):
next_operand.target.id = self.variable()
self.variables_overwrite[left_res.id] = next_operand.target.id
self.variables_overwrite[self.scope][
left_res.id
] = next_operand # type:ignore[assignment]
next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = f"({next_expl})"
results.append(next_res)
sym = BINOP_MAP[op.__class__]
syms.append(ast.Str(sym))
syms.append(astStr(sym))
expl = f"{left_expl} {sym} {next_expl}"
expls.append(ast.Str(expl))
expls.append(astStr(expl))
res_expr = ast.Compare(left_res, [op], [next_res])
self.statements.append(ast.Assign([store_names[i]], res_expr))
left_res, left_expl = next_res, next_expl

View File

@@ -27,7 +27,7 @@ from _pytest.deprecated import check_ispytest
from _pytest.fixtures import fixture
from _pytest.fixtures import FixtureRequest
from _pytest.main import Session
from _pytest.python import Module
from _pytest.nodes import File
from _pytest.python import Package
from _pytest.reports import TestReport
@@ -179,16 +179,22 @@ class Cache:
else:
cache_dir_exists_already = self._cachedir.exists()
path.parent.mkdir(exist_ok=True, parents=True)
except OSError:
self.warn("could not create cache path {path}", path=path, _ispytest=True)
except OSError as exc:
self.warn(
f"could not create cache path {path}: {exc}",
_ispytest=True,
)
return
if not cache_dir_exists_already:
self._ensure_supporting_files()
data = json.dumps(value, ensure_ascii=False, indent=2)
try:
f = path.open("w", encoding="UTF-8")
except OSError:
self.warn("cache could not write path {path}", path=path, _ispytest=True)
except OSError as exc:
self.warn(
f"cache could not write path {path}: {exc}",
_ispytest=True,
)
else:
with f:
f.write(data)
@@ -213,22 +219,30 @@ class LFPluginCollWrapper:
@hookimpl(hookwrapper=True)
def pytest_make_collect_report(self, collector: nodes.Collector):
if isinstance(collector, Session):
if isinstance(collector, (Session, Package)):
out = yield
res: CollectReport = out.get_result()
# Sort any lf-paths to the beginning.
lf_paths = self.lfplugin._last_failed_paths
# Use stable sort to priorize last failed.
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
# Package.path is the __init__.py file, we need the directory.
if isinstance(node, Package):
path = node.path.parent
else:
path = node.path
return path in lf_paths
res.result = sorted(
res.result,
# use stable sort to priorize last failed
key=lambda x: x.path in lf_paths,
key=sort_key,
reverse=True,
)
return
elif isinstance(collector, Module):
elif isinstance(collector, File):
if collector.path in self.lfplugin._last_failed_paths:
out = yield
res = out.get_result()
@@ -266,10 +280,9 @@ class LFPluginCollSkipfiles:
def pytest_make_collect_report(
self, collector: nodes.Collector
) -> Optional[CollectReport]:
# Packages are Modules, but _last_failed_paths only contains
# test-bearing paths and doesn't try to include the paths of their
# packages, so don't filter them.
if isinstance(collector, Module) and not isinstance(collector, Package):
# Packages are Files, but we only want to skip test-bearing Files,
# so don't filter Packages.
if isinstance(collector, File) and not isinstance(collector, Package):
if collector.path not in self.lfplugin._last_failed_paths:
self.lfplugin._skipped_files += 1
@@ -299,9 +312,14 @@ class LFPlugin:
)
def get_last_failed_paths(self) -> Set[Path]:
"""Return a set with all Paths()s of the previously failed nodeids."""
"""Return a set with all Paths of the previously failed nodeids and
their parents."""
rootpath = self.config.rootpath
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
result = set()
for nodeid in self.lastfailed:
path = rootpath / nodeid.split("::")[0]
result.add(path)
result.update(path.parents)
return {x for x in result if x.exists()}
def pytest_report_collectionfinish(self) -> Optional[str]:
@@ -487,7 +505,11 @@ def pytest_addoption(parser: Parser) -> None:
dest="last_failed_no_failures",
choices=("all", "none"),
default="all",
help="Which tests to run with no previously (known) failures",
help="With ``--lf``, determines whether to execute tests when there "
"are no previously (known) failures or when no "
"cached ``lastfailed`` data was found. "
"``all`` (the default) runs the full test suite again. "
"``none`` just emits a message about no known failures and exits successfully.",
)

View File

@@ -241,7 +241,7 @@ class DontReadFromInput(TextIO):
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
def truncate(self, size: Optional[int] = None) -> int:
raise UnsupportedOperation("cannont truncate stdin")
raise UnsupportedOperation("cannot truncate stdin")
def write(self, data: str) -> int:
raise UnsupportedOperation("cannot write to stdin")

View File

@@ -380,15 +380,24 @@ else:
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 the current process's real user id or None if it could not be
determined.
:return: The user id or None if it could not be determined.
"""
# mypy follows the version and platform checking expectation of PEP 484:
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
# Containment checks are too complex for mypy v1.5.0 and cause failure.
if sys.platform == "win32" or sys.platform == "emscripten":
# win32 does not have a getuid() function.
# Emscripten has a return 0 stub.
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
else:
# On other platforms, a return value of -1 is assumed to indicate that
# the current process's real user id could not be determined.
ERROR = -1
uid = os.getuid()
return uid if uid != ERROR else None
# Perform exhaustiveness checking.

View File

@@ -49,7 +49,7 @@ from _pytest._code import ExceptionInfo
from _pytest._code import filter_traceback
from _pytest._io import TerminalWriter
from _pytest.compat import final
from _pytest.compat import importlib_metadata
from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
from _pytest.outcomes import fail
from _pytest.outcomes import Skipped
from _pytest.pathlib import absolutepath
@@ -57,6 +57,7 @@ from _pytest.pathlib import bestrelpath
from _pytest.pathlib import import_path
from _pytest.pathlib import ImportMode
from _pytest.pathlib import resolve_package_path
from _pytest.pathlib import safe_exists
from _pytest.stash import Stash
from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for
@@ -137,7 +138,9 @@ def main(
) -> Union[int, ExitCode]:
"""Perform an in-process test run.
:param args: List of command line arguments.
:param args:
List of command line arguments. If `None` or not given, defaults to reading
arguments directly from the process command line (:data:`sys.argv`).
:param plugins: List of plugin objects to be auto-registered during initialization.
:returns: An exit code.
@@ -442,10 +445,10 @@ class PytestPluginManager(PluginManager):
# so we avoid accessing possibly non-readable attributes
# (see issue #1073).
if not name.startswith("pytest_"):
return
return None
# Ignore names which can not be hooks.
if name == "pytest_plugins":
return
return None
opts = super().parse_hookimpl_opts(plugin, name)
if opts is not None:
@@ -454,9 +457,9 @@ class PytestPluginManager(PluginManager):
method = getattr(plugin, name)
# Consider only actual functions for hooks (#3775).
if not inspect.isroutine(method):
return
return None
# Collect unmarked hooks as long as they have the `pytest_' prefix.
return _get_legacy_hook_marks(
return _get_legacy_hook_marks( # type: ignore[return-value]
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
)
@@ -465,7 +468,7 @@ class PytestPluginManager(PluginManager):
if opts is None:
method = getattr(module_or_class, name)
if name.startswith("pytest_"):
opts = _get_legacy_hook_marks(
opts = _get_legacy_hook_marks( # type: ignore[assignment]
method,
"spec",
("firstresult", "historic"),
@@ -526,7 +529,13 @@ class PytestPluginManager(PluginManager):
# Internal API for local conftest plugin handling.
#
def _set_initial_conftests(
self, namespace: argparse.Namespace, rootpath: Path
self,
args: Sequence[Union[str, Path]],
pyargs: bool,
noconftest: bool,
rootpath: Path,
confcutdir: Optional[Path],
importmode: Union[ImportMode, str],
) -> None:
"""Load initial conftest files given a preparsed "namespace".
@@ -536,27 +545,25 @@ class PytestPluginManager(PluginManager):
common options will not confuse our logic here.
"""
current = Path.cwd()
self._confcutdir = (
absolutepath(current / namespace.confcutdir)
if namespace.confcutdir
else None
)
self._noconftest = namespace.noconftest
self._using_pyargs = namespace.pyargs
testpaths = namespace.file_or_dir
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
self._noconftest = noconftest
self._using_pyargs = pyargs
foundanchor = False
for testpath in testpaths:
path = str(testpath)
for intitial_path in args:
path = str(intitial_path)
# remove node-id syntax
i = path.find("::")
if i != -1:
path = path[:i]
anchor = absolutepath(current / path)
if anchor.exists(): # we found some file object
self._try_load_conftest(anchor, namespace.importmode, rootpath)
# Ensure we do not break if what appears to be an anchor
# is in fact a very long option (#10169, #11394).
if safe_exists(anchor):
self._try_load_conftest(anchor, importmode, rootpath)
foundanchor = True
if not foundanchor:
self._try_load_conftest(current, namespace.importmode, rootpath)
self._try_load_conftest(current, importmode, rootpath)
def _is_in_confcutdir(self, path: Path) -> bool:
"""Whether a path is within the confcutdir.
@@ -1055,9 +1062,10 @@ class Config:
fin()
def get_terminal_writer(self) -> TerminalWriter:
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
"terminalreporter"
)
assert terminalreporter is not None
return terminalreporter._tw
def pytest_cmdline_parse(
@@ -1130,8 +1138,25 @@ class Config:
@hookimpl(trylast=True)
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
# We haven't fully parsed the command line arguments yet, so
# early_config.args it not set yet. But we need it for
# discovering the initial conftests. So "pre-run" the logic here.
# It will be done for real in `parse()`.
args, args_source = early_config._decide_args(
args=early_config.known_args_namespace.file_or_dir,
pyargs=early_config.known_args_namespace.pyargs,
testpaths=early_config.getini("testpaths"),
invocation_dir=early_config.invocation_params.dir,
rootpath=early_config.rootpath,
warn=False,
)
self.pluginmanager._set_initial_conftests(
early_config.known_args_namespace, rootpath=early_config.rootpath
args=args,
pyargs=early_config.known_args_namespace.pyargs,
noconftest=early_config.known_args_namespace.noconftest,
rootpath=early_config.rootpath,
confcutdir=early_config.known_args_namespace.confcutdir,
importmode=early_config.known_args_namespace.importmode,
)
def _initini(self, args: Sequence[str]) -> None:
@@ -1211,6 +1236,49 @@ class Config:
return args
def _decide_args(
self,
*,
args: List[str],
pyargs: List[str],
testpaths: List[str],
invocation_dir: Path,
rootpath: Path,
warn: bool,
) -> Tuple[List[str], ArgsSource]:
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
:param warn: Whether can issue warnings.
"""
if args:
source = Config.ArgsSource.ARGS
result = args
else:
if invocation_dir == rootpath:
source = Config.ArgsSource.TESTPATHS
if pyargs:
result = testpaths
else:
result = []
for path in testpaths:
result.extend(sorted(glob.iglob(path, recursive=True)))
if testpaths and not result:
if warn:
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
)
else:
result = []
if not result:
source = Config.ArgsSource.INCOVATION_DIR
result = [str(invocation_dir)]
return result, source
def _preparse(self, args: List[str], addopts: bool = True) -> None:
if addopts:
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
@@ -1249,8 +1317,11 @@ class Config:
_pytest.deprecated.STRICT_OPTION, stacklevel=2
)
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
confcutdir = str(self.inipath.parent)
if self.known_args_namespace.confcutdir is None:
if self.inipath is not None:
confcutdir = str(self.inipath.parent)
else:
confcutdir = str(self.rootpath)
self.known_args_namespace.confcutdir = confcutdir
try:
self.hook.pytest_load_initial_conftests(
@@ -1356,25 +1427,17 @@ class Config:
self.hook.pytest_cmdline_preparse(config=self, args=args)
self._parser.after_preparse = True # type: ignore
try:
source = Config.ArgsSource.ARGS
args = self._parser.parse_setoption(
args, self.option, namespace=self.option
)
if not args:
if self.invocation_params.dir == self.rootpath:
source = Config.ArgsSource.TESTPATHS
testpaths: List[str] = self.getini("testpaths")
if self.known_args_namespace.pyargs:
args = testpaths
else:
args = []
for path in testpaths:
args.extend(sorted(glob.iglob(path, recursive=True)))
if not args:
source = Config.ArgsSource.INCOVATION_DIR
args = [str(self.invocation_params.dir)]
self.args = args
self.args_source = source
self.args, self.args_source = self._decide_args(
args=args,
pyargs=self.known_args_namespace.pyargs,
testpaths=self.getini("testpaths"),
invocation_dir=self.invocation_params.dir,
rootpath=self.rootpath,
warn=True,
)
except PrintHelp:
pass

View File

@@ -16,6 +16,7 @@ from .exceptions import UsageError
from _pytest.outcomes import fail
from _pytest.pathlib import absolutepath
from _pytest.pathlib import commonpath
from _pytest.pathlib import safe_exists
if TYPE_CHECKING:
from . import Config
@@ -151,14 +152,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
return path
return path.parent
def safe_exists(path: Path) -> bool:
# This can throw on paths that contain characters unrepresentable at the OS level,
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
try:
return path.exists()
except OSError:
return False
# These look like paths but may not exist
possible_paths = (
absolutepath(get_file_part_from_node_id(arg))

View File

@@ -1,5 +1,6 @@
"""Discover and run doctests in modules and test files."""
import bdb
import functools
import inspect
import os
import platform
@@ -536,6 +537,25 @@ class DoctestModule(Module):
tests, obj, name, module, source_lines, globs, seen
)
if sys.version_info < (3, 13):
def _from_module(self, module, object):
"""`cached_property` objects are never considered a part
of the 'current module'. As such they are skipped by doctest.
Here we override `_from_module` to check the underlying
function instead. https://github.com/python/cpython/issues/107995
"""
if hasattr(functools, "cached_property") and isinstance(
object, functools.cached_property
):
object = object.func
# Type ignored because this is a private function.
return super()._from_module(module, object) # type: ignore[misc]
else: # pragma: no cover
pass
if self.path.name == "conftest.py":
module = self.config.pluginmanager._importconftest(
self.path,

View File

@@ -1,8 +1,6 @@
import io
import os
import sys
from typing import Generator
from typing import TextIO
import pytest
from _pytest.config import Config
@@ -11,7 +9,7 @@ from _pytest.nodes import Item
from _pytest.stash import StashKey
fault_handler_stderr_key = StashKey[TextIO]()
fault_handler_stderr_fd_key = StashKey[int]()
fault_handler_originally_enabled_key = StashKey[bool]()
@@ -26,10 +24,9 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_configure(config: Config) -> None:
import faulthandler
stderr_fd_copy = os.dup(get_stderr_fileno())
config.stash[fault_handler_stderr_key] = open(stderr_fd_copy, "w")
config.stash[fault_handler_stderr_fd_key] = os.dup(get_stderr_fileno())
config.stash[fault_handler_originally_enabled_key] = faulthandler.is_enabled()
faulthandler.enable(file=config.stash[fault_handler_stderr_key])
faulthandler.enable(file=config.stash[fault_handler_stderr_fd_key])
def pytest_unconfigure(config: Config) -> None:
@@ -37,9 +34,9 @@ def pytest_unconfigure(config: Config) -> None:
faulthandler.disable()
# Close the dup file installed during pytest_configure.
if fault_handler_stderr_key in config.stash:
config.stash[fault_handler_stderr_key].close()
del config.stash[fault_handler_stderr_key]
if fault_handler_stderr_fd_key in config.stash:
os.close(config.stash[fault_handler_stderr_fd_key])
del config.stash[fault_handler_stderr_fd_key]
if config.stash.get(fault_handler_originally_enabled_key, False):
# Re-enable the faulthandler if it was originally enabled.
faulthandler.enable(file=get_stderr_fileno())
@@ -53,7 +50,7 @@ def get_stderr_fileno() -> int:
if fileno == -1:
raise AttributeError()
return fileno
except (AttributeError, io.UnsupportedOperation):
except (AttributeError, ValueError):
# pytest-xdist monkeypatches sys.stderr with an object that is not an actual file.
# https://docs.python.org/3/library/faulthandler.html#issue-with-file-descriptors
# This is potentially dangerous, but the best we can do.
@@ -67,10 +64,10 @@ def get_timeout_config_value(config: Config) -> float:
@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
timeout = get_timeout_config_value(item.config)
stderr = item.config.stash[fault_handler_stderr_key]
if timeout > 0 and stderr is not None:
if timeout > 0:
import faulthandler
stderr = item.config.stash[fault_handler_stderr_fd_key]
faulthandler.dump_traceback_later(timeout, file=stderr)
try:
yield

View File

@@ -46,6 +46,7 @@ from _pytest.compat import getimfunc
from _pytest.compat import getlocation
from _pytest.compat import is_generator
from _pytest.compat import NOTSET
from _pytest.compat import NotSetType
from _pytest.compat import overload
from _pytest.compat import safe_getattr
from _pytest.config import _PluggyPlugin
@@ -112,16 +113,18 @@ def pytest_sessionstart(session: "Session") -> None:
session._fixturemanager = FixtureManager(session)
def get_scope_package(node, fixturedef: "FixtureDef[object]"):
import pytest
def get_scope_package(
node: nodes.Item,
fixturedef: "FixtureDef[object]",
) -> Optional[Union[nodes.Item, nodes.Collector]]:
from _pytest.python import Package
cls = pytest.Package
current = node
current: Optional[Union[nodes.Item, nodes.Collector]] = node
fixture_package_name = "{}/{}".format(fixturedef.baseid, "__init__.py")
while current and (
type(current) is not cls or fixture_package_name != current.nodeid
not isinstance(current, Package) or fixture_package_name != current.nodeid
):
current = current.parent
current = current.parent # type: ignore[assignment]
if current is None:
return node.session
return current
@@ -434,7 +437,23 @@ class FixtureRequest:
@property
def node(self):
"""Underlying collection node (depends on current request scope)."""
return self._getscopeitem(self._scope)
scope = self._scope
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on FixtureRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def _getnextfixturedef(self, argname: str) -> "FixtureDef[Any]":
fixturedefs = self._arg2fixturedefs.get(argname, None)
@@ -518,11 +537,7 @@ class FixtureRequest:
"""Add finalizer/teardown function to be called without arguments after
the last test within the requesting test context finished execution."""
# XXX usually this method is shadowed by fixturedef specific ones.
self._addfinalizer(finalizer, scope=self.scope)
def _addfinalizer(self, finalizer: Callable[[], object], scope) -> None:
node = self._getscopeitem(scope)
node.addfinalizer(finalizer)
self.node.addfinalizer(finalizer)
def applymarker(self, marker: Union[str, MarkDecorator]) -> None:
"""Apply a marker to a single test function invocation.
@@ -717,28 +732,6 @@ class FixtureRequest:
lines.append("%s:%d: def %s%s" % (p, lineno + 1, factory.__name__, args))
return lines
def _getscopeitem(
self, scope: Union[Scope, "_ScopeName"]
) -> Union[nodes.Item, nodes.Collector]:
if isinstance(scope, str):
scope = Scope(scope)
if scope is Scope.Function:
# This might also be a non-function Item despite its attribute name.
node: Optional[Union[nodes.Item, nodes.Collector]] = self._pyfuncitem
elif scope is Scope.Package:
# FIXME: _fixturedef is not defined on FixtureRequest (this class),
# but on FixtureRequest (a subclass).
node = get_scope_package(self._pyfuncitem, self._fixturedef) # type: ignore[attr-defined]
else:
node = get_scope_node(self._pyfuncitem, scope)
if node is None and scope is Scope.Class:
# Fallback to function item itself.
node = self._pyfuncitem
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(
scope, self._pyfuncitem
)
return node
def __repr__(self) -> str:
return "<FixtureRequest for %r>" % (self.node)
@@ -1593,13 +1586,52 @@ class FixtureManager:
# Separate parametrized setups.
items[:] = reorder_items(items)
@overload
def parsefactories(
self, node_or_obj, nodeid=NOTSET, unittest: bool = False
self,
node_or_obj: nodes.Node,
*,
unittest: bool = ...,
) -> None:
raise NotImplementedError()
@overload
def parsefactories( # noqa: F811
self,
node_or_obj: object,
nodeid: Optional[str],
*,
unittest: bool = ...,
) -> None:
raise NotImplementedError()
def parsefactories( # noqa: F811
self,
node_or_obj: Union[nodes.Node, object],
nodeid: Union[str, NotSetType, None] = NOTSET,
*,
unittest: bool = False,
) -> None:
"""Collect fixtures from a collection node or object.
Found fixtures are parsed into `FixtureDef`s and saved.
If `node_or_object` is a collection node (with an underlying Python
object), the node's object is traversed and the node's nodeid is used to
determine the fixtures' visibilty. `nodeid` must not be specified in
this case.
If `node_or_object` is an object (e.g. a plugin), the object is
traversed and the given `nodeid` is used to determine the fixtures'
visibility. `nodeid` must be specified in this case; None and "" mean
total visibility.
"""
if nodeid is not NOTSET:
holderobj = node_or_obj
else:
holderobj = node_or_obj.obj
assert isinstance(node_or_obj, nodes.Node)
holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
assert isinstance(node_or_obj.nodeid, str)
nodeid = node_or_obj.nodeid
if holderobj in self._holderobjseen:
return

View File

@@ -11,6 +11,7 @@ from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PrintHelp
from _pytest.config.argparsing import Parser
from _pytest.terminal import TerminalReporter
class HelpAction(Action):
@@ -105,7 +106,7 @@ def pytest_cmdline_parse():
if config.option.debug:
# --debug | --debug <file.log> was provided.
path = config.option.debug
debugfile = open(path, "w")
debugfile = open(path, "w", encoding="utf-8")
debugfile.write(
"versions pytest-%s, "
"python-%s\ncwd=%s\nargs=%s\n\n"
@@ -159,7 +160,10 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
def showhelp(config: Config) -> None:
import textwrap
reporter = config.pluginmanager.get_plugin("terminalreporter")
reporter: Optional[TerminalReporter] = config.pluginmanager.get_plugin(
"terminalreporter"
)
assert reporter is not None
tw = reporter._tw
tw.write(config._parser.optparser.format_help())
tw.line()

View File

@@ -21,7 +21,7 @@ if TYPE_CHECKING:
from typing_extensions import Literal
from _pytest._code.code import ExceptionRepr
from _pytest.code import ExceptionInfo
from _pytest._code.code import ExceptionInfo
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import PytestPluginManager
@@ -41,6 +41,7 @@ if TYPE_CHECKING:
from _pytest.reports import TestReport
from _pytest.runner import CallInfo
from _pytest.terminal import TerminalReporter
from _pytest.terminal import TestShortLogReport
from _pytest.compat import LEGACY_PATH
@@ -806,7 +807,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body]
@hookspec(firstresult=True)
def pytest_report_teststatus( # type:ignore[empty-body]
report: Union["CollectReport", "TestReport"], config: "Config"
) -> Tuple[str, str, Union[str, Mapping[str, bool]]]:
) -> "TestShortLogReport | Tuple[str, str, Union[str, Tuple[str, Mapping[str, bool]]]]":
"""Return result-category, shortletter and verbose word for status
reporting.

View File

@@ -502,6 +502,10 @@ class LogXML:
# Local hack to handle xdist report order.
workernode = getattr(report, "node", None)
reporter = self.node_reporters.pop((nodeid, workernode))
for propname, propvalue in report.user_properties:
reporter.add_property(propname, str(propvalue))
if reporter is not None:
reporter.finalize()
@@ -599,9 +603,6 @@ class LogXML:
reporter = self._opentestcase(report)
reporter.write_captured_output(report)
for propname, propvalue in report.user_properties:
reporter.add_property(propname, str(propvalue))
self.finalize(report)
report_wid = getattr(report, "worker_id", None)
report_ii = getattr(report, "item_index", None)

View File

@@ -5,7 +5,11 @@ import os
import re
from contextlib import contextmanager
from contextlib import nullcontext
from datetime import datetime
from datetime import timedelta
from datetime import timezone
from io import StringIO
from logging import LogRecord
from pathlib import Path
from typing import AbstractSet
from typing import Dict
@@ -53,7 +57,25 @@ def _remove_ansi_escape_sequences(text: str) -> str:
return _ANSI_ESCAPE_SEQ.sub("", text)
class ColoredLevelFormatter(logging.Formatter):
class DatetimeFormatter(logging.Formatter):
"""A logging formatter which formats record with
:func:`datetime.datetime.strftime` formatter instead of
:func:`time.strftime` in case of microseconds in format string.
"""
def formatTime(self, record: LogRecord, datefmt=None) -> str:
if datefmt and "%f" in datefmt:
ct = self.converter(record.created)
tz = timezone(timedelta(seconds=ct.tm_gmtoff), ct.tm_zone)
# Construct `datetime.datetime` object from `struct_time`
# and msecs information from `record`
dt = datetime(*ct[0:6], microsecond=round(record.msecs * 1000), tzinfo=tz)
return dt.strftime(datefmt)
# Use `logging.Formatter` for non-microsecond formats
return super().formatTime(record, datefmt)
class ColoredLevelFormatter(DatetimeFormatter):
"""A logging formatter which colorizes the %(levelname)..s part of the
log format passed to __init__."""
@@ -302,7 +324,7 @@ def pytest_addoption(parser: Parser) -> None:
action="append",
default=[],
dest="logger_disable",
help="Disable a logger by name. Can be passed multipe times.",
help="Disable a logger by name. Can be passed multiple times.",
)
@@ -376,11 +398,12 @@ class LogCaptureFixture:
self._initial_handler_level: Optional[int] = None
# Dict of log name -> log level.
self._initial_logger_levels: Dict[Optional[str], int] = {}
self._initial_disabled_logging_level: Optional[int] = None
def _finalize(self) -> None:
"""Finalize the fixture.
This restores the log levels changed by :meth:`set_level`.
This restores the log levels and the disabled logging levels changed by :meth:`set_level`.
"""
# Restore log levels.
if self._initial_handler_level is not None:
@@ -388,6 +411,10 @@ class LogCaptureFixture:
for logger_name, level in self._initial_logger_levels.items():
logger = logging.getLogger(logger_name)
logger.setLevel(level)
# Disable logging at the original disabled logging level.
if self._initial_disabled_logging_level is not None:
logging.disable(self._initial_disabled_logging_level)
self._initial_disabled_logging_level = None
@property
def handler(self) -> LogCaptureHandler:
@@ -453,13 +480,51 @@ class LogCaptureFixture:
"""Reset the list of log records and the captured log text."""
self.handler.clear()
def _force_enable_logging(
self, level: Union[int, str], logger_obj: logging.Logger
) -> int:
"""Enable the desired logging level if the global level was disabled via ``logging.disabled``.
Only enables logging levels greater than or equal to the requested ``level``.
Does nothing if the desired ``level`` wasn't disabled.
:param level:
The logger level caplog should capture.
All logging is enabled if a non-standard logging level string is supplied.
Valid level strings are in :data:`logging._nameToLevel`.
:param logger_obj: The logger object to check.
:return: The original disabled logging level.
"""
original_disable_level: int = logger_obj.manager.disable # type: ignore[attr-defined]
if isinstance(level, str):
# Try to translate the level string to an int for `logging.disable()`
level = logging.getLevelName(level)
if not isinstance(level, int):
# The level provided was not valid, so just un-disable all logging.
logging.disable(logging.NOTSET)
elif not logger_obj.isEnabledFor(level):
# Each level is `10` away from other levels.
# https://docs.python.org/3/library/logging.html#logging-levels
disable_level = max(level - 10, logging.NOTSET)
logging.disable(disable_level)
return original_disable_level
def set_level(self, level: Union[int, str], logger: Optional[str] = None) -> None:
"""Set the level of a logger for the duration of a test.
"""Set the threshold level of a logger for the duration of a test.
Logging messages which are less severe than this level will not be captured.
.. versionchanged:: 3.4
The levels of the loggers changed by this function will be
restored to their initial values at the end of the test.
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
:param level: The level.
:param logger: The logger to update. If not given, the root logger.
"""
@@ -470,6 +535,9 @@ class LogCaptureFixture:
if self._initial_handler_level is None:
self._initial_handler_level = self.handler.level
self.handler.setLevel(level)
initial_disabled_logging_level = self._force_enable_logging(level, logger_obj)
if self._initial_disabled_logging_level is None:
self._initial_disabled_logging_level = initial_disabled_logging_level
@contextmanager
def at_level(
@@ -479,6 +547,8 @@ class LogCaptureFixture:
the end of the 'with' statement the level is restored to its original
value.
Will enable the requested logging level if it was disabled via :meth:`logging.disable`.
:param level: The level.
:param logger: The logger to update. If not given, the root logger.
"""
@@ -487,11 +557,13 @@ class LogCaptureFixture:
logger_obj.setLevel(level)
handler_orig_level = self.handler.level
self.handler.setLevel(level)
original_disable_level = self._force_enable_logging(level, logger_obj)
try:
yield
finally:
logger_obj.setLevel(orig_level)
self.handler.setLevel(handler_orig_level)
logging.disable(original_disable_level)
@fixture
@@ -577,7 +649,7 @@ class LoggingPlugin:
config, "log_file_date_format", "log_date_format"
)
log_file_formatter = logging.Formatter(
log_file_formatter = DatetimeFormatter(
log_file_format, datefmt=log_file_date_format
)
self.log_file_handler.setFormatter(log_file_formatter)
@@ -588,6 +660,8 @@ class LoggingPlugin:
)
if self._log_cli_enabled():
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
# Guaranteed by `_log_cli_enabled()`.
assert terminal_reporter is not None
capture_manager = config.pluginmanager.get_plugin("capturemanager")
# if capturemanager plugin is disabled, live logging still works.
self.log_cli_handler: Union[
@@ -621,7 +695,7 @@ class LoggingPlugin:
create_terminal_writer(self._config), log_format, log_date_format
)
else:
formatter = logging.Formatter(log_format, log_date_format)
formatter = DatetimeFormatter(log_format, log_date_format)
formatter._style = PercentStyleMultiline(
formatter._style._fmt, auto_indent=auto_indent

View File

@@ -36,6 +36,7 @@ from _pytest.outcomes import exit
from _pytest.pathlib import absolutepath
from _pytest.pathlib import bestrelpath
from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import safe_exists
from _pytest.pathlib import visit
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
@@ -122,11 +123,12 @@ def pytest_addoption(parser: Parser) -> None:
)
group._addoption(
"-c",
metavar="file",
"--config-file",
metavar="FILE",
type=str,
dest="inifilename",
help="Load configuration from `file` instead of trying to locate one of the "
"implicit configuration files",
help="Load configuration from `FILE` instead of trying to locate one of the "
"implicit configuration files.",
)
group._addoption(
"--continue-on-collection-errors",
@@ -399,6 +401,12 @@ def pytest_ignore_collect(collection_path: Path, config: Config) -> Optional[boo
allow_in_venv = config.getoption("collect_in_virtualenv")
if not allow_in_venv and _in_venv(collection_path):
return True
if collection_path.is_dir():
norecursepatterns = config.getini("norecursedirs")
if any(fnmatch_ex(pat, collection_path) for pat in norecursepatterns):
return True
return None
@@ -455,6 +463,11 @@ class _bestrelpath_cache(Dict[Path, str]):
@final
class Session(nodes.FSCollector):
"""The root of the collection tree.
``Session`` collects the initial paths given as arguments to pytest.
"""
Interrupted = Interrupted
Failed = Failed
# Set on the session by runner.pytest_sessionstart.
@@ -562,9 +575,6 @@ class Session(nodes.FSCollector):
ihook = self.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True
def _collectfile(
@@ -685,8 +695,8 @@ class Session(nodes.FSCollector):
# are not collected more than once.
matchnodes_cache: Dict[Tuple[Type[nodes.Collector], str], CollectReport] = {}
# Dirnames of pkgs with dunder-init files.
pkg_roots: Dict[str, Package] = {}
# Directories of pkgs with dunder-init files.
pkg_roots: Dict[Path, Package] = {}
for argpath, names in self._initial_parts:
self.trace("processing argument", (argpath, names))
@@ -707,7 +717,7 @@ class Session(nodes.FSCollector):
col = self._collectfile(pkginit, handle_dupes=False)
if col:
if isinstance(col[0], Package):
pkg_roots[str(parent)] = col[0]
pkg_roots[parent] = col[0]
node_cache1[col[0].path] = [col[0]]
# If it's a directory argument, recurse and look for any Subpackages.
@@ -716,7 +726,7 @@ class Session(nodes.FSCollector):
assert not names, f"invalid arg {(argpath, names)!r}"
seen_dirs: Set[Path] = set()
for direntry in visit(str(argpath), self._recurse):
for direntry in visit(argpath, self._recurse):
if not direntry.is_file():
continue
@@ -731,8 +741,8 @@ class Session(nodes.FSCollector):
for x in self._collectfile(pkginit):
yield x
if isinstance(x, Package):
pkg_roots[str(dirpath)] = x
if str(dirpath) in pkg_roots:
pkg_roots[dirpath] = x
if dirpath in pkg_roots:
# Do not collect packages here.
continue
@@ -749,7 +759,7 @@ class Session(nodes.FSCollector):
if argpath in node_cache1:
col = node_cache1[argpath]
else:
collect_root = pkg_roots.get(str(argpath.parent), self)
collect_root = pkg_roots.get(argpath.parent, self)
col = collect_root._collectfile(argpath, handle_dupes=False)
if col:
node_cache1[argpath] = col
@@ -886,7 +896,7 @@ def resolve_collection_argument(
strpath = search_pypath(strpath)
fspath = invocation_path / strpath
fspath = absolutepath(fspath)
if not fspath.exists():
if not safe_exists(fspath):
msg = (
"module or package not found: {arg} (missing __init__.py?)"
if as_pypath

View File

@@ -18,6 +18,7 @@ import ast
import dataclasses
import enum
import re
import sys
import types
from typing import Callable
from typing import Iterator
@@ -26,6 +27,11 @@ from typing import NoReturn
from typing import Optional
from typing import Sequence
if sys.version_info >= (3, 8):
astNameConstant = ast.Constant
else:
astNameConstant = ast.NameConstant
__all__ = [
"Expression",
@@ -132,7 +138,7 @@ IDENT_PREFIX = "$"
def expression(s: Scanner) -> ast.Expression:
if s.accept(TokenType.EOF):
ret: ast.expr = ast.NameConstant(False)
ret: ast.expr = astNameConstant(False)
else:
ret = expr(s)
s.accept(TokenType.EOF, reject=True)

View File

@@ -373,7 +373,9 @@ def get_unpacked_marks(
if not consider_mro:
mark_lists = [obj.__dict__.get("pytestmark", [])]
else:
mark_lists = [x.__dict__.get("pytestmark", []) for x in obj.__mro__]
mark_lists = [
x.__dict__.get("pytestmark", []) for x in reversed(obj.__mro__)
]
mark_list = []
for item in mark_lists:
if isinstance(item, list):

View File

@@ -7,6 +7,7 @@ from contextlib import contextmanager
from typing import Any
from typing import Generator
from typing import List
from typing import Mapping
from typing import MutableMapping
from typing import Optional
from typing import overload
@@ -129,7 +130,7 @@ class MonkeyPatch:
def __init__(self) -> None:
self._setattr: List[Tuple[object, str, object]] = []
self._setitem: List[Tuple[MutableMapping[Any, Any], object, object]] = []
self._setitem: List[Tuple[Mapping[Any, Any], object, object]] = []
self._cwd: Optional[str] = None
self._savesyspath: Optional[List[str]] = None
@@ -290,12 +291,13 @@ class MonkeyPatch:
self._setattr.append((target, name, oldval))
delattr(target, name)
def setitem(self, dic: MutableMapping[K, V], name: K, value: V) -> None:
def setitem(self, dic: Mapping[K, V], name: K, value: V) -> None:
"""Set dictionary entry ``name`` to value."""
self._setitem.append((dic, name, dic.get(name, notset)))
dic[name] = value
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
dic[name] = value # type: ignore[index]
def delitem(self, dic: MutableMapping[K, V], name: K, raising: bool = True) -> None:
def delitem(self, dic: Mapping[K, V], name: K, raising: bool = True) -> None:
"""Delete ``name`` from dict.
Raises ``KeyError`` if it doesn't exist, unless ``raising`` is set to
@@ -306,7 +308,8 @@ class MonkeyPatch:
raise KeyError(name)
else:
self._setitem.append((dic, name, dic.get(name, notset)))
del dic[name]
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
del dic[name] # type: ignore[attr-defined]
def setenv(self, name: str, value: str, prepend: Optional[str] = None) -> None:
"""Set environment variable ``name`` to ``value``.
@@ -401,11 +404,13 @@ class MonkeyPatch:
for dictionary, key, value in reversed(self._setitem):
if value is notset:
try:
del dictionary[key]
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
del dictionary[key] # type: ignore[attr-defined]
except KeyError:
pass # Was already deleted, so we have the desired state.
else:
dictionary[key] = value
# Not all Mapping types support indexing, but MutableMapping doesn't support TypedDict
dictionary[key] = value # type: ignore[index]
self._setitem[:] = []
if self._savesyspath is not None:
sys.path[:] = self._savesyspath

View File

@@ -22,6 +22,7 @@ import _pytest._code
from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback
from _pytest.compat import cached_property
from _pytest.compat import LEGACY_PATH
from _pytest.config import Config
@@ -156,10 +157,11 @@ class NodeMeta(type):
class Node(metaclass=NodeMeta):
"""Base class for Collector and Item, the components of the test
collection tree.
r"""Base class of :class:`Collector` and :class:`Item`, the components of
the test collection tree.
Collector subclasses have children; Items are leaf nodes.
``Collector``\'s are the internal nodes of the tree, and ``Item``\'s are the
leaf nodes.
"""
# Implemented in the legacypath plugin.
@@ -432,8 +434,8 @@ class Node(metaclass=NodeMeta):
assert current is None or isinstance(current, cls)
return current
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
pass
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
return excinfo.traceback
def _repr_failure_py(
self,
@@ -449,13 +451,13 @@ class Node(metaclass=NodeMeta):
style = "value"
if isinstance(excinfo.value, FixtureLookupError):
return excinfo.value.formatrepr()
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]]
if self.config.getoption("fulltrace", False):
style = "long"
tbfilter = False
else:
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
self._prunetraceback(excinfo)
if len(excinfo.traceback) == 0:
excinfo.traceback = tb
tbfilter = self._traceback_filter
if style == "auto":
style = "long"
# XXX should excinfo.getrepr record all data and toterminal() process it?
@@ -486,7 +488,7 @@ class Node(metaclass=NodeMeta):
abspath=abspath,
showlocals=self.config.getoption("showlocals", False),
style=style,
tbfilter=False, # pruned already, or in --fulltrace mode.
tbfilter=tbfilter,
truncate_locals=truncate_locals,
)
@@ -524,15 +526,17 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
class Collector(Node):
"""Collector instances create children through collect() and thus
iteratively build a tree."""
"""Base class of all collectors.
Collector create children through `collect()` and thus iteratively build
the collection tree.
"""
class CollectError(Exception):
"""An error during collection, contains a custom message."""
def collect(self) -> Iterable[Union["Item", "Collector"]]:
"""Return a list of children (items and collectors) for this
collection node."""
"""Collect children (items and collectors) for this collector."""
raise NotImplementedError("abstract")
# TODO: This omits the style= parameter which breaks Liskov Substitution.
@@ -557,13 +561,14 @@ class Collector(Node):
return self._repr_failure_py(excinfo, style=tbstyle)
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
if hasattr(self, "path"):
traceback = excinfo.traceback
ntraceback = traceback.cut(path=self.path)
if ntraceback == traceback:
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
excinfo.traceback = ntraceback.filter()
return excinfo.traceback.filter(excinfo)
return excinfo.traceback
def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[str]:
@@ -575,6 +580,8 @@ def _check_initialpaths_for_relpath(session: "Session", path: Path) -> Optional[
class FSCollector(Collector):
"""Base class for filesystem collectors."""
def __init__(
self,
fspath: Optional[LEGACY_PATH] = None,
@@ -658,7 +665,7 @@ class File(FSCollector):
class Item(Node):
"""A basic test invocation item.
"""Base class of all test invocation items.
Note that for a single function there might be multiple test invocation items.
"""

View File

@@ -6,6 +6,7 @@ import itertools
import os
import shutil
import sys
import types
import uuid
import warnings
from enum import Enum
@@ -26,8 +27,11 @@ from typing import Callable
from typing import Dict
from typing import Iterable
from typing import Iterator
from typing import List
from typing import Optional
from typing import Set
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
@@ -63,21 +67,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
return path.joinpath(".lock")
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
def on_rm_rf_error(
func,
path: str,
excinfo: Union[
BaseException,
Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
],
*,
start_path: Path,
) -> bool:
"""Handle known read-only errors during rmtree.
The returned value is used only by our own tests.
"""
exctype, excvalue = exc[:2]
if isinstance(excinfo, BaseException):
exc = excinfo
else:
exc = excinfo[1]
# Another process removed the file in the middle of the "rm_rf" (xdist for example).
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
if isinstance(excvalue, FileNotFoundError):
if isinstance(exc, FileNotFoundError):
return False
if not isinstance(excvalue, PermissionError):
if not isinstance(exc, PermissionError):
warnings.warn(
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}")
PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
)
return False
@@ -86,7 +102,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
warnings.warn(
PytestWarning(
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
func, path, exctype, excvalue
func, path, type(exc), exc
)
)
)
@@ -149,7 +165,10 @@ def rm_rf(path: Path) -> None:
are read-only."""
path = ensure_extended_length_path(path)
onerror = partial(on_rm_rf_error, start_path=path)
shutil.rmtree(str(path), onerror=onerror)
if sys.version_info >= (3, 12):
shutil.rmtree(str(path), onexc=onerror)
else:
shutil.rmtree(str(path), onerror=onerror)
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:
@@ -335,7 +354,7 @@ def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]:
yield path
def cleanup_dead_symlink(root: Path):
def cleanup_dead_symlinks(root: Path):
for left_dir in root.iterdir():
if left_dir.is_symlink():
if not left_dir.resolve().exists():
@@ -353,7 +372,7 @@ def cleanup_numbered_dir(
for path in root.glob("garbage-*"):
try_cleanup(path, consider_lock_dead_if_created_before)
cleanup_dead_symlink(root)
cleanup_dead_symlinks(root)
def make_numbered_dir_with_cleanup(
@@ -504,6 +523,8 @@ def import_path(
if mode is ImportMode.importlib:
module_name = module_name_from_path(path, root)
with contextlib.suppress(KeyError):
return sys.modules[module_name]
for meta_importer in sys.meta_path:
spec = meta_importer.find_spec(module_name, [str(path.parent)])
@@ -602,6 +623,11 @@ def module_name_from_path(path: Path, root: Path) -> str:
# Use the parts for the relative path to the root path.
path_parts = relative_path.parts
# Module name for packages do not contain the __init__ file, unless
# the `__init__.py` file is at the root.
if len(path_parts) >= 2 and path_parts[-1] == "__init__":
path_parts = path_parts[:-1]
return ".".join(path_parts)
@@ -614,6 +640,9 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
otherwise "src.tests.test_foo" is not importable by ``__import__``.
"""
module_parts = module_name.split(".")
child_module: Union[ModuleType, None] = None
module: Union[ModuleType, None] = None
child_name: str = ""
while module_name:
if module_name not in modules:
try:
@@ -623,13 +652,22 @@ def insert_missing_modules(modules: Dict[str, ModuleType], module_name: str) ->
# ourselves to fall back to creating a dummy module.
if not sys.meta_path:
raise ModuleNotFoundError
importlib.import_module(module_name)
module = importlib.import_module(module_name)
except ModuleNotFoundError:
module = ModuleType(
module_name,
doc="Empty module created by pytest's importmode=importlib.",
)
else:
module = modules[module_name]
if child_module:
# Add child attribute to the parent that can reference the child
# modules.
if not hasattr(module, child_name):
setattr(module, child_name, child_module)
modules[module_name] = module
# Keep track of the child module while moving up the tree.
child_module, child_name = module, module_name.rpartition(".")[-1]
module_parts.pop(-1)
module_name = ".".join(module_parts)
@@ -651,30 +689,38 @@ def resolve_package_path(path: Path) -> Optional[Path]:
return result
def scandir(path: Union[str, "os.PathLike[str]"]) -> List["os.DirEntry[str]"]:
"""Scan a directory recursively, in breadth-first order.
The returned entries are sorted.
"""
entries = []
with os.scandir(path) as s:
# Skip entries with symlink loops and other brokenness, so the caller
# doesn't have to deal with it.
for entry in s:
try:
entry.is_file()
except OSError as err:
if _ignore_error(err):
continue
raise
entries.append(entry)
entries.sort(key=lambda entry: entry.name)
return entries
def visit(
path: Union[str, "os.PathLike[str]"], recurse: Callable[["os.DirEntry[str]"], bool]
) -> Iterator["os.DirEntry[str]"]:
"""Walk a directory recursively, in breadth-first order.
The `recurse` predicate determines whether a directory is recursed.
Entries at each directory level are sorted.
"""
# Skip entries with symlink loops and other brokenness, so the caller doesn't
# have to deal with it.
entries = []
for entry in os.scandir(path):
try:
entry.is_file()
except OSError as err:
if _ignore_error(err):
continue
raise
entries.append(entry)
entries.sort(key=lambda entry: entry.name)
entries = scandir(path)
yield from entries
for entry in entries:
if entry.is_dir() and recurse(entry):
yield from visit(entry.path, recurse)
@@ -746,3 +792,13 @@ def copytree(source: Path, target: Path) -> None:
shutil.copyfile(x, newx)
elif x.is_dir():
newx.mkdir(exist_ok=True)
def safe_exists(p: Path) -> bool:
"""Like Path.exists(), but account for input arguments that might be too long (#11394)."""
try:
return p.exists()
except (ValueError, OSError):
# ValueError: stat: path too long for Windows
# OSError: [WinError 123] The filename, directory name, or volume label syntax is incorrect
return False

View File

@@ -6,6 +6,7 @@ import collections.abc
import contextlib
import gc
import importlib
import locale
import os
import platform
import re
@@ -129,6 +130,7 @@ class LsofFdLeakChecker:
stderr=subprocess.DEVNULL,
check=True,
text=True,
encoding=locale.getpreferredencoding(False),
).stdout
def isopen(line: str) -> bool:
@@ -750,7 +752,7 @@ class Pytester:
def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
"""Create a new :class:`HookRecorder` for a :class:`PytestPluginManager`."""
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True)
pluginmanager.reprec = reprec = HookRecorder(pluginmanager, _ispytest=True) # type: ignore[attr-defined]
self._request.addfinalizer(reprec.finish_recording)
return reprec

View File

@@ -35,6 +35,7 @@ from _pytest._code import filter_traceback
from _pytest._code import getfslineno
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import TerminalRepr
from _pytest._code.code import Traceback
from _pytest._io import TerminalWriter
from _pytest._io.saferepr import saferepr
from _pytest.compat import ascii_escaped
@@ -56,7 +57,6 @@ from _pytest.config import ExitCode
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest
from _pytest.deprecated import FSCOLLECTOR_GETHOOKPROXY_ISINITPATH
from _pytest.deprecated import INSTANCE_COLLECTOR
from _pytest.deprecated import NOSE_SUPPORT_METHOD
from _pytest.fixtures import FuncFixtureInfo
@@ -522,7 +522,7 @@ class PyCollector(PyobjMixin, nodes.Collector):
class Module(nodes.File, PyCollector):
"""Collector for test classes and functions."""
"""Collector for test classes and functions in a Python module."""
def _getobj(self):
return self._importtestmodule()
@@ -659,6 +659,9 @@ class Module(nodes.File, PyCollector):
class Package(Module):
"""Collector for files and directories in a Python packages -- directories
with an `__init__.py` file."""
def __init__(
self,
fspath: Optional[LEGACY_PATH],
@@ -667,7 +670,7 @@ class Package(Module):
config=None,
session=None,
nodeid=None,
path=Optional[Path],
path: Optional[Path] = None,
) -> None:
# NOTE: Could be just the following, but kept as-is for compat.
# nodes.FSCollector.__init__(self, fspath, parent=parent)
@@ -699,14 +702,6 @@ class Package(Module):
func = partial(_call_with_optional_argument, teardown_module, self.obj)
self.addfinalizer(func)
def gethookproxy(self, fspath: "os.PathLike[str]"):
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.gethookproxy(fspath)
def isinitpath(self, path: Union[str, "os.PathLike[str]"]) -> bool:
warnings.warn(FSCOLLECTOR_GETHOOKPROXY_ISINITPATH, stacklevel=2)
return self.session.isinitpath(path)
def _recurse(self, direntry: "os.DirEntry[str]") -> bool:
if direntry.name == "__pycache__":
return False
@@ -714,9 +709,6 @@ class Package(Module):
ihook = self.session.gethookproxy(fspath.parent)
if ihook.pytest_ignore_collect(collection_path=fspath, config=self.config):
return False
norecursepatterns = self.config.getini("norecursedirs")
if any(fnmatch_ex(pat, fspath) for pat in norecursepatterns):
return False
return True
def _collectfile(
@@ -745,11 +737,11 @@ class Package(Module):
def collect(self) -> Iterable[Union[nodes.Item, nodes.Collector]]:
this_path = self.path.parent
init_module = this_path / "__init__.py"
if init_module.is_file() and path_matches_patterns(
init_module, self.config.getini("python_files")
):
yield Module.from_parent(self, path=init_module)
# Always collect the __init__ first.
if path_matches_patterns(self.path, self.config.getini("python_files")):
yield Module.from_parent(self, path=self.path)
pkg_prefixes: Set[Path] = set()
for direntry in visit(str(this_path), recurse=self._recurse):
path = Path(direntry.path)
@@ -799,7 +791,7 @@ def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[o
class Class(PyCollector):
"""Collector for test methods."""
"""Collector for test methods (and nested classes) in a Python class."""
@classmethod
def from_parent(cls, parent, *, name, obj=None, **kw):
@@ -1160,7 +1152,7 @@ class CallSpec2:
arg2scope = self._arg2scope.copy()
for arg, val in zip(argnames, valset):
if arg in params or arg in funcargs:
raise ValueError(f"duplicate {arg!r}")
raise ValueError(f"duplicate parametrization of {arg!r}")
valtype_for_arg = valtypes[arg]
if valtype_for_arg == "params":
params[arg] = val
@@ -1251,8 +1243,9 @@ class Metafunc:
during the collection phase. If you need to setup expensive resources
see about setting indirect to do it rather than at test setup time.
Can be called multiple times, in which case each call parametrizes all
previous parametrizations, e.g.
Can be called multiple times per test function (but only on different
argument names), in which case each call parametrizes all previous
parametrizations, e.g.
::
@@ -1684,7 +1677,7 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
class Function(PyobjMixin, nodes.Item):
"""An Item responsible for setting up and executing a Python test function.
"""Item responsible for setting up and executing a Python test function.
:param name:
The full function name, including any decorations like those
@@ -1801,7 +1794,7 @@ class Function(PyobjMixin, nodes.Item):
def setup(self) -> None:
self._request._fillfixtures()
def _prunetraceback(self, excinfo: ExceptionInfo[BaseException]) -> None:
def _traceback_filter(self, excinfo: ExceptionInfo[BaseException]) -> Traceback:
if hasattr(self, "_obj") and not self.config.getoption("fulltrace", False):
code = _pytest._code.Code.from_function(get_real_func(self.obj))
path, firstlineno = code.path, code.firstlineno
@@ -1813,14 +1806,21 @@ class Function(PyobjMixin, nodes.Item):
ntraceback = ntraceback.filter(filter_traceback)
if not ntraceback:
ntraceback = traceback
ntraceback = ntraceback.filter(excinfo)
excinfo.traceback = ntraceback.filter()
# issue364: mark all but first and last frames to
# only show a single-line message for each frame.
if self.config.getoption("tbstyle", "auto") == "auto":
if len(excinfo.traceback) > 2:
for entry in excinfo.traceback[1:-1]:
entry.set_repr_style("short")
if len(ntraceback) > 2:
ntraceback = Traceback(
entry
if i == 0 or i == len(ntraceback) - 1
else entry.with_repr_style("short")
for i, entry in enumerate(ntraceback)
)
return ntraceback
return excinfo.traceback
# TODO: Type ignored -- breaks Liskov Substitution.
def repr_failure( # type: ignore[override]
@@ -1834,10 +1834,8 @@ class Function(PyobjMixin, nodes.Item):
class FunctionDefinition(Function):
"""
This class is a step gap solution until we evolve to have actual function definition nodes
and manage to get rid of ``metafunc``.
"""
"""This class is a stop gap solution until we evolve to have actual function
definition nodes and manage to get rid of ``metafunc``."""
def runtest(self) -> None:
raise RuntimeError("function definitions are not supposed to be run as tests")

View File

@@ -266,19 +266,20 @@ class ApproxMapping(ApproxBase):
approx_side_as_map.items(), other_side.values()
):
if approx_value != other_value:
max_abs_diff = max(
max_abs_diff, abs(approx_value.expected - other_value)
)
if approx_value.expected == 0.0:
max_rel_diff = math.inf
else:
max_rel_diff = max(
max_rel_diff,
abs(
(approx_value.expected - other_value)
/ approx_value.expected
),
if approx_value.expected is not None and other_value is not None:
max_abs_diff = max(
max_abs_diff, abs(approx_value.expected - other_value)
)
if approx_value.expected == 0.0:
max_rel_diff = math.inf
else:
max_rel_diff = max(
max_rel_diff,
abs(
(approx_value.expected - other_value)
/ approx_value.expected
),
)
different_ids.append(approx_key)
message_data = [
@@ -950,11 +951,7 @@ def raises( # noqa: F811
try:
func(*args[1:], **kwargs)
except expected_exception as e:
# We just caught the exception - there is a traceback.
assert e.__traceback__ is not None
return _pytest._code.ExceptionInfo.from_exc_info(
(type(e), e, e.__traceback__)
)
return _pytest._code.ExceptionInfo.from_exception(e)
fail(message)

View File

@@ -347,10 +347,9 @@ class TestReport(BaseReport):
elif isinstance(excinfo.value, skip.Exception):
outcome = "skipped"
r = excinfo._getreprcrash()
if r is None:
raise ValueError(
"There should always be a traceback entry for skipping a test."
)
assert (
r is not None
), "There should always be a traceback entry for skipping a test."
if excinfo.value._use_item_location:
path, line = item.reportinfo()[:2]
assert line is not None

View File

@@ -8,6 +8,7 @@ import datetime
import inspect
import platform
import sys
import textwrap
import warnings
from collections import Counter
from functools import partial
@@ -20,6 +21,7 @@ from typing import Dict
from typing import Generator
from typing import List
from typing import Mapping
from typing import NamedTuple
from typing import Optional
from typing import Sequence
from typing import Set
@@ -111,6 +113,26 @@ class MoreQuietAction(argparse.Action):
namespace.quiet = getattr(namespace, "quiet", 0) + 1
class TestShortLogReport(NamedTuple):
"""Used to store the test status result category, shortletter and verbose word.
For example ``"rerun", "R", ("RERUN", {"yellow": True})``.
:ivar category:
The class of result, for example ``“passed”``, ``“skipped”``, ``“error”``, or the empty string.
:ivar letter:
The short letter shown as testing progresses, for example ``"."``, ``"s"``, ``"E"``, or the empty string.
:ivar word:
Verbose word is shown as testing progresses in verbose mode, for example ``"PASSED"``, ``"SKIPPED"``,
``"ERROR"``, or the empty string.
"""
category: str
letter: str
word: Union[str, Tuple[str, Mapping[str, bool]]]
def pytest_addoption(parser: Parser) -> None:
group = parser.getgroup("terminal reporting", "Reporting", after="general")
group._addoption(
@@ -426,6 +448,28 @@ class TerminalReporter:
self._tw.line()
self.currentfspath = None
def wrap_write(
self,
content: str,
*,
flush: bool = False,
margin: int = 8,
line_sep: str = "\n",
**markup: bool,
) -> None:
"""Wrap message with margin for progress info."""
width_of_current_line = self._tw.width_of_current_line
wrapped = line_sep.join(
textwrap.wrap(
" " * width_of_current_line + content,
width=self._screen_width - margin,
drop_whitespace=True,
replace_whitespace=False,
),
)
wrapped = wrapped[width_of_current_line:]
self._tw.write(wrapped, flush=flush, **markup)
def write(self, content: str, *, flush: bool = False, **markup: bool) -> None:
self._tw.write(content, flush=flush, **markup)
@@ -525,10 +569,11 @@ class TerminalReporter:
def pytest_runtest_logreport(self, report: TestReport) -> None:
self._tests_ran = True
rep = report
res: Tuple[
str, str, Union[str, Tuple[str, Mapping[str, bool]]]
] = self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
category, letter, word = res
res = TestShortLogReport(
*self.config.hook.pytest_report_teststatus(report=rep, config=self.config)
)
category, letter, word = res.category, res.letter, res.word
if not isinstance(word, tuple):
markup = None
else:
@@ -572,7 +617,7 @@ class TerminalReporter:
formatted_reason = f" ({reason})"
if reason and formatted_reason is not None:
self._tw.write(formatted_reason)
self.wrap_write(formatted_reason)
if self._show_progress_info:
self._write_progress_information_filling_space()
else:

View File

@@ -28,7 +28,7 @@ from .pathlib import LOCK_TIMEOUT
from .pathlib import make_numbered_dir
from .pathlib import make_numbered_dir_with_cleanup
from .pathlib import rm_rf
from .pathlib import cleanup_dead_symlink
from .pathlib import cleanup_dead_symlinks
from _pytest.compat import final, get_user_id
from _pytest.config import Config
from _pytest.config import ExitCode
@@ -100,7 +100,7 @@ class TempPathFactory:
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 intput: {policy}."
f"tmp_path_retention_policy must be either all, failed, none. Current input: {policy}."
)
return cls(
@@ -289,31 +289,30 @@ def tmp_path(
del request.node.stash[tmppath_result_key]
# remove dead symlink
basetemp = tmp_path_factory._basetemp
if basetemp is None:
return
cleanup_dead_symlink(basetemp)
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
if tmp_path_factory._basetemp is None:
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
):
passed_dir = tmp_path_factory._basetemp
if passed_dir.exists():
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(passed_dir, ignore_errors=True)
rmtree(basetemp, ignore_errors=True)
# Remove dead symlinks.
if basetemp.is_dir():
cleanup_dead_symlinks(basetemp)
@hookimpl(tryfirst=True, hookwrapper=True)

View File

@@ -298,6 +298,9 @@ class TestCaseFunction(Function):
def stopTest(self, testcase: "unittest.TestCase") -> None:
pass
def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None:
pass
def runtest(self) -> None:
from _pytest.debugging import maybe_wrap_pytest_function_for_tracing
@@ -331,15 +334,16 @@ class TestCaseFunction(Function):
finally:
delattr(self._testcase, self.name)
def _prunetraceback(
def _traceback_filter(
self, excinfo: _pytest._code.ExceptionInfo[BaseException]
) -> None:
super()._prunetraceback(excinfo)
traceback = excinfo.traceback.filter(
lambda x: not x.frame.f_globals.get("__unittest")
) -> _pytest._code.Traceback:
traceback = super()._traceback_filter(excinfo)
ntraceback = traceback.filter(
lambda x: not x.frame.f_globals.get("__unittest"),
)
if traceback:
excinfo.traceback = traceback
if not ntraceback:
ntraceback = traceback
return ntraceback
@hookimpl(tryfirst=True)

View File

@@ -149,7 +149,7 @@ def warn_explicit_for(method: FunctionType, message: PytestWarning) -> None:
"""
Issue the warning :param:`message` for the definition of the given :param:`method`
this helps to log warnigns for functions defined prior to finding an issue with them
this helps to log warnings for functions defined prior to finding an issue with them
(like hook wrappers being marked in a legacy mechanism)
"""
lineno = method.__code__.co_firstlineno

View File

@@ -62,6 +62,7 @@ from _pytest.reports import TestReport
from _pytest.runner import CallInfo
from _pytest.stash import Stash
from _pytest.stash import StashKey
from _pytest.terminal import TestShortLogReport
from _pytest.tmpdir import TempPathFactory
from _pytest.warning_types import PytestAssertRewriteWarning
from _pytest.warning_types import PytestCacheWarning
@@ -152,6 +153,7 @@ __all__ = [
"TempPathFactory",
"Testdir",
"TestReport",
"TestShortLogReport",
"UsageError",
"WarningsRecorder",
"warns",

View File

@@ -1,7 +1,9 @@
import contextlib
import multiprocessing
import os
import sys
import time
import warnings
from unittest import mock
import pytest
@@ -9,6 +11,14 @@ from py import error
from py.path import local
@contextlib.contextmanager
def ignore_encoding_warning():
with warnings.catch_warnings():
with contextlib.suppress(NameError): # new in 3.10
warnings.simplefilter("ignore", EncodingWarning)
yield
class CommonFSTests:
def test_constructor_equality(self, path1):
p = path1.__class__(path1)
@@ -223,7 +233,8 @@ class CommonFSTests:
assert not (path1 < path1)
def test_simple_read(self, path1):
x = path1.join("samplefile").read("r")
with ignore_encoding_warning():
x = path1.join("samplefile").read("r")
assert x == "samplefile\n"
def test_join_div_operator(self, path1):
@@ -265,12 +276,14 @@ class CommonFSTests:
def test_readlines(self, path1):
fn = path1.join("samplefile")
contents = fn.readlines()
with ignore_encoding_warning():
contents = fn.readlines()
assert contents == ["samplefile\n"]
def test_readlines_nocr(self, path1):
fn = path1.join("samplefile")
contents = fn.readlines(cr=0)
with ignore_encoding_warning():
contents = fn.readlines(cr=0)
assert contents == ["samplefile", ""]
def test_file(self, path1):
@@ -362,8 +375,8 @@ class CommonFSTests:
initpy.copy(copied)
try:
assert copied.check()
s1 = initpy.read()
s2 = copied.read()
s1 = initpy.read_text(encoding="utf-8")
s2 = copied.read_text(encoding="utf-8")
assert s1 == s2
finally:
if copied.check():
@@ -376,8 +389,8 @@ class CommonFSTests:
otherdir.copy(copied)
assert copied.check(dir=1)
assert copied.join("__init__.py").check(file=1)
s1 = otherdir.join("__init__.py").read()
s2 = copied.join("__init__.py").read()
s1 = otherdir.join("__init__.py").read_text(encoding="utf-8")
s2 = copied.join("__init__.py").read_text(encoding="utf-8")
assert s1 == s2
finally:
if copied.check(dir=1):
@@ -463,13 +476,13 @@ def setuptestfs(path):
return
# print "setting up test fs for", repr(path)
samplefile = path.ensure("samplefile")
samplefile.write("samplefile\n")
samplefile.write_text("samplefile\n", encoding="utf-8")
execfile = path.ensure("execfile")
execfile.write("x=42")
execfile.write_text("x=42", encoding="utf-8")
execfilepy = path.ensure("execfile.py")
execfilepy.write("x=42")
execfilepy.write_text("x=42", encoding="utf-8")
d = {1: 2, "hello": "world", "answer": 42}
path.ensure("samplepickle").dump(d)
@@ -481,22 +494,24 @@ def setuptestfs(path):
otherdir.ensure("__init__.py")
module_a = otherdir.ensure("a.py")
module_a.write("from .b import stuff as result\n")
module_a.write_text("from .b import stuff as result\n", encoding="utf-8")
module_b = otherdir.ensure("b.py")
module_b.write('stuff="got it"\n')
module_b.write_text('stuff="got it"\n', encoding="utf-8")
module_c = otherdir.ensure("c.py")
module_c.write(
module_c.write_text(
"""import py;
import otherdir.a
value = otherdir.a.result
"""
""",
encoding="utf-8",
)
module_d = otherdir.ensure("d.py")
module_d.write(
module_d.write_text(
"""import py;
from otherdir import a
value2 = a.result
"""
""",
encoding="utf-8",
)
@@ -534,9 +549,11 @@ def batch_make_numbered_dirs(rootdir, repeats):
for i in range(repeats):
dir_ = local.make_numbered_dir(prefix="repro-", rootdir=rootdir)
file_ = dir_.join("foo")
file_.write("%s" % i)
actual = int(file_.read())
assert actual == i, f"int(file_.read()) is {actual} instead of {i}"
file_.write_text("%s" % i, encoding="utf-8")
actual = int(file_.read_text(encoding="utf-8"))
assert (
actual == i
), f"int(file_.read_text(encoding='utf-8')) is {actual} instead of {i}"
dir_.join(".lock").remove(ignore_errors=True)
return True
@@ -692,14 +709,14 @@ class TestLocalPath(CommonFSTests):
def test_open_and_ensure(self, path1):
p = path1.join("sub1", "sub2", "file")
with p.open("w", ensure=1) as f:
with p.open("w", ensure=1, encoding="utf-8") as f:
f.write("hello")
assert p.read() == "hello"
assert p.read_text(encoding="utf-8") == "hello"
def test_write_and_ensure(self, path1):
p = path1.join("sub1", "sub2", "file")
p.write("hello", ensure=1)
assert p.read() == "hello"
p.write_text("hello", ensure=1, encoding="utf-8")
assert p.read_text(encoding="utf-8") == "hello"
@pytest.mark.parametrize("bin", (False, True))
def test_dump(self, tmpdir, bin):
@@ -770,9 +787,9 @@ class TestLocalPath(CommonFSTests):
newfile = tmpdir.join("test1", "test")
newfile.ensure()
assert newfile.check(file=1)
newfile.write("42")
newfile.write_text("42", encoding="utf-8")
newfile.ensure()
s = newfile.read()
s = newfile.read_text(encoding="utf-8")
assert s == "42"
def test_ensure_filepath_withoutdir(self, tmpdir):
@@ -806,9 +823,9 @@ class TestLocalPath(CommonFSTests):
newfilename = "/test" * 60 # type:ignore[unreachable]
l1 = tmpdir.join(newfilename)
l1.ensure(file=True)
l1.write("foo")
l1.write_text("foo", encoding="utf-8")
l2 = tmpdir.join(newfilename)
assert l2.read() == "foo"
assert l2.read_text(encoding="utf-8") == "foo"
def test_visit_depth_first(self, tmpdir):
tmpdir.ensure("a", "1")
@@ -1278,14 +1295,14 @@ class TestPOSIXLocalPath:
def test_hardlink(self, tmpdir):
linkpath = tmpdir.join("test")
filepath = tmpdir.join("file")
filepath.write("Hello")
filepath.write_text("Hello", encoding="utf-8")
nlink = filepath.stat().nlink
linkpath.mklinkto(filepath)
assert filepath.stat().nlink == nlink + 1
def test_symlink_are_identical(self, tmpdir):
filepath = tmpdir.join("file")
filepath.write("Hello")
filepath.write_text("Hello", encoding="utf-8")
linkpath = tmpdir.join("test")
linkpath.mksymlinkto(filepath)
assert linkpath.readlink() == str(filepath)
@@ -1293,7 +1310,7 @@ class TestPOSIXLocalPath:
def test_symlink_isfile(self, tmpdir):
linkpath = tmpdir.join("test")
filepath = tmpdir.join("file")
filepath.write("")
filepath.write_text("", encoding="utf-8")
linkpath.mksymlinkto(filepath)
assert linkpath.check(file=1)
assert not linkpath.check(link=0, file=1)
@@ -1302,10 +1319,12 @@ class TestPOSIXLocalPath:
def test_symlink_relative(self, tmpdir):
linkpath = tmpdir.join("test")
filepath = tmpdir.join("file")
filepath.write("Hello")
filepath.write_text("Hello", encoding="utf-8")
linkpath.mksymlinkto(filepath, absolute=False)
assert linkpath.readlink() == "file"
assert filepath.read() == linkpath.read()
assert filepath.read_text(encoding="utf-8") == linkpath.read_text(
encoding="utf-8"
)
def test_symlink_not_existing(self, tmpdir):
linkpath = tmpdir.join("testnotexisting")
@@ -1338,7 +1357,7 @@ class TestPOSIXLocalPath:
def test_realpath_file(self, tmpdir):
linkpath = tmpdir.join("test")
filepath = tmpdir.join("file")
filepath.write("")
filepath.write_text("", encoding="utf-8")
linkpath.mksymlinkto(filepath)
realpath = linkpath.realpath()
assert realpath.basename == "file"
@@ -1383,7 +1402,7 @@ class TestPOSIXLocalPath:
atime1 = path.atime()
# we could wait here but timer resolution is very
# system dependent
path.read()
path.read_binary()
time.sleep(ATIME_RESOLUTION)
atime2 = path.atime()
time.sleep(ATIME_RESOLUTION)
@@ -1467,7 +1486,7 @@ class TestPOSIXLocalPath:
test_files = ["a", "b", "c"]
src = tmpdir.join("src")
for f in test_files:
src.join(f).write(f, ensure=True)
src.join(f).write_text(f, ensure=True, encoding="utf-8")
dst = tmpdir.join("dst")
# a small delay before the copy
time.sleep(ATIME_RESOLUTION)
@@ -1521,10 +1540,11 @@ class TestUnicodePy2Py3:
def test_read_write(self, tmpdir):
x = tmpdir.join("hello")
part = "hällo"
x.write(part)
assert x.read() == part
x.write(part.encode(sys.getdefaultencoding()))
assert x.read() == part.encode(sys.getdefaultencoding())
with ignore_encoding_warning():
x.write(part)
assert x.read() == part
x.write(part.encode(sys.getdefaultencoding()))
assert x.read() == part.encode(sys.getdefaultencoding())
class TestBinaryAndTextMethods:

View File

@@ -267,7 +267,7 @@ class TestGeneralUsage:
def test_issue109_sibling_conftests_not_loaded(self, pytester: Pytester) -> None:
sub1 = pytester.mkdir("sub1")
sub2 = pytester.mkdir("sub2")
sub1.joinpath("conftest.py").write_text("assert 0")
sub1.joinpath("conftest.py").write_text("assert 0", encoding="utf-8")
result = pytester.runpytest(sub2)
assert result.ret == ExitCode.NO_TESTS_COLLECTED
sub2.joinpath("__init__.py").touch()
@@ -467,7 +467,7 @@ class TestGeneralUsage:
assert "invalid" in str(excinfo.value)
p = pytester.path.joinpath("test_test_plugins_given_as_strings.py")
p.write_text("def test_foo(): pass")
p.write_text("def test_foo(): pass", encoding="utf-8")
mod = types.ModuleType("myplugin")
monkeypatch.setitem(sys.modules, "myplugin", mod)
assert pytest.main(args=[str(pytester.path)], plugins=["myplugin"]) == 0
@@ -587,7 +587,7 @@ class TestInvocationVariants:
def test_pyargs_importerror(self, pytester: Pytester, monkeypatch) -> None:
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
path = pytester.mkpydir("tpkg")
path.joinpath("test_hello.py").write_text("raise ImportError")
path.joinpath("test_hello.py").write_text("raise ImportError", encoding="utf-8")
result = pytester.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
assert result.ret != 0
@@ -597,10 +597,10 @@ class TestInvocationVariants:
def test_pyargs_only_imported_once(self, pytester: Pytester) -> None:
pkg = pytester.mkpydir("foo")
pkg.joinpath("test_foo.py").write_text(
"print('hello from test_foo')\ndef test(): pass"
"print('hello from test_foo')\ndef test(): pass", encoding="utf-8"
)
pkg.joinpath("conftest.py").write_text(
"def pytest_configure(config): print('configuring')"
"def pytest_configure(config): print('configuring')", encoding="utf-8"
)
result = pytester.runpytest(
@@ -613,7 +613,7 @@ class TestInvocationVariants:
def test_pyargs_filename_looks_like_module(self, pytester: Pytester) -> None:
pytester.path.joinpath("conftest.py").touch()
pytester.path.joinpath("t.py").write_text("def test(): pass")
pytester.path.joinpath("t.py").write_text("def test(): pass", encoding="utf-8")
result = pytester.runpytest("--pyargs", "t.py")
assert result.ret == ExitCode.OK
@@ -622,8 +622,12 @@ class TestInvocationVariants:
monkeypatch.delenv("PYTHONDONTWRITEBYTECODE", False)
path = pytester.mkpydir("tpkg")
path.joinpath("test_hello.py").write_text("def test_hello(): pass")
path.joinpath("test_world.py").write_text("def test_world(): pass")
path.joinpath("test_hello.py").write_text(
"def test_hello(): pass", encoding="utf-8"
)
path.joinpath("test_world.py").write_text(
"def test_world(): pass", encoding="utf-8"
)
result = pytester.runpytest("--pyargs", "tpkg")
assert result.ret == 0
result.stdout.fnmatch_lines(["*2 passed*"])
@@ -662,13 +666,15 @@ class TestInvocationVariants:
ns = d.joinpath("ns_pkg")
ns.mkdir()
ns.joinpath("__init__.py").write_text(
"__import__('pkg_resources').declare_namespace(__name__)"
"__import__('pkg_resources').declare_namespace(__name__)",
encoding="utf-8",
)
lib = ns.joinpath(dirname)
lib.mkdir()
lib.joinpath("__init__.py").touch()
lib.joinpath(f"test_{dirname}.py").write_text(
f"def test_{dirname}(): pass\ndef test_other():pass"
f"def test_{dirname}(): pass\ndef test_other():pass",
encoding="utf-8",
)
# The structure of the test directory is now:
@@ -695,11 +701,15 @@ class TestInvocationVariants:
monkeypatch.chdir("world")
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
# pgk_resources has been deprecated entirely.
# While we could change the test to use implicit namespace packages, seems better
# to still ensure the old declaration via declare_namespace still works.
ignore_w = r"-Wignore:Deprecated call to `pkg_resources.declare_namespace"
ignore_w = (
r"-Wignore:Deprecated call to `pkg_resources.declare_namespace",
r"-Wignore:pkg_resources is deprecated",
)
result = pytester.runpytest(
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", ignore_w
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", *ignore_w
)
assert result.ret == 0
result.stdout.fnmatch_lines(
@@ -750,10 +760,10 @@ class TestInvocationVariants:
lib.mkdir()
lib.joinpath("__init__.py").touch()
lib.joinpath("test_bar.py").write_text(
"def test_bar(): pass\ndef test_other(a_fixture):pass"
"def test_bar(): pass\ndef test_other(a_fixture):pass", encoding="utf-8"
)
lib.joinpath("conftest.py").write_text(
"import pytest\n@pytest.fixture\ndef a_fixture():pass"
"import pytest\n@pytest.fixture\ndef a_fixture():pass", encoding="utf-8"
)
d_local = pytester.mkdir("symlink_root")
@@ -1272,8 +1282,7 @@ def test_tee_stdio_captures_and_live_prints(pytester: Pytester) -> None:
result.stderr.fnmatch_lines(["*@this is stderr@*"])
# now ensure the output is in the junitxml
with open(pytester.path.joinpath("output.xml")) as f:
fullXml = f.read()
fullXml = pytester.path.joinpath("output.xml").read_text(encoding="utf-8")
assert "@this is stdout@\n" in fullXml
assert "@this is stderr@\n" in fullXml
@@ -1299,12 +1308,47 @@ def test_no_brokenpipeerror_message(pytester: Pytester) -> None:
popen.stderr.close()
def test_function_return_non_none_warning(testdir) -> None:
testdir.makepyfile(
def test_function_return_non_none_warning(pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_stuff():
return "something"
"""
)
res = testdir.runpytest()
res = pytester.runpytest()
res.stdout.fnmatch_lines(["*Did you mean to use `assert` instead of `return`?*"])
def test_doctest_and_normal_imports_with_importlib(pytester: Pytester) -> None:
"""
Regression test for #10811: previously import_path with ImportMode.importlib would
not return a module if already in sys.modules, resulting in modules being imported
multiple times, which causes problems with modules that have import side effects.
"""
# Uses the exact reproducer form #10811, given it is very minimal
# and illustrates the problem well.
pytester.makepyfile(
**{
"pmxbot/commands.py": "from . import logging",
"pmxbot/logging.py": "",
"tests/__init__.py": "",
"tests/test_commands.py": """
import importlib
from pmxbot import logging
class TestCommands:
def test_boo(self):
assert importlib.import_module('pmxbot.logging') is logging
""",
}
)
pytester.makeini(
"""
[pytest]
addopts=
--doctest-modules
--import-mode importlib
"""
)
result = pytester.runpytest_subprocess()
result.stdout.fnmatch_lines("*1 passed*")

View File

@@ -11,7 +11,7 @@ from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import _pytest
import _pytest._code
import pytest
from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo
@@ -53,6 +53,20 @@ def test_excinfo_from_exc_info_simple() -> None:
assert info.type == ValueError
def test_excinfo_from_exception_simple() -> None:
try:
raise ValueError
except ValueError as e:
assert e.__traceback__ is not None
info = _pytest._code.ExceptionInfo.from_exception(e)
assert info.type == ValueError
def test_excinfo_from_exception_missing_traceback_assertion() -> None:
with pytest.raises(AssertionError, match=r"must have.*__traceback__"):
_pytest._code.ExceptionInfo.from_exception(ValueError())
def test_excinfo_getstatement():
def g():
raise ValueError
@@ -172,7 +186,7 @@ class TestTraceback_f_g_h:
def test_traceback_filter(self):
traceback = self.excinfo.traceback
ntraceback = traceback.filter()
ntraceback = traceback.filter(self.excinfo)
assert len(ntraceback) == len(traceback) - 1
@pytest.mark.parametrize(
@@ -203,7 +217,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, h)
traceback = excinfo.traceback
ntraceback = traceback.filter()
ntraceback = traceback.filter(excinfo)
print(f"old: {traceback!r}")
print(f"new: {ntraceback!r}")
@@ -276,7 +290,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, fail)
assert excinfo.traceback.recursionindex() is None
def test_traceback_getcrashentry(self):
def test_getreprcrash(self):
def i():
__tracebackhide__ = True
raise ValueError
@@ -292,15 +306,13 @@ class TestTraceback_f_g_h:
g()
excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback
entry = tb.getcrashentry()
assert entry is not None
reprcrash = excinfo._getreprcrash()
assert reprcrash is not None
co = _pytest._code.Code.from_function(h)
assert entry.frame.code.path == co.path
assert entry.lineno == co.firstlineno + 1
assert entry.frame.code.name == "h"
assert reprcrash.path == str(co.path)
assert reprcrash.lineno == co.firstlineno + 1 + 1
def test_traceback_getcrashentry_empty(self):
def test_getreprcrash_empty(self):
def g():
__tracebackhide__ = True
raise ValueError
@@ -310,9 +322,7 @@ class TestTraceback_f_g_h:
g()
excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback
entry = tb.getcrashentry()
assert entry is None
assert excinfo._getreprcrash() is None
def test_excinfo_exconly():
@@ -364,7 +374,7 @@ def test_excinfo_no_sourcecode():
def test_excinfo_no_python_sourcecode(tmp_path: Path) -> None:
# XXX: simplified locally testable version
tmp_path.joinpath("test.txt").write_text("{{ h()}}:")
tmp_path.joinpath("test.txt").write_text("{{ h()}}:", encoding="utf-8")
jinja2 = pytest.importorskip("jinja2")
loader = jinja2.FileSystemLoader(str(tmp_path))
@@ -441,7 +451,7 @@ class TestFormattedExcinfo:
source = textwrap.dedent(source)
modpath = tmp_path.joinpath("mod.py")
tmp_path.joinpath("__init__.py").touch()
modpath.write_text(source)
modpath.write_text(source, encoding="utf-8")
importlib.invalidate_caches()
return import_path(modpath, root=tmp_path)
@@ -614,7 +624,7 @@ raise ValueError()
"""
)
excinfo = pytest.raises(ValueError, mod.func1)
excinfo.traceback = excinfo.traceback.filter()
excinfo.traceback = excinfo.traceback.filter(excinfo)
p = FormattedExcinfo()
reprtb = p.repr_traceback_entry(excinfo.traceback[-1])
@@ -647,7 +657,7 @@ raise ValueError()
"""
)
excinfo = pytest.raises(ValueError, mod.func1, "m" * 90, 5, 13, "z" * 120)
excinfo.traceback = excinfo.traceback.filter()
excinfo.traceback = excinfo.traceback.filter(excinfo)
entry = excinfo.traceback[-1]
p = FormattedExcinfo(funcargs=True)
reprfuncargs = p.repr_args(entry)
@@ -674,7 +684,7 @@ raise ValueError()
"""
)
excinfo = pytest.raises(ValueError, mod.func1, "a", "b", c="d")
excinfo.traceback = excinfo.traceback.filter()
excinfo.traceback = excinfo.traceback.filter(excinfo)
entry = excinfo.traceback[-1]
p = FormattedExcinfo(funcargs=True)
reprfuncargs = p.repr_args(entry)
@@ -948,7 +958,7 @@ raise ValueError()
"""
)
excinfo = pytest.raises(ValueError, mod.f)
excinfo.traceback = excinfo.traceback.filter()
excinfo.traceback = excinfo.traceback.filter(excinfo)
repr = excinfo.getrepr()
repr.toterminal(tw_mock)
assert tw_mock.lines[0] == ""
@@ -982,7 +992,7 @@ raise ValueError()
)
excinfo = pytest.raises(ValueError, mod.f)
tmp_path.joinpath("mod.py").unlink()
excinfo.traceback = excinfo.traceback.filter()
excinfo.traceback = excinfo.traceback.filter(excinfo)
repr = excinfo.getrepr()
repr.toterminal(tw_mock)
assert tw_mock.lines[0] == ""
@@ -1013,8 +1023,8 @@ raise ValueError()
"""
)
excinfo = pytest.raises(ValueError, mod.f)
tmp_path.joinpath("mod.py").write_text("asdf")
excinfo.traceback = excinfo.traceback.filter()
tmp_path.joinpath("mod.py").write_text("asdf", encoding="utf-8")
excinfo.traceback = excinfo.traceback.filter(excinfo)
repr = excinfo.getrepr()
repr.toterminal(tw_mock)
assert tw_mock.lines[0] == ""
@@ -1111,9 +1121,11 @@ raise ValueError()
"""
)
excinfo = pytest.raises(ValueError, mod.f)
excinfo.traceback = excinfo.traceback.filter()
excinfo.traceback[1].set_repr_style("short")
excinfo.traceback[2].set_repr_style("short")
excinfo.traceback = excinfo.traceback.filter(excinfo)
excinfo.traceback = _pytest._code.Traceback(
entry if i not in (1, 2) else entry.with_repr_style("short")
for i, entry in enumerate(excinfo.traceback)
)
r = excinfo.getrepr(style="long")
r.toterminal(tw_mock)
for line in tw_mock.lines:
@@ -1379,7 +1391,7 @@ raise ValueError()
with pytest.raises(TypeError) as excinfo:
mod.f()
# previously crashed with `AttributeError: list has no attribute get`
excinfo.traceback.filter()
excinfo.traceback.filter(excinfo)
@pytest.mark.parametrize("style", ["short", "long"])
@@ -1573,3 +1585,66 @@ def test_exceptiongroup(pytester: Pytester, outer_chain, inner_chain) -> None:
# with py>=3.11 does not depend on exceptiongroup, though there is a toxenv for it
pytest.importorskip("exceptiongroup")
_exceptiongroup_common(pytester, outer_chain, inner_chain, native=False)
@pytest.mark.parametrize("tbstyle", ("long", "short", "auto", "line", "native"))
def test_all_entries_hidden(pytester: Pytester, tbstyle: str) -> None:
"""Regression test for #10903."""
pytester.makepyfile(
"""
def test():
__tracebackhide__ = True
1 / 0
"""
)
result = pytester.runpytest("--tb", tbstyle)
assert result.ret == 1
if tbstyle != "line":
result.stdout.fnmatch_lines(["*ZeroDivisionError: division by zero"])
if tbstyle not in ("line", "native"):
result.stdout.fnmatch_lines(["All traceback entries are hidden.*"])
def test_hidden_entries_of_chained_exceptions_are_not_shown(pytester: Pytester) -> None:
"""Hidden entries of chained exceptions are not shown (#1904)."""
p = pytester.makepyfile(
"""
def g1():
__tracebackhide__ = True
str.does_not_exist
def f3():
__tracebackhide__ = True
1 / 0
def f2():
try:
f3()
except Exception:
g1()
def f1():
__tracebackhide__ = True
f2()
def test():
f1()
"""
)
result = pytester.runpytest(str(p), "--tb=short")
assert result.ret == 1
result.stdout.fnmatch_lines(
[
"*.py:11: in f2",
" f3()",
"E ZeroDivisionError: division by zero",
"",
"During handling of the above exception, another exception occurred:",
"*.py:20: in test",
" f1()",
"*.py:13: in f2",
" g1()",
"E AttributeError:*'does_not_exist'",
],
consecutive=True,
)

View File

@@ -294,7 +294,7 @@ def test_source_of_class_at_eof_without_newline(_sys_snapshot, tmp_path: Path) -
"""
)
path = tmp_path.joinpath("a.py")
path.write_text(str(source))
path.write_text(str(source), encoding="utf-8")
mod: Any = import_path(path, root=tmp_path)
s2 = Source(mod.A)
assert str(source).strip() == str(s2).strip()

View File

@@ -21,6 +21,15 @@ if sys.gettrace():
sys.settrace(orig_trace)
@pytest.fixture(autouse=True)
def set_column_width(monkeypatch: pytest.MonkeyPatch) -> None:
"""
Force terminal width to 80: some tests check the formatting of --help, which is sensible
to terminal width.
"""
monkeypatch.setenv("COLUMNS", "80")
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_collection_modifyitems(items):
"""Prefer faster tests.
@@ -105,7 +114,7 @@ def tw_mock():
@pytest.fixture
def dummy_yaml_custom_test(pytester: Pytester):
def dummy_yaml_custom_test(pytester: Pytester) -> None:
"""Writes a conftest file that collects and executes a dummy yaml test.
Taken from the docs, but stripped down to the bare minimum, useful for

View File

@@ -1,4 +1,7 @@
# mypy: disable-error-code="attr-defined"
# mypy: disallow-untyped-defs
import logging
from typing import Iterator
import pytest
from _pytest.logging import caplog_records_key
@@ -8,12 +11,25 @@ logger = logging.getLogger(__name__)
sublogger = logging.getLogger(__name__ + ".baz")
@pytest.fixture(autouse=True)
def cleanup_disabled_logging() -> Iterator[None]:
"""Simple fixture that ensures that a test doesn't disable logging.
This is necessary because ``logging.disable()`` is global, so a test disabling logging
and not cleaning up after will break every test that runs after it.
This behavior was moved to a fixture so that logging will be un-disabled even if the test fails an assertion.
"""
yield
logging.disable(logging.NOTSET)
def test_fixture_help(pytester: Pytester) -> None:
result = pytester.runpytest("--fixtures")
result.stdout.fnmatch_lines(["*caplog*"])
def test_change_level(caplog):
def test_change_level(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.debug("handler DEBUG level")
logger.info("handler INFO level")
@@ -28,10 +44,27 @@ def test_change_level(caplog):
assert "CRITICAL" in caplog.text
def test_change_level_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
logging.disable(logging.CRITICAL)
assert logging.root.manager.disable == logging.CRITICAL
caplog.set_level(logging.WARNING)
logger.info("handler INFO level")
logger.warning("handler WARNING level")
caplog.set_level(logging.CRITICAL, logger=sublogger.name)
sublogger.warning("logger SUB_WARNING level")
sublogger.critical("logger SUB_CRITICAL level")
assert "INFO" not in caplog.text
assert "WARNING" in caplog.text
assert "SUB_WARNING" not in caplog.text
assert "SUB_CRITICAL" in caplog.text
def test_change_level_undo(pytester: Pytester) -> None:
"""Ensure that 'set_level' is undone after the end of the test.
Tests the logging output themselves (affacted both by logger and handler levels).
Tests the logging output themselves (affected both by logger and handler levels).
"""
pytester.makepyfile(
"""
@@ -54,6 +87,35 @@ def test_change_level_undo(pytester: Pytester) -> None:
result.stdout.no_fnmatch_line("*log from test2*")
def test_change_disabled_level_undo(pytester: Pytester) -> None:
"""Ensure that '_force_enable_logging' in 'set_level' is undone after the end of the test.
Tests the logging output themselves (affected by disabled logging level).
"""
pytester.makepyfile(
"""
import logging
def test1(caplog):
logging.disable(logging.CRITICAL)
caplog.set_level(logging.INFO)
# using + operator here so fnmatch_lines doesn't match the code in the traceback
logging.info('log from ' + 'test1')
assert 0
def test2(caplog):
# using + operator here so fnmatch_lines doesn't match the code in the traceback
# use logging.warning because we need a level that will show up if logging.disabled
# isn't reset to ``CRITICAL`` after test1.
logging.warning('log from ' + 'test2')
assert 0
"""
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"])
result.stdout.no_fnmatch_line("*log from test2*")
def test_change_level_undos_handler_level(pytester: Pytester) -> None:
"""Ensure that 'set_level' is undone after the end of the test (handler).
@@ -82,7 +144,7 @@ def test_change_level_undos_handler_level(pytester: Pytester) -> None:
result.assert_outcomes(passed=3)
def test_with_statement(caplog):
def test_with_statement(caplog: pytest.LogCaptureFixture) -> None:
with caplog.at_level(logging.INFO):
logger.debug("handler DEBUG level")
logger.info("handler INFO level")
@@ -97,7 +159,66 @@ def test_with_statement(caplog):
assert "CRITICAL" in caplog.text
def test_log_access(caplog):
def test_with_statement_logging_disabled(caplog: pytest.LogCaptureFixture) -> None:
logging.disable(logging.CRITICAL)
assert logging.root.manager.disable == logging.CRITICAL
with caplog.at_level(logging.WARNING):
logger.debug("handler DEBUG level")
logger.info("handler INFO level")
logger.warning("handler WARNING level")
logger.error("handler ERROR level")
logger.critical("handler CRITICAL level")
assert logging.root.manager.disable == logging.INFO
with caplog.at_level(logging.CRITICAL, logger=sublogger.name):
sublogger.warning("logger SUB_WARNING level")
sublogger.critical("logger SUB_CRITICAL level")
assert "DEBUG" not in caplog.text
assert "INFO" not in caplog.text
assert "WARNING" in caplog.text
assert "ERROR" in caplog.text
assert " CRITICAL" in caplog.text
assert "SUB_WARNING" not in caplog.text
assert "SUB_CRITICAL" in caplog.text
assert logging.root.manager.disable == logging.CRITICAL
@pytest.mark.parametrize(
"level_str,expected_disable_level",
[
("CRITICAL", logging.ERROR),
("ERROR", logging.WARNING),
("WARNING", logging.INFO),
("INFO", logging.DEBUG),
("DEBUG", logging.NOTSET),
("NOTSET", logging.NOTSET),
("NOTVALIDLEVEL", logging.NOTSET),
],
)
def test_force_enable_logging_level_string(
caplog: pytest.LogCaptureFixture, level_str: str, expected_disable_level: int
) -> None:
"""Test _force_enable_logging using a level string.
``expected_disable_level`` is one level below ``level_str`` because the disabled log level
always needs to be *at least* one level lower than the level that caplog is trying to capture.
"""
test_logger = logging.getLogger("test_str_level_force_enable")
# Emulate a testing environment where all logging is disabled.
logging.disable(logging.CRITICAL)
# Make sure all logging is disabled.
assert not test_logger.isEnabledFor(logging.CRITICAL)
# Un-disable logging for `level_str`.
caplog._force_enable_logging(level_str, test_logger)
# Make sure that the disabled level is now one below the requested logging level.
# We don't use `isEnabledFor` here because that also checks the level set by
# `logging.setLevel()` which is irrelevant to `logging.disable()`.
assert test_logger.manager.disable == expected_disable_level
def test_log_access(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.info("boo %s", "arg")
assert caplog.records[0].levelname == "INFO"
@@ -105,7 +226,7 @@ def test_log_access(caplog):
assert "boo arg" in caplog.text
def test_messages(caplog):
def test_messages(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.info("boo %s", "arg")
logger.info("bar %s\nbaz %s", "arg1", "arg2")
@@ -126,14 +247,14 @@ def test_messages(caplog):
assert "Exception" not in caplog.messages[-1]
def test_record_tuples(caplog):
def test_record_tuples(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.info("boo %s", "arg")
assert caplog.record_tuples == [(__name__, logging.INFO, "boo arg")]
def test_unicode(caplog):
def test_unicode(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.info("")
assert caplog.records[0].levelname == "INFO"
@@ -141,7 +262,7 @@ def test_unicode(caplog):
assert "" in caplog.text
def test_clear(caplog):
def test_clear(caplog: pytest.LogCaptureFixture) -> None:
caplog.set_level(logging.INFO)
logger.info("")
assert len(caplog.records)
@@ -152,7 +273,9 @@ def test_clear(caplog):
@pytest.fixture
def logging_during_setup_and_teardown(caplog):
def logging_during_setup_and_teardown(
caplog: pytest.LogCaptureFixture,
) -> Iterator[None]:
caplog.set_level("INFO")
logger.info("a_setup_log")
yield
@@ -160,7 +283,9 @@ def logging_during_setup_and_teardown(caplog):
assert [x.message for x in caplog.get_records("teardown")] == ["a_teardown_log"]
def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardown):
def test_caplog_captures_for_all_stages(
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
) -> None:
assert not caplog.records
assert not caplog.get_records("call")
logger.info("a_call_log")
@@ -169,25 +294,31 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
# This reaches into private API, don't use this type of thing in real tests!
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
caplog_records = caplog._item.stash[caplog_records_key]
assert set(caplog_records) == {"setup", "call"}
def test_clear_for_call_stage(caplog, logging_during_setup_and_teardown):
def test_clear_for_call_stage(
caplog: pytest.LogCaptureFixture, logging_during_setup_and_teardown: None
) -> None:
logger.info("a_call_log")
assert [x.message for x in caplog.get_records("call")] == ["a_call_log"]
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
caplog_records = caplog._item.stash[caplog_records_key]
assert set(caplog_records) == {"setup", "call"}
caplog.clear()
assert caplog.get_records("call") == []
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
caplog_records = caplog._item.stash[caplog_records_key]
assert set(caplog_records) == {"setup", "call"}
logging.info("a_call_log_after_clear")
assert [x.message for x in caplog.get_records("call")] == ["a_call_log_after_clear"]
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
assert set(caplog._item.stash[caplog_records_key]) == {"setup", "call"}
caplog_records = caplog._item.stash[caplog_records_key]
assert set(caplog_records) == {"setup", "call"}
def test_ini_controls_global_log_level(pytester: Pytester) -> None:

View File

@@ -81,7 +81,7 @@ def test_root_logger_affected(pytester: Pytester) -> None:
# not the info one, because the default level of the root logger is
# WARNING.
assert os.path.isfile(log_file)
with open(log_file) as rfh:
with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read()
assert "info text going to logger" not in contents
assert "warning text going to logger" in contents
@@ -656,7 +656,7 @@ def test_log_file_cli(pytester: Pytester) -> None:
# make sure that we get a '0' exit code for the testsuite
assert result.ret == 0
assert os.path.isfile(log_file)
with open(log_file) as rfh:
with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read()
assert "This log message will be shown" in contents
assert "This log message won't be shown" not in contents
@@ -687,7 +687,7 @@ def test_log_file_cli_level(pytester: Pytester) -> None:
# make sure that we get a '0' exit code for the testsuite
assert result.ret == 0
assert os.path.isfile(log_file)
with open(log_file) as rfh:
with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read()
assert "This log message will be shown" in contents
assert "This log message won't be shown" not in contents
@@ -738,7 +738,7 @@ def test_log_file_ini(pytester: Pytester) -> None:
# make sure that we get a '0' exit code for the testsuite
assert result.ret == 0
assert os.path.isfile(log_file)
with open(log_file) as rfh:
with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read()
assert "This log message will be shown" in contents
assert "This log message won't be shown" not in contents
@@ -777,7 +777,7 @@ def test_log_file_ini_level(pytester: Pytester) -> None:
# make sure that we get a '0' exit code for the testsuite
assert result.ret == 0
assert os.path.isfile(log_file)
with open(log_file) as rfh:
with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read()
assert "This log message will be shown" in contents
assert "This log message won't be shown" not in contents
@@ -985,7 +985,7 @@ def test_log_in_hooks(pytester: Pytester) -> None:
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*sessionstart*", "*runtestloop*", "*sessionfinish*"])
with open(log_file) as rfh:
with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read()
assert "sessionstart" in contents
assert "runtestloop" in contents
@@ -1021,7 +1021,7 @@ def test_log_in_runtest_logreport(pytester: Pytester) -> None:
"""
)
pytester.runpytest()
with open(log_file) as rfh:
with open(log_file, encoding="utf-8") as rfh:
contents = rfh.read()
assert contents.count("logreport") == 3
@@ -1065,11 +1065,11 @@ def test_log_set_path(pytester: Pytester) -> None:
"""
)
pytester.runpytest()
with open(os.path.join(report_dir_base, "test_first")) as rfh:
with open(os.path.join(report_dir_base, "test_first"), encoding="utf-8") as rfh:
content = rfh.read()
assert "message from test 1" in content
with open(os.path.join(report_dir_base, "test_second")) as rfh:
with open(os.path.join(report_dir_base, "test_second"), encoding="utf-8") as rfh:
content = rfh.read()
assert "message from test 2" in content
@@ -1167,8 +1167,8 @@ def test_log_file_cli_subdirectories_are_successfully_created(
assert result.ret == ExitCode.OK
def test_disable_loggers(testdir):
testdir.makepyfile(
def test_disable_loggers(pytester: Pytester) -> None:
pytester.makepyfile(
"""
import logging
import os
@@ -1181,13 +1181,13 @@ def test_disable_loggers(testdir):
assert caplog.record_tuples == [('test', 10, 'Visible text!')]
"""
)
result = testdir.runpytest("--log-disable=disabled", "-s")
result = pytester.runpytest("--log-disable=disabled", "-s")
assert result.ret == ExitCode.OK
assert not result.stderr.lines
def test_disable_loggers_does_not_propagate(testdir):
testdir.makepyfile(
def test_disable_loggers_does_not_propagate(pytester: Pytester) -> None:
pytester.makepyfile(
"""
import logging
import os
@@ -1205,13 +1205,13 @@ def test_disable_loggers_does_not_propagate(testdir):
"""
)
result = testdir.runpytest("--log-disable=parent.child", "-s")
result = pytester.runpytest("--log-disable=parent.child", "-s")
assert result.ret == ExitCode.OK
assert not result.stderr.lines
def test_log_disabling_works_with_log_cli(testdir):
testdir.makepyfile(
def test_log_disabling_works_with_log_cli(pytester: Pytester) -> None:
pytester.makepyfile(
"""
import logging
disabled_log = logging.getLogger('disabled')
@@ -1222,7 +1222,7 @@ def test_log_disabling_works_with_log_cli(testdir):
disabled_log.warning("This string will be suppressed.")
"""
)
result = testdir.runpytest(
result = pytester.runpytest(
"--log-cli-level=DEBUG",
"--log-disable=disabled",
)
@@ -1234,3 +1234,100 @@ def test_log_disabling_works_with_log_cli(testdir):
"WARNING disabled:test_log_disabling_works_with_log_cli.py:7 This string will be suppressed."
)
assert not result.stderr.lines
def test_without_date_format_log(pytester: Pytester) -> None:
"""Check that date is not printed by default."""
pytester.makepyfile(
"""
import logging
logger = logging.getLogger(__name__)
def test_foo():
logger.warning('text')
assert False
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.fnmatch_lines(
["WARNING test_without_date_format_log:test_without_date_format_log.py:6 text"]
)
def test_date_format_log(pytester: Pytester) -> None:
"""Check that log_date_format affects output."""
pytester.makepyfile(
"""
import logging
logger = logging.getLogger(__name__)
def test_foo():
logger.warning('text')
assert False
"""
)
pytester.makeini(
"""
[pytest]
log_format=%(asctime)s; %(levelname)s; %(message)s
log_date_format=%Y-%m-%d %H:%M:%S
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}; WARNING; text"])
def test_date_format_percentf_log(pytester: Pytester) -> None:
"""Make sure that microseconds are printed in log."""
pytester.makepyfile(
"""
import logging
logger = logging.getLogger(__name__)
def test_foo():
logger.warning('text')
assert False
"""
)
pytester.makeini(
"""
[pytest]
log_format=%(asctime)s; %(levelname)s; %(message)s
log_date_format=%Y-%m-%d %H:%M:%S.%f
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.re_match_lines([r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}; WARNING; text"])
def test_date_format_percentf_tz_log(pytester: Pytester) -> None:
"""Make sure that timezone and microseconds are properly formatted together."""
pytester.makepyfile(
"""
import logging
logger = logging.getLogger(__name__)
def test_foo():
logger.warning('text')
assert False
"""
)
pytester.makeini(
"""
[pytest]
log_format=%(asctime)s; %(levelname)s; %(message)s
log_date_format=%Y-%m-%d %H:%M:%S.%f%z
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.re_match_lines(
[r"^[0-9-]{10} [0-9:]{8}.[0-9]{6}[+-][0-9\.]+; WARNING; text"]
)

View File

@@ -1,15 +1,15 @@
anyio[curio,trio]==3.6.2
django==4.1.7
anyio[curio,trio]==3.7.0
django==4.2.2
pytest-asyncio==0.21.0
pytest-bdd==6.1.1
pytest-cov==4.0.0
pytest-cov==4.1.0
pytest-django==4.5.2
pytest-flakes==4.0.5
pytest-html==3.2.0
pytest-mock==3.10.0
pytest-mock==3.11.1
pytest-rerunfailures==11.1.2
pytest-sugar==0.9.5
pytest-sugar==0.9.7
pytest-trio==0.7.0
pytest-twisted==1.14.0
twisted==22.8.0
pytest-xvfb==2.0.0
pytest-xvfb==3.0.0

View File

@@ -122,6 +122,23 @@ class TestApprox:
],
)
assert_approx_raises_regex(
{"a": 1.0, "b": None, "c": None},
{
"a": None,
"b": 1000.0,
"c": None,
},
[
r" comparison failed. Mismatched elements: 2 / 3:",
r" Max absolute difference: -inf",
r" Max relative difference: -inf",
r" Index \| Obtained\s+\| Expected\s+",
rf" a \| {SOME_FLOAT} \| None",
rf" b \| None\s+\| {SOME_FLOAT} ± {SOME_FLOAT}",
],
)
assert_approx_raises_regex(
[1.0, 2.0, 3.0, 4.0],
[1.0, 3.0, 3.0, 5.0],

View File

@@ -60,7 +60,8 @@ class TestModule:
""".format(
str(root2)
)
)
),
encoding="utf-8",
)
with monkeypatch.context() as mp:
mp.chdir(root2)
@@ -832,7 +833,8 @@ class TestConftestCustomization:
mod = outcome.get_result()
mod.obj.hello = "world"
"""
)
),
encoding="utf-8",
)
b.joinpath("test_module.py").write_text(
textwrap.dedent(
@@ -840,7 +842,8 @@ class TestConftestCustomization:
def test_hello():
assert hello == "world"
"""
)
),
encoding="utf-8",
)
reprec = pytester.inline_run()
reprec.assertoutcome(passed=1)
@@ -861,7 +864,8 @@ class TestConftestCustomization:
for func in result:
func._some123 = "world"
"""
)
),
encoding="utf-8",
)
b.joinpath("test_module.py").write_text(
textwrap.dedent(
@@ -874,7 +878,8 @@ class TestConftestCustomization:
def test_hello(obj):
assert obj == "world"
"""
)
),
encoding="utf-8",
)
reprec = pytester.inline_run()
reprec.assertoutcome(passed=1)
@@ -897,25 +902,29 @@ class TestConftestCustomization:
def test_issue2369_collect_module_fileext(self, pytester: Pytester) -> None:
"""Ensure we can collect files with weird file extensions as Python
modules (#2369)"""
# We'll implement a little finder and loader to import files containing
# Implement a little meta path finder to import files containing
# Python source code whose file extension is ".narf".
pytester.makeconftest(
"""
import sys, os, imp
import sys
import os.path
from importlib.util import spec_from_loader
from importlib.machinery import SourceFileLoader
from _pytest.python import Module
class Loader(object):
def load_module(self, name):
return imp.load_source(name, name + ".narf")
class Finder(object):
def find_module(self, name, path=None):
if os.path.exists(name + ".narf"):
return Loader()
sys.meta_path.append(Finder())
class MetaPathFinder:
def find_spec(self, fullname, path, target=None):
if os.path.exists(fullname + ".narf"):
return spec_from_loader(
fullname,
SourceFileLoader(fullname, fullname + ".narf"),
)
sys.meta_path.append(MetaPathFinder())
def pytest_collect_file(file_path, parent):
if file_path.suffix == ".narf":
return Module.from_parent(path=file_path, parent=parent)"""
return Module.from_parent(path=file_path, parent=parent)
"""
)
pytester.makefile(
".narf",
@@ -970,7 +979,8 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
def pytest_runtest_teardown(item):
assert item.path.stem == "test_in_sub1"
"""
)
),
encoding="utf-8",
)
sub2.joinpath("conftest.py").write_text(
textwrap.dedent(
@@ -983,10 +993,11 @@ def test_setup_only_available_in_subdir(pytester: Pytester) -> None:
def pytest_runtest_teardown(item):
assert item.path.stem == "test_in_sub2"
"""
)
),
encoding="utf-8",
)
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass")
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass")
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass", encoding="utf-8")
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass", encoding="utf-8")
result = pytester.runpytest("-v", "-s")
result.assert_outcomes(passed=2)
@@ -1003,9 +1014,9 @@ class TestTracebackCutting:
with pytest.raises(pytest.skip.Exception) as excinfo:
pytest.skip("xxx")
assert excinfo.traceback[-1].frame.code.name == "skip"
assert excinfo.traceback[-1].ishidden()
assert excinfo.traceback[-1].ishidden(excinfo)
assert excinfo.traceback[-2].frame.code.name == "test_skip_simple"
assert not excinfo.traceback[-2].ishidden()
assert not excinfo.traceback[-2].ishidden(excinfo)
def test_traceback_argsetup(self, pytester: Pytester) -> None:
pytester.makeconftest(
@@ -1374,7 +1385,8 @@ def test_skip_duplicates_by_default(pytester: Pytester) -> None:
def test_real():
pass
"""
)
),
encoding="utf-8",
)
result = pytester.runpytest(str(a), str(a))
result.stdout.fnmatch_lines(["*collected 1 item*"])
@@ -1394,7 +1406,8 @@ def test_keep_duplicates(pytester: Pytester) -> None:
def test_real():
pass
"""
)
),
encoding="utf-8",
)
result = pytester.runpytest("--keep-duplicates", str(a), str(a))
result.stdout.fnmatch_lines(["*collected 2 item*"])
@@ -1439,8 +1452,12 @@ def test_package_with_modules(pytester: Pytester) -> None:
sub2_test = sub2.joinpath("test")
sub2_test.mkdir(parents=True)
sub1_test.joinpath("test_in_sub1.py").write_text("def test_1(): pass")
sub2_test.joinpath("test_in_sub2.py").write_text("def test_2(): pass")
sub1_test.joinpath("test_in_sub1.py").write_text(
"def test_1(): pass", encoding="utf-8"
)
sub2_test.joinpath("test_in_sub2.py").write_text(
"def test_2(): pass", encoding="utf-8"
)
# Execute from .
result = pytester.runpytest("-v", "-s")
@@ -1484,9 +1501,11 @@ def test_package_ordering(pytester: Pytester) -> None:
sub2_test = sub2.joinpath("test")
sub2_test.mkdir(parents=True)
root.joinpath("Test_root.py").write_text("def test_1(): pass")
sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass")
sub2_test.joinpath("test_sub2.py").write_text("def test_3(): pass")
root.joinpath("Test_root.py").write_text("def test_1(): pass", encoding="utf-8")
sub1.joinpath("Test_sub1.py").write_text("def test_2(): pass", encoding="utf-8")
sub2_test.joinpath("test_sub2.py").write_text(
"def test_3(): pass", encoding="utf-8"
)
# Execute from .
result = pytester.runpytest("-v", "-s")

View File

@@ -287,7 +287,8 @@ class TestFillFixtures:
def spam():
return 'spam'
"""
)
),
encoding="utf-8",
)
testfile = subdir.joinpath("test_spam.py")
testfile.write_text(
@@ -296,7 +297,8 @@ class TestFillFixtures:
def test_spam(spam):
assert spam == "spam"
"""
)
),
encoding="utf-8",
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 passed*"])
@@ -359,7 +361,8 @@ class TestFillFixtures:
def spam(request):
return request.param
"""
)
),
encoding="utf-8",
)
testfile = subdir.joinpath("test_spam.py")
testfile.write_text(
@@ -371,7 +374,8 @@ class TestFillFixtures:
assert spam == params['spam']
params['spam'] += 1
"""
)
),
encoding="utf-8",
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*3 passed*"])
@@ -403,7 +407,8 @@ class TestFillFixtures:
def spam(request):
return request.param
"""
)
),
encoding="utf-8",
)
testfile = subdir.joinpath("test_spam.py")
testfile.write_text(
@@ -415,7 +420,8 @@ class TestFillFixtures:
assert spam == params['spam']
params['spam'] += 1
"""
)
),
encoding="utf-8",
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*3 passed*"])
@@ -1037,10 +1043,11 @@ class TestRequestBasic:
def arg1():
pass
"""
)
),
encoding="utf-8",
)
p = b.joinpath("test_module.py")
p.write_text("def test_func(arg1): pass")
p.write_text("def test_func(arg1): pass", encoding="utf-8")
result = pytester.runpytest(p, "--fixtures")
assert result.ret == 0
result.stdout.fnmatch_lines(
@@ -1617,7 +1624,8 @@ class TestFixtureManagerParseFactories:
def one():
return 1
"""
)
),
encoding="utf-8",
)
package.joinpath("test_x.py").write_text(
textwrap.dedent(
@@ -1625,7 +1633,8 @@ class TestFixtureManagerParseFactories:
def test_x(one):
assert one == 1
"""
)
),
encoding="utf-8",
)
sub = package.joinpath("sub")
sub.mkdir()
@@ -1638,7 +1647,8 @@ class TestFixtureManagerParseFactories:
def one():
return 2
"""
)
),
encoding="utf-8",
)
sub.joinpath("test_y.py").write_text(
textwrap.dedent(
@@ -1646,7 +1656,8 @@ class TestFixtureManagerParseFactories:
def test_x(one):
assert one == 2
"""
)
),
encoding="utf-8",
)
reprec = pytester.inline_run()
reprec.assertoutcome(passed=2)
@@ -1671,7 +1682,8 @@ class TestFixtureManagerParseFactories:
def teardown_module():
values[:] = []
"""
)
),
encoding="utf-8",
)
package.joinpath("test_x.py").write_text(
textwrap.dedent(
@@ -1680,7 +1692,8 @@ class TestFixtureManagerParseFactories:
def test_x():
assert values == ["package"]
"""
)
),
encoding="utf-8",
)
package = pytester.mkdir("package2")
package.joinpath("__init__.py").write_text(
@@ -1692,7 +1705,8 @@ class TestFixtureManagerParseFactories:
def teardown_module():
values[:] = []
"""
)
),
encoding="utf-8",
)
package.joinpath("test_x.py").write_text(
textwrap.dedent(
@@ -1701,7 +1715,8 @@ class TestFixtureManagerParseFactories:
def test_x():
assert values == ["package2"]
"""
)
),
encoding="utf-8",
)
reprec = pytester.inline_run()
reprec.assertoutcome(passed=2)
@@ -1714,7 +1729,7 @@ class TestFixtureManagerParseFactories:
)
pytester.syspathinsert(pytester.path.name)
package = pytester.mkdir("package")
package.joinpath("__init__.py").write_text("")
package.joinpath("__init__.py").write_text("", encoding="utf-8")
package.joinpath("conftest.py").write_text(
textwrap.dedent(
"""\
@@ -1731,7 +1746,8 @@ class TestFixtureManagerParseFactories:
yield values
values.pop()
"""
)
),
encoding="utf-8",
)
package.joinpath("test_x.py").write_text(
textwrap.dedent(
@@ -1742,7 +1758,8 @@ class TestFixtureManagerParseFactories:
def test_package(one):
assert values == ["package-auto", "package"]
"""
)
),
encoding="utf-8",
)
reprec = pytester.inline_run()
reprec.assertoutcome(passed=2)
@@ -1892,8 +1909,12 @@ class TestAutouseDiscovery:
"""
)
conftest.rename(a.joinpath(conftest.name))
a.joinpath("test_something.py").write_text("def test_func(): pass")
b.joinpath("test_otherthing.py").write_text("def test_func(): pass")
a.joinpath("test_something.py").write_text(
"def test_func(): pass", encoding="utf-8"
)
b.joinpath("test_otherthing.py").write_text(
"def test_func(): pass", encoding="utf-8"
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(
"""
@@ -1939,7 +1960,8 @@ class TestAutouseManagement:
import sys
sys._myapp = "hello"
"""
)
),
encoding="utf-8",
)
sub = pkgdir.joinpath("tests")
sub.mkdir()
@@ -1952,7 +1974,8 @@ class TestAutouseManagement:
def test_app():
assert sys._myapp == "hello"
"""
)
),
encoding="utf-8",
)
reprec = pytester.inline_run("-s")
reprec.assertoutcome(passed=1)
@@ -2882,7 +2905,7 @@ class TestFixtureMarker:
def browser(request):
def finalize():
sys.stdout.write_text('Finalized')
sys.stdout.write_text('Finalized', encoding='utf-8')
request.addfinalizer(finalize)
return {}
"""
@@ -2900,7 +2923,8 @@ class TestFixtureMarker:
def test_browser(browser):
assert browser['visited'] is True
"""
)
),
encoding="utf-8",
)
reprec = pytester.runpytest("-s")
for test in ["test_browser"]:
@@ -3855,7 +3879,8 @@ class TestParameterizedSubRequest:
def fix_with_param(request):
return request.param
"""
)
),
encoding="utf-8",
)
testfile = tests_dir.joinpath("test_foos.py")
@@ -3867,7 +3892,8 @@ class TestParameterizedSubRequest:
def test_foo(request):
request.getfixturevalue('fix_with_param')
"""
)
),
encoding="utf-8",
)
os.chdir(tests_dir)
@@ -4196,7 +4222,7 @@ class TestScopeOrdering:
└── test_2.py
"""
root = pytester.mkdir("root")
root.joinpath("__init__.py").write_text("values = []")
root.joinpath("__init__.py").write_text("values = []", encoding="utf-8")
sub1 = root.joinpath("sub1")
sub1.mkdir()
sub1.joinpath("__init__.py").touch()
@@ -4211,7 +4237,8 @@ class TestScopeOrdering:
yield values
assert values.pop() == "pre-sub1"
"""
)
),
encoding="utf-8",
)
sub1.joinpath("test_1.py").write_text(
textwrap.dedent(
@@ -4220,7 +4247,8 @@ class TestScopeOrdering:
def test_1(fix):
assert values == ["pre-sub1"]
"""
)
),
encoding="utf-8",
)
sub2 = root.joinpath("sub2")
sub2.mkdir()
@@ -4236,7 +4264,8 @@ class TestScopeOrdering:
yield values
assert values.pop() == "pre-sub2"
"""
)
),
encoding="utf-8",
)
sub2.joinpath("test_2.py").write_text(
textwrap.dedent(
@@ -4245,7 +4274,8 @@ class TestScopeOrdering:
def test_2(fix):
assert values == ["pre-sub2"]
"""
)
),
encoding="utf-8",
)
reprec = pytester.inline_run()
reprec.assertoutcome(passed=2)

View File

@@ -1443,7 +1443,8 @@ class TestMetafuncFunctional:
def pytest_generate_tests(metafunc):
assert metafunc.function.__name__ == "test_1"
"""
)
),
encoding="utf-8",
)
sub2.joinpath("conftest.py").write_text(
textwrap.dedent(
@@ -1451,10 +1452,15 @@ class TestMetafuncFunctional:
def pytest_generate_tests(metafunc):
assert metafunc.function.__name__ == "test_2"
"""
)
),
encoding="utf-8",
)
sub1.joinpath("test_in_sub1.py").write_text(
"def test_1(): pass", encoding="utf-8"
)
sub2.joinpath("test_in_sub2.py").write_text(
"def test_2(): pass", encoding="utf-8"
)
sub1.joinpath("test_in_sub1.py").write_text("def test_1(): pass")
sub2.joinpath("test_in_sub2.py").write_text("def test_2(): pass")
result = pytester.runpytest("--keep-duplicates", "-v", "-s", sub1, sub2, sub1)
result.assert_outcomes(passed=3)

View File

@@ -1392,14 +1392,14 @@ def test_sequence_comparison_uses_repr(pytester: Pytester) -> None:
def test_assertrepr_loaded_per_dir(pytester: Pytester) -> None:
pytester.makepyfile(test_base=["def test_base(): assert 1 == 2"])
a = pytester.mkdir("a")
a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2")
a.joinpath("test_a.py").write_text("def test_a(): assert 1 == 2", encoding="utf-8")
a.joinpath("conftest.py").write_text(
'def pytest_assertrepr_compare(): return ["summary a"]'
'def pytest_assertrepr_compare(): return ["summary a"]', encoding="utf-8"
)
b = pytester.mkdir("b")
b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2")
b.joinpath("test_b.py").write_text("def test_b(): assert 1 == 2", encoding="utf-8")
b.joinpath("conftest.py").write_text(
'def pytest_assertrepr_compare(): return ["summary b"]'
'def pytest_assertrepr_compare(): return ["summary b"]', encoding="utf-8"
)
result = pytester.runpytest()

View File

@@ -160,7 +160,8 @@ class TestAssertionRewrite:
"def special_asserter():\n"
" def special_assert(x, y):\n"
" assert x == y\n"
" return special_assert\n"
" return special_assert\n",
encoding="utf-8",
)
pytester.makeconftest('pytest_plugins = ["plugin"]')
pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n")
@@ -173,7 +174,9 @@ class TestAssertionRewrite:
pytester.makepyfile(test_y="x = 1")
xdir = pytester.mkdir("x")
pytester.mkpydir(str(xdir.joinpath("test_Y")))
xdir.joinpath("test_Y").joinpath("__init__.py").write_text("x = 2")
xdir.joinpath("test_Y").joinpath("__init__.py").write_text(
"x = 2", encoding="utf-8"
)
pytester.makepyfile(
"import test_y\n"
"import test_Y\n"
@@ -726,7 +729,7 @@ class TestAssertionRewrite:
class TestRewriteOnImport:
def test_pycache_is_a_file(self, pytester: Pytester) -> None:
pytester.path.joinpath("__pycache__").write_text("Hello")
pytester.path.joinpath("__pycache__").write_text("Hello", encoding="utf-8")
pytester.makepyfile(
"""
def test_rewritten():
@@ -903,7 +906,8 @@ def test_rewritten():
pkg.joinpath("test_blah.py").write_text(
"""
def test_rewritten():
assert "@py_builtins" in globals()"""
assert "@py_builtins" in globals()""",
encoding="utf-8",
)
assert pytester.runpytest().ret == 0
@@ -1066,7 +1070,7 @@ class TestAssertionRewriteHookDetails:
source = tmp_path / "source.py"
pyc = Path(str(source) + "c")
source.write_text("def test(): pass")
source.write_text("def test(): pass", encoding="utf-8")
py_compile.compile(str(source), str(pyc))
contents = pyc.read_bytes()
@@ -1092,7 +1096,7 @@ class TestAssertionRewriteHookDetails:
fn = tmp_path / "source.py"
pyc = Path(str(fn) + "c")
fn.write_text("def test(): assert True")
fn.write_text("def test(): assert True", encoding="utf-8")
source_stat, co = _rewrite_test(fn, config)
_write_pyc(state, co, source_stat, pyc)
@@ -1157,7 +1161,7 @@ class TestAssertionRewriteHookDetails:
return False
def rewrite_self():
with open(__file__, 'w') as self:
with open(__file__, 'w', encoding='utf-8') as self:
self.write('def reloaded(): return True')
""",
test_fun="""
@@ -1187,9 +1191,10 @@ class TestAssertionRewriteHookDetails:
data = pkgutil.get_data('foo.test_foo', 'data.txt')
assert data == b'Hey'
"""
)
),
encoding="utf-8",
)
path.joinpath("data.txt").write_text("Hey")
path.joinpath("data.txt").write_text("Hey", encoding="utf-8")
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*1 passed*"])
@@ -1436,6 +1441,118 @@ class TestIssue10743:
assert result.ret == 0
@pytest.mark.skipif(
sys.version_info < (3, 8), reason="walrus operator not available in py<38"
)
class TestIssue11028:
def test_assertion_walrus_operator_in_operand(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_in_string():
assert (obj := "foo") in obj
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_in_operand_json_dumps(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
import json
def test_json_encoder():
assert (obj := "foo") in json.dumps(obj)
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_equals_operand_function(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def f(a):
return a
def test_call_other_function_arg():
assert (obj := "foo") == f(obj)
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_equals_operand_function_keyword_arg(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def f(a='test'):
return a
def test_call_other_function_k_arg():
assert (obj := "foo") == f(a=obj)
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_equals_operand_function_arg_as_function(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def f(a='test'):
return a
def test_function_of_function():
assert (obj := "foo") == f(f(obj))
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_gt_operand_function(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def add_one(a):
return a + 1
def test_gt():
assert (obj := 4) > add_one(obj)
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.fnmatch_lines(["*assert 4 > 5", "*where 5 = add_one(4)"])
class TestIssue11239:
@pytest.mark.skipif(sys.version_info[:2] <= (3, 7), reason="Only Python 3.8+")
def test_assertion_walrus_different_test_cases(self, pytester: Pytester) -> None:
"""Regression for (#11239)
Walrus operator rewriting would leak to separate test cases if they used the same variables.
"""
pytester.makepyfile(
"""
def test_1():
state = {"x": 2}.get("x")
assert state is not None
def test_2():
db = {"x": 2}
assert (state := db.get("x")) is not None
"""
)
result = pytester.runpytest()
assert result.ret == 0
@pytest.mark.skipif(
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
)

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