Compare commits

...

91 Commits

Author SHA1 Message Date
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
Kodi Arfer
3683722bcb FormattedExcinfo.get_source: avoid crash when line number is out-of-bounds/negative
pytest could crash given pathological AST position attributes, which shouldn't happen when testing real Python code, but could happen when testing AST produced by e.g. Hylang.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Update .pre-commit-config.yaml

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Bruno Oliveira <nicoddemus@gmail.com>
2023-02-07 19:30:33 -03:00
Mahesh Vashishtha
af99040123 Add a note about -W vs filterwarnings. (#10713)
Closes #10687

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

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

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

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

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

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

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

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

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

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

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

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

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

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

This notably does not work around an edge case where the parent path of the XML could be created as a file instead of a folder or link. That vanishingly rare case should cause file creation to fail on the next line, with a fairly obvious exception message.
2023-01-06 09:12:24 -03:00
Bruno Oliveira
6bf7f55555 Merge pull request #10632 from danigm/fix-tests
Fix tests pygments 2.14.0
2023-01-05 12:59:24 -03:00
Daniel Garcia Moreno
61f70a5a75 Fix tests pygments 2.14.0
Fix https://github.com/pytest-dev/pytest/issues/10630
2023-01-04 10:30:28 +01:00
Bruno Oliveira
326ae0cd88 Merge pull request #10609 from yusuke-kadowaki/default_policy_all
Change the default `tmp_path_retention_policy` to `all`
2022-12-25 14:09:45 -03:00
Yusuke Kadowaki
10220d3f31 Change the default policy to all 2022-12-25 00:18:38 +09:00
95 changed files with 3415 additions and 1943 deletions

View File

@@ -28,25 +28,30 @@ jobs:
fetch-depth: 0
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v4
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5
- name: Download Package
uses: actions/download-artifact@v3
with:
python-version: "3.7"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install --upgrade build tox
- name: Build package
run: |
python -m build
name: Packages
path: dist
- name: Publish package to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.pypi_token }}
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.7"
- name: Install tox
run: |
python -m pip install --upgrade pip
pip install --upgrade tox
- name: Publish GitHub release notes
env:
GH_RELEASE_NOTES_TOKEN: ${{ github.token }}

View File

@@ -18,6 +18,11 @@ on:
env:
PYTEST_ADDOPTS: "--color=yes"
# Cancel running jobs for the same workflow and branch.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Set permissions at the job level.
permissions: {}
@@ -189,3 +194,10 @@ jobs:
fail_ci_if_error: true
files: ./coverage.xml
verbose: true
check-package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build and Check Package
uses: hynek/build-and-inspect-python-package@v1.5

View File

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

View File

@@ -2,15 +2,15 @@ default_language_version:
python: "3.10"
repos:
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 23.3.0
hooks:
- id: black
args: [--safe, --quiet]
- repo: https://github.com/asottile/blacken-docs
rev: v1.12.1
rev: 1.13.0
hooks:
- id: blacken-docs
additional_dependencies: [black==20.8b1]
additional_dependencies: [black==23.1.0]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
@@ -23,7 +23,7 @@ repos:
exclude: _pytest/(debugging|hookspec).py
language_version: python3
- repo: https://github.com/PyCQA/autoflake
rev: v2.0.0
rev: v2.0.2
hooks:
- id: autoflake
name: autoflake
@@ -54,11 +54,11 @@ repos:
- id: setup-cfg-fmt
args: ["--max-py-version=3.11", "--include-version-classifiers"]
- repo: https://github.com/pre-commit/pygrep-hooks
rev: v1.9.0
rev: v1.10.0
hooks:
- id: python-use-type-annotations
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.991
rev: v1.1.1
hooks:
- id: mypy
files: ^(src/|testing/)

10
AUTHORS
View File

@@ -12,6 +12,7 @@ Adam Uhlir
Ahn Ki-Wook
Akiomi Kamakura
Alan Velasco
Alessio Izzo
Alexander Johnson
Alexander King
Alexei Kozlenok
@@ -127,6 +128,7 @@ Erik M. Bray
Evan Kepner
Fabien Zarifian
Fabio Zadrozny
Felix Hofstätter
Felix Nieuwenhuizen
Feng Ma
Florian Bruhin
@@ -161,6 +163,7 @@ Ionuț Turturică
Itxaso Aizpurua
Iwan Briquemont
Jaap Broekhuizen
Jake VanderPlas
Jakob van Santen
Jakub Mitoraj
James Bourbeau
@@ -290,12 +293,14 @@ Prashant Sharma
Pulkit Goyal
Punyashloka Biswal
Quentin Pradet
q0w
Ralf Schmitt
Ralph Giles
Ram Rachum
Ran Benita
Raphael Castaneda
Raphael Pierzina
Rafal Semik
Raquel Alegre
Ravi Chandra
Robert Holt
@@ -315,6 +320,7 @@ Samuel Searles-Bryant
Samuele Pedroni
Sanket Duthade
Sankt Petersbug
Saravanan Padmanaban
Segev Finer
Serhii Mozghovyi
Seth Junot
@@ -328,6 +334,7 @@ Srinivas Reddy Thatiparthy
Stefan Farmbauer
Stefan Scherfke
Stefan Zimmermann
Stefanie Molin
Stefano Taschini
Steffen Allner
Stephan Obermann
@@ -346,6 +353,7 @@ Thomas Grainger
Thomas Hisch
Tim Hoffmann
Tim Strazny
TJ Bruno
Tobias Diez
Tom Dalton
Tom Viner
@@ -376,7 +384,9 @@ Wouter van Ackooy
Xixi Zhao
Xuan Luong
Xuecong Liao
Yannick Péroux
Yoav Caspi
Yuliang Shao
Yusuke Kadowaki
Yuval Shimon
Zac Hatfield-Dodds

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 @@
Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12.

View File

@@ -1 +0,0 @@
If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location.

View File

@@ -1 +0,0 @@
Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows.

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,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,2 +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.
The default behavior has changed to keep only directories for failed tests, equivalent to `tmp_path_retention_policy="failed"`.

View File

@@ -6,6 +6,9 @@ Release announcements
:maxdepth: 2
release-7.3.0
release-7.2.2
release-7.2.1
release-7.2.0
release-7.1.3
release-7.1.2

View File

@@ -0,0 +1,25 @@
pytest-7.2.1
=======================================
pytest 7.2.1 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Anthony Sottile
* Bruno Oliveira
* Daniel Valenzuela
* Kadino
* Prerak Patel
* Ronny Pfannschmidt
* Santiago Castro
* s-padmanaban
Happy testing,
The pytest Development Team

View File

@@ -0,0 +1,25 @@
pytest-7.2.2
=======================================
pytest 7.2.2 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
Thanks to all of the contributors to this release:
* Bruno Oliveira
* Garvit Shubham
* Mahesh Vashishtha
* Ramsey
* Ronny Pfannschmidt
* Teejay
* q0w
* vin01
Happy testing,
The pytest Development Team

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

@@ -33,25 +33,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
Values can be any object handled by the json stdlib module.
capsys -- .../_pytest/capture.py:905
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsys.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``text`` objects.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
print("hello")
captured = capsys.readouterr()
assert captured.out == "hello\n"
capsysbinary -- .../_pytest/capture.py:933
capsysbinary -- .../_pytest/capture.py:1001
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsysbinary.readouterr()``
@@ -69,7 +51,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capsysbinary.readouterr()
assert captured.out == b"hello\n"
capfd -- .../_pytest/capture.py:961
capfd -- .../_pytest/capture.py:1029
Enable text capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
@@ -87,7 +69,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capfd.readouterr()
assert captured.out == "hello\n"
capfdbinary -- .../_pytest/capture.py:989
capfdbinary -- .../_pytest/capture.py:1057
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
The captured output is made available via ``capfd.readouterr()`` method
@@ -105,7 +87,25 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
captured = capfdbinary.readouterr()
assert captured.out == b"hello\n"
doctest_namespace [session scope] -- .../_pytest/doctest.py:738
capsys -- .../_pytest/capture.py:973
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
The captured output is made available via ``capsys.readouterr()`` method
calls, which return a ``(out, err)`` namedtuple.
``out`` and ``err`` will be ``text`` objects.
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
Example:
.. code-block:: python
def test_output(capsys):
print("hello")
captured = capsys.readouterr()
assert captured.out == "hello\n"
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
Fixture that returns a :py:class:`dict` that will be injected into the
namespace of doctests.
@@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
For more details: :ref:`doctest_namespace`.
pytestconfig [session scope] -- .../_pytest/fixtures.py:1351
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
Session-scoped fixture that returns the session's :class:`pytest.Config`
object.
@@ -196,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
caplog -- .../_pytest/logging.py:491
caplog -- .../_pytest/logging.py:498
Access and control log capturing.
Captured logs are available through the following properties/methods::
@@ -237,17 +237,19 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
on warning categories.
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:188
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:245
Return a :class:`pytest.TempPathFactory` instance for the test session.
tmp_path -- .../_pytest/tmpdir.py:203
tmp_path -- .../_pytest/tmpdir.py:260
Return a temporary directory path object which is unique to each test
function invocation, created as a sub directory of the base temporary
directory.
By default, a new base temporary directory is created each test session,
and old bases are removed after 3 sessions, to aid in debugging. If
``--basetemp`` is used then it is cleared each session. See :ref:`base
and old bases are removed after 3 sessions, to aid in debugging.
This behavior can be configured with :confval:`tmp_path_retention_count` and
:confval:`tmp_path_retention_policy`.
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.
The returned object is a :class:`pathlib.Path` object.

View File

@@ -28,6 +28,133 @@ with advance notice in the **Deprecations** section of releases.
.. towncrier release notes start
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.
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 depends on the `attrs` package (don't worry, nice diffs for attrs classes are still supported).
pytest 7.2.2 (2023-03-03)
=========================
Bug Fixes
---------
- `#10533 <https://github.com/pytest-dev/pytest/issues/10533>`_: Fixed :func:`pytest.approx` handling of dictionaries containing one or more values of `0.0`.
- `#10592 <https://github.com/pytest-dev/pytest/issues/10592>`_: Fixed crash if `--cache-show` and `--help` are passed at the same time.
- `#10597 <https://github.com/pytest-dev/pytest/issues/10597>`_: Fixed bug where a fixture method named ``teardown`` would be called as part of ``nose`` teardown stage.
- `#10626 <https://github.com/pytest-dev/pytest/issues/10626>`_: Fixed crash if ``--fixtures`` and ``--help`` are passed at the same time.
- `#10660 <https://github.com/pytest-dev/pytest/issues/10660>`_: Fixed :py:func:`pytest.raises` to return a 'ContextManager' so that type-checkers could narrow
:code:`pytest.raises(...) if ... else nullcontext()` down to 'ContextManager' rather than 'object'.
Improved Documentation
----------------------
- `#10690 <https://github.com/pytest-dev/pytest/issues/10690>`_: Added `CI` and `BUILD_NUMBER` environment variables to the documentation.
- `#10721 <https://github.com/pytest-dev/pytest/issues/10721>`_: Fixed entry-points declaration in the documentation example using Hatch.
- `#10753 <https://github.com/pytest-dev/pytest/issues/10753>`_: Changed wording of the module level skip to be very explicit
about not collecting tests and not executing the rest of the module.
pytest 7.2.1 (2023-01-13)
=========================
Bug Fixes
---------
- `#10452 <https://github.com/pytest-dev/pytest/issues/10452>`_: Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12.
- `#10457 <https://github.com/pytest-dev/pytest/issues/10457>`_: If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location.
- `#10506 <https://github.com/pytest-dev/pytest/issues/10506>`_: Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows.
- `#10607 <https://github.com/pytest-dev/pytest/issues/10607>`_: Fix a race condition when creating junitxml reports, which could occur when multiple instances of pytest execute in parallel.
- `#10641 <https://github.com/pytest-dev/pytest/issues/10641>`_: Fix a race condition when creating or updating the stepwise plugin's cache, which could occur when multiple xdist worker nodes try to simultaneously update the stepwise plugin's cache.
pytest 7.2.0 (2022-10-23)
=========================

View File

@@ -1052,7 +1052,7 @@ that are then turned into proper test methods. Example:
.. code-block:: python
def check(x, y):
assert x ** x == y
assert x**x == y
def test_squared():
@@ -1067,7 +1067,7 @@ This form of test function doesn't support fixtures properly, and users should s
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
def test_squared(x, y):
assert x ** x == y
assert x**x == y
.. _internal classes accessed through node deprecated:

View File

@@ -17,7 +17,7 @@ def b(a, order):
@pytest.fixture
def c(a, b, order):
def c(b, order):
order.append("c")

View File

@@ -504,9 +504,9 @@ Running it results in some skips if we don't have all the python interpreters in
. $ pytest -rs -q multipython.py
sssssssssssssssssssssssssss [100%]
========================= short test summary info ==========================
SKIPPED [9] multipython.py:29: 'python3.5' not found
SKIPPED [9] multipython.py:29: 'python3.6' not found
SKIPPED [9] multipython.py:29: 'python3.7' not found
SKIPPED [9] multipython.py:69: 'python3.5' not found
SKIPPED [9] multipython.py:69: 'python3.6' not found
SKIPPED [9] multipython.py:69: 'python3.7' not found
27 skipped in 0.12s
Indirect parametrization of optional implementations/imports
@@ -574,7 +574,7 @@ If you run this with reporting for skips enabled:
test_module.py .s [100%]
========================= short test summary info ==========================
SKIPPED [1] conftest.py:12: could not import 'opt2': No module named 'opt2'
SKIPPED [1] test_module.py:3: could not import 'opt2': No module named 'opt2'
======================= 1 passed, 1 skipped in 0.12s =======================
You'll see that we don't have an ``opt2`` module and thus the second test run

View File

@@ -148,7 +148,8 @@ The test collection would look like this:
$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini
rootdir: /home/sweet/project
configfile: pytest.ini
collected 2 items
<Module check_myapp.py>
@@ -209,7 +210,8 @@ You can always peek at the collection tree without running tests like this:
. $ pytest --collect-only pythoncollection.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini
rootdir: /home/sweet/project
configfile: pytest.ini
collected 3 items
<Module CWD/pythoncollection.py>
@@ -290,7 +292,8 @@ file will be left out:
$ pytest --collect-only
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini
rootdir: /home/sweet/project
configfile: pytest.ini
collected 0 items
======================= no tests collected in 0.12s ========================

View File

@@ -144,7 +144,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E 1
E 1...
E
E ...Full output truncated (7 lines hidden), use '-vv' to show
E ...Full output truncated (6 lines hidden), use '-vv' to show
failure_demo.py:60: AssertionError
_________________ TestSpecialisedExplanations.test_eq_list _________________
@@ -184,9 +184,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E Left contains 1 more item:
E {'c': 0}
E Right contains 1 more item:
E {'d': 0}...
E
E ...Full output truncated (2 lines hidden), use '-vv' to show
E {'d': 0}
E Use -v to get more diff
failure_demo.py:71: AssertionError
_________________ TestSpecialisedExplanations.test_eq_set __________________
@@ -195,16 +194,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
def test_eq_set(self):
> assert {0, 10, 11, 12} == {0, 20, 21}
E AssertionError: assert {0, 10, 11, 12} == {0, 20, 21}
E assert {0, 10, 11, 12} == {0, 20, 21}
E Extra items in the left set:
E 10
E 11
E 12
E Extra items in the right set:
E 20
E 21...
E
E ...Full output truncated (2 lines hidden), use '-vv' to show
E 21
E Use -v to get more diff
failure_demo.py:74: AssertionError
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
@@ -241,9 +239,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E which
E includes foo
E ? +++
E and a...
E
E ...Full output truncated (2 lines hidden), use '-vv' to show
E and a
E tail
failure_demo.py:84: AssertionError
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
@@ -307,9 +304,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E ['b']
E
E Drill down into differing attribute b:
E b: 'b' != 'c'...
E
E ...Full output truncated (3 lines hidden), use '-vv' to show
E b: 'b' != 'c'
E - c
E + b
failure_demo.py:108: AssertionError
________________ TestSpecialisedExplanations.test_eq_attrs _________________
@@ -334,9 +331,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
E ['b']
E
E Drill down into differing attribute b:
E b: 'b' != 'c'...
E
E ...Full output truncated (3 lines hidden), use '-vv' to show
E b: 'b' != 'c'
E - c
E + b
failure_demo.py:120: AssertionError
______________________________ test_attribute ______________________________
@@ -673,7 +670,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list - asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_long - ...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dict - Asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - Assert...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - assert...
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_longer_list
FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser...
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline

View File

@@ -892,8 +892,9 @@ here is a little example implemented via a local plugin:
.. code-block:: python
# content of conftest.py
from typing import Dict
import pytest
from pytest import StashKey, CollectReport
phase_report_key = StashKey[Dict[str, CollectReport]]()
@@ -956,8 +957,8 @@ and run it:
rootdir: /home/sweet/project
collected 3 items
test_module.py Esetting up a test failed! test_module.py::test_setup_fails
Fexecuting test failed test_module.py::test_call_fails
test_module.py Esetting up a test failed or skipped test_module.py::test_setup_fails
Fexecuting test failed or skipped test_module.py::test_call_fails
F
================================== ERRORS ==================================

View File

@@ -24,8 +24,9 @@ The first few lines should look like this:
[project]
name = "PACKAGENAME"
version = "PACKAGEVERSION"
where ``PACKAGENAME`` is the name of your package.
where ``PACKAGENAME`` and ``PACKAGEVERSION`` are the name and version of your package respectively.
You can then install your package in "editable" mode by running from the same directory:

View File

@@ -16,7 +16,7 @@ import process can be controlled through the ``--import-mode`` command-line flag
these values:
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
of :py:data:`sys.path` if not already there, and then imported with the :func:`__import__ <__import__>` builtin.
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
This requires test module names to be unique when the test directory tree is not arranged in
packages, because the modules will put in :py:data:`sys.modules` after importing.
@@ -24,7 +24,7 @@ these values:
This is the classic mechanism, dating back from the time Python 2 was still supported.
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
there, and imported with ``__import__``.
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
This better allows to run test modules against installed versions of a package even if the
package under test has the same import root. For example:
@@ -43,7 +43,7 @@ these values:
Same as ``prepend``, requires test module names to be unique when the test directory tree is
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
* ``importlib``: new in pytest-6.0, this mode uses :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
For this reason this doesn't require test module names to be unique.

View File

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

View File

@@ -109,6 +109,18 @@ When a warning matches more than one option in the list, the action for the last
is performed.
.. note::
The ``-W`` flag and the ``filterwarnings`` ini option use warning filters that are
similar in structure, but each configuration option interprets its filter
differently. For example, *message* in ``filterwarnings`` is a string containing a
regular expression that the start of the warning message must match,
case-insensitively, while *message* in ``-W`` is a literal string that the start of
the warning message must contain (case-insensitively), ignoring any whitespace at
the start or end of message. Consult the `warning filter`_ documentation for more
details.
.. _`filterwarnings`:
``@pytest.mark.filterwarnings``
@@ -270,20 +282,34 @@ which works in a similar manner to :ref:`raises <assertraises>` (except that
warnings.warn("my warning", UserWarning)
The test will fail if the warning in question is not raised. Use the keyword
argument ``match`` to assert that the warning matches a text or regex::
argument ``match`` to assert that the warning matches a text or regex.
To match a literal string that may contain regular expression metacharacters like ``(`` or ``.``, the pattern can
first be escaped with ``re.escape``.
>>> with warns(UserWarning, match='must be 0 or None'):
Some examples:
.. code-block:: pycon
>>> with warns(UserWarning, match="must be 0 or None"):
... warnings.warn("value must be 0 or None", UserWarning)
...
>>> with warns(UserWarning, match=r'must be \d+$'):
>>> with warns(UserWarning, match=r"must be \d+$"):
... warnings.warn("value must be 42", UserWarning)
...
>>> with warns(UserWarning, match=r'must be \d+$'):
>>> with warns(UserWarning, match=r"must be \d+$"):
... warnings.warn("this is not here", UserWarning)
...
Traceback (most recent call last):
...
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
>>> with warns(UserWarning, match=re.escape("issue with foo() func")):
... warnings.warn("issue with foo() func")
...
You can also call :func:`pytest.warns` on a function or code string:
.. code-block:: python

View File

@@ -1237,7 +1237,6 @@ If the data created by the factory requires managing, the fixture can take care
@pytest.fixture
def make_customer_record():
created_records = []
def _make_customer_record(name):

View File

@@ -135,10 +135,10 @@ This can be done in our test file by defining a class to represent ``r``.
# this is the previous code block example
import app
# custom class to be the mock return value
# will override the requests.Response returned from requests.get
class MockResponse:
# mock json() method always returns a specific testing dictionary
@staticmethod
def json():
@@ -146,7 +146,6 @@ This can be done in our test file by defining a class to represent ``r``.
def test_get_json(monkeypatch):
# Any arguments may be passed and mock_get() will always return our
# mocked object, which only has the .json() method.
def mock_get(*args, **kwargs):
@@ -181,6 +180,7 @@ This mock can be shared across tests using a ``fixture``:
# app.py that includes the get_json() function
import app
# custom class to be the mock return value of requests.get()
class MockResponse:
@staticmethod
@@ -358,7 +358,6 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific
def test_connection(monkeypatch):
# Patch the values of DEFAULT_CONFIG to specific
# testing values only for this test.
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
@@ -383,7 +382,6 @@ You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove v
def test_missing_user(monkeypatch):
# patch the DEFAULT_CONFIG t be missing the 'user' key
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
@@ -404,6 +402,7 @@ separate fixtures for each potential mock and reference them in the needed tests
# app.py with the connection string function
import app
# all of the mocks are moved into separated fixtures
@pytest.fixture
def mock_test_user(monkeypatch):
@@ -425,7 +424,6 @@ separate fixtures for each potential mock and reference them in the needed tests
# tests reference only the fixture mocks that are needed
def test_connection(mock_test_user, mock_test_database):
expected = "User Id=test_user; Location=test_db;"
result = app.create_connection_string()
@@ -433,7 +431,6 @@ separate fixtures for each potential mock and reference them in the needed tests
def test_missing_user(mock_missing_default_user):
with pytest.raises(KeyError):
_ = app.create_connection_string()

View File

@@ -167,9 +167,9 @@ Now we can increase pytest's verbosity:
E Right contains 4 more items:
E {'10': 10, '20': 20, '30': 30, '40': 40}
E Full diff:
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}...
E
E ...Full output truncated (3 lines hidden), use '-vv' to show
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
E ? - - - - - - - -
E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
test_verbosity_example.py:14: AssertionError
___________________________ test_long_text_fail ____________________________

View File

@@ -132,8 +132,7 @@ The default base temporary directory
Temporary directories are by default created as sub-directories of
the system temporary directory. The base name will be ``pytest-NUM`` where
``NUM`` will be incremented with each test run.
By default, only the directories of failed tests will be kept.
Also only the last 3 directries will remain at most.
By default, entries older than 3 temporary directories will be removed.
This behavior can be configured with :confval:`tmp_path_retention_count` and
:confval:`tmp_path_retention_policy`.

View File

@@ -249,6 +249,7 @@ and use pytest_addoption as follows:
# contents of hooks.py
# Use firstresult=True because we only want one plugin to define this
# default value
@hookspec(firstresult=True)

View File

@@ -167,13 +167,8 @@ it in your ``pyproject.toml`` file.
"Framework :: Pytest",
]
[tool.setuptools]
packages = ["myproject"]
[project.entry_points]
pytest11 = [
"myproject = myproject.pluginmodule",
]
[project.entry-points.pytest11]
myproject = "myproject.pluginmodule"
If a package is installed this way, ``pytest`` will load
``myproject.pluginmodule`` as a plugin which can define
@@ -454,7 +449,8 @@ in our ``pytest.ini`` to tell pytest where to look for example files.
$ pytest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
rootdir: /home/sweet/project, configfile: pytest.ini
rootdir: /home/sweet/project
configfile: pytest.ini
collected 2 items
test_example.py .. [100%]

View File

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

View File

@@ -335,7 +335,7 @@ For example:
.. literalinclude:: /example/fixtures/test_fixtures_order_dependencies.py
If we map out what depends on what, we get something that look like this:
If we map out what depends on what, we get something that looks like this:
.. image:: /example/fixtures/test_fixtures_order_dependencies.*
:align: center

File diff suppressed because it is too large Load Diff

View File

@@ -1047,6 +1047,14 @@ Environment Variables
Environment variables that can be used to change pytest's behavior.
.. envvar:: CI
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative 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.
.. envvar:: PYTEST_ADDOPTS
This contains a command-line (parsed by the py:mod:`shlex` module) that will be **prepended** to the command line given
@@ -1212,6 +1220,7 @@ passed multiple times. The expected format is ``name=value``. For example::
* ``classic``: classic pytest output.
* ``progress``: like classic pytest output, but with a progress indicator.
* ``progress-even-when-capture-no``: allows the use of the progress indicator even when ``capture=no``.
* ``count``: like progress, but shows progress as the number of tests completed instead of a percent.
The default is ``progress``, but you can fallback to ``classic`` if you prefer or
@@ -1754,7 +1763,7 @@ passed multiple times. The expected format is ``name=value``. For example::
[pytest]
tmp_path_retention_policy = "all"
Default: failed
Default: all
.. confval:: usefixtures
@@ -1986,8 +1995,11 @@ All the command-line flags can be obtained by running ``pytest --help``::
--log-auto-indent=LOG_AUTO_INDENT
Auto-indent multiline messages passed to the logging
module. Accepts true|on, false|off or an integer.
--log-disable=LOGGER_DISABLE
Disable a logger by name. Can be passed multipe
times.
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
markers (linelist): Markers for test functions
empty_parameter_set_mark (string):
@@ -2015,9 +2027,18 @@ All the command-line flags can be obtained by running ``pytest --help``::
console_output_style (string):
Console output: "classic", or with additional
progress information ("progress" (percentage) |
"count")
"count" | "progress-even-when-capture-no" (forces
progress even when capture=no)
xfail_strict (bool): Default for the strict parameter of xfail markers
when not given explicitly (default: False)
tmp_path_retention_count (string):
How many sessions should we keep the `tmp_path`
directories, according to
`tmp_path_retention_policy`.
tmp_path_retention_policy (string):
Controls which directories created by the `tmp_path`
fixture are kept around, based on test outcome.
(all/failed/none)
enable_assertion_pass_hook (bool):
Enables the pytest_assertion_pass hook. Make sure to
delete any previously generated pyc cache files.

View File

@@ -114,3 +114,8 @@ template = "changelog/_template.rst"
[tool.black]
target-version = ['py37']
# check-wheel-contents is executed by the build-and-inspect-python-package action.
[tool.check-wheel-contents]
# W009: Wheel contains multiple toplevel library entries
ignore = "W009"

View File

@@ -44,7 +44,6 @@ packages =
pytest
py_modules = py
install_requires =
attrs>=19.2.0
iniconfig
packaging
pluggy>=0.12,<2.0
@@ -68,6 +67,7 @@ console_scripts =
[options.extras_require]
testing =
argcomplete
attrs>=19.2.0
hypothesis>=3.56
mock
nose

View File

@@ -1,4 +1,5 @@
import ast
import dataclasses
import inspect
import os
import re
@@ -32,7 +33,6 @@ from typing import TypeVar
from typing import Union
from weakref import ref
import attr
import pluggy
import _pytest
@@ -411,13 +411,13 @@ class Traceback(List[TracebackEntry]):
"""
return Traceback(filter(fn, self), self._excinfo)
def getcrashentry(self) -> TracebackEntry:
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 self[-1]
return None
def recursionindex(self) -> Optional[int]:
"""Return the index of the frame/TracebackEntry where recursion originates if
@@ -445,7 +445,7 @@ E = TypeVar("E", bound=BaseException, covariant=True)
@final
@attr.s(repr=False, init=False, auto_attribs=True)
@dataclasses.dataclass
class ExceptionInfo(Generic[E]):
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
@@ -602,11 +602,13 @@ class ExceptionInfo(Generic[E]):
"""
return isinstance(self.value, exc)
def _getreprcrash(self) -> "ReprFileLocation":
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
exconly = self.exconly(tryshort=True)
entry = self.traceback.getcrashentry()
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
if entry:
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
return ReprFileLocation(path, lineno + 1, exconly)
return None
def getrepr(
self,
@@ -649,12 +651,12 @@ class ExceptionInfo(Generic[E]):
"""
if style == "native":
return ReprExceptionInfo(
ReprTracebackNative(
reprtraceback=ReprTracebackNative(
traceback.format_exception(
self.type, self.value, self.traceback[0]._rawentry
)
),
self._getreprcrash(),
reprcrash=self._getreprcrash(),
)
fmt = FormattedExcinfo(
@@ -684,7 +686,7 @@ class ExceptionInfo(Generic[E]):
return True
@attr.s(auto_attribs=True)
@dataclasses.dataclass
class FormattedExcinfo:
"""Presenting information about failing Functions and Generators."""
@@ -699,8 +701,8 @@ class FormattedExcinfo:
funcargs: bool = False
truncate_locals: bool = True
chain: bool = True
astcache: Dict[Union[str, Path], ast.AST] = attr.ib(
factory=dict, init=False, repr=False
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
default_factory=dict, init=False, repr=False
)
def _getindent(self, source: "Source") -> int:
@@ -741,11 +743,13 @@ class FormattedExcinfo:
) -> List[str]:
"""Return formatted and marked up source lines."""
lines = []
if source is None or line_index >= len(source.lines):
if source is not None and line_index < 0:
line_index += len(source)
if source is None or line_index >= len(source.lines) or line_index < 0:
# `line_index` could still be outside `range(len(source.lines))` if
# we're processing AST with pathological position attributes.
source = Source("???")
line_index = 0
if line_index < 0:
line_index += len(source)
space_prefix = " "
if short:
lines.append(space_prefix + source.lines[line_index].strip())
@@ -942,9 +946,14 @@ class FormattedExcinfo:
)
else:
reprtraceback = self.repr_traceback(excinfo_)
reprcrash: Optional[ReprFileLocation] = (
excinfo_._getreprcrash() if self.style != "value" else None
)
# 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)]
else:
# Fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work.
@@ -952,8 +961,8 @@ 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_ = (
@@ -978,7 +987,7 @@ class FormattedExcinfo:
return ExceptionChainRepr(repr_chain)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class TerminalRepr:
def __str__(self) -> str:
# FYI this is called from pytest-xdist's serialization of exception
@@ -996,14 +1005,14 @@ class TerminalRepr:
# This class is abstract -- only subclasses are instantiated.
@attr.s(eq=False)
@dataclasses.dataclass(eq=False)
class ExceptionRepr(TerminalRepr):
# Provided by subclasses.
reprcrash: Optional["ReprFileLocation"]
reprtraceback: "ReprTraceback"
def __attrs_post_init__(self) -> None:
self.sections: List[Tuple[str, str, str]] = []
reprcrash: Optional["ReprFileLocation"]
sections: List[Tuple[str, str, str]] = dataclasses.field(
init=False, default_factory=list
)
def addsection(self, name: str, content: str, sep: str = "-") -> None:
self.sections.append((name, content, sep))
@@ -1014,16 +1023,23 @@ class ExceptionRepr(TerminalRepr):
tw.line(content)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ExceptionChainRepr(ExceptionRepr):
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
def __attrs_post_init__(self) -> None:
super().__attrs_post_init__()
def __init__(
self,
chain: Sequence[
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
],
) -> None:
# reprcrash and reprtraceback of the outermost (the newest) exception
# in the chain.
self.reprtraceback = self.chain[-1][0]
self.reprcrash = self.chain[-1][1]
super().__init__(
reprtraceback=chain[-1][0],
reprcrash=chain[-1][1],
)
self.chain = chain
def toterminal(self, tw: TerminalWriter) -> None:
for element in self.chain:
@@ -1034,17 +1050,17 @@ class ExceptionChainRepr(ExceptionRepr):
super().toterminal(tw)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprExceptionInfo(ExceptionRepr):
reprtraceback: "ReprTraceback"
reprcrash: "ReprFileLocation"
reprcrash: Optional["ReprFileLocation"]
def toterminal(self, tw: TerminalWriter) -> None:
self.reprtraceback.toterminal(tw)
super().toterminal(tw)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprTraceback(TerminalRepr):
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
extraline: Optional[str]
@@ -1073,12 +1089,12 @@ class ReprTraceback(TerminalRepr):
class ReprTracebackNative(ReprTraceback):
def __init__(self, tblines: Sequence[str]) -> None:
self.style = "native"
self.reprentries = [ReprEntryNative(tblines)]
self.extraline = None
self.style = "native"
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprEntryNative(TerminalRepr):
lines: Sequence[str]
@@ -1088,7 +1104,7 @@ class ReprEntryNative(TerminalRepr):
tw.write("".join(self.lines))
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprEntry(TerminalRepr):
lines: Sequence[str]
reprfuncargs: Optional["ReprFuncArgs"]
@@ -1168,12 +1184,15 @@ class ReprEntry(TerminalRepr):
)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprFileLocation(TerminalRepr):
path: str = attr.ib(converter=str)
path: str
lineno: int
message: str
def __post_init__(self) -> None:
self.path = str(self.path)
def toterminal(self, tw: TerminalWriter) -> None:
# Filename and lineno output for each entry, using an output format
# that most editors understand.
@@ -1185,7 +1204,7 @@ class ReprFileLocation(TerminalRepr):
tw.line(f":{self.lineno}: {msg}")
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprLocals(TerminalRepr):
lines: Sequence[str]
@@ -1194,7 +1213,7 @@ class ReprLocals(TerminalRepr):
tw.line(indent + line)
@attr.s(eq=False, auto_attribs=True)
@dataclasses.dataclass(eq=False)
class ReprFuncArgs(TerminalRepr):
args: Sequence[Tuple[str, object]]

View File

@@ -44,10 +44,14 @@ from _pytest.stash import StashKey
if TYPE_CHECKING:
from _pytest.assertion import AssertionState
if sys.version_info >= (3, 8):
namedExpr = ast.NamedExpr
else:
namedExpr = ast.Expr
assertstate_key = StashKey["AssertionState"]()
# pytest caches rewritten pycs in pycache dirs
PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
PYC_EXT = ".py" + (__debug__ and "c" or "o")
@@ -274,7 +278,6 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
return f.read()
if sys.version_info >= (3, 10):
if sys.version_info >= (3, 12):
from importlib.resources.abc import TraversableResources
else:
@@ -636,8 +639,12 @@ class AssertionRewriter(ast.NodeVisitor):
.push_format_context() and .pop_format_context() which allows
to build another %-formatted string while already building one.
This state is reset on every new assert statement visited and used
by the other visitors.
:variables_overwrite: A dict filled with references to variables
that change value within an assert. This happens when a variable is
reassigned with the walrus operator
This state, except the variables_overwrite, is reset on every new assert
statement visited and used by the other visitors.
"""
def __init__(
@@ -653,6 +660,7 @@ class AssertionRewriter(ast.NodeVisitor):
else:
self.enable_assertion_pass_hook = False
self.source = source
self.variables_overwrite: Dict[str, str] = {}
def run(self, mod: ast.Module) -> None:
"""Find all assert statements in *mod* and rewrite them."""
@@ -667,7 +675,7 @@ class AssertionRewriter(ast.NodeVisitor):
if doc is not None and self.is_rewrite_disabled(doc):
return
pos = 0
lineno = 1
item = None
for item in mod.body:
if (
expect_docstring
@@ -938,6 +946,18 @@ class AssertionRewriter(ast.NodeVisitor):
ast.copy_location(node, assert_)
return self.statements
def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
# This method handles the 'walrus operator' repr of the target
# name if it's a local variable or _should_repr_global_name()
# thinks it's acceptable.
locs = ast.Call(self.builtin("locals"), [], [])
target_id = name.target.id # type: ignore[attr-defined]
inlocs = ast.Compare(ast.Str(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))
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.
@@ -964,6 +984,20 @@ class AssertionRewriter(ast.NodeVisitor):
# cond is set in a prior loop iteration below
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
self.expl_stmts = fail_inner
# Check if the left operand is a namedExpr and the value has already been visited
if (
isinstance(v, ast.Compare)
and isinstance(v.left, namedExpr)
and v.left.target.id
in [
ast_expr.id
for ast_expr in boolop.values[:i]
if hasattr(ast_expr, "id")
]
):
pytest_temp = self.variable()
self.variables_overwrite[v.left.target.id] = pytest_temp
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))
@@ -1039,6 +1073,9 @@ 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]
left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = f"({left_expl})"
@@ -1050,6 +1087,13 @@ class AssertionRewriter(ast.NodeVisitor):
syms = []
results = [left_res]
for i, op, next_operand in it:
if (
isinstance(next_operand, namedExpr)
and isinstance(left_res, ast.Name)
and next_operand.target.id == left_res.id
):
next_operand.target.id = self.variable()
self.variables_overwrite[left_res.id] = next_operand.target.id
next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = f"({next_expl})"
@@ -1073,6 +1117,7 @@ class AssertionRewriter(ast.NodeVisitor):
res: ast.expr = ast.BoolOp(ast.And(), load_names)
else:
res = load_names[0]
return res, self.explanation_param(self.pop_format_context(expl_call))

View File

@@ -1,6 +1,7 @@
"""Implementation of the cache provider."""
# This plugin was not named "cache" to avoid conflicts with the external
# pytest-cache version.
import dataclasses
import json
import os
from pathlib import Path
@@ -12,8 +13,6 @@ from typing import Optional
from typing import Set
from typing import Union
import attr
from .pathlib import resolve_from_str
from .pathlib import rm_rf
from .reports import CollectReport
@@ -32,7 +31,6 @@ from _pytest.python import Module
from _pytest.python import Package
from _pytest.reports import TestReport
README_CONTENT = """\
# pytest cache directory #
@@ -53,10 +51,12 @@ Signature: 8a477f597d28d172789f06886806bc55
@final
@attr.s(init=False, auto_attribs=True)
@dataclasses.dataclass
class Cache:
_cachedir: Path = attr.ib(repr=False)
_config: Config = attr.ib(repr=False)
"""Instance of the `cache` fixture."""
_cachedir: Path = dataclasses.field(repr=False)
_config: Config = dataclasses.field(repr=False)
# Sub-directory under cache-dir for directories created by `mkdir()`.
_CACHE_PREFIX_DIRS = "d"
@@ -492,7 +492,7 @@ def pytest_addoption(parser: Parser) -> None:
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
if config.option.cacheshow:
if config.option.cacheshow and not config.option.help:
from _pytest.main import wrap_session
return wrap_session(config, cacheshow)

View File

@@ -1,19 +1,26 @@
"""Per-test stdout/stderr capturing mechanism."""
import abc
import collections
import contextlib
import functools
import io
import os
import sys
from io import UnsupportedOperation
from tempfile import TemporaryFile
from types import TracebackType
from typing import Any
from typing import AnyStr
from typing import BinaryIO
from typing import Generator
from typing import Generic
from typing import Iterable
from typing import Iterator
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import TextIO
from typing import Tuple
from typing import Type
from typing import TYPE_CHECKING
from typing import Union
@@ -29,6 +36,7 @@ from _pytest.nodes import File
from _pytest.nodes import Item
if TYPE_CHECKING:
from typing_extensions import Final
from typing_extensions import Literal
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
@@ -185,19 +193,27 @@ class TeeCaptureIO(CaptureIO):
return self._other.write(s)
class DontReadFromInput:
encoding = None
class DontReadFromInput(TextIO):
@property
def encoding(self) -> str:
return sys.__stdin__.encoding
def read(self, *args):
def read(self, size: int = -1) -> str:
raise OSError(
"pytest: reading from stdin while output is captured! Consider using `-s`."
)
readline = read
readlines = read
__next__ = read
def __iter__(self):
def __next__(self) -> str:
return self.readline()
def readlines(self, hint: Optional[int] = -1) -> List[str]:
raise OSError(
"pytest: reading from stdin while output is captured! Consider using `-s`."
)
def __iter__(self) -> Iterator[str]:
return self
def fileno(self) -> int:
@@ -215,7 +231,7 @@ class DontReadFromInput:
def readable(self) -> bool:
return False
def seek(self, offset: int) -> int:
def seek(self, offset: int, whence: int = 0) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
def seekable(self) -> bool:
@@ -224,41 +240,104 @@ class DontReadFromInput:
def tell(self) -> int:
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
def truncate(self, size: int) -> None:
def truncate(self, size: Optional[int] = None) -> int:
raise UnsupportedOperation("cannont truncate stdin")
def write(self, *args) -> None:
def write(self, data: str) -> int:
raise UnsupportedOperation("cannot write to stdin")
def writelines(self, *args) -> None:
def writelines(self, lines: Iterable[str]) -> None:
raise UnsupportedOperation("Cannot write to stdin")
def writable(self) -> bool:
return False
@property
def buffer(self):
def __enter__(self) -> "DontReadFromInput":
return self
def __exit__(
self,
type: Optional[Type[BaseException]],
value: Optional[BaseException],
traceback: Optional[TracebackType],
) -> None:
pass
@property
def buffer(self) -> BinaryIO:
# The str/bytes doesn't actually matter in this type, so OK to fake.
return self # type: ignore[return-value]
# Capture classes.
class CaptureBase(abc.ABC, Generic[AnyStr]):
EMPTY_BUFFER: AnyStr
@abc.abstractmethod
def __init__(self, fd: int) -> None:
raise NotImplementedError()
@abc.abstractmethod
def start(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def done(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def suspend(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def resume(self) -> None:
raise NotImplementedError()
@abc.abstractmethod
def writeorg(self, data: AnyStr) -> None:
raise NotImplementedError()
@abc.abstractmethod
def snap(self) -> AnyStr:
raise NotImplementedError()
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
class NoCapture:
EMPTY_BUFFER = None
__init__ = start = done = suspend = resume = lambda *args: None
class NoCapture(CaptureBase[str]):
EMPTY_BUFFER = ""
def __init__(self, fd: int) -> None:
pass
def start(self) -> None:
pass
def done(self) -> None:
pass
def suspend(self) -> None:
pass
def resume(self) -> None:
pass
def snap(self) -> str:
return ""
def writeorg(self, data: str) -> None:
pass
class SysCaptureBinary:
EMPTY_BUFFER = b""
def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
class SysCaptureBase(CaptureBase[AnyStr]):
def __init__(
self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
) -> None:
name = patchsysdict[fd]
self._old = getattr(sys, name)
self._old: TextIO = getattr(sys, name)
self.name = name
if tmpfile is None:
if name == "stdin":
@@ -298,14 +377,6 @@ class SysCaptureBinary:
setattr(sys, self.name, self.tmpfile)
self._state = "started"
def snap(self):
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def done(self) -> None:
self._assert_state("done", ("initialized", "started", "suspended", "done"))
if self._state == "done":
@@ -327,36 +398,43 @@ class SysCaptureBinary:
setattr(sys, self.name, self.tmpfile)
self._state = "started"
def writeorg(self, data) -> None:
class SysCaptureBinary(SysCaptureBase[bytes]):
EMPTY_BUFFER = b""
def snap(self) -> bytes:
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def writeorg(self, data: bytes) -> None:
self._assert_state("writeorg", ("started", "suspended"))
self._old.flush()
self._old.buffer.write(data)
self._old.buffer.flush()
class SysCapture(SysCaptureBinary):
EMPTY_BUFFER = "" # type: ignore[assignment]
class SysCapture(SysCaptureBase[str]):
EMPTY_BUFFER = ""
def snap(self):
def snap(self) -> str:
self._assert_state("snap", ("started", "suspended"))
assert isinstance(self.tmpfile, CaptureIO)
res = self.tmpfile.getvalue()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def writeorg(self, data):
def writeorg(self, data: str) -> None:
self._assert_state("writeorg", ("started", "suspended"))
self._old.write(data)
self._old.flush()
class FDCaptureBinary:
"""Capture IO to/from a given OS-level file descriptor.
snap() produces `bytes`.
"""
EMPTY_BUFFER = b""
class FDCaptureBase(CaptureBase[AnyStr]):
def __init__(self, targetfd: int) -> None:
self.targetfd = targetfd
@@ -382,7 +460,7 @@ class FDCaptureBinary:
if targetfd == 0:
self.tmpfile = open(os.devnull, encoding="utf-8")
self.syscapture = SysCapture(targetfd)
self.syscapture: CaptureBase[str] = SysCapture(targetfd)
else:
self.tmpfile = EncodedFile(
TemporaryFile(buffering=0),
@@ -394,7 +472,7 @@ class FDCaptureBinary:
if targetfd in patchsysdict:
self.syscapture = SysCapture(targetfd, self.tmpfile)
else:
self.syscapture = NoCapture()
self.syscapture = NoCapture(targetfd)
self._state = "initialized"
@@ -421,14 +499,6 @@ class FDCaptureBinary:
self.syscapture.start()
self._state = "started"
def snap(self):
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def done(self) -> None:
"""Stop capturing, restore streams, return original capture file,
seeked to position zero."""
@@ -461,22 +531,38 @@ class FDCaptureBinary:
os.dup2(self.tmpfile.fileno(), self.targetfd)
self._state = "started"
def writeorg(self, data):
class FDCaptureBinary(FDCaptureBase[bytes]):
"""Capture IO to/from a given OS-level file descriptor.
snap() produces `bytes`.
"""
EMPTY_BUFFER = b""
def snap(self) -> bytes:
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.buffer.read()
self.tmpfile.seek(0)
self.tmpfile.truncate()
return res
def writeorg(self, data: bytes) -> None:
"""Write to original file descriptor."""
self._assert_state("writeorg", ("started", "suspended"))
os.write(self.targetfd_save, data)
class FDCapture(FDCaptureBinary):
class FDCapture(FDCaptureBase[str]):
"""Capture IO to/from a given OS-level file descriptor.
snap() produces text.
"""
# Ignore type because it doesn't match the type in the superclass (bytes).
EMPTY_BUFFER = "" # type: ignore
EMPTY_BUFFER = ""
def snap(self):
def snap(self) -> str:
self._assert_state("snap", ("started", "suspended"))
self.tmpfile.seek(0)
res = self.tmpfile.read()
@@ -484,77 +570,49 @@ class FDCapture(FDCaptureBinary):
self.tmpfile.truncate()
return res
def writeorg(self, data):
def writeorg(self, data: str) -> None:
"""Write to original file descriptor."""
super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
self._assert_state("writeorg", ("started", "suspended"))
# XXX use encoding of original stream
os.write(self.targetfd_save, data.encode("utf-8"))
# MultiCapture
# This class was a namedtuple, but due to mypy limitation[0] it could not be
# made generic, so was replaced by a regular class which tries to emulate the
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
# make it a namedtuple again.
# [0]: https://github.com/python/mypy/issues/685
@final
@functools.total_ordering
class CaptureResult(Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`."""
# Generic NamedTuple only supported since Python 3.11.
if sys.version_info >= (3, 11) or TYPE_CHECKING:
__slots__ = ("out", "err")
@final
class CaptureResult(NamedTuple, Generic[AnyStr]):
"""The result of :method:`CaptureFixture.readouterr`."""
def __init__(self, out: AnyStr, err: AnyStr) -> None:
self.out: AnyStr = out
self.err: AnyStr = err
out: AnyStr
err: AnyStr
def __len__(self) -> int:
return 2
else:
def __iter__(self) -> Iterator[AnyStr]:
return iter((self.out, self.err))
class CaptureResult(
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
):
"""The result of :method:`CaptureFixture.readouterr`."""
def __getitem__(self, item: int) -> AnyStr:
return tuple(self)[item]
def _replace(
self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None
) -> "CaptureResult[AnyStr]":
return CaptureResult(
out=self.out if out is None else out, err=self.err if err is None else err
)
def count(self, value: AnyStr) -> int:
return tuple(self).count(value)
def index(self, value) -> int:
return tuple(self).index(value)
def __eq__(self, other: object) -> bool:
if not isinstance(other, (CaptureResult, tuple)):
return NotImplemented
return tuple(self) == tuple(other)
def __hash__(self) -> int:
return hash(tuple(self))
def __lt__(self, other: object) -> bool:
if not isinstance(other, (CaptureResult, tuple)):
return NotImplemented
return tuple(self) < tuple(other)
def __repr__(self) -> str:
return f"CaptureResult(out={self.out!r}, err={self.err!r})"
__slots__ = ()
class MultiCapture(Generic[AnyStr]):
_state = None
_in_suspended = False
def __init__(self, in_, out, err) -> None:
self.in_ = in_
self.out = out
self.err = err
def __init__(
self,
in_: Optional[CaptureBase[AnyStr]],
out: Optional[CaptureBase[AnyStr]],
err: Optional[CaptureBase[AnyStr]],
) -> None:
self.in_: Optional[CaptureBase[AnyStr]] = in_
self.out: Optional[CaptureBase[AnyStr]] = out
self.err: Optional[CaptureBase[AnyStr]] = err
def __repr__(self) -> str:
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
@@ -578,8 +636,10 @@ class MultiCapture(Generic[AnyStr]):
"""Pop current snapshot out/err capture and flush to orig streams."""
out, err = self.readouterr()
if out:
assert self.out is not None
self.out.writeorg(out)
if err:
assert self.err is not None
self.err.writeorg(err)
return out, err
@@ -600,6 +660,7 @@ class MultiCapture(Generic[AnyStr]):
if self.err:
self.err.resume()
if self._in_suspended:
assert self.in_ is not None
self.in_.resume()
self._in_suspended = False
@@ -622,7 +683,8 @@ class MultiCapture(Generic[AnyStr]):
def readouterr(self) -> CaptureResult[AnyStr]:
out = self.out.snap() if self.out else ""
err = self.err.snap() if self.err else ""
return CaptureResult(out, err)
# TODO: This type error is real, need to fix.
return CaptureResult(out, err) # type: ignore[arg-type]
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
@@ -662,7 +724,7 @@ class CaptureManager:
"""
def __init__(self, method: "_CaptureMethod") -> None:
self._method = method
self._method: Final = method
self._global_capturing: Optional[MultiCapture[str]] = None
self._capture_fixture: Optional[CaptureFixture[Any]] = None
@@ -831,14 +893,18 @@ class CaptureFixture(Generic[AnyStr]):
:fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
def __init__(
self, captureclass, request: SubRequest, *, _ispytest: bool = False
self,
captureclass: Type[CaptureBase[AnyStr]],
request: SubRequest,
*,
_ispytest: bool = False,
) -> None:
check_ispytest(_ispytest)
self.captureclass = captureclass
self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
self.request = request
self._capture: Optional[MultiCapture[AnyStr]] = None
self._captured_out = self.captureclass.EMPTY_BUFFER
self._captured_err = self.captureclass.EMPTY_BUFFER
self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
def _start(self) -> None:
if self._capture is None:
@@ -893,7 +959,9 @@ class CaptureFixture(Generic[AnyStr]):
@contextlib.contextmanager
def disabled(self) -> Generator[None, None, None]:
"""Temporarily disable capturing while inside the ``with`` block."""
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
"capturemanager"
)
with capmanager.global_and_fixture_disabled():
yield
@@ -920,8 +988,8 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
captured = capsys.readouterr()
assert captured.out == "hello\n"
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -948,8 +1016,8 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
captured = capsysbinary.readouterr()
assert captured.out == b"hello\n"
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -976,8 +1044,8 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
captured = capfd.readouterr()
assert captured.out == "hello\n"
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture
@@ -1005,8 +1073,8 @@ def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, N
assert captured.out == b"hello\n"
"""
capman = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
capman.set_fixture(capture_fixture)
capture_fixture._start()
yield capture_fixture

View File

@@ -1,4 +1,7 @@
"""Python version compatibility code."""
from __future__ import annotations
import dataclasses
import enum
import functools
import inspect
@@ -11,13 +14,8 @@ from typing import Any
from typing import Callable
from typing import Generic
from typing import NoReturn
from typing import Optional
from typing import Tuple
from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
import py
@@ -47,7 +45,7 @@ LEGACY_PATH = py.path. local
# fmt: on
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
return LEGACY_PATH(path)
@@ -57,7 +55,7 @@ def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class NotSetType(enum.Enum):
token = 0
NOTSET: "Final" = NotSetType.token # noqa: E305
NOTSET: Final = NotSetType.token # noqa: E305
# fmt: on
if sys.version_info >= (3, 8):
@@ -95,7 +93,7 @@ def is_async_function(func: object) -> bool:
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
def getlocation(function, curdir: Optional[str] = None) -> str:
def getlocation(function, curdir: str | None = None) -> str:
function = get_real_func(function)
fn = Path(inspect.getfile(function))
lineno = function.__code__.co_firstlineno
@@ -133,8 +131,8 @@ def getfuncargnames(
*,
name: str = "",
is_method: bool = False,
cls: Optional[type] = None,
) -> Tuple[str, ...]:
cls: type | None = None,
) -> tuple[str, ...]:
"""Return the names of a function's mandatory arguments.
Should return the names of all function arguments that:
@@ -198,7 +196,7 @@ def getfuncargnames(
return arg_names
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
# Note: this code intentionally mirrors the code at the beginning of
# getfuncargnames, to get the arguments which were excluded from its result
# because they had default values.
@@ -229,7 +227,7 @@ def _bytes_to_ascii(val: bytes) -> str:
return val.decode("ascii", "backslashreplace")
def ascii_escaped(val: Union[bytes, str]) -> str:
def ascii_escaped(val: bytes | str) -> str:
r"""If val is pure ASCII, return it as an str, otherwise, escape
bytes objects into a sequence of escaped bytes:
@@ -253,7 +251,7 @@ def ascii_escaped(val: Union[bytes, str]) -> str:
return _translate_non_printable(ret)
@attr.s
@dataclasses.dataclass
class _PytestWrapper:
"""Dummy wrapper around a function object for internal use only.
@@ -262,7 +260,7 @@ class _PytestWrapper:
decorator to issue warnings when the fixture function is called directly.
"""
obj = attr.ib()
obj: Any
def get_real_func(obj):
@@ -356,7 +354,6 @@ else:
if sys.version_info >= (3, 8):
from functools import cached_property as cached_property
else:
from typing import Type
class cached_property(Generic[_S, _T]):
__slots__ = ("func", "__doc__")
@@ -367,12 +364,12 @@ else:
@overload
def __get__(
self, instance: None, owner: Optional[Type[_S]] = ...
) -> "cached_property[_S, _T]":
self, instance: None, owner: type[_S] | None = ...
) -> cached_property[_S, _T]:
...
@overload
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T:
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
...
def __get__(self, instance, owner=None):
@@ -382,6 +379,18 @@ else:
return value
def get_user_id() -> int | None:
"""Return the current user id, or None if we cannot get it reliably on the current platform."""
# win32 does not have a getuid() function.
# On Emscripten, getuid() is a stub that always returns 0.
if sys.platform in ("win32", "emscripten"):
return None
# getuid shouldn't fail, but cpython defines such a case.
# Let's hope for the best.
uid = os.getuid()
return uid if uid != -1 else None
# Perform exhaustiveness checking.
#
# Consider this example:

View File

@@ -2,6 +2,7 @@
import argparse
import collections.abc
import copy
import dataclasses
import enum
import glob
import inspect
@@ -34,7 +35,6 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import attr
from pluggy import HookimplMarker
from pluggy import HookspecMarker
from pluggy import PluginManager
@@ -62,7 +62,6 @@ from _pytest.warning_types import PytestConfigWarning
from _pytest.warning_types import warn_explicit_for
if TYPE_CHECKING:
from _pytest._code.code import _TracebackStyle
from _pytest.terminal import TerminalReporter
from .argparsing import Argument
@@ -697,6 +696,7 @@ class PytestPluginManager(PluginManager):
parg = opt[2:]
else:
continue
parg = parg.strip()
if exclude_only and not parg.startswith("no:"):
continue
self.consider_pluginarg(parg)
@@ -886,10 +886,6 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
yield from _iter_rewritable_modules(new_package_files)
def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
return tuple(args)
@final
class Config:
"""Access to configuration values, pluginmanager and plugin hooks.
@@ -903,7 +899,7 @@ class Config:
"""
@final
@attr.s(frozen=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class InvocationParams:
"""Holds parameters passed during :func:`pytest.main`.
@@ -919,13 +915,24 @@ class Config:
Plugins accessing ``InvocationParams`` must be aware of that.
"""
args: Tuple[str, ...] = attr.ib(converter=_args_converter)
args: Tuple[str, ...]
"""The command-line arguments as passed to :func:`pytest.main`."""
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
"""Extra plugins, might be `None`."""
dir: Path
"""The directory from which :func:`pytest.main` was invoked."""
def __init__(
self,
*,
args: Iterable[str],
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]],
dir: Path,
) -> None:
object.__setattr__(self, "args", tuple(args))
object.__setattr__(self, "plugins", plugins)
object.__setattr__(self, "dir", dir)
class ArgsSource(enum.Enum):
"""Indicates the source of the test arguments.
@@ -998,6 +1005,8 @@ class Config:
self.hook.pytest_addoption.call_historic(
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
)
self.args_source = Config.ArgsSource.ARGS
self.args: List[str] = []
if TYPE_CHECKING:
from _pytest.cacheprovider import Cache
@@ -1057,7 +1066,6 @@ class Config:
try:
self.parse(args)
except UsageError:
# Handle --version and --help here in a minimal fashion.
# This gets done via helpconfig normally, but its
# pytest_cmdline_main is not called in case of errors.
@@ -1337,8 +1345,8 @@ class Config:
def parse(self, args: List[str], addopts: bool = True) -> None:
# Parse given cmdline arguments into this config object.
assert not hasattr(
self, "args"
assert (
self.args == []
), "can only parse cmdline args at most once per Config object"
self.hook.pytest_addhooks.call_historic(
kwargs=dict(pluginmanager=self.pluginmanager)

View File

@@ -43,7 +43,6 @@ class PathAwareHookProxy:
@_wraps(hook)
def fixed_hook(**kw):
path_value: Optional[Path] = kw.pop(path_var, None)
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
if fspath_value is not None:

View File

@@ -531,7 +531,6 @@ class DoctestModule(Module):
if _is_mocked(obj):
return
with _patch_unwrap_mock_aware():
# Type ignored because this is a private function.
super()._find( # type:ignore[misc]
tests, obj, name, module, source_lines, globs, seen

View File

@@ -1,3 +1,4 @@
import dataclasses
import functools
import inspect
import os
@@ -28,8 +29,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
import _pytest
from _pytest import nodes
from _pytest._code import getfslineno
@@ -103,7 +102,7 @@ _FixtureCachedResult = Union[
]
@attr.s(frozen=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class PseudoFixtureDef(Generic[FixtureValue]):
cached_result: "_FixtureCachedResult[FixtureValue]"
_scope: Scope
@@ -350,8 +349,10 @@ def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
return request.param
@attr.s(slots=True, auto_attribs=True)
@dataclasses.dataclass
class FuncFixtureInfo:
__slots__ = ("argnames", "initialnames", "names_closure", "name2fixturedefs")
# Original function argument names.
argnames: Tuple[str, ...]
# Argnames that function immediately requires. These include argnames +
@@ -1181,19 +1182,21 @@ def wrap_function_to_error_out_if_called_directly(
@final
@attr.s(frozen=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class FixtureFunctionMarker:
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
params: Optional[Tuple[object, ...]]
autouse: bool = False
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = attr.ib(
default=None,
converter=_ensure_immutable_ids,
)
] = None
name: Optional[str] = None
_ispytest: dataclasses.InitVar[bool] = False
def __post_init__(self, _ispytest: bool) -> None:
check_ispytest(_ispytest)
def __call__(self, function: FixtureFunction) -> FixtureFunction:
if inspect.isclass(function):
raise ValueError("class fixtures not supported (maybe in the future)")
@@ -1313,10 +1316,11 @@ def fixture( # noqa: F811
"""
fixture_marker = FixtureFunctionMarker(
scope=scope,
params=params,
params=tuple(params) if params is not None else None,
autouse=autouse,
ids=ids,
ids=None if ids is None else ids if callable(ids) else tuple(ids),
name=name,
_ispytest=True,
)
# Direct decoration.

View File

@@ -164,7 +164,8 @@ def showhelp(config: Config) -> None:
tw.write(config._parser.optparser.format_help())
tw.line()
tw.line(
"[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:"
"[pytest] ini-options in the first "
"pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:"
)
tw.line()

View File

@@ -505,7 +505,9 @@ def pytest_runtest_logstart(
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
:param nodeid: Full node ID of the item.
:param location: A tuple of ``(filename, lineno, testname)``.
:param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based.
"""
@@ -517,7 +519,9 @@ def pytest_runtest_logfinish(
See :hook:`pytest_runtest_protocol` for a description of the runtest protocol.
:param nodeid: Full node ID of the item.
:param location: A tuple of ``(filename, lineno, testname)``.
:param location: A tuple of ``(filename, lineno, testname)``
where ``filename`` is a file path relative to ``config.rootpath``
and ``lineno`` is 0-based.
"""

View File

@@ -645,8 +645,8 @@ class LogXML:
def pytest_sessionfinish(self) -> None:
dirname = os.path.dirname(os.path.abspath(self.logfile))
if not os.path.isdir(dirname):
os.makedirs(dirname)
# exist_ok avoids filesystem race conditions between checking path existence and requesting creation
os.makedirs(dirname, exist_ok=True)
with open(self.logfile, "w", encoding="utf-8") as logfile:
suite_stop_time = timing.time()

View File

@@ -1,4 +1,5 @@
"""Add backward compatibility support for the legacy py path type."""
import dataclasses
import shlex
import subprocess
from pathlib import Path
@@ -7,7 +8,6 @@ from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
import attr
from iniconfig import SectionWrapper
from _pytest.cacheprovider import Cache
@@ -268,7 +268,7 @@ class LegacyTestdirPlugin:
@final
@attr.s(init=False, auto_attribs=True)
@dataclasses.dataclass
class TempdirFactory:
"""Backward compatibility wrapper that implements :class:`py.path.local`
for :class:`TempPathFactory`.

View File

@@ -1,5 +1,6 @@
"""Core implementation of the testing process: init, session, runtest loop."""
import argparse
import dataclasses
import fnmatch
import functools
import importlib
@@ -19,8 +20,6 @@ from typing import Type
from typing import TYPE_CHECKING
from typing import Union
import attr
import _pytest._code
from _pytest import nodes
from _pytest.compat import final
@@ -442,8 +441,10 @@ class Failed(Exception):
"""Signals a stop as failed test run."""
@attr.s(slots=True, auto_attribs=True)
@dataclasses.dataclass
class _bestrelpath_cache(Dict[Path, str]):
__slots__ = ("path",)
path: Path
def __missing__(self, path: Path) -> str:

View File

@@ -1,4 +1,5 @@
"""Generic mechanism for marking and selecting python functions."""
import dataclasses
from typing import AbstractSet
from typing import Collection
from typing import List
@@ -6,8 +7,6 @@ from typing import Optional
from typing import TYPE_CHECKING
from typing import Union
import attr
from .expression import Expression
from .expression import ParseError
from .structures import EMPTY_PARAMETERSET_OPTION
@@ -130,7 +129,7 @@ def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
return None
@attr.s(slots=True, auto_attribs=True)
@dataclasses.dataclass
class KeywordMatcher:
"""A matcher for keywords.
@@ -145,6 +144,8 @@ class KeywordMatcher:
any item, as well as names directly assigned to test functions.
"""
__slots__ = ("_names",)
_names: AbstractSet[str]
@classmethod
@@ -201,13 +202,15 @@ def deselect_by_keyword(items: "List[Item]", config: Config) -> None:
items[:] = remaining
@attr.s(slots=True, auto_attribs=True)
@dataclasses.dataclass
class MarkMatcher:
"""A matcher for markers which are present.
Tries to match on any marker names, attached to the given colitem.
"""
__slots__ = ("own_mark_names",)
own_mark_names: AbstractSet[str]
@classmethod

View File

@@ -15,6 +15,7 @@ The semantics are:
- or/and/not evaluate according to the usual boolean semantics.
"""
import ast
import dataclasses
import enum
import re
import types
@@ -25,8 +26,6 @@ from typing import NoReturn
from typing import Optional
from typing import Sequence
import attr
__all__ = [
"Expression",
@@ -44,8 +43,9 @@ class TokenType(enum.Enum):
EOF = "end of input"
@attr.s(frozen=True, slots=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class Token:
__slots__ = ("type", "value", "pos")
type: TokenType
value: str
pos: int

View File

@@ -1,4 +1,5 @@
import collections.abc
import dataclasses
import inspect
import warnings
from typing import Any
@@ -20,8 +21,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
from .._code import getfslineno
from ..compat import ascii_escaped
from ..compat import final
@@ -191,8 +190,10 @@ class ParameterSet(NamedTuple):
@final
@attr.s(frozen=True, init=False, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class Mark:
"""A pytest mark."""
#: Name of the mark.
name: str
#: Positional arguments of the mark decorator.
@@ -201,9 +202,11 @@ class Mark:
kwargs: Mapping[str, Any]
#: Source Mark for ids with parametrize Marks.
_param_ids_from: Optional["Mark"] = attr.ib(default=None, repr=False)
_param_ids_from: Optional["Mark"] = dataclasses.field(default=None, repr=False)
#: Resolved/generated ids with parametrize Marks.
_param_ids_generated: Optional[Sequence[str]] = attr.ib(default=None, repr=False)
_param_ids_generated: Optional[Sequence[str]] = dataclasses.field(
default=None, repr=False
)
def __init__(
self,
@@ -261,7 +264,7 @@ class Mark:
Markable = TypeVar("Markable", bound=Union[Callable[..., object], type])
@attr.s(init=False, auto_attribs=True)
@dataclasses.dataclass
class MarkDecorator:
"""A decorator for applying a mark on test functions and classes.

View File

@@ -511,7 +511,7 @@ def get_fslocation_from_item(node: "Node") -> Tuple[Union[str, Path], Optional[i
* "obj": a Python object that the node wraps.
* "fspath": just a path
:rtype: A tuple of (str|Path, int) with filename and line number.
:rtype: A tuple of (str|Path, int) with filename and 0-based line number.
"""
# See Item.location.
location: Optional[Tuple[str, Optional[int], str]] = getattr(node, "location", None)
@@ -755,7 +755,7 @@ class Item(Node):
Returns a tuple with three elements:
- The path of the test (default ``self.path``)
- The line number of the test (default ``None``)
- The 0-based line number of the test (default ``None``)
- A name of the test to be shown (default ``""``)
.. seealso:: :ref:`non-python tests`
@@ -764,6 +764,11 @@ class Item(Node):
@cached_property
def location(self) -> Tuple[str, Optional[int], str]:
"""
Returns a tuple of ``(relfspath, lineno, testname)`` for this item
where ``relfspath`` is file path relative to ``config.rootpath``
and lineno is a 0-based line number.
"""
location = self.reportinfo()
path = absolutepath(os.fspath(location[0]))
relfspath = self.session._node_location_to_relpath(path)

View File

@@ -157,8 +157,12 @@ def skip(
The message to show the user as reason for the skip.
:param allow_module_level:
Allows this function to be called at module level, skipping the rest
of the module. Defaults to False.
Allows this function to be called at module level.
Raising the skip exception at module level will stop
the execution of the module and prevent the collection of all tests in the module,
even those defined before the `skip` call.
Defaults to False.
:param msg:
Same as ``reason``, but deprecated. Will be removed in a future version, use ``reason`` instead.
@@ -219,7 +223,6 @@ def _resolve_msg_to_reason(
"""
__tracebackhide__ = True
if msg is not None:
if reason:
from pytest import UsageError

View File

@@ -477,14 +477,14 @@ def import_path(
* `mode == ImportMode.prepend`: the directory containing the module (or package, taking
`__init__.py` files into account) will be put at the *start* of `sys.path` before
being imported with `__import__.
being imported with `importlib.import_module`.
* `mode == ImportMode.append`: same as `prepend`, but the directory will be appended
to the end of `sys.path`, if not already in `sys.path`.
* `mode == ImportMode.importlib`: uses more fine control mechanisms provided by `importlib`
to import the module, which avoids having to use `__import__` and muck with `sys.path`
at all. It effectively allows having same-named test modules in different places.
to import the module, which avoids having to muck with `sys.path` at all. It effectively
allows having same-named test modules in different places.
:param root:
Used as an anchor when mode == ImportMode.importlib to obtain

View File

@@ -1,4 +1,5 @@
"""Python test discovery, setup and run of test functions."""
import dataclasses
import enum
import fnmatch
import inspect
@@ -27,8 +28,6 @@ from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import attr
import _pytest
from _pytest import fixtures
from _pytest import nodes
@@ -790,7 +789,8 @@ def _call_with_optional_argument(func, arg) -> None:
def _get_first_non_fixture_func(obj: object, names: Iterable[str]) -> Optional[object]:
"""Return the attribute from the given object to be used as a setup/teardown
xunit-style function, but only if not marked as a fixture to avoid calling it twice."""
xunit-style function, but only if not marked as a fixture to avoid calling it twice.
"""
for name in names:
meth: Optional[object] = getattr(obj, name, None)
if meth is not None and fixtures.getfixturemarker(meth) is None:
@@ -848,7 +848,7 @@ class Class(PyCollector):
other fixtures (#517).
"""
setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",))
teardown_class = getattr(self.obj, "teardown_class", None)
teardown_class = _get_first_non_fixture_func(self.obj, ("teardown_class",))
if setup_class is None and teardown_class is None:
return
@@ -885,12 +885,12 @@ class Class(PyCollector):
emit_nose_setup_warning = True
setup_method = _get_first_non_fixture_func(self.obj, (setup_name,))
teardown_name = "teardown_method"
teardown_method = getattr(self.obj, teardown_name, None)
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
emit_nose_teardown_warning = False
if teardown_method is None and has_nose:
teardown_name = "teardown"
emit_nose_teardown_warning = True
teardown_method = getattr(self.obj, teardown_name, None)
teardown_method = _get_first_non_fixture_func(self.obj, (teardown_name,))
if setup_method is None and teardown_method is None:
return
@@ -956,10 +956,20 @@ def hasnew(obj: object) -> bool:
@final
@attr.s(frozen=True, auto_attribs=True, slots=True)
@dataclasses.dataclass(frozen=True)
class IdMaker:
"""Make IDs for a parametrization."""
__slots__ = (
"argnames",
"parametersets",
"idfn",
"ids",
"config",
"nodeid",
"func_name",
)
# The argnames of the parametrization.
argnames: Sequence[str]
# The ParameterSets of the parametrization.
@@ -1109,7 +1119,7 @@ class IdMaker:
@final
@attr.s(frozen=True, slots=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class CallSpec2:
"""A planned parameterized invocation of a test function.
@@ -1120,18 +1130,18 @@ class CallSpec2:
# arg name -> arg value which will be passed to the parametrized test
# function (direct parameterization).
funcargs: Dict[str, object] = attr.Factory(dict)
funcargs: Dict[str, object] = dataclasses.field(default_factory=dict)
# arg name -> arg value which will be passed to a fixture of the same name
# (indirect parametrization).
params: Dict[str, object] = attr.Factory(dict)
params: Dict[str, object] = dataclasses.field(default_factory=dict)
# arg name -> arg index.
indices: Dict[str, int] = attr.Factory(dict)
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
# Used for sorting parametrized resources.
_arg2scope: Dict[str, Scope] = attr.Factory(dict)
_arg2scope: Dict[str, Scope] = dataclasses.field(default_factory=dict)
# Parts which will be added to the item's name in `[..]` separated by "-".
_idlist: List[str] = attr.Factory(list)
_idlist: List[str] = dataclasses.field(default_factory=list)
# Marks which will be applied to the item.
marks: List[Mark] = attr.Factory(list)
marks: List[Mark] = dataclasses.field(default_factory=list)
def setmulti(
self,
@@ -1163,9 +1173,9 @@ class CallSpec2:
return CallSpec2(
funcargs=funcargs,
params=params,
arg2scope=arg2scope,
indices=indices,
idlist=[*self._idlist, id],
_arg2scope=arg2scope,
_idlist=[*self._idlist, id],
marks=[*self.marks, *normalize_mark_list(marks)],
)

View File

@@ -8,7 +8,7 @@ from types import TracebackType
from typing import Any
from typing import Callable
from typing import cast
from typing import Generic
from typing import ContextManager
from typing import List
from typing import Mapping
from typing import Optional
@@ -269,10 +269,16 @@ class ApproxMapping(ApproxBase):
max_abs_diff = max(
max_abs_diff, abs(approx_value.expected - other_value)
)
max_rel_diff = max(
max_rel_diff,
abs((approx_value.expected - other_value) / approx_value.expected),
)
if approx_value.expected == 0.0:
max_rel_diff = math.inf
else:
max_rel_diff = max(
max_rel_diff,
abs(
(approx_value.expected - other_value)
/ approx_value.expected
),
)
different_ids.append(approx_key)
message_data = [
@@ -802,7 +808,7 @@ def raises( # noqa: F811
:param typing.Type[E] | typing.Tuple[typing.Type[E], ...] expected_exception:
The expected exception type, or a tuple if one of multiple possible
exception types are excepted.
exception types are expected.
:kwparam str | typing.Pattern[str] | None match:
If specified, a string containing a regular expression,
or a regular expression object, that is tested against the string
@@ -918,10 +924,10 @@ def raises( # noqa: F811
f"any special code to say 'this should never raise an exception'."
)
if isinstance(expected_exception, type):
excepted_exceptions: Tuple[Type[E], ...] = (expected_exception,)
expected_exceptions: Tuple[Type[E], ...] = (expected_exception,)
else:
excepted_exceptions = expected_exception
for exc in excepted_exceptions:
expected_exceptions = expected_exception
for exc in expected_exceptions:
if not isinstance(exc, type) or not issubclass(exc, BaseException):
msg = "expected exception must be a BaseException type, not {}" # type: ignore[unreachable]
not_a = exc.__name__ if isinstance(exc, type) else type(exc).__name__
@@ -957,7 +963,7 @@ raises.Exception = fail.Exception # type: ignore
@final
class RaisesContext(Generic[E]):
class RaisesContext(ContextManager[_pytest._code.ExceptionInfo[E]]):
def __init__(
self,
expected_exception: Union[Type[E], Tuple[Type[E], ...]],

View File

@@ -1,3 +1,4 @@
import dataclasses
import os
from io import StringIO
from pprint import pprint
@@ -16,8 +17,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionInfo
from _pytest._code.code import ExceptionRepr
@@ -263,6 +262,8 @@ class TestReport(BaseReport):
when: "Literal['setup', 'call', 'teardown']",
sections: Iterable[Tuple[str, str]] = (),
duration: float = 0,
start: float = 0,
stop: float = 0,
user_properties: Optional[Iterable[Tuple[str, object]]] = None,
**extra,
) -> None:
@@ -272,6 +273,8 @@ class TestReport(BaseReport):
#: A (filesystempath, lineno, domaininfo) tuple indicating the
#: actual location of a test item - it might be different from the
#: collected one e.g. if a method is inherited from a different module.
#: The filesystempath may be relative to ``config.rootdir``.
#: The line number is 0-based.
self.location: Tuple[str, Optional[int], str] = location
#: A name -> value dictionary containing all keywords and
@@ -300,6 +303,11 @@ class TestReport(BaseReport):
#: Time it took to run just the test.
self.duration: float = duration
#: The system time when the call started, in seconds since the epoch.
self.start: float = start
#: The system time when the call ended, in seconds since the epoch.
self.stop: float = stop
self.__dict__.update(extra)
def __repr__(self) -> str:
@@ -318,6 +326,8 @@ class TestReport(BaseReport):
# Remove "collect" from the Literal type -- only for collection calls.
assert when != "collect"
duration = call.duration
start = call.start
stop = call.stop
keywords = {x: 1 for x in item.keywords}
excinfo = call.excinfo
sections = []
@@ -337,6 +347,10 @@ 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."
)
if excinfo.value._use_item_location:
path, line = item.reportinfo()[:2]
assert line is not None
@@ -362,6 +376,8 @@ class TestReport(BaseReport):
when,
sections,
duration,
start,
stop,
user_properties=item.user_properties,
)
@@ -407,7 +423,9 @@ class CollectReport(BaseReport):
self.__dict__.update(extra)
@property
def location(self):
def location( # type:ignore[override]
self,
) -> Optional[Tuple[str, Optional[int], str]]:
return (self.fspath, None, self.fspath)
def __repr__(self) -> str:
@@ -459,15 +477,15 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
def serialize_repr_entry(
entry: Union[ReprEntry, ReprEntryNative]
) -> Dict[str, Any]:
data = attr.asdict(entry)
data = dataclasses.asdict(entry)
for key, value in data.items():
if hasattr(value, "__dict__"):
data[key] = attr.asdict(value)
data[key] = dataclasses.asdict(value)
entry_data = {"type": type(entry).__name__, "data": data}
return entry_data
def serialize_repr_traceback(reprtraceback: ReprTraceback) -> Dict[str, Any]:
result = attr.asdict(reprtraceback)
result = dataclasses.asdict(reprtraceback)
result["reprentries"] = [
serialize_repr_entry(x) for x in reprtraceback.reprentries
]
@@ -477,7 +495,7 @@ def _report_to_json(report: BaseReport) -> Dict[str, Any]:
reprcrash: Optional[ReprFileLocation],
) -> Optional[Dict[str, Any]]:
if reprcrash is not None:
return attr.asdict(reprcrash)
return dataclasses.asdict(reprcrash)
else:
return None
@@ -573,7 +591,6 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
and "reprcrash" in reportdict["longrepr"]
and "reprtraceback" in reportdict["longrepr"]
):
reprtraceback = deserialize_repr_traceback(
reportdict["longrepr"]["reprtraceback"]
)
@@ -594,7 +611,10 @@ def _report_kwargs_from_json(reportdict: Dict[str, Any]) -> Dict[str, Any]:
ExceptionChainRepr, ReprExceptionInfo
] = ExceptionChainRepr(chain)
else:
exception_info = ReprExceptionInfo(reprtraceback, reprcrash)
exception_info = ReprExceptionInfo(
reprtraceback=reprtraceback,
reprcrash=reprcrash,
)
for section in reportdict["longrepr"]["sections"]:
exception_info.addsection(*section)

View File

@@ -1,5 +1,6 @@
"""Basic collect and runtest protocol implementations."""
import bdb
import dataclasses
import os
import sys
from typing import Callable
@@ -14,8 +15,6 @@ from typing import TYPE_CHECKING
from typing import TypeVar
from typing import Union
import attr
from .reports import BaseReport
from .reports import CollectErrorRepr
from .reports import CollectReport
@@ -268,7 +267,7 @@ TResult = TypeVar("TResult", covariant=True)
@final
@attr.s(repr=False, init=False, auto_attribs=True)
@dataclasses.dataclass
class CallInfo(Generic[TResult]):
"""Result/Exception info of a function invocation."""

View File

@@ -1,4 +1,5 @@
"""Support for skip/xfail functions and markers."""
import dataclasses
import os
import platform
import sys
@@ -9,8 +10,6 @@ from typing import Optional
from typing import Tuple
from typing import Type
import attr
from _pytest.config import Config
from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser
@@ -157,7 +156,7 @@ def evaluate_condition(item: Item, mark: Mark, condition: object) -> Tuple[bool,
return result, reason
@attr.s(slots=True, frozen=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class Skip:
"""The result of evaluate_skip_marks()."""
@@ -192,10 +191,12 @@ def evaluate_skip_marks(item: Item) -> Optional[Skip]:
return None
@attr.s(slots=True, frozen=True, auto_attribs=True)
@dataclasses.dataclass(frozen=True)
class Xfail:
"""The result of evaluate_xfail_marks()."""
__slots__ = ("reason", "run", "strict", "raises")
reason: str
run: bool
strict: bool

View File

@@ -48,6 +48,10 @@ def pytest_configure(config: Config) -> None:
def pytest_sessionfinish(session: Session) -> None:
if not session.config.getoption("stepwise"):
assert session.config.cache is not None
if hasattr(session.config, "workerinput"):
# Do not update cache if this process is a xdist worker to prevent
# race conditions (#10641).
return
# Clear the list of failing tests if the plugin is not active.
session.config.cache.set(STEPWISE_CACHE_DIR, [])
@@ -119,4 +123,8 @@ class StepwisePlugin:
return None
def pytest_sessionfinish(self) -> None:
if hasattr(self.config, "workerinput"):
# Do not update cache if this process is a xdist worker to prevent
# race conditions (#10641).
return
self.cache.set(STEPWISE_CACHE_DIR, self.lastfailed)

View File

@@ -3,6 +3,7 @@
This is a good source for looking at the various reporting hooks.
"""
import argparse
import dataclasses
import datetime
import inspect
import platform
@@ -27,7 +28,6 @@ from typing import Tuple
from typing import TYPE_CHECKING
from typing import Union
import attr
import pluggy
import _pytest._version
@@ -229,7 +229,8 @@ def pytest_addoption(parser: Parser) -> None:
parser.addini(
"console_output_style",
help='Console output: "classic", or with additional progress information '
'("progress" (percentage) | "count")',
'("progress" (percentage) | "count" | "progress-even-when-capture-no" (forces '
"progress even when capture=no)",
default="progress",
)
@@ -287,7 +288,7 @@ def pytest_report_teststatus(report: BaseReport) -> Tuple[str, str, str]:
return outcome, letter, outcome.upper()
@attr.s(auto_attribs=True)
@dataclasses.dataclass
class WarningReport:
"""Simple structure to hold warnings information captured by ``pytest_warning_recorded``.
@@ -346,14 +347,19 @@ class TerminalReporter:
def _determine_show_progress_info(self) -> "Literal['progress', 'count', False]":
"""Return whether we should display progress information based on the current config."""
# do not show progress if we are not capturing output (#3038)
if self.config.getoption("capture", "no") == "no":
# do not show progress if we are not capturing output (#3038) unless explicitly
# overridden by progress-even-when-capture-no
if (
self.config.getoption("capture", "no") == "no"
and self.config.getini("console_output_style")
!= "progress-even-when-capture-no"
):
return False
# do not show progress if we are showing fixture setup/teardown
if self.config.getoption("setupshow", False):
return False
cfg: str = self.config.getini("console_output_style")
if cfg == "progress":
if cfg == "progress" or cfg == "progress-even-when-capture-no":
return "progress"
elif cfg == "count":
return "count"
@@ -733,16 +739,14 @@ class TerminalReporter:
self.write_line(line)
def pytest_report_header(self, config: Config) -> List[str]:
line = "rootdir: %s" % config.rootpath
result = [f"rootdir: {config.rootpath}"]
if config.inipath:
line += ", configfile: " + bestrelpath(config.rootpath, config.inipath)
result.append("configfile: " + bestrelpath(config.rootpath, config.inipath))
if config.args_source == Config.ArgsSource.TESTPATHS:
testpaths: List[str] = config.getini("testpaths")
line += ", testpaths: {}".format(", ".join(testpaths))
result = [line]
result.append("testpaths: {}".format(", ".join(testpaths)))
plugininfo = config.pluginmanager.list_plugin_distinfo()
if plugininfo:

View File

@@ -1,10 +1,11 @@
"""Support for providing temporary directories to test functions."""
import dataclasses
import os
import re
import sys
import tempfile
from pathlib import Path
from shutil import rmtree
from typing import Any
from typing import Dict
from typing import Generator
from typing import Optional
@@ -21,7 +22,6 @@ if TYPE_CHECKING:
RetentionType = Literal["all", "failed", "none"]
import attr
from _pytest.config.argparsing import Parser
from .pathlib import LOCK_TIMEOUT
@@ -29,7 +29,7 @@ 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 _pytest.compat import final
from _pytest.compat import final, get_user_id
from _pytest.config import Config
from _pytest.config import ExitCode
from _pytest.config import hookimpl
@@ -42,18 +42,19 @@ tmppath_result_key = StashKey[Dict[str, bool]]()
@final
@attr.s(init=False)
@dataclasses.dataclass
class TempPathFactory:
"""Factory for temporary directories under the common base temp directory.
The base directory can be configured using the ``--basetemp`` option.
"""
_given_basetemp = attr.ib(type=Optional[Path])
_trace = attr.ib()
_basetemp = attr.ib(type=Optional[Path])
_retention_count = attr.ib(type=int)
_retention_policy = attr.ib(type="RetentionType")
_given_basetemp: Optional[Path]
# pluggy TagTracerSub, not currently exposed, so Any.
_trace: Any
_basetemp: Optional[Path]
_retention_count: int
_retention_policy: "RetentionType"
def __init__(
self,
@@ -174,19 +175,16 @@ class TempPathFactory:
# Also, to keep things private, fixup any world-readable temp
# rootdir's permissions. Historically 0o755 was used, so we can't
# just error out on this, at least for a while.
if sys.platform != "win32":
uid = os.getuid()
uid = get_user_id()
if uid is not None:
rootdir_stat = rootdir.stat()
# getuid shouldn't fail, but cpython defines such a case.
# Let's hope for the best.
if uid != -1:
if rootdir_stat.st_uid != uid:
raise OSError(
f"The temporary directory {rootdir} is not owned by the current user. "
"Fix this and try again."
)
if (rootdir_stat.st_mode & 0o077) != 0:
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
if rootdir_stat.st_uid != uid:
raise OSError(
f"The temporary directory {rootdir} is not owned by the current user. "
"Fix this and try again."
)
if (rootdir_stat.st_mode & 0o077) != 0:
os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
keep = self._retention_count
if self._retention_policy == "none":
keep = 0
@@ -239,7 +237,7 @@ def pytest_addoption(parser: Parser) -> None:
"tmp_path_retention_policy",
help="Controls which directories created by the `tmp_path` fixture are kept around, based on test outcome. "
"(all/failed/none)",
default="failed",
default="all",
)
@@ -267,8 +265,8 @@ def tmp_path(
directory.
By default, a new base temporary directory is created each test session,
and only the base of failed session is kept. Also it only keeps the last 3 bases
at most. This can be configured with :confval:`tmp_path_retention_count` and
and old bases are removed after 3 sessions, to aid in debugging.
This behavior can be configured with :confval:`tmp_path_retention_count` and
:confval:`tmp_path_retention_policy`.
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
temporary directory`.

View File

@@ -1,3 +1,4 @@
import dataclasses
import inspect
import warnings
from types import FunctionType
@@ -6,8 +7,6 @@ from typing import Generic
from typing import Type
from typing import TypeVar
import attr
from _pytest.compat import final
@@ -130,7 +129,7 @@ _W = TypeVar("_W", bound=PytestWarning)
@final
@attr.s(auto_attribs=True)
@dataclasses.dataclass
class UnformattedWarning(Generic[_W]):
"""A warning meant to be formatted during runtime.

View File

@@ -1,9 +1,8 @@
import dataclasses
import os
import sys
import types
import attr
import pytest
from _pytest.compat import importlib_metadata
from _pytest.config import ExitCode
@@ -115,11 +114,11 @@ class TestGeneralUsage:
loaded = []
@attr.s
@dataclasses.dataclass
class DummyEntryPoint:
name = attr.ib()
module = attr.ib()
group = "pytest11"
name: str
module: str
group: str = "pytest11"
def load(self):
__import__(self.module)
@@ -132,10 +131,10 @@ class TestGeneralUsage:
DummyEntryPoint("mycov", "mycov_module"),
]
@attr.s
@dataclasses.dataclass
class DummyDist:
entry_points = attr.ib()
files = ()
entry_points: object
files: object = ()
def my_dists():
return (DummyDist(entry_points),)
@@ -694,7 +693,14 @@ class TestInvocationVariants:
# mixed module and filenames:
monkeypatch.chdir("world")
result = pytester.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world")
# pgk_resources.declare_namespace has been deprecated in favor of implicit namespace packages.
# 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"
result = pytester.runpytest(
"--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world", ignore_w
)
assert result.ret == 0
result.stdout.fnmatch_lines(
[
@@ -872,7 +878,6 @@ class TestDurations:
)
def test_calls_show_2(self, pytester: Pytester, mock_timing) -> None:
pytester.makepyfile(self.source)
result = pytester.runpytest_inprocess("--durations=2")
assert result.ret == 0
@@ -1037,14 +1042,14 @@ def test_fixture_values_leak(pytester: Pytester) -> None:
"""
pytester.makepyfile(
"""
import attr
import dataclasses
import gc
import pytest
import weakref
@attr.s
class SomeObj(object):
name = attr.ib()
@dataclasses.dataclass
class SomeObj:
name: str
fix_of_test1_ref = None
session_ref = None

View File

@@ -294,6 +294,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback
entry = tb.getcrashentry()
assert entry is not None
co = _pytest._code.Code.from_function(h)
assert entry.frame.code.path == co.path
assert entry.lineno == co.firstlineno + 1
@@ -311,10 +312,7 @@ class TestTraceback_f_g_h:
excinfo = pytest.raises(ValueError, f)
tb = excinfo.traceback
entry = tb.getcrashentry()
co = _pytest._code.Code.from_function(g)
assert entry.frame.code.path == co.path
assert entry.lineno == co.firstlineno + 2
assert entry.frame.code.name == "g"
assert entry is None
def test_excinfo_exconly():
@@ -463,6 +461,24 @@ class TestFormattedExcinfo:
assert lines[0] == "| def f(x):"
assert lines[1] == " pass"
def test_repr_source_out_of_bounds(self):
pr = FormattedExcinfo()
source = _pytest._code.Source(
"""\
def f(x):
pass
"""
).strip()
pr.flow_marker = "|" # type: ignore[misc]
lines = pr.get_source(source, 100)
assert len(lines) == 1
assert lines[0] == "| ???"
lines = pr.get_source(source, -100)
assert len(lines) == 1
assert lines[0] == "| ???"
def test_repr_source_excinfo(self) -> None:
"""Check if indentation is right."""
try:

View File

@@ -1,3 +1,4 @@
import dataclasses
import re
import sys
from typing import List
@@ -157,6 +158,7 @@ def color_mapping():
"number": "\x1b[94m",
"str": "\x1b[33m",
"print": "\x1b[96m",
"endline": "\x1b[90m\x1b[39;49;00m",
}
RE_COLORS = {k: re.escape(v) for k, v in COLORS.items()}
@@ -191,20 +193,18 @@ def mock_timing(monkeypatch: MonkeyPatch):
Time is static, and only advances through `sleep` calls, thus tests might sleep over large
numbers and obtain accurate time() calls at the end, making tests reliable and instant.
"""
import attr
@attr.s
@dataclasses.dataclass
class MockTiming:
_current_time: float = 1590150050.0
_current_time = attr.ib(default=1590150050.0)
def sleep(self, seconds):
def sleep(self, seconds: float) -> None:
self._current_time += seconds
def time(self):
def time(self) -> float:
return self._current_time
def patch(self):
def patch(self) -> None:
from _pytest import timing
monkeypatch.setattr(timing, "sleep", self.sleep)

View File

@@ -254,7 +254,7 @@ class TestTerminalWriterLineWidth:
pytest.param(
True,
True,
"{kw}assert{hl-reset} {number}0{hl-reset}\n",
"{kw}assert{hl-reset} {number}0{hl-reset}{endline}\n",
id="with markup and code_highlight",
),
pytest.param(

View File

@@ -1,13 +1,13 @@
anyio[curio,trio]==3.6.2
django==4.1.3
pytest-asyncio==0.20.2
django==4.1.7
pytest-asyncio==0.21.0
pytest-bdd==6.1.1
pytest-cov==4.0.0
pytest-django==4.5.2
pytest-flakes==4.0.5
pytest-html==3.2.0
pytest-mock==3.10.0
pytest-rerunfailures==10.3
pytest-rerunfailures==11.1.2
pytest-sugar==0.9.5
pytest-trio==0.7.0
pytest-twisted==1.14.0

View File

@@ -630,6 +630,19 @@ class TestApprox:
def test_dict_vs_other(self):
assert 1 != approx({"a": 0})
def test_dict_for_div_by_zero(self, assert_approx_raises_regex):
assert_approx_raises_regex(
{"foo": 42.0},
{"foo": 0.0},
[
r" comparison failed. Mismatched elements: 1 / 1:",
rf" Max absolute difference: {SOME_FLOAT}",
r" Max relative difference: inf",
r" Index \| Obtained\s+\| Expected ",
rf" foo | {SOME_FLOAT} \| {SOME_FLOAT} ± {SOME_FLOAT}",
],
)
def test_numpy_array(self):
np = pytest.importorskip("numpy")

View File

@@ -3338,6 +3338,10 @@ class TestShowFixtures:
config = pytester.parseconfigure("--funcargs")
assert config.option.showfixtures
def test_show_help(self, pytester: Pytester) -> None:
result = pytester.runpytest("--fixtures", "--help")
assert not result.ret
def test_show_fixtures(self, pytester: Pytester) -> None:
result = pytester.runpytest("--fixtures")
result.stdout.fnmatch_lines(

View File

@@ -1,3 +1,4 @@
import dataclasses
import itertools
import re
import sys
@@ -12,7 +13,6 @@ from typing import Sequence
from typing import Tuple
from typing import Union
import attr
import hypothesis
from hypothesis import strategies
@@ -39,14 +39,14 @@ class TestMetafunc:
def __init__(self, names):
self.names_closure = names
@attr.s
@dataclasses.dataclass
class DefinitionMock(python.FunctionDefinition):
obj = attr.ib()
_nodeid = attr.ib()
_nodeid: str
obj: object
names = getfuncargnames(func)
fixtureinfo: Any = FuncFixtureInfoMock(names)
definition: Any = DefinitionMock._create(func, "mock::nodeid")
definition: Any = DefinitionMock._create(obj=func, _nodeid="mock::nodeid")
return python.Metafunc(definition, fixtureinfo, config, _ispytest=True)
def test_no_funcargs(self) -> None:
@@ -140,9 +140,9 @@ class TestMetafunc:
"""Unit test for _find_parametrized_scope (#3941)."""
from _pytest.python import _find_parametrized_scope
@attr.s
@dataclasses.dataclass
class DummyFixtureDef:
_scope = attr.ib()
_scope: Scope
fixtures_defs = cast(
Dict[str, Sequence[fixtures.FixtureDef[object]]],

View File

@@ -1265,6 +1265,177 @@ class TestIssue2121:
result.stdout.fnmatch_lines(["*E*assert (1 + 1) == 3"])
@pytest.mark.skipif(
sys.version_info < (3, 8), reason="walrus operator not available in py<38"
)
class TestIssue10743:
def test_assertion_walrus_operator(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
def my_func(before, after):
return before == after
def change_value(value):
return value.lower()
def test_walrus_conversion():
a = "Hello"
assert not my_func(a, a := change_value(a))
assert a == "hello"
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_dont_rewrite(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
'PYTEST_DONT_REWRITE'
def my_func(before, after):
return before == after
def change_value(value):
return value.lower()
def test_walrus_conversion_dont_rewrite():
a = "Hello"
assert not my_func(a, a := change_value(a))
assert a == "hello"
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_inline_walrus_operator(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
def my_func(before, after):
return before == after
def test_walrus_conversion_inline():
a = "Hello"
assert not my_func(a, a := a.lower())
assert a == "hello"
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_inline_walrus_operator_reverse(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
def my_func(before, after):
return before == after
def test_walrus_conversion_reverse():
a = "Hello"
assert my_func(a := a.lower(), a)
assert a == 'hello'
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_no_variable_name_conflict(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def test_walrus_conversion_no_conflict():
a = "Hello"
assert a == (b := a.lower())
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.fnmatch_lines(["*AssertionError: assert 'Hello' == 'hello'"])
def test_assertion_walrus_operator_true_assertion_and_changes_variable_value(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def test_walrus_conversion_succeed():
a = "Hello"
assert a != (a := a.lower())
assert a == 'hello'
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_fail_assertion(self, pytester: Pytester) -> None:
pytester.makepyfile(
"""
def test_walrus_conversion_fails():
a = "Hello"
assert a == (a := a.lower())
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.fnmatch_lines(["*AssertionError: assert 'Hello' == 'hello'"])
def test_assertion_walrus_operator_boolean_composite(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def test_walrus_operator_change_boolean_value():
a = True
assert a and True and ((a := False) is False) and (a is False) and ((a := None) is None)
assert a is None
"""
)
result = pytester.runpytest()
assert result.ret == 0
def test_assertion_walrus_operator_compare_boolean_fails(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def test_walrus_operator_change_boolean_value():
a = True
assert not (a and ((a := False) is False))
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.fnmatch_lines(["*assert not (True and False is False)"])
def test_assertion_walrus_operator_boolean_none_fails(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def test_walrus_operator_change_boolean_value():
a = True
assert not (a and ((a := None) is None))
"""
)
result = pytester.runpytest()
assert result.ret == 1
result.stdout.fnmatch_lines(["*assert not (True and None is None)"])
def test_assertion_walrus_operator_value_changes_cleared_after_each_test(
self, pytester: Pytester
) -> None:
pytester.makepyfile(
"""
def test_walrus_operator_change_value():
a = True
assert (a := None) is None
def test_walrus_operator_not_override_value():
a = True
assert a is True
"""
)
result = pytester.runpytest()
assert result.ret == 0
@pytest.mark.skipif(
sys.maxsize <= (2**31 - 1), reason="Causes OverflowError on 32bit systems"
)

View File

@@ -494,7 +494,6 @@ class TestLastFailed:
def test_lastfailed_collectfailure(
self, pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:
pytester.makepyfile(
test_maybe="""
import os
@@ -1249,3 +1248,8 @@ def test_cachedir_tag(pytester: Pytester) -> None:
cache.set("foo", "bar")
cachedir_tag_path = cache._cachedir.joinpath("CACHEDIR.TAG")
assert cachedir_tag_path.read_bytes() == CACHEDIR_TAG_CONTENT
def test_clioption_with_cacheshow_and_help(pytester: Pytester) -> None:
result = pytester.runpytest("--cache-show", "--help")
assert result.ret == 0

View File

@@ -890,7 +890,7 @@ def test_dontreadfrominput() -> None:
from _pytest.capture import DontReadFromInput
f = DontReadFromInput()
assert f.buffer is f
assert f.buffer is f # type: ignore[comparison-overlap]
assert not f.isatty()
pytest.raises(OSError, f.read)
pytest.raises(OSError, f.readlines)
@@ -906,7 +906,10 @@ def test_dontreadfrominput() -> None:
pytest.raises(UnsupportedOperation, f.write, b"")
pytest.raises(UnsupportedOperation, f.writelines, [])
assert not f.writable()
assert isinstance(f.encoding, str)
f.close() # just for completeness
with f:
pass
def test_captureresult() -> None:
@@ -1049,6 +1052,7 @@ class TestFDCapture:
)
)
# Should not crash with missing "_old".
assert isinstance(cap.syscapture, capture.SysCapture)
assert repr(cap.syscapture) == (
"<SysCapture stdout _old=<UNSET> _state='done' tmpfile={!r}>".format(
cap.syscapture.tmpfile
@@ -1349,6 +1353,7 @@ def test_capsys_results_accessible_by_attribute(capsys: CaptureFixture[str]) ->
def test_fdcapture_tmpfile_remains_the_same() -> None:
cap = StdCaptureFD(out=False, err=True)
assert isinstance(cap.err, capture.FDCapture)
try:
cap.start_capturing()
capfile = cap.err.tmpfile

View File

@@ -1,3 +1,4 @@
import dataclasses
import os
import re
import sys
@@ -10,8 +11,6 @@ from typing import Tuple
from typing import Type
from typing import Union
import attr
import _pytest._code
import pytest
from _pytest.compat import importlib_metadata
@@ -75,7 +74,7 @@ class TestParseIni:
% p1.name,
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(["*, configfile: setup.cfg, *", "* 1 passed in *"])
result.stdout.fnmatch_lines(["configfile: setup.cfg", "* 1 passed in *"])
assert result.ret == 0
def test_append_parse_args(
@@ -423,11 +422,11 @@ class TestParseIni:
This test installs a mock "myplugin-1.5" which is used in the parametrized test cases.
"""
@attr.s
@dataclasses.dataclass
class DummyEntryPoint:
name = attr.ib()
module = attr.ib()
group = "pytest11"
name: str
module: str
group: str = "pytest11"
def load(self):
__import__(self.module)
@@ -437,11 +436,11 @@ class TestParseIni:
DummyEntryPoint("myplugin1", "myplugin1_module"),
]
@attr.s
@dataclasses.dataclass
class DummyDist:
entry_points = attr.ib()
files = ()
version = plugin_version
entry_points: object
files: object = ()
version: str = plugin_version
@property
def metadata(self):
@@ -1809,6 +1808,10 @@ def test_config_does_not_load_blocked_plugin_from_args(pytester: Pytester) -> No
result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"])
assert result.ret == ExitCode.USAGE_ERROR
result = pytester.runpytest(str(p), "-p no:capture", "-s")
result.stderr.fnmatch_lines(["*: error: unrecognized arguments: -s"])
assert result.ret == ExitCode.USAGE_ERROR
def test_invocation_args(pytester: Pytester) -> None:
"""Ensure that Config.invocation_* arguments are correctly defined"""

View File

@@ -1236,7 +1236,6 @@ class TestDoctestSkips:
class TestDoctestAutoUseFixtures:
SCOPES = ["module", "session", "class", "function"]
def test_doctest_module_session_fixture(self, pytester: Pytester):
@@ -1379,7 +1378,6 @@ class TestDoctestAutoUseFixtures:
class TestDoctestNamespaceFixture:
SCOPES = ["module", "session", "class", "function"]
@pytest.mark.parametrize("scope", SCOPES)

View File

@@ -253,7 +253,6 @@ class TestPython:
duration_report: str,
run_and_parse: RunAndParse,
) -> None:
# mock LogXML.node_reporter so it always sets a known duration to each test report object
original_node_reporter = LogXML.node_reporter
@@ -603,7 +602,6 @@ class TestPython:
node.assert_attr(failures=3, tests=3)
for index, char in enumerate("<&'"):
tnode = node.find_nth_by_tag("testcase", index)
tnode.assert_attr(
classname="test_failure_escape", name="test_func[%s]" % char

View File

@@ -425,6 +425,9 @@ def test_context_classmethod() -> None:
assert A.x == 1
@pytest.mark.filterwarnings(
"ignore:Deprecated call to `pkg_resources.declare_namespace"
)
def test_syspath_prepend_with_namespace_packages(
pytester: Pytester, monkeypatch: MonkeyPatch
) -> None:

View File

@@ -496,3 +496,24 @@ def test_nose_setup_skipped_if_non_callable(pytester: Pytester) -> None:
)
result = pytester.runpytest(p, "-p", "nose")
assert result.ret == 0
@pytest.mark.parametrize("fixture_name", ("teardown", "teardown_class"))
def test_teardown_fixture_not_called_directly(fixture_name, pytester: Pytester) -> None:
"""Regression test for #10597."""
p = pytester.makepyfile(
f"""
import pytest
class TestHello:
@pytest.fixture
def {fixture_name}(self):
yield
def test_hello(self, {fixture_name}):
assert True
"""
)
result = pytester.runpytest(p, "-p", "nose")
assert result.ret == 0

View File

@@ -518,10 +518,10 @@ class TestImportLibMode:
fn1.write_text(
dedent(
"""
import attr
import dataclasses
import pickle
@attr.s(auto_attribs=True)
@dataclasses.dataclass
class Data:
x: int = 42
"""
@@ -533,10 +533,10 @@ class TestImportLibMode:
fn2.write_text(
dedent(
"""
import attr
import dataclasses
import pickle
@attr.s(auto_attribs=True)
@dataclasses.dataclass
class Data:
x: str = ""
"""

View File

@@ -6,6 +6,7 @@ from _pytest._code.code import ExceptionChainRepr
from _pytest._code.code import ExceptionRepr
from _pytest.config import Config
from _pytest.pytester import Pytester
from _pytest.python_api import approx
from _pytest.reports import CollectReport
from _pytest.reports import TestReport
@@ -415,6 +416,26 @@ class TestReportSerialization:
result.stdout.fnmatch_lines(["E *Error: No module named 'unknown'"])
result.stdout.no_fnmatch_line("ERROR - *ConftestImportFailure*")
def test_report_timestamps_match_duration(self, pytester: Pytester, mock_timing):
reprec = pytester.inline_runsource(
"""
import pytest
from _pytest import timing
@pytest.fixture
def fixture_():
timing.sleep(5)
yield
timing.sleep(5)
def test_1(fixture_): timing.sleep(10)
"""
)
reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 3
for report in reports:
data = report._to_json()
loaded_report = TestReport._from_json(data)
assert loaded_report.stop - loaded_report.start == approx(report.duration)
class TestHooks:
"""Test that the hooks are working correctly for plugins"""

View File

@@ -473,6 +473,7 @@ class TestSessionReports:
assert not rep.skipped
assert rep.passed
locinfo = rep.location
assert locinfo is not None
assert locinfo[0] == col.path.name
assert not locinfo[1]
assert locinfo[2] == col.path.name
@@ -906,6 +907,7 @@ def test_makereport_getsource_dynamic_code(
def test_store_except_info_on_error() -> None:
"""Test that upon test failure, the exception info is stored on
sys.last_traceback and friends."""
# Simulate item that might raise a specific exception, depending on `raise_error` class var
class ItemMightRaise:
nodeid = "item_that_raises"

View File

@@ -1,6 +1,10 @@
from pathlib import Path
import pytest
from _pytest.cacheprovider import Cache
from _pytest.monkeypatch import MonkeyPatch
from _pytest.pytester import Pytester
from _pytest.stepwise import STEPWISE_CACHE_DIR
@pytest.fixture
@@ -278,3 +282,76 @@ def test_stepwise_skip_is_independent(pytester: Pytester) -> None:
def test_sw_skip_help(pytester: Pytester) -> None:
result = pytester.runpytest("-h")
result.stdout.fnmatch_lines("*Implicitly enables --stepwise.")
def test_stepwise_xdist_dont_store_lastfailed(pytester: Pytester) -> None:
pytester.makefile(
ext=".ini",
pytest=f"[pytest]\ncache_dir = {pytester.path}\n",
)
pytester.makepyfile(
conftest="""
import pytest
@pytest.hookimpl(tryfirst=True)
def pytest_configure(config) -> None:
config.workerinput = True
"""
)
pytester.makepyfile(
test_one="""
def test_one():
assert False
"""
)
result = pytester.runpytest("--stepwise")
assert result.ret == pytest.ExitCode.INTERRUPTED
stepwise_cache_file = (
pytester.path / Cache._CACHE_PREFIX_VALUES / STEPWISE_CACHE_DIR
)
assert not Path(stepwise_cache_file).exists()
def test_disabled_stepwise_xdist_dont_clear_cache(pytester: Pytester) -> None:
pytester.makefile(
ext=".ini",
pytest=f"[pytest]\ncache_dir = {pytester.path}\n",
)
stepwise_cache_file = (
pytester.path / Cache._CACHE_PREFIX_VALUES / STEPWISE_CACHE_DIR
)
stepwise_cache_dir = stepwise_cache_file.parent
stepwise_cache_dir.mkdir(exist_ok=True, parents=True)
stepwise_cache_file_relative = f"{Cache._CACHE_PREFIX_VALUES}/{STEPWISE_CACHE_DIR}"
expected_value = '"test_one.py::test_one"'
content = {f"{stepwise_cache_file_relative}": expected_value}
pytester.makefile(ext="", **content)
pytester.makepyfile(
conftest="""
import pytest
@pytest.hookimpl(tryfirst=True)
def pytest_configure(config) -> None:
config.workerinput = True
"""
)
pytester.makepyfile(
test_one="""
def test_one():
assert True
"""
)
result = pytester.runpytest()
assert result.ret == 0
assert Path(stepwise_cache_file).exists()
with stepwise_cache_file.open() as file_handle:
observed_value = file_handle.readlines()
assert [expected_value] == observed_value

View File

@@ -909,7 +909,7 @@ class TestTerminalFunctional:
# with configfile
pytester.makeini("""[pytest]""")
result = pytester.runpytest()
result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"])
result.stdout.fnmatch_lines(["rootdir: *test_header0", "configfile: tox.ini"])
# with testpaths option, and not passing anything in the command-line
pytester.makeini(
@@ -920,12 +920,12 @@ class TestTerminalFunctional:
)
result = pytester.runpytest()
result.stdout.fnmatch_lines(
["rootdir: *test_header0, configfile: tox.ini, testpaths: tests, gui"]
["rootdir: *test_header0", "configfile: tox.ini", "testpaths: tests, gui"]
)
# with testpaths option, passing directory in command-line: do not show testpaths then
result = pytester.runpytest("tests")
result.stdout.fnmatch_lines(["rootdir: *test_header0, configfile: tox.ini"])
result.stdout.fnmatch_lines(["rootdir: *test_header0", "configfile: tox.ini"])
def test_header_absolute_testpath(
self, pytester: Pytester, monkeypatch: MonkeyPatch
@@ -944,9 +944,9 @@ class TestTerminalFunctional:
result = pytester.runpytest()
result.stdout.fnmatch_lines(
[
"rootdir: *absolute_testpath0, configfile: pyproject.toml, testpaths: {}".format(
tests
)
"rootdir: *absolute_testpath0",
"configfile: pyproject.toml",
f"testpaths: {tests}",
]
)
@@ -1265,14 +1265,14 @@ def test_color_yes(pytester: Pytester, color_mapping) -> None:
"=*= FAILURES =*=",
"{red}{bold}_*_ test_this _*_{reset}",
"",
" {kw}def{hl-reset} {function}test_this{hl-reset}():",
"> fail()",
" {kw}def{hl-reset} {function}test_this{hl-reset}():{endline}",
"> fail(){endline}",
"",
"{bold}{red}test_color_yes.py{reset}:5: ",
"_ _ * _ _*",
"",
" {kw}def{hl-reset} {function}fail{hl-reset}():",
"> {kw}assert{hl-reset} {number}0{hl-reset}",
" {kw}def{hl-reset} {function}fail{hl-reset}():{endline}",
"> {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
"{bold}{red}E assert 0{reset}",
"",
"{bold}{red}test_color_yes.py{reset}:2: AssertionError",
@@ -1292,9 +1292,9 @@ def test_color_yes(pytester: Pytester, color_mapping) -> None:
"=*= FAILURES =*=",
"{red}{bold}_*_ test_this _*_{reset}",
"{bold}{red}test_color_yes.py{reset}:5: in test_this",
" fail()",
" fail(){endline}",
"{bold}{red}test_color_yes.py{reset}:2: in fail",
" {kw}assert{hl-reset} {number}0{hl-reset}",
" {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
"{bold}{red}E assert 0{reset}",
"{red}=*= {red}{bold}1 failed{reset}{red} in *s{reset}{red} =*={reset}",
]
@@ -2213,6 +2213,24 @@ class TestProgressOutputStyle:
output = pytester.runpytest("--capture=no")
output.stdout.no_fnmatch_line("*%]*")
def test_capture_no_progress_enabled(
self, many_tests_files, pytester: Pytester
) -> None:
pytester.makeini(
"""
[pytest]
console_output_style = progress-even-when-capture-no
"""
)
output = pytester.runpytest("-s")
output.stdout.re_match_lines(
[
r"test_bar.py \.{10} \s+ \[ 50%\]",
r"test_foo.py \.{5} \s+ \[ 75%\]",
r"test_foobar.py \.{5} \s+ \[100%\]",
]
)
class TestProgressWithTeardown:
"""Ensure we show the correct percentages for tests that fail during teardown (#3088)"""
@@ -2472,8 +2490,8 @@ class TestCodeHighlight:
result.stdout.fnmatch_lines(
color_mapping.format_for_fnmatch(
[
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}",
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
"{bold}{red}E assert 1 == 10{reset}",
]
)
@@ -2494,9 +2512,9 @@ class TestCodeHighlight:
result.stdout.fnmatch_lines(
color_mapping.format_for_fnmatch(
[
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
" {print}print{hl-reset}({str}'''{hl-reset}{str}{hl-reset}",
"> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}",
"> {str} {hl-reset}{str}'''{hl-reset}); {kw}assert{hl-reset} {number}0{hl-reset}{endline}",
"{bold}{red}E assert 0{reset}",
]
)
@@ -2517,8 +2535,8 @@ class TestCodeHighlight:
result.stdout.fnmatch_lines(
color_mapping.format_for_fnmatch(
[
" {kw}def{hl-reset} {function}test_foo{hl-reset}():",
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}",
" {kw}def{hl-reset} {function}test_foo{hl-reset}():{endline}",
"> {kw}assert{hl-reset} {number}1{hl-reset} == {number}10{hl-reset}{endline}",
"{bold}{red}E assert 1 == 10{reset}",
]
)

View File

@@ -1,3 +1,4 @@
import dataclasses
import os
import stat
import sys
@@ -6,8 +7,7 @@ from pathlib import Path
from typing import Callable
from typing import cast
from typing import List
import attr
from typing import Union
import pytest
from _pytest import pathlib
@@ -31,9 +31,9 @@ def test_tmp_path_fixture(pytester: Pytester) -> None:
results.stdout.fnmatch_lines(["*1 passed*"])
@attr.s
@dataclasses.dataclass
class FakeConfig:
basetemp = attr.ib()
basetemp: Union[str, Path]
@property
def trace(self):
@@ -46,7 +46,7 @@ class FakeConfig:
if name == "tmp_path_retention_count":
return 3
elif name == "tmp_path_retention_policy":
return "failed"
return "all"
else:
assert False
@@ -56,7 +56,7 @@ class FakeConfig:
class TestTmpPathHandler:
def test_mktemp(self, tmp_path):
def test_mktemp(self, tmp_path: Path) -> None:
config = cast(Config, FakeConfig(tmp_path))
t = TempPathFactory.from_config(config, _ispytest=True)
tmp = t.mktemp("world")
@@ -67,7 +67,9 @@ class TestTmpPathHandler:
assert str(tmp2.relative_to(t.getbasetemp())).startswith("this")
assert tmp2 != tmp
def test_tmppath_relative_basetemp_absolute(self, tmp_path, monkeypatch):
def test_tmppath_relative_basetemp_absolute(
self, tmp_path: Path, monkeypatch: MonkeyPatch
) -> None:
"""#4425"""
monkeypatch.chdir(tmp_path)
config = cast(Config, FakeConfig("hello"))
@@ -101,6 +103,12 @@ class TestConfigTmpPath:
assert 0 == 1
"""
)
pytester.makepyprojecttoml(
"""
[tool.pytest.ini_options]
tmp_path_retention_policy = "failed"
"""
)
pytester.inline_run(p)
root = pytester._test_tmproot
@@ -128,6 +136,12 @@ class TestConfigTmpPath:
assert 0 == 0
"""
)
pytester.makepyprojecttoml(
"""
[tool.pytest.ini_options]
tmp_path_retention_policy = "failed"
"""
)
pytester.inline_run(p)
root = pytester._test_tmproot
@@ -155,6 +169,13 @@ class TestConfigTmpPath:
pass
"""
)
pytester.makepyprojecttoml(
"""
[tool.pytest.ini_options]
tmp_path_retention_policy = "failed"
"""
)
pytester.inline_run(p)
# Check if the whole directory is removed
@@ -570,7 +591,7 @@ def test_tmp_path_factory_create_directory_with_safe_permissions(
"""Verify that pytest creates directories under /tmp with private permissions."""
# Use the test's tmp_path as the system temproot (/tmp).
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True)
basetemp = tmp_factory.getbasetemp()
# No world-readable permissions.
@@ -590,14 +611,14 @@ def test_tmp_path_factory_fixes_up_world_readable_permissions(
"""
# Use the test's tmp_path as the system temproot (/tmp).
monkeypatch.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path))
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True)
basetemp = tmp_factory.getbasetemp()
# Before - simulate bad perms.
os.chmod(basetemp.parent, 0o777)
assert (basetemp.parent.stat().st_mode & 0o077) != 0
tmp_factory = TempPathFactory(None, 3, "failed", lambda *args: None, _ispytest=True)
tmp_factory = TempPathFactory(None, 3, "all", lambda *args: None, _ispytest=True)
basetemp = tmp_factory.getbasetemp()
# After - fixed.

View File

@@ -0,0 +1,25 @@
def test_tbh_chained(testdir):
"""Ensure chained exceptions whose frames contain "__tracebackhide__" are not shown (#1904)."""
p = testdir.makepyfile(
"""
import pytest
def f1():
__tracebackhide__ = True
try:
return f1.meh
except AttributeError:
pytest.fail("fail")
@pytest.fixture
def fix():
f1()
def test(fix):
pass
"""
)
result = testdir.runpytest(str(p))
assert "'function' object has no attribute 'meh'" not in result.stdout.str()
assert result.ret == 1

View File

@@ -3,6 +3,11 @@
This file is not executed, it is only checked by mypy to ensure that
none of the code triggers any mypy errors.
"""
import contextlib
from typing import Optional
from typing_extensions import assert_type
import pytest
@@ -22,3 +27,9 @@ def check_fixture_ids_callable() -> None:
@pytest.mark.parametrize("func", [str, int], ids=lambda x: str(x.__name__))
def check_parametrize_ids_callable(func) -> None:
pass
def check_raises_is_a_context_manager(val: bool) -> None:
with pytest.raises(RuntimeError) if val else contextlib.nullcontext() as excinfo:
pass
assert_type(excinfo, Optional[pytest.ExceptionInfo[RuntimeError]])

View File

@@ -104,7 +104,7 @@ deps =
PyYAML
regendoc>=0.8.1
sphinx
whitelist_externals =
allowlist_externals =
make
commands =
make regen