Compare commits
455 Commits
7.3.1
...
graingert-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
662fc9c012 | ||
|
|
db62b80e8d | ||
|
|
4821a41967 | ||
|
|
ff12e083c5 | ||
|
|
6e40458fc1 | ||
|
|
e74a6da699 | ||
|
|
5e6dc297c2 | ||
|
|
5ba3351b52 | ||
|
|
f34091f0ec | ||
|
|
dc2080f3be | ||
|
|
cdddd6d695 | ||
|
|
dd68f9c95a | ||
|
|
3ab70cd561 | ||
|
|
23825f2983 | ||
|
|
fb3a46dd8a | ||
|
|
3d6d93d0c2 | ||
|
|
2401d76655 | ||
|
|
af9b1dcc24 | ||
|
|
696859fc43 | ||
|
|
e966dcd93c | ||
|
|
54623f0f33 | ||
|
|
9bbfe995ee | ||
|
|
d015bc1b8f | ||
|
|
b73b4c464c | ||
|
|
1a16bac131 | ||
|
|
81192ca85f | ||
|
|
486a9ed057 | ||
|
|
4ae102c003 | ||
|
|
c614590ec9 | ||
|
|
d2b214220f | ||
|
|
a38ad254ef | ||
|
|
9f22d3281c | ||
|
|
9a58e6283d | ||
|
|
8bac8d7807 | ||
|
|
5e081162df | ||
|
|
8062743f6b | ||
|
|
8b7f94f145 | ||
|
|
5ace48ca5b | ||
|
|
e7caaa0b3e | ||
|
|
a47fcb4873 | ||
|
|
ab8f5ce7f4 | ||
|
|
f43a8db618 | ||
|
|
6c2feb75d2 | ||
|
|
fcb8e73288 | ||
|
|
241f2a890e | ||
|
|
39f9306357 | ||
|
|
e0d04bdfab | ||
|
|
1949b09fd3 | ||
|
|
3d1c52f203 | ||
|
|
a60c23c3d8 | ||
|
|
24a6ee1ffd | ||
|
|
e2acc1a99b | ||
|
|
71f265f1f3 | ||
|
|
7259e8db98 | ||
|
|
dd7beb39d6 | ||
|
|
6ad9499c9c | ||
|
|
2ed2e9208d | ||
|
|
ab63ebb3dc | ||
|
|
b3a981d385 | ||
|
|
48b0395648 | ||
|
|
9c11275553 | ||
|
|
e5c81fa41a | ||
|
|
0a06db0729 | ||
|
|
5936a79fdb | ||
|
|
28ccf476b9 | ||
|
|
333e4eba6b | ||
|
|
e787d2ed48 | ||
|
|
f6b6478868 | ||
|
|
3ce63bc768 | ||
|
|
faa8f2ea08 | ||
|
|
194a782e38 | ||
|
|
bc71561ad9 | ||
|
|
574e0f45d9 | ||
|
|
b8906b29a7 | ||
|
|
d2b5177dd6 | ||
|
|
65c01f531b | ||
|
|
82bd63d318 | ||
|
|
d4872f5df7 | ||
|
|
8d815ca55b | ||
|
|
8032d21271 | ||
|
|
9c8937b480 | ||
|
|
0d8b87f161 | ||
|
|
65c73a09e7 | ||
|
|
917ce9aa01 | ||
|
|
4e3a0874df | ||
|
|
ba0da81f88 | ||
|
|
f08782d8d0 | ||
|
|
5b528bd131 | ||
|
|
e4794b26b2 | ||
|
|
ab6cae2126 | ||
|
|
afb8d66e42 | ||
|
|
76ba7db6ce | ||
|
|
941b203c94 | ||
|
|
19d6b12b2a | ||
|
|
17e8f2b3fc | ||
|
|
ff23347f1f | ||
|
|
00fedcc439 | ||
|
|
77f7f59b2a | ||
|
|
7500fe44b2 | ||
|
|
23b899f31f | ||
|
|
43d1398fc7 | ||
|
|
03832fa31e | ||
|
|
8f36fd5454 | ||
|
|
cada6c105a | ||
|
|
3f446b68fd | ||
|
|
050f402816 | ||
|
|
d1722d5c18 | ||
|
|
4deb38b2ed | ||
|
|
370eacd3ca | ||
|
|
010e1742d8 | ||
|
|
6e5f10b28f | ||
|
|
dd5ae0c3b8 | ||
|
|
fc653d0d12 | ||
|
|
a357c7abc8 | ||
|
|
61133ba83d | ||
|
|
049eec8474 | ||
|
|
37bb186175 | ||
|
|
3cb3cd1a08 | ||
|
|
7a625481da | ||
|
|
0ddfdfcc04 | ||
|
|
87bfc83aa0 | ||
|
|
ebd571bb18 | ||
|
|
176d728b7b | ||
|
|
d4fb6ac9f7 | ||
|
|
15fadd8c5c | ||
|
|
73d754bd74 | ||
|
|
10056865d2 | ||
|
|
1827d8d5f9 | ||
|
|
9e164fc4fe | ||
|
|
c0cf822ca1 | ||
|
|
ec1053cc16 | ||
|
|
d2dc8a70b5 | ||
|
|
b9cb87d862 | ||
|
|
e938580257 | ||
|
|
47c0fc3d78 | ||
|
|
556e075d23 | ||
|
|
3ad3fc6b8f | ||
|
|
09b78737a5 | ||
|
|
b2186e2455 | ||
|
|
18bc6c9a0e | ||
|
|
24c9aa6c30 | ||
|
|
9c67b7aeb6 | ||
|
|
1cc58ed67f | ||
|
|
84a342e27c | ||
|
|
e8a8a5f320 | ||
|
|
1c04a92503 | ||
|
|
cc0adf6bf3 | ||
|
|
b8b74331b4 | ||
|
|
4797deab99 | ||
|
|
c9163402e0 | ||
|
|
01ac13a77d | ||
|
|
485c555812 | ||
|
|
a21fb87a90 | ||
|
|
c754da10d2 | ||
|
|
71e627aa8f | ||
|
|
d3552ef4c0 | ||
|
|
448563caaa | ||
|
|
e8aa906e06 | ||
|
|
12054a4972 | ||
|
|
430ad145c1 | ||
|
|
13e2b00258 | ||
|
|
4e42421ebf | ||
|
|
161ba87300 | ||
|
|
f2b6040e9e | ||
|
|
e3247834e2 | ||
|
|
0b4a557087 | ||
|
|
ffdcce67f4 | ||
|
|
1ded74739b | ||
|
|
497a1d798a | ||
|
|
1de0923e83 | ||
|
|
7c30f674c5 | ||
|
|
396bfbf30b | ||
|
|
02ba39bfcd | ||
|
|
29010d23a6 | ||
|
|
de1f6f58ba | ||
|
|
cfda801ebf | ||
|
|
c5262b0c42 | ||
|
|
ff6e110161 | ||
|
|
0e0ed2af95 | ||
|
|
a3fbf24389 | ||
|
|
bf451d47a1 | ||
|
|
578fbe3dfd | ||
|
|
a668719626 | ||
|
|
04e0db7e48 | ||
|
|
32f480814c | ||
|
|
40ed678885 | ||
|
|
fb55615d5e | ||
|
|
9d0ddb4625 | ||
|
|
01f38aca44 | ||
|
|
78d81ef865 | ||
|
|
b41acaea12 | ||
|
|
7008385253 | ||
|
|
b25a3adff5 | ||
|
|
ecfab4dc8b | ||
|
|
2c80de532f | ||
|
|
7967b2e710 | ||
|
|
f1c9570a0e | ||
|
|
b20e7f6d0c | ||
|
|
b91d5a3112 | ||
|
|
4e75bff71a | ||
|
|
99ab8ae884 | ||
|
|
782cacf86b | ||
|
|
a3b4220d76 | ||
|
|
fd7a4d2429 | ||
|
|
b73ec8e5d1 | ||
|
|
d790e96765 | ||
|
|
d4265448a5 | ||
|
|
db37e34613 | ||
|
|
c5b13099e6 | ||
|
|
97ed533f63 | ||
|
|
f5a9aa0b84 | ||
|
|
901316b3f4 | ||
|
|
b81003f6fb | ||
|
|
ddd773ecb1 | ||
|
|
44604f49cd | ||
|
|
2203897086 | ||
|
|
119cec0279 | ||
|
|
b5bc53e441 | ||
|
|
084d756ae6 | ||
|
|
d7dbadbffc | ||
|
|
cb732f7f49 | ||
|
|
7775e494b1 | ||
|
|
1f5058e972 | ||
|
|
6baf9f2d31 | ||
|
|
c4876c7106 | ||
|
|
6badb6f01e | ||
|
|
4517af1e28 | ||
|
|
3d0dedb5ec | ||
|
|
7b7bd304aa | ||
|
|
2706271f66 | ||
|
|
8ac3c645fa | ||
|
|
18e87c9831 | ||
|
|
6995257cf4 | ||
|
|
bea56b30af | ||
|
|
b847084224 | ||
|
|
024e62e6d2 | ||
|
|
f9410fddcd | ||
|
|
561f1a993b | ||
|
|
b77d0deaf5 | ||
|
|
2f7415cfbc | ||
|
|
ba60649680 | ||
|
|
679bd6f2ed | ||
|
|
a50ea1b8e7 | ||
|
|
0353a94cd1 | ||
|
|
0311fc3384 | ||
|
|
7fdc8391e2 | ||
|
|
15524f34d2 | ||
|
|
2d48171e88 | ||
|
|
7022fb455d | ||
|
|
a1b37022af | ||
|
|
9279ea2882 | ||
|
|
165fbbd12a | ||
|
|
f617bab0a2 | ||
|
|
f4e3b4ad98 | ||
|
|
81cfb3fc87 | ||
|
|
dd5f3773d9 | ||
|
|
e132d6488f | ||
|
|
4719d998d8 | ||
|
|
5332656906 | ||
|
|
a14fc10cac | ||
|
|
05d7e60904 | ||
|
|
4ebb2b94c2 | ||
|
|
9859c110cc | ||
|
|
5eaf8207eb | ||
|
|
518ca37cae | ||
|
|
45f1a462d5 | ||
|
|
4cd95eeabf | ||
|
|
1306b84241 | ||
|
|
fe51121f39 | ||
|
|
c8b1790ee7 | ||
|
|
2870157234 | ||
|
|
1b7896f83d | ||
|
|
a4a189ad99 | ||
|
|
57d352460c | ||
|
|
a14745550a | ||
|
|
64319dbc01 | ||
|
|
1e8135df16 | ||
|
|
1e32a4b570 | ||
|
|
faa1f9d2ad | ||
|
|
14890329dc | ||
|
|
d97d44a97a | ||
|
|
f6b995e9d5 | ||
|
|
661b938fca | ||
|
|
7e510769b4 | ||
|
|
a704605cf1 | ||
|
|
797b924fc4 | ||
|
|
b5ec092525 | ||
|
|
5b35518389 | ||
|
|
8528052a95 | ||
|
|
1eb83706b6 | ||
|
|
fa09f6dcc6 | ||
|
|
b55c02c3c9 | ||
|
|
bd88a6412d | ||
|
|
264e7ac327 | ||
|
|
b7a142f4ba | ||
|
|
2d824329eb | ||
|
|
ecb23106d8 | ||
|
|
8174a30164 | ||
|
|
0142bb6687 | ||
|
|
52cf700f1b | ||
|
|
6b3ece5bdb | ||
|
|
4059000834 | ||
|
|
7d5207a736 | ||
|
|
3e65a461c7 | ||
|
|
fc3b5b2610 | ||
|
|
9335a0b445 | ||
|
|
0ded3297a9 | ||
|
|
fc9cbbd4c4 | ||
|
|
1a17539065 | ||
|
|
32de8e2893 | ||
|
|
5d4a342a7a | ||
|
|
1790f17228 | ||
|
|
ee8baa2676 | ||
|
|
ae38b076da | ||
|
|
85c5bd26b6 | ||
|
|
e1204e1e23 | ||
|
|
313b61471f | ||
|
|
1daa8129c6 | ||
|
|
b5ff089d2c | ||
|
|
fda8024622 | ||
|
|
4b823a42ce | ||
|
|
6c9b277ce4 | ||
|
|
3de43e5102 | ||
|
|
c76ae74bd7 | ||
|
|
24534cdd29 | ||
|
|
99c78aa93a | ||
|
|
3a6bdcd76b | ||
|
|
4a1bba25b9 | ||
|
|
9e1add75f7 | ||
|
|
4da9026766 | ||
|
|
7c231baa64 | ||
|
|
fc538c5766 | ||
|
|
fbfd4b5005 | ||
|
|
ec752537ea | ||
|
|
dd667336ce | ||
|
|
29d16d2939 | ||
|
|
5313d50e18 | ||
|
|
4f3f36c396 | ||
|
|
af124c7f21 | ||
|
|
4e6d53fef5 | ||
|
|
751d726d21 | ||
|
|
9e491f430e | ||
|
|
63f258f432 | ||
|
|
baaa67dfb9 | ||
|
|
519f351b4f | ||
|
|
5d53447a73 | ||
|
|
1716d3c9bf | ||
|
|
ac699e7b25 | ||
|
|
a5f37199a9 | ||
|
|
9fa82598a9 | ||
|
|
a2feb6bd00 | ||
|
|
ba32a3bd87 | ||
|
|
c8641f879f | ||
|
|
6041511fb4 | ||
|
|
739408b958 | ||
|
|
1636322995 | ||
|
|
b8edacb8f1 | ||
|
|
612489e2bd | ||
|
|
383774db10 | ||
|
|
3b5b3cf50e | ||
|
|
23e343af60 | ||
|
|
f9a995b56d | ||
|
|
d9d78a8aef | ||
|
|
76d15231f5 | ||
|
|
4cc05e7bee | ||
|
|
2d57d5c32f | ||
|
|
faeb16146b | ||
|
|
b241c0b479 | ||
|
|
78403237cf | ||
|
|
f84fea0888 | ||
|
|
271bdf6c23 | ||
|
|
fd56968f2b | ||
|
|
5b75b0d03f | ||
|
|
aac5d5d08b | ||
|
|
b1460f3261 | ||
|
|
a88ae8289c | ||
|
|
62320e4ff7 | ||
|
|
6514041a35 | ||
|
|
7d548c38e2 | ||
|
|
07eeeb8dfc | ||
|
|
be774667c2 | ||
|
|
762bb61562 | ||
|
|
725de3a0d3 | ||
|
|
fcada1ea47 | ||
|
|
6f7f89f3c4 | ||
|
|
0a20452f78 | ||
|
|
cc23ec91d0 | ||
|
|
a15f544962 | ||
|
|
3823ce60dd | ||
|
|
e03f82c359 | ||
|
|
158f41fdf8 | ||
|
|
fd6a4507ac | ||
|
|
15156757b6 | ||
|
|
0860f4e916 | ||
|
|
11965d1c27 | ||
|
|
14be71b234 | ||
|
|
2d6206b89a | ||
|
|
41f57ef95d | ||
|
|
4eca6063c8 | ||
|
|
819f5abd73 | ||
|
|
adf891ab1d | ||
|
|
f08184ba20 | ||
|
|
28783e5d23 | ||
|
|
7834b3b07f | ||
|
|
ece756fcb4 | ||
|
|
d380771065 | ||
|
|
b893d2a0fe | ||
|
|
e3b1799766 | ||
|
|
5d1385320f | ||
|
|
eff54aece1 | ||
|
|
bc1fc3f0fc | ||
|
|
784ffa0fad | ||
|
|
90412827c3 | ||
|
|
424c3eebde | ||
|
|
9c2247ec1b | ||
|
|
61f7c27ec0 | ||
|
|
1b81d636e2 | ||
|
|
1b196fbeaf | ||
|
|
22524046cf | ||
|
|
6dcd652d4a | ||
|
|
be9faa68d8 | ||
|
|
e4e13dd913 | ||
|
|
5a399030c1 | ||
|
|
19c8ef63a4 | ||
|
|
22951bba67 | ||
|
|
ec8e23951d | ||
|
|
bf47357511 | ||
|
|
2d2dc4a2a8 | ||
|
|
f1c7585184 | ||
|
|
246ceb84bd | ||
|
|
2fd160110c | ||
|
|
2998b596b4 | ||
|
|
7f73722c4a | ||
|
|
f7542f6292 | ||
|
|
0de2aa93f0 | ||
|
|
7759a9d3c2 | ||
|
|
d86df89a92 | ||
|
|
24fd292878 | ||
|
|
a1b10b552a | ||
|
|
aa9cc7e8b4 | ||
|
|
5c286d19d8 | ||
|
|
faac197fcf | ||
|
|
bb6155adfa | ||
|
|
5fefd7de96 | ||
|
|
750ce30392 | ||
|
|
de1f378b60 | ||
|
|
14d5b4ca6c | ||
|
|
307dbf15c4 | ||
|
|
a8697601ad | ||
|
|
0ed1b0ac12 | ||
|
|
26b0702b98 | ||
|
|
5227279c15 | ||
|
|
a7e0ae2455 | ||
|
|
12efc58479 | ||
|
|
3f71680ac0 |
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
64
.github/workflows/deploy.yml
vendored
64
.github/workflows/deploy.yml
vendored
@@ -1,29 +1,26 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
# These tags are protected, see:
|
||||
# https://github.com/pytest-dev/pytest/settings/tag_protection
|
||||
- "[0-9]+.[0-9]+.[0-9]+"
|
||||
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version'
|
||||
required: true
|
||||
default: '1.2.3'
|
||||
|
||||
|
||||
# Set permissions at the job level.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
@@ -31,6 +28,18 @@ jobs:
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
needs: [package]
|
||||
runs-on: ubuntu-latest
|
||||
environment: deploy
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@@ -38,14 +47,35 @@ jobs:
|
||||
path: dist
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.10
|
||||
|
||||
- name: Push tag
|
||||
run: |
|
||||
git config user.name "pytest bot"
|
||||
git config user.email "pytestbot@gmail.com"
|
||||
git tag --annotate --message=v${{ github.event.inputs.version }} v${{ github.event.inputs.version }} ${{ github.sha }}
|
||||
git push origin v${{ github.event.inputs.version }}
|
||||
|
||||
release-notes:
|
||||
|
||||
# todo: generate the content in the build job
|
||||
# the goal being of using a github action script to push the release data
|
||||
# after success instead of creating a complete python/tox env
|
||||
needs: [deploy]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
password: ${{ secrets.pypi_token }}
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.7"
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
|
||||
2
.github/workflows/prepare-release-pr.yml
vendored
2
.github/workflows/prepare-release-pr.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
23
.github/workflows/stale.yml
vendored
Normal file
23
.github/workflows/stale.yml
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
name: close needs-information issues
|
||||
on:
|
||||
schedule:
|
||||
- cron: "30 1 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
close-issues:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
debug-only: false
|
||||
days-before-issue-stale: 14
|
||||
days-before-issue-close: 7
|
||||
only-labels: "status: needs information"
|
||||
stale-issue-label: "stale"
|
||||
stale-issue-message: "This issue is stale because it has been open for 14 days with no activity."
|
||||
close-issue-message: "This issue was closed because it has been inactive for 7 days since being marked as stale."
|
||||
days-before-pr-stale: -1
|
||||
days-before-pr-close: -1
|
||||
114
.github/workflows/test.yml
vendored
114
.github/workflows/test.yml
vendored
@@ -27,7 +27,19 @@ concurrency:
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
build:
|
||||
needs: [package]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
@@ -37,46 +49,41 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
name: [
|
||||
"windows-py37",
|
||||
"windows-py37-pluggy",
|
||||
"windows-py38",
|
||||
"windows-py38-pluggy",
|
||||
"windows-py39",
|
||||
"windows-py310",
|
||||
"windows-py311",
|
||||
"windows-py312",
|
||||
|
||||
"ubuntu-py37",
|
||||
"ubuntu-py37-pluggy",
|
||||
"ubuntu-py37-freeze",
|
||||
"ubuntu-py38",
|
||||
"ubuntu-py38-pluggy",
|
||||
"ubuntu-py38-freeze",
|
||||
"ubuntu-py39",
|
||||
"ubuntu-py310",
|
||||
"ubuntu-py311",
|
||||
"ubuntu-py312",
|
||||
"ubuntu-pypy3",
|
||||
|
||||
"macos-py37",
|
||||
"macos-py38",
|
||||
"macos-py39",
|
||||
"macos-py310",
|
||||
"macos-py312",
|
||||
|
||||
"docs",
|
||||
"doctesting",
|
||||
"plugins",
|
||||
]
|
||||
|
||||
include:
|
||||
- name: "windows-py37"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-numpy"
|
||||
- name: "windows-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-pluggymain-pylib-xdist"
|
||||
- name: "windows-py38"
|
||||
python: "3.8"
|
||||
os: windows-latest
|
||||
tox_env: "py38-unittestextras"
|
||||
use_coverage: true
|
||||
- name: "windows-py38-pluggy"
|
||||
python: "3.8"
|
||||
os: windows-latest
|
||||
tox_env: "py38-pluggymain-pylib-xdist"
|
||||
- name: "windows-py39"
|
||||
python: "3.9"
|
||||
os: windows-latest
|
||||
@@ -86,27 +93,27 @@ jobs:
|
||||
os: windows-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "windows-py311"
|
||||
python: "3.11-dev"
|
||||
python: "3.11"
|
||||
os: windows-latest
|
||||
tox_env: "py311"
|
||||
- name: "windows-py312"
|
||||
python: "3.12-dev"
|
||||
os: windows-latest
|
||||
tox_env: "py312"
|
||||
|
||||
- name: "ubuntu-py37"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-lsof-numpy-pexpect"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-pluggymain-pylib-xdist"
|
||||
- name: "ubuntu-py37-freeze"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-freeze"
|
||||
- name: "ubuntu-py38"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-xdist"
|
||||
tox_env: "py38-lsof-numpy-pexpect"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py38-pluggy"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-pluggymain-pylib-xdist"
|
||||
- name: "ubuntu-py38-freeze"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-freeze"
|
||||
- name: "ubuntu-py39"
|
||||
python: "3.9"
|
||||
os: ubuntu-latest
|
||||
@@ -116,58 +123,66 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "ubuntu-py311"
|
||||
python: "3.11-dev"
|
||||
python: "3.11"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py311"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py312"
|
||||
python: "3.12-dev"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py312"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-pypy3"
|
||||
python: "pypy-3.7"
|
||||
python: "pypy-3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "pypy3-xdist"
|
||||
|
||||
- name: "macos-py37"
|
||||
python: "3.7"
|
||||
os: macos-latest
|
||||
tox_env: "py37-xdist"
|
||||
- name: "macos-py38"
|
||||
python: "3.8"
|
||||
os: macos-latest
|
||||
tox_env: "py38-xdist"
|
||||
use_coverage: true
|
||||
- name: "macos-py39"
|
||||
python: "3.9"
|
||||
os: macos-latest
|
||||
tox_env: "py39-xdist"
|
||||
use_coverage: true
|
||||
- name: "macos-py310"
|
||||
python: "3.10"
|
||||
os: macos-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "macos-py312"
|
||||
python: "3.12-dev"
|
||||
os: macos-latest
|
||||
tox_env: "py312-xdist"
|
||||
|
||||
- name: "plugins"
|
||||
python: "3.9"
|
||||
os: ubuntu-latest
|
||||
tox_env: "plugins"
|
||||
|
||||
- name: "docs"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "docs"
|
||||
- name: "doctesting"
|
||||
python: "3.7"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "doctesting"
|
||||
use_coverage: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Set up Python ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -176,11 +191,13 @@ jobs:
|
||||
|
||||
- name: Test without coverage
|
||||
if: "! matrix.use_coverage"
|
||||
run: "tox -e ${{ matrix.tox_env }}"
|
||||
shell: bash
|
||||
run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
|
||||
|
||||
- name: Test with coverage
|
||||
if: "matrix.use_coverage"
|
||||
run: "tox -e ${{ matrix.tox_env }}-coverage"
|
||||
shell: bash
|
||||
run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz`
|
||||
|
||||
- name: Generate coverage report
|
||||
if: "matrix.use_coverage"
|
||||
@@ -194,10 +211,3 @@ jobs:
|
||||
fail_ci_if_error: true
|
||||
files: ./coverage.xml
|
||||
verbose: true
|
||||
|
||||
check-package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
16
.github/workflows/update-plugin-list.yml
vendored
16
.github/workflows/update-plugin-list.yml
vendored
@@ -20,25 +20,33 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
- name: requests-cache
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/pytest-plugin-list/
|
||||
key: plugins-http-cache-${{ github.run_id }} # Can use time based key as well
|
||||
restore-keys: plugins-http-cache-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install packaging requests tabulate[widechars] tqdm
|
||||
pip install packaging requests tabulate[widechars] tqdm requests-cache platformdirs
|
||||
|
||||
|
||||
- name: Update Plugin List
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54
|
||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
default_language_version:
|
||||
python: "3.10"
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 23.3.0
|
||||
rev: 23.9.1
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: 1.13.0
|
||||
rev: 1.16.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==23.1.0]
|
||||
additional_dependencies: [black==23.7.0]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.4.0
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -23,7 +21,7 @@ repos:
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.0.2
|
||||
rev: v2.2.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
@@ -31,34 +29,34 @@ repos:
|
||||
language: python
|
||||
files: \.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 6.0.0
|
||||
rev: 6.1.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
additional_dependencies:
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v3.9.0
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.12.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py37-plus]
|
||||
args: ['--application-directories=.:src', --py38-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.3.1
|
||||
rev: v3.15.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
args: [--py38-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v2.2.0
|
||||
rev: v2.5.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: ["--max-py-version=3.11", "--include-version-classifiers"]
|
||||
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.1.1
|
||||
rev: v1.5.1
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
|
||||
29
AUTHORS
29
AUTHORS
@@ -8,11 +8,15 @@ Abdeali JK
|
||||
Abdelrahman Elbehery
|
||||
Abhijeet Kasurde
|
||||
Adam Johnson
|
||||
Adam Stewart
|
||||
Adam Uhlir
|
||||
Ahn Ki-Wook
|
||||
Akhilesh Ramakrishnan
|
||||
Akiomi Kamakura
|
||||
Alan Velasco
|
||||
Alessio Izzo
|
||||
Alex Jones
|
||||
Alex Lambson
|
||||
Alexander Johnson
|
||||
Alexander King
|
||||
Alexei Kozlenok
|
||||
@@ -53,8 +57,10 @@ Ben Gartner
|
||||
Ben Webb
|
||||
Benjamin Peterson
|
||||
Bernard Pratz
|
||||
Bo Wu
|
||||
Bob Ippolito
|
||||
Brian Dorsey
|
||||
Brian Larsen
|
||||
Brian Maissy
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
@@ -68,6 +74,7 @@ Charles Cloud
|
||||
Charles Machalow
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Cheuk Ting Ho
|
||||
Chris Mahoney
|
||||
Chris Lamb
|
||||
Chris NeJame
|
||||
Chris Rose
|
||||
@@ -124,15 +131,19 @@ Eric Hunsberger
|
||||
Eric Liu
|
||||
Eric Siegerman
|
||||
Erik Aronesty
|
||||
Erik Hasse
|
||||
Erik M. Bray
|
||||
Evan Kepner
|
||||
Evgeny Seliverstov
|
||||
Fabien Zarifian
|
||||
Fabio Zadrozny
|
||||
Felix Hofstätter
|
||||
Felix Nieuwenhuizen
|
||||
Feng Ma
|
||||
Florian Bruhin
|
||||
Florian Dahlitz
|
||||
Floris Bruynooghe
|
||||
Fraser Stark
|
||||
Gabriel Landau
|
||||
Gabriel Reis
|
||||
Garvit Shubham
|
||||
@@ -159,6 +170,8 @@ Ian Bicking
|
||||
Ian Lesperance
|
||||
Ilya Konstantinov
|
||||
Ionuț Turturică
|
||||
Isaac Virshup
|
||||
Israel Fruchter
|
||||
Itxaso Aizpurua
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
@@ -191,6 +204,7 @@ Justice Ndou
|
||||
Justyna Janczyszyn
|
||||
Kale Kundert
|
||||
Kamran Ahmad
|
||||
Kenny Y
|
||||
Karl O. Pinc
|
||||
Karthikeyan Singaravelan
|
||||
Katarzyna Jachim
|
||||
@@ -221,6 +235,7 @@ Maho
|
||||
Maik Figura
|
||||
Mandeep Bhutani
|
||||
Manuel Krebber
|
||||
Marc Mueller
|
||||
Marc Schlaich
|
||||
Marcelo Duarte Trevisani
|
||||
Marcin Bachry
|
||||
@@ -251,8 +266,10 @@ Michal Wajszczuk
|
||||
Michał Zięba
|
||||
Mickey Pashov
|
||||
Mihai Capotă
|
||||
Mihail Milushev
|
||||
Mike Hoyle (hoylemd)
|
||||
Mike Lundy
|
||||
Milan Lesnek
|
||||
Miro Hrončok
|
||||
Nathaniel Compton
|
||||
Nathaniel Waisbrot
|
||||
@@ -302,7 +319,9 @@ Raphael Pierzina
|
||||
Rafal Semik
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
Reagan Lee
|
||||
Robert Holt
|
||||
Roberto Aldera
|
||||
Roberto Polli
|
||||
Roland Puntaier
|
||||
Romain Dorgueil
|
||||
@@ -311,7 +330,9 @@ Ronny Pfannschmidt
|
||||
Ross Lawley
|
||||
Ruaridh Williamson
|
||||
Russel Winder
|
||||
Ryan Puddephatt
|
||||
Ryan Wooden
|
||||
Sadra Barikbin
|
||||
Saiprasad Kale
|
||||
Samuel Colvin
|
||||
Samuel Dion-Girardeau
|
||||
@@ -320,16 +341,20 @@ Samuele Pedroni
|
||||
Sanket Duthade
|
||||
Sankt Petersbug
|
||||
Saravanan Padmanaban
|
||||
Sean Malloy
|
||||
Segev Finer
|
||||
Serhii Mozghovyi
|
||||
Seth Junot
|
||||
Shantanu Jain
|
||||
Sharad Nair
|
||||
Shubham Adep
|
||||
Simon Blanchard
|
||||
Simon Gomizelj
|
||||
Simon Holesch
|
||||
Simon Kerr
|
||||
Skylar Downes
|
||||
Srinivas Reddy Thatiparthy
|
||||
Stefaan Lippens
|
||||
Stefan Farmbauer
|
||||
Stefan Scherfke
|
||||
Stefan Zimmermann
|
||||
@@ -343,6 +368,7 @@ Tadek Teleżyński
|
||||
Takafumi Arakaki
|
||||
Taneli Hukkinen
|
||||
Tanvi Mehta
|
||||
Tanya Agarwal
|
||||
Tarcisio Fischer
|
||||
Tareq Alayan
|
||||
Tatiana Ovary
|
||||
@@ -361,13 +387,16 @@ Tomer Keren
|
||||
Tony Narlock
|
||||
Tor Colvin
|
||||
Trevor Bekolay
|
||||
Tushar Sadhwani
|
||||
Tyler Goodlet
|
||||
Tyler Smart
|
||||
Tzu-ping Chung
|
||||
Vasily Kuznetsov
|
||||
Victor Maryama
|
||||
Victor Rodriguez
|
||||
Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
Vijay Arora
|
||||
Virgil Dupras
|
||||
Vitaly Lashmanov
|
||||
Vivaan Verma
|
||||
|
||||
@@ -50,7 +50,7 @@ Fix bugs
|
||||
--------
|
||||
|
||||
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
|
||||
See also the `"status: easy" issues <https://github.com/pytest-dev/pytest/labels/status%3A%20easy>`_
|
||||
See also the `"good first issue" issues <https://github.com/pytest-dev/pytest/labels/good%20first%20issue>`_
|
||||
that are friendly to new contributors.
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs. To indicate that you are going
|
||||
@@ -197,11 +197,12 @@ Short version
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
#. Fork the repository.
|
||||
#. Fetch tags from upstream if necessary (if you cloned only main `git fetch --tags https://github.com/pytest-dev/pytest`).
|
||||
#. Enable and install `pre-commit <https://pre-commit.com>`_ to ensure style-guides and code checks are followed.
|
||||
#. Follow **PEP-8** for naming and `black <https://github.com/psf/black>`_ for formatting.
|
||||
#. Tests are run using ``tox``::
|
||||
|
||||
tox -e linting,py37
|
||||
tox -e linting,py39
|
||||
|
||||
The test environments above are usually enough to cover most cases locally.
|
||||
|
||||
@@ -236,6 +237,7 @@ Here is a simple overview, with pytest-specific bits:
|
||||
|
||||
$ git clone git@github.com:YOUR_GITHUB_USERNAME/pytest.git
|
||||
$ cd pytest
|
||||
$ git fetch --tags https://github.com/pytest-dev/pytest
|
||||
# now, create your own branch off "main":
|
||||
|
||||
$ git checkout -b your-bugfix-branch-name main
|
||||
@@ -272,24 +274,24 @@ Here is a simple overview, with pytest-specific bits:
|
||||
|
||||
#. Run all the tests
|
||||
|
||||
You need to have Python 3.7 available in your system. Now
|
||||
You need to have Python 3.8 or later available in your system. Now
|
||||
running tests is as simple as issuing this command::
|
||||
|
||||
$ tox -e linting,py37
|
||||
$ tox -e linting,py39
|
||||
|
||||
This command will run tests via the "tox" tool against Python 3.7
|
||||
This command will run tests via the "tox" tool against Python 3.9
|
||||
and also perform "lint" coding-style checks.
|
||||
|
||||
#. You can now edit your local working copy and run the tests again as necessary. Please follow PEP-8 for naming.
|
||||
|
||||
You can pass different options to ``tox``. For example, to run tests on Python 3.7 and pass options to pytest
|
||||
You can pass different options to ``tox``. For example, to run tests on Python 3.9 and pass options to pytest
|
||||
(e.g. enter pdb on failure) to pytest you can do::
|
||||
|
||||
$ tox -e py37 -- --pdb
|
||||
$ tox -e py39 -- --pdb
|
||||
|
||||
Or to only run tests in a particular test module on Python 3.7::
|
||||
Or to only run tests in a particular test module on Python 3.9::
|
||||
|
||||
$ tox -e py37 -- testing/test_config.py
|
||||
$ tox -e py39 -- testing/test_config.py
|
||||
|
||||
|
||||
When committing, ``pre-commit`` will re-format the files if necessary.
|
||||
|
||||
@@ -100,7 +100,7 @@ Features
|
||||
- Can run `unittest <https://docs.pytest.org/en/stable/how-to/unittest.html>`_ (or trial),
|
||||
`nose <https://docs.pytest.org/en/stable/how-to/nose.html>`_ test suites out of the box
|
||||
|
||||
- Python 3.7+ or PyPy3
|
||||
- Python 3.8+ or PyPy3
|
||||
|
||||
- Rich plugin architecture, with over 850+ `external plugins <https://docs.pytest.org/en/latest/reference/plugin_list.html>`_ and thriving community
|
||||
|
||||
|
||||
@@ -133,14 +133,12 @@ Releasing
|
||||
|
||||
Both automatic and manual processes described above follow the same steps from this point onward.
|
||||
|
||||
#. After all tests pass and the PR has been approved, tag the release commit
|
||||
in the ``release-MAJOR.MINOR.PATCH`` branch and push it. This will publish to PyPI::
|
||||
#. After all tests pass and the PR has been approved, trigger the ``deploy`` job
|
||||
in https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml, using the ``release-MAJOR.MINOR.PATCH`` branch
|
||||
as source.
|
||||
|
||||
git fetch upstream
|
||||
git tag MAJOR.MINOR.PATCH upstream/release-MAJOR.MINOR.PATCH
|
||||
git push upstream MAJOR.MINOR.PATCH
|
||||
|
||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
||||
This job will require approval from ``pytest-dev/core``, after which it will publish to PyPI
|
||||
and tag the repository.
|
||||
|
||||
#. Merge the PR. **Make sure it's not squash-merged**, so that the tagged commit ends up in the main branch.
|
||||
|
||||
|
||||
2
changelog/10441.feature.rst
Normal file
2
changelog/10441.feature.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Added :func:`ExceptionInfo.group_contains() <pytest.ExceptionInfo.group_contains>`, an assertion
|
||||
helper that tests if an `ExceptionGroup` contains a matching exception.
|
||||
2
changelog/10447.bugfix.rst
Normal file
2
changelog/10447.bugfix.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
markers are now considered in the reverse mro order to ensure base class markers are considered first
|
||||
this resolves a regression.
|
||||
1
changelog/10465.deprecation.rst
Normal file
1
changelog/10465.deprecation.rst
Normal file
@@ -0,0 +1 @@
|
||||
Test functions returning a value other than None will now issue a :class:`pytest.PytestWarning` instead of :class:`pytest.PytestRemovedIn8Warning`, meaning this will stay a warning instead of becoming an error in the future.
|
||||
2
changelog/10617.feature.rst
Normal file
2
changelog/10617.feature.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Added more comprehensive set assertion rewrites for comparisons other than equality ``==``, with
|
||||
the following operations now providing better failure messages: ``!=``, ``<=``, ``>=``, ``<``, and ``>``.
|
||||
2
changelog/10701.bugfix.rst
Normal file
2
changelog/10701.bugfix.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
:meth:`pytest.WarningsRecorder.pop` will return the most-closely-matched warning in the list,
|
||||
rather than the first warning which is an instance of the requested type.
|
||||
1
changelog/11011.doc.rst
Normal file
1
changelog/11011.doc.rst
Normal file
@@ -0,0 +1 @@
|
||||
Added a warning about modifying the root logger during tests when using ``caplog``.
|
||||
1
changelog/11091.doc.rst
Normal file
1
changelog/11091.doc.rst
Normal file
@@ -0,0 +1 @@
|
||||
Updated documentation and tests to refer to hyphonated options: replaced ``--junitxml`` with ``--junit-xml`` and ``--collectonly`` with ``--collect-only``.
|
||||
6
changelog/11122.improvement.rst
Normal file
6
changelog/11122.improvement.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
``pluggy>=1.2.0`` is now required.
|
||||
|
||||
pytest now uses "new-style" hook wrappers internally, available since pluggy 1.2.0.
|
||||
See `pluggy's 1.2.0 changelog <https://pluggy.readthedocs.io/en/latest/changelog.html#pluggy-1-2-0-2023-06-21>`_ and the :ref:`updated docs <hookwrapper>` for details.
|
||||
|
||||
Plugins which want to use new-style wrappers can do so if they require this version of pytest or later.
|
||||
11
changelog/11137.breaking.rst
Normal file
11
changelog/11137.breaking.rst
Normal file
@@ -0,0 +1,11 @@
|
||||
:class:`pytest.Package` is no longer a :class:`pytest.Module` or :class:`pytest.File`.
|
||||
|
||||
The ``Package`` collector node designates a Python package, that is, a directory with an `__init__.py` file.
|
||||
Previously ``Package`` was a subtype of ``pytest.Module`` (which represents a single Python module),
|
||||
the module being the `__init__.py` file.
|
||||
This has been deemed a design mistake (see :issue:`11137` and :issue:`7777` for details).
|
||||
|
||||
The ``path`` property of ``Package`` nodes now points to the package directory instead of the ``__init__.py`` file.
|
||||
|
||||
Note that a ``Module`` node for ``__init__.py`` (which is not a ``Package``) may still exist,
|
||||
if it is picked up during collection (e.g. if you configured :confval:`python_files` to include ``__init__.py`` files).
|
||||
1
changelog/11146.bugfix.rst
Normal file
1
changelog/11146.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
- Prevent constants at the top of file from being detected as docstrings.
|
||||
1
changelog/11151.breaking.rst
Normal file
1
changelog/11151.breaking.rst
Normal file
@@ -0,0 +1 @@
|
||||
Dropped support for Python 3.7, which `reached end-of-life on 2023-06-27 <https://devguide.python.org/versions/>`__.
|
||||
2
changelog/11208.trivial.rst
Normal file
2
changelog/11208.trivial.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
The (internal) ``FixtureDef.cached_result`` type has changed.
|
||||
Now the third item ``cached_result[2]``, when set, is an exception instance instead of an exception triplet.
|
||||
1
changelog/11216.improvement.rst
Normal file
1
changelog/11216.improvement.rst
Normal file
@@ -0,0 +1 @@
|
||||
If a test is skipped from inside an :ref:`xunit setup fixture <classic xunit>`, the test summary now shows the test location instead of the fixture location.
|
||||
5
changelog/11218.trivial.rst
Normal file
5
changelog/11218.trivial.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
(This entry is meant to assist plugins which access private pytest internals to instantiate ``FixtureRequest`` objects.)
|
||||
|
||||
:class:`~pytest.FixtureRequest` is now an abstract class which can't be instantiated directly.
|
||||
A new concrete ``TopRequest`` subclass of ``FixtureRequest`` has been added for the ``request`` fixture in test functions,
|
||||
as counterpart to the existing ``SubRequest`` subclass for the ``request`` fixture in fixture functions.
|
||||
1
changelog/11227.improvement.rst
Normal file
1
changelog/11227.improvement.rst
Normal file
@@ -0,0 +1 @@
|
||||
Allow :func:`pytest.raises` ``match`` argument to match against `PEP-678 <https://peps.python.org/pep-0678/>` ``__notes__``.
|
||||
1
changelog/11239.bugfix.rst
Normal file
1
changelog/11239.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
Fixed ``:=`` in asserts impacting unrelated test cases.
|
||||
1
changelog/11255.bugfix.rst
Normal file
1
changelog/11255.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
Fixed crash on `parametrize(..., scope="package")` without a package present.
|
||||
2
changelog/11277.bugfix.rst
Normal file
2
changelog/11277.bugfix.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Fixed a bug that when there are multiple fixtures for an indirect parameter,
|
||||
the scope of the highest-scope fixture is picked for the parameter set, instead of that of the one with the narrowest scope.
|
||||
2
changelog/11314.improvement.rst
Normal file
2
changelog/11314.improvement.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Logging to a file using the ``--log-file`` option will use ``--log-level``, ``--log-format`` and ``--log-date-format`` as fallback
|
||||
if ``--log-file-level``, ``--log-file-format`` and ``--log-file-date-format`` are not provided respectively.
|
||||
3
changelog/11315.trivial.rst
Normal file
3
changelog/11315.trivial.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
The :fixture:`pytester` fixture now uses the :fixture:`monkeypatch` fixture to manage the current working directory.
|
||||
If you use ``pytester`` in combination with :func:`monkeypatch.undo() <pytest.MonkeyPatch.undo>`, the CWD might get restored.
|
||||
Use :func:`monkeypatch.context() <pytest.MonkeyPatch.context>` instead.
|
||||
2
changelog/11333.trivial.rst
Normal file
2
changelog/11333.trivial.rst
Normal file
@@ -0,0 +1,2 @@
|
||||
Corrected the spelling of ``Config.ArgsSource.INVOCATION_DIR``.
|
||||
The previous spelling ``INCOVATION_DIR`` remains as an alias.
|
||||
1
changelog/11353.trivial.rst
Normal file
1
changelog/11353.trivial.rst
Normal file
@@ -0,0 +1 @@
|
||||
pluggy>=1.3.0 is now required. This adds typing to :class:`~pytest.PytestPluginManager`.
|
||||
1
changelog/11439.bugfix.rst
Normal file
1
changelog/11439.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
Handle an edge case where :data:`sys.stderr` might already be closed when :ref:`faulthandler` is tearing down.
|
||||
1
changelog/11447.improvement.rst
Normal file
1
changelog/11447.improvement.rst
Normal file
@@ -0,0 +1 @@
|
||||
:func:`pytest.deprecated_call` now also considers warnings of type :class:`FutureWarning`.
|
||||
4
changelog/11456.bugfix.rst
Normal file
4
changelog/11456.bugfix.rst
Normal file
@@ -0,0 +1,4 @@
|
||||
Parametrized tests now *really do* ensure that the ids given to each input are unique - for
|
||||
example, ``a, a, a0`` now results in ``a1, a2, a0`` instead of the previous (buggy) ``a0, a1, a0``.
|
||||
This necessarily means changing nodeids where these were previously colliding, and for
|
||||
readability adds an underscore when non-unique ids end in a number.
|
||||
3
changelog/3664.deprecation.rst
Normal file
3
changelog/3664.deprecation.rst
Normal file
@@ -0,0 +1,3 @@
|
||||
Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work.
|
||||
|
||||
This will become an error in the future.
|
||||
22
changelog/7363.breaking.rst
Normal file
22
changelog/7363.breaking.rst
Normal file
@@ -0,0 +1,22 @@
|
||||
**PytestRemovedIn8Warning deprecation warnings are now errors by default.**
|
||||
|
||||
Following our plan to remove deprecated features with as little disruption as
|
||||
possible, all warnings of type ``PytestRemovedIn8Warning`` now generate errors
|
||||
instead of warning messages by default.
|
||||
|
||||
**The affected features will be effectively removed in pytest 8.1**, so please consult the
|
||||
:ref:`deprecations` section in the docs for directions on how to update existing code.
|
||||
|
||||
In the pytest ``8.0.X`` series, it is possible to change the errors back into warnings as a
|
||||
stopgap measure by adding this to your ``pytest.ini`` file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore::pytest.PytestRemovedIn8Warning
|
||||
|
||||
But this will stop working when pytest ``8.1`` is released.
|
||||
|
||||
**If you have concerns** about the removal of a specific feature, please add a
|
||||
comment to :issue:`7363`.
|
||||
1
changelog/7469.feature.rst
Normal file
1
changelog/7469.feature.rst
Normal file
@@ -0,0 +1 @@
|
||||
:class:`~pytest.FixtureDef` is now exported as ``pytest.FixtureDef`` for typing purposes.
|
||||
1
changelog/7966.bugfix.rst
Normal file
1
changelog/7966.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
Removes unhelpful error message from assertion rewrite mechanism when exceptions raised in __iter__ methods, and instead treats them as un-iterable.
|
||||
5
changelog/8976.breaking.rst
Normal file
5
changelog/8976.breaking.rst
Normal file
@@ -0,0 +1,5 @@
|
||||
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
|
||||
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
|
||||
(unless :confval:`python_files` was changed to allow `__init__.py` file).
|
||||
|
||||
To collect the entire package, specify just the directory: `pytest pkg`.
|
||||
1
changelog/9036.bugfix.rst
Normal file
1
changelog/9036.bugfix.rst
Normal file
@@ -0,0 +1 @@
|
||||
``pytest.warns`` and similar functions now capture warnings when an exception is raised inside a ``with`` block.
|
||||
7
changelog/9288.breaking.rst
Normal file
7
changelog/9288.breaking.rst
Normal file
@@ -0,0 +1,7 @@
|
||||
:func:`pytest.warns <warns>` now re-emits unmatched warnings when the context
|
||||
closes -- previously it would consume all warnings, hiding those that were not
|
||||
matched by the function.
|
||||
|
||||
While this is a new feature, we decided to announce this as a breaking change
|
||||
because many test suites are configured to error-out on warnings, and will
|
||||
therefore fail on the newly-re-emitted warnings.
|
||||
@@ -14,7 +14,7 @@ Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
|
||||
``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
|
||||
|
||||
* ``feature``: new user facing features, like new command-line options and new behavior.
|
||||
* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc).
|
||||
* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junit-xml``, improved colors in terminal, etc).
|
||||
* ``bugfix``: fixes a bug.
|
||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||
* ``deprecation``: feature deprecation.
|
||||
|
||||
@@ -6,6 +6,10 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.4.2
|
||||
release-7.4.1
|
||||
release-7.4.0
|
||||
release-7.3.2
|
||||
release-7.3.1
|
||||
release-7.3.0
|
||||
release-7.2.2
|
||||
|
||||
21
doc/en/announce/release-7.3.2.rst
Normal file
21
doc/en/announce/release-7.3.2.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
pytest-7.3.2
|
||||
=======================================
|
||||
|
||||
pytest 7.3.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Adam J. Stewart
|
||||
* Alessio Izzo
|
||||
* Bruno Oliveira
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
49
doc/en/announce/release-7.4.0.rst
Normal file
49
doc/en/announce/release-7.4.0.rst
Normal file
@@ -0,0 +1,49 @@
|
||||
pytest-7.4.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 7.4.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Adam J. Stewart
|
||||
* Alessio Izzo
|
||||
* Alex
|
||||
* Alex Lambson
|
||||
* Brian Larsen
|
||||
* Bruno Oliveira
|
||||
* Bryan Ricker
|
||||
* Chris Mahoney
|
||||
* Facundo Batista
|
||||
* Florian Bruhin
|
||||
* Jarrett Keifer
|
||||
* Kenny Y
|
||||
* Miro Hrončok
|
||||
* Ran Benita
|
||||
* Roberto Aldera
|
||||
* Ronny Pfannschmidt
|
||||
* Sergey Kim
|
||||
* Stefanie Molin
|
||||
* Vijay Arora
|
||||
* Ville Skyttä
|
||||
* Zac Hatfield-Dodds
|
||||
* bzoracler
|
||||
* leeyueh
|
||||
* nondescryptid
|
||||
* theirix
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
20
doc/en/announce/release-7.4.1.rst
Normal file
20
doc/en/announce/release-7.4.1.rst
Normal file
@@ -0,0 +1,20 @@
|
||||
pytest-7.4.1
|
||||
=======================================
|
||||
|
||||
pytest 7.4.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-7.4.2.rst
Normal file
18
doc/en/announce/release-7.4.2.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-7.4.2
|
||||
=======================================
|
||||
|
||||
pytest 7.4.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -87,8 +87,11 @@ Released pytest versions support all Python versions that are actively maintaine
|
||||
============== ===================
|
||||
pytest version min. Python version
|
||||
============== ===================
|
||||
8.0+ 3.8+
|
||||
7.1+ 3.7+
|
||||
6.2 - 7.0 3.6+
|
||||
5.0 - 6.1 3.5+
|
||||
3.3 - 4.6 2.7, 3.4+
|
||||
============== ===================
|
||||
|
||||
`Status of Python Versions <https://devguide.python.org/versions/>`__.
|
||||
|
||||
@@ -22,7 +22,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
cachedir: .pytest_cache
|
||||
rootdir: /home/sweet/project
|
||||
collected 0 items
|
||||
cache -- .../_pytest/cacheprovider.py:510
|
||||
cache -- .../_pytest/cacheprovider.py:532
|
||||
Return a cache object that can persist state between testing sessions.
|
||||
|
||||
cache.get(key, default)
|
||||
@@ -105,7 +105,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:737
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:757
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
@@ -119,7 +119,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1360
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1353
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
@@ -196,7 +196,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
caplog -- .../_pytest/logging.py:498
|
||||
caplog -- .../_pytest/logging.py:570
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following properties/methods::
|
||||
@@ -207,7 +207,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
* caplog.record_tuples -> list of (logger_name, level, message) tuples
|
||||
* caplog.clear() -> clear captured records and formatted log output string
|
||||
|
||||
monkeypatch -- .../_pytest/monkeypatch.py:29
|
||||
monkeypatch -- .../_pytest/monkeypatch.py:30
|
||||
A convenient fixture for monkey-patching.
|
||||
|
||||
The fixture provides these methods to modify objects, dictionaries, or
|
||||
|
||||
@@ -28,6 +28,164 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 7.4.2 (2023-09-07)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects.
|
||||
|
||||
|
||||
- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.
|
||||
|
||||
|
||||
- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
|
||||
|
||||
|
||||
- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.
|
||||
|
||||
|
||||
pytest 7.4.1 (2023-09-02)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug where fake intermediate modules generated by ``--import-mode=importlib`` would not include the
|
||||
child modules as attributes of the parent modules.
|
||||
|
||||
|
||||
- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
|
||||
|
||||
|
||||
- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
|
||||
to be imported more than once, causing problems with modules that have import side effects.
|
||||
|
||||
|
||||
pytest 7.4.0 (2023-06-23)
|
||||
=========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#10901 <https://github.com/pytest-dev/pytest/issues/10901>`_: Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
|
||||
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10872 <https://github.com/pytest-dev/pytest/issues/10872>`_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
|
||||
|
||||
|
||||
- `#10907 <https://github.com/pytest-dev/pytest/issues/10907>`_: When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
|
||||
|
||||
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
|
||||
|
||||
Previously, the last frame of the traceback was shown, even though it was hidden.
|
||||
|
||||
|
||||
- `#10940 <https://github.com/pytest-dev/pytest/issues/10940>`_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
|
||||
|
||||
Added ``TerminalReporter.wrap_write()`` as a helper for that.
|
||||
|
||||
|
||||
- `#10991 <https://github.com/pytest-dev/pytest/issues/10991>`_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
|
||||
|
||||
|
||||
- `#11005 <https://github.com/pytest-dev/pytest/issues/11005>`_: Added the underlying exception to the cache provider's path creation and write warning messages.
|
||||
|
||||
|
||||
- `#11013 <https://github.com/pytest-dev/pytest/issues/11013>`_: Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
|
||||
|
||||
|
||||
- `#11043 <https://github.com/pytest-dev/pytest/issues/11043>`_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir <rootdir>`.
|
||||
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
|
||||
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
|
||||
|
||||
|
||||
- `#11081 <https://github.com/pytest-dev/pytest/issues/11081>`_: The :confval:`norecursedirs` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
|
||||
|
||||
If after updating to this version you see that your `norecursedirs` setting is not being respected,
|
||||
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
|
||||
Most likely, your hook returns `False` for paths it does not want to ignore,
|
||||
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
|
||||
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
|
||||
|
||||
|
||||
- `#8711 <https://github.com/pytest-dev/pytest/issues/8711>`_: :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :func:`caplog.at_level() <pytest.LogCaptureFixture.at_level>`
|
||||
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
|
||||
``logging.disable(LEVEL)``.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10831 <https://github.com/pytest-dev/pytest/issues/10831>`_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
|
||||
|
||||
|
||||
- `#11068 <https://github.com/pytest-dev/pytest/issues/11068>`_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
|
||||
|
||||
|
||||
- `#11104 <https://github.com/pytest-dev/pytest/issues/11104>`_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
|
||||
even when it was not utilized (e.g. when explicit paths were given on the command line).
|
||||
Now the ``testpaths`` are only considered when they are in use.
|
||||
|
||||
|
||||
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
|
||||
|
||||
|
||||
- `#7781 <https://github.com/pytest-dev/pytest/issues/7781>`_: Fix writing non-encodable text to log file when using ``--debug``.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#9146 <https://github.com/pytest-dev/pytest/issues/9146>`_: Improved documentation for :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>`.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#11031 <https://github.com/pytest-dev/pytest/issues/11031>`_: Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
|
||||
|
||||
|
||||
pytest 7.3.2 (2023-06-10)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
|
||||
|
||||
|
||||
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
|
||||
|
||||
|
||||
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
|
||||
|
||||
|
||||
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
|
||||
|
||||
|
||||
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
|
||||
|
||||
|
||||
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
|
||||
|
||||
|
||||
pytest 7.3.1 (2023-04-14)
|
||||
=========================
|
||||
|
||||
@@ -567,7 +725,7 @@ Breaking Changes
|
||||
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
||||
|
||||
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
||||
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
|
||||
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
|
||||
|
||||
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
||||
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
||||
@@ -4067,7 +4225,7 @@ Removals
|
||||
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
||||
|
||||
|
||||
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
|
||||
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
|
||||
|
||||
Use ``Node.get_closest_marker(name)`` as a replacement.
|
||||
|
||||
|
||||
@@ -15,12 +15,10 @@
|
||||
#
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
# The short X.Y version.
|
||||
import ast
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
from typing import List
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from _pytest import __version__ as version
|
||||
@@ -341,7 +339,7 @@ epub_copyright = "2013, holger krekel et alii"
|
||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
||||
# epub_scheme = ''
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# The unique identifier of the text. This can be an ISBN number
|
||||
# or the project homepage.
|
||||
# epub_identifier = ''
|
||||
|
||||
@@ -451,25 +449,6 @@ def setup(app: "sphinx.application.Sphinx") -> None:
|
||||
|
||||
configure_logging(app)
|
||||
|
||||
# Make Sphinx mark classes with "final" when decorated with @final.
|
||||
# We need this because we import final from pytest._compat, not from
|
||||
# typing (for Python < 3.8 compat), so Sphinx doesn't detect it.
|
||||
# To keep things simple we accept any `@final` decorator.
|
||||
# Ref: https://github.com/pytest-dev/pytest/pull/7780
|
||||
import sphinx.pycode.ast
|
||||
import sphinx.pycode.parser
|
||||
|
||||
original_is_final = sphinx.pycode.parser.VariableCommentPicker.is_final
|
||||
|
||||
def patched_is_final(self, decorators: List[ast.expr]) -> bool:
|
||||
if original_is_final(self, decorators):
|
||||
return True
|
||||
return any(
|
||||
sphinx.pycode.ast.unparse(decorator) == "final" for decorator in decorators
|
||||
)
|
||||
|
||||
sphinx.pycode.parser.VariableCommentPicker.is_final = patched_is_final
|
||||
|
||||
# legacypath.py monkey-patches pytest.Testdir in. Import the file so
|
||||
# that autodoc can discover references to it.
|
||||
import _pytest.legacypath # noqa: F401
|
||||
|
||||
@@ -380,6 +380,25 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of
|
||||
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
|
||||
deprecation warning is now raised.
|
||||
|
||||
Applying a mark to a fixture function
|
||||
-------------------------------------
|
||||
|
||||
.. deprecated:: 7.4
|
||||
|
||||
Applying a mark to a fixture function never had any effect, but it is a common user error.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.usefixtures("clean_database")
|
||||
@pytest.fixture
|
||||
def user() -> User:
|
||||
...
|
||||
|
||||
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
|
||||
|
||||
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
|
||||
|
||||
|
||||
Backward compatibilities in ``Parser.addoption``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -467,12 +486,42 @@ The ``yield_fixture`` function/decorator
|
||||
It has been so for a very long time, so can be search/replaced safely.
|
||||
|
||||
|
||||
Removed Features
|
||||
----------------
|
||||
Removed Features and Breaking Changes
|
||||
-------------------------------------
|
||||
|
||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||
an appropriate period of deprecation has passed.
|
||||
|
||||
Some breaking changes which could not be deprecated are also listed.
|
||||
|
||||
|
||||
:class:`pytest.Package` is no longer a :class:`pytest.Module` or :class:`pytest.File`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionchanged:: 8.0
|
||||
|
||||
The ``Package`` collector node designates a Python package, that is, a directory with an `__init__.py` file.
|
||||
Previously ``Package`` was a subtype of ``pytest.Module`` (which represents a single Python module),
|
||||
the module being the `__init__.py` file.
|
||||
This has been deemed a design mistake (see :issue:`11137` and :issue:`7777` for details).
|
||||
|
||||
The ``path`` property of ``Package`` nodes now points to the package directory instead of the ``__init__.py`` file.
|
||||
|
||||
Note that a ``Module`` node for ``__init__.py`` (which is not a ``Package``) may still exist,
|
||||
if it is picked up during collection (e.g. if you configured :confval:`python_files` to include ``__init__.py`` files).
|
||||
|
||||
|
||||
Collecting ``__init__.py`` files no longer collects package
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. versionremoved:: 8.0
|
||||
|
||||
Running `pytest pkg/__init__.py` now collects the `pkg/__init__.py` file (module) only.
|
||||
Previously, it collected the entire `pkg` package, including other test files in the directory, but excluding tests in the `__init__.py` file itself
|
||||
(unless :confval:`python_files` was changed to allow `__init__.py` file).
|
||||
|
||||
To collect the entire package, specify just the directory: `pytest pkg`.
|
||||
|
||||
|
||||
The ``pytest.collect`` module
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -596,7 +645,7 @@ By using ``legacy`` you will keep using the legacy/xunit1 format when upgrading
|
||||
pytest 6.0, where the default format will be ``xunit2``.
|
||||
|
||||
In order to let users know about the transition, pytest will issue a warning in case
|
||||
the ``--junitxml`` option is given in the command line but ``junit_family`` is not explicitly
|
||||
the ``--junit-xml`` option is given in the command line but ``junit_family`` is not explicitly
|
||||
configured in ``pytest.ini``.
|
||||
|
||||
Services known to support the ``xunit2`` format:
|
||||
|
||||
@@ -136,7 +136,7 @@ Or select multiple nodes:
|
||||
|
||||
Node IDs for failing tests are displayed in the test summary info
|
||||
when running pytest with the ``-rf`` option. You can also
|
||||
construct Node IDs from the output of ``pytest --collectonly``.
|
||||
construct Node IDs from the output of ``pytest --collect-only``.
|
||||
|
||||
Using ``-k expr`` to select tests based on their name
|
||||
-------------------------------------------------------
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"""
|
||||
module containing a parametrized tests testing cross-python
|
||||
serialization via the pickle module.
|
||||
"""
|
||||
"""Module containing a parametrized tests testing cross-python serialization
|
||||
via the pickle module."""
|
||||
import shutil
|
||||
import subprocess
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
pythonlist = ["python3.5", "python3.6", "python3.7"]
|
||||
|
||||
pythonlist = ["python3.9", "python3.10", "python3.11"]
|
||||
|
||||
|
||||
@pytest.fixture(params=pythonlist)
|
||||
@@ -43,7 +42,7 @@ class Python:
|
||||
)
|
||||
)
|
||||
)
|
||||
subprocess.check_call((self.pythonpath, str(dumpfile)))
|
||||
subprocess.run((self.pythonpath, str(dumpfile)), check=True)
|
||||
|
||||
def load_and_is_true(self, expression):
|
||||
loadfile = self.picklefile.with_name("load.py")
|
||||
@@ -63,7 +62,7 @@ class Python:
|
||||
)
|
||||
)
|
||||
print(loadfile)
|
||||
subprocess.check_call((self.pythonpath, str(loadfile)))
|
||||
subprocess.run((self.pythonpath, str(loadfile)), check=True)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("obj", [42, {}, {1: 3}])
|
||||
|
||||
@@ -12,7 +12,7 @@ class YamlFile(pytest.File):
|
||||
# We need a yaml parser, e.g. PyYAML.
|
||||
import yaml
|
||||
|
||||
raw = yaml.safe_load(self.path.open())
|
||||
raw = yaml.safe_load(self.path.open(encoding="utf-8"))
|
||||
for name, spec in sorted(raw.items()):
|
||||
yield YamlItem.from_parent(self, name=name, spec=spec)
|
||||
|
||||
@@ -38,6 +38,7 @@ class YamlItem(pytest.Item):
|
||||
" no further details known at this point.",
|
||||
]
|
||||
)
|
||||
return super().repr_failure(excinfo)
|
||||
|
||||
def reportinfo(self):
|
||||
return self.path, 0, f"usecase: {self.name}"
|
||||
|
||||
@@ -483,8 +483,8 @@ argument sets to use for each test function. Let's run it:
|
||||
FAILED test_parametrize.py::TestClass::test_equals[1-2] - assert 1 == 2
|
||||
1 failed, 2 passed in 0.12s
|
||||
|
||||
Indirect parametrization with multiple fixtures
|
||||
--------------------------------------------------------------
|
||||
Parametrization with multiple fixtures
|
||||
--------------------------------------
|
||||
|
||||
Here is a stripped down real-life example of using parametrized
|
||||
testing for testing serialization of objects between different python
|
||||
@@ -509,8 +509,8 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||
SKIPPED [9] multipython.py:69: 'python3.7' not found
|
||||
27 skipped in 0.12s
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
--------------------------------------------------------------------
|
||||
Parametrization of optional implementations/imports
|
||||
---------------------------------------------------
|
||||
|
||||
If you want to compare the outcomes of several implementations of a given
|
||||
API, you can write test functions that receive the already imported implementations
|
||||
@@ -657,13 +657,16 @@ Use :func:`pytest.raises` with the
|
||||
:ref:`pytest.mark.parametrize ref` decorator to write parametrized tests
|
||||
in which some tests raise exceptions and others do not.
|
||||
|
||||
It may be helpful to use ``nullcontext`` as a complement to ``raises``.
|
||||
``contextlib.nullcontext`` can be used to test cases that are not expected to
|
||||
raise exceptions but that should result in some value. The value is given as the
|
||||
``enter_result`` parameter, which will be available as the ``with`` statement’s
|
||||
target (``e`` in the example below).
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
from contextlib import nullcontext
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -671,16 +674,17 @@ For example:
|
||||
@pytest.mark.parametrize(
|
||||
"example_input,expectation",
|
||||
[
|
||||
(3, does_not_raise()),
|
||||
(2, does_not_raise()),
|
||||
(1, does_not_raise()),
|
||||
(3, nullcontext(2)),
|
||||
(2, nullcontext(3)),
|
||||
(1, nullcontext(6)),
|
||||
(0, pytest.raises(ZeroDivisionError)),
|
||||
],
|
||||
)
|
||||
def test_division(example_input, expectation):
|
||||
"""Test how much I know division."""
|
||||
with expectation:
|
||||
assert (6 / example_input) is not None
|
||||
with expectation as e:
|
||||
assert (6 / example_input) == e
|
||||
|
||||
In the example above, the first three test cases should run unexceptionally,
|
||||
while the fourth should raise ``ZeroDivisionError``.
|
||||
In the example above, the first three test cases should run without any
|
||||
exceptions, while the fourth should raise a``ZeroDivisionError`` exception,
|
||||
which is expected by pytest.
|
||||
|
||||
@@ -691,7 +691,7 @@ Here is an example for making a ``db`` fixture available in a directory:
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@pytest.fixture(scope="package")
|
||||
def db():
|
||||
return DB()
|
||||
|
||||
@@ -808,16 +808,15 @@ case we just write some information out to a ``failures`` file:
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
# execute all other hooks to obtain the report object
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
rep = yield
|
||||
|
||||
# we only look at actual failing test calls, not setup/teardown
|
||||
if rep.when == "call" and rep.failed:
|
||||
mode = "a" if os.path.exists("failures") else "w"
|
||||
with open("failures", mode) as f:
|
||||
with open("failures", mode, encoding="utf-8") as f:
|
||||
# let's also access a fixture for the fun of it
|
||||
if "tmp_path" in item.fixturenames:
|
||||
extra = " ({})".format(item.funcargs["tmp_path"])
|
||||
@@ -826,6 +825,8 @@ case we just write some information out to a ``failures`` file:
|
||||
|
||||
f.write(rep.nodeid + extra + "\n")
|
||||
|
||||
return rep
|
||||
|
||||
|
||||
if you then have failing tests:
|
||||
|
||||
@@ -899,16 +900,17 @@ here is a little example implemented via a local plugin:
|
||||
phase_report_key = StashKey[Dict[str, CollectReport]]()
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
@pytest.hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
# execute all other hooks to obtain the report object
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
rep = yield
|
||||
|
||||
# store test results for each phase of a call, which can
|
||||
# be "setup", "call", "teardown"
|
||||
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
|
||||
|
||||
return rep
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def something(request):
|
||||
@@ -1088,4 +1090,4 @@ application with standard ``pytest`` command-line options:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/
|
||||
./app_main --pytest --verbose --tb=long --junit=xml=results.xml test-suite/
|
||||
|
||||
@@ -34,7 +34,7 @@ a function/method call.
|
||||
|
||||
**Assert** is where we look at that resulting state and check if it looks how
|
||||
we'd expect after the dust has settled. It's where we gather evidence to say the
|
||||
behavior does or does not aligns with what we expect. The ``assert`` in our test
|
||||
behavior does or does not align with what we expect. The ``assert`` in our test
|
||||
is where we take that measurement/observation and apply our judgement to it. If
|
||||
something should be green, we'd say ``assert thing == "green"``.
|
||||
|
||||
|
||||
@@ -294,3 +294,20 @@ See also `pypa/setuptools#1684 <https://github.com/pypa/setuptools/issues/1684>`
|
||||
|
||||
setuptools intends to
|
||||
`remove the test command <https://github.com/pypa/setuptools/issues/931>`_.
|
||||
|
||||
Checking with flake8-pytest-style
|
||||
---------------------------------
|
||||
|
||||
In order to ensure that pytest is being used correctly in your project,
|
||||
it can be helpful to use the `flake8-pytest-style <https://github.com/m-burst/flake8-pytest-style>`_ flake8 plugin.
|
||||
|
||||
flake8-pytest-style checks for common mistakes and coding style violations in pytest code,
|
||||
such as incorrect use of fixtures, test function names, and markers.
|
||||
By using this plugin, you can catch these errors early in the development process
|
||||
and ensure that your pytest code is consistent and easy to maintain.
|
||||
|
||||
A list of the lints detected by flake8-pytest-style can be found on its `PyPI page <https://pypi.org/project/flake8-pytest-style/>`_.
|
||||
|
||||
.. note::
|
||||
|
||||
flake8-pytest-style is not an official pytest project. Some of the rules enforce certain style choices, such as using `@pytest.fixture()` over `@pytest.fixture`, but you can configure the plugin to fit your preferred style.
|
||||
|
||||
@@ -9,7 +9,7 @@ Get Started
|
||||
Install ``pytest``
|
||||
----------------------------------------
|
||||
|
||||
``pytest`` requires: Python 3.7+ or PyPy3.
|
||||
``pytest`` requires: Python 3.8+ or PyPy3.
|
||||
|
||||
1. Run the following command in your command line:
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.3.1
|
||||
pytest 7.4.2
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
@@ -97,6 +97,30 @@ Use the :ref:`raises <assertraises>` helper to assert that some code raises an e
|
||||
with pytest.raises(SystemExit):
|
||||
f()
|
||||
|
||||
You can also use the context provided by :ref:`raises <assertraises>` to
|
||||
assert that an expected exception is part of a raised ``ExceptionGroup``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_exceptiongroup.py
|
||||
import pytest
|
||||
|
||||
|
||||
def f():
|
||||
raise ExceptionGroup(
|
||||
"Group message",
|
||||
[
|
||||
RuntimeError(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def test_exception_in_group():
|
||||
with pytest.raises(ExceptionGroup) as excinfo:
|
||||
f()
|
||||
assert excinfo.group_contains(RuntimeError)
|
||||
assert not excinfo.group_contains(TypeError)
|
||||
|
||||
Execute the test function with “quiet” reporting mode:
|
||||
|
||||
.. code-block:: pytest
|
||||
|
||||
@@ -54,14 +54,13 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the
|
||||
idiomatic python constructs without boilerplate code while not losing
|
||||
introspection information.
|
||||
|
||||
However, if you specify a message with the assertion like this:
|
||||
If a message is specified with the assertion like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
assert a % 2 == 0, "value was odd, should be even"
|
||||
|
||||
then no assertion introspection takes places at all and the message
|
||||
will be simply shown in the traceback.
|
||||
it is printed alongside the assertion introspection in the traceback.
|
||||
|
||||
See :ref:`assert-details` for more information on assertion introspection.
|
||||
|
||||
@@ -116,10 +115,56 @@ that a regular expression matches on the string representation of an exception
|
||||
with pytest.raises(ValueError, match=r".* 123 .*"):
|
||||
myfunc()
|
||||
|
||||
The regexp parameter of the ``match`` method is matched with the ``re.search``
|
||||
The regexp parameter of the ``match`` parameter is matched with the ``re.search``
|
||||
function, so in the above example ``match='123'`` would have worked as
|
||||
well.
|
||||
|
||||
You can also use the :func:`excinfo.group_contains() <pytest.ExceptionInfo.group_contains>`
|
||||
method to test for exceptions returned as part of an ``ExceptionGroup``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_exception_in_group():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
raise ExceptionGroup(
|
||||
"Group message",
|
||||
[
|
||||
RuntimeError("Exception 123 raised"),
|
||||
],
|
||||
)
|
||||
assert excinfo.group_contains(RuntimeError, match=r".* 123 .*")
|
||||
assert not excinfo.group_contains(TypeError)
|
||||
|
||||
The optional ``match`` keyword parameter works the same way as for
|
||||
:func:`pytest.raises`.
|
||||
|
||||
By default ``group_contains()`` will recursively search for a matching
|
||||
exception at any level of nested ``ExceptionGroup`` instances. You can
|
||||
specify a ``depth`` keyword parameter if you only want to match an
|
||||
exception at a specific level; exceptions contained directly in the top
|
||||
``ExceptionGroup`` would match ``depth=1``.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_exception_in_group_at_given_depth():
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
raise ExceptionGroup(
|
||||
"Group message",
|
||||
[
|
||||
RuntimeError(),
|
||||
ExceptionGroup(
|
||||
"Nested group",
|
||||
[
|
||||
TypeError(),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
assert excinfo.group_contains(RuntimeError, depth=1)
|
||||
assert excinfo.group_contains(TypeError, depth=2)
|
||||
assert not excinfo.group_contains(RuntimeError, depth=2)
|
||||
assert not excinfo.group_contains(TypeError, depth=1)
|
||||
|
||||
There's an alternate form of the :func:`pytest.raises` function where you pass
|
||||
a function that will be executed with the given ``*args`` and ``**kwargs`` and
|
||||
assert that the given exception is raised:
|
||||
|
||||
@@ -176,14 +176,21 @@ with more recent files coming first.
|
||||
Behavior when no tests failed in the last run
|
||||
---------------------------------------------
|
||||
|
||||
When no tests failed in the last run, or when no cached ``lastfailed`` data was
|
||||
found, ``pytest`` can be configured either to run all of the tests or no tests,
|
||||
using the ``--last-failed-no-failures`` option, which takes one of the following values:
|
||||
The ``--lfnf/--last-failed-no-failures`` option governs the behavior of ``--last-failed``.
|
||||
Determines whether to execute tests when there are no previously (known)
|
||||
failures or when no cached ``lastfailed`` data was found.
|
||||
|
||||
There are two options:
|
||||
|
||||
* ``all``: when there are no known test failures, runs all tests (the full test suite). This is the default.
|
||||
* ``none``: when there are no known test failures, just emits a message stating this and exit successfully.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --last-failed --last-failed-no-failures all # run all tests (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # run no tests and exit
|
||||
pytest --last-failed --last-failed-no-failures all # runs the full test suite (default behavior)
|
||||
pytest --last-failed --last-failed-no-failures none # runs no tests and exits successfully
|
||||
|
||||
The new config.cache object
|
||||
--------------------------------
|
||||
|
||||
@@ -135,10 +135,6 @@ Warning about unraisable exceptions and unhandled thread exceptions
|
||||
|
||||
.. versionadded:: 6.2
|
||||
|
||||
.. note::
|
||||
|
||||
These features only work on Python>=3.8.
|
||||
|
||||
Unhandled exceptions are exceptions that are raised in a situation in which
|
||||
they cannot propagate to a caller. The most common case is an exception raised
|
||||
in a :meth:`__del__ <object.__del__>` implementation.
|
||||
|
||||
@@ -1698,7 +1698,7 @@ and declare its use in a test module via a ``usefixtures`` marker:
|
||||
class TestDirectoryInit:
|
||||
def test_cwd_starts_empty(self):
|
||||
assert os.listdir(os.getcwd()) == []
|
||||
with open("myfile", "w") as f:
|
||||
with open("myfile", "w", encoding="utf-8") as f:
|
||||
f.write("hello")
|
||||
|
||||
def test_cwd_again_starts_empty(self):
|
||||
@@ -1752,8 +1752,7 @@ into an ini-file:
|
||||
def my_fixture_that_sadly_wont_use_my_other_fixture():
|
||||
...
|
||||
|
||||
Currently this will not generate any error or warning, but this is intended
|
||||
to be handled by :issue:`3664`.
|
||||
This generates a deprecation warning, and will become an error in Pytest 8.
|
||||
|
||||
.. _`override fixtures`:
|
||||
|
||||
|
||||
@@ -172,6 +172,13 @@ the records for the ``setup`` and ``call`` stages during teardown like so:
|
||||
|
||||
The full API is available at :class:`pytest.LogCaptureFixture`.
|
||||
|
||||
.. warning::
|
||||
|
||||
The ``caplog`` fixture adds a handler to the root logger to capture logs. If the root logger is
|
||||
modified during a test, for example with ``logging.config.dictConfig``, this handler may be
|
||||
removed and cause no logs to be captured. To avoid this, ensure that any root logger configuration
|
||||
only adds to the existing handlers.
|
||||
|
||||
|
||||
.. _live_logs:
|
||||
|
||||
|
||||
@@ -47,8 +47,7 @@ Unsupported idioms / known issues
|
||||
- nose imports test modules with the same import path (e.g.
|
||||
``tests.test_mode``) but different file system paths
|
||||
(e.g. ``tests/test_mode.py`` and ``other/tests/test_mode.py``)
|
||||
by extending sys.path/import semantics. pytest does not do that
|
||||
but there is discussion in :issue:`268` for adding some support. Note that
|
||||
by extending sys.path/import semantics. pytest does not do that. Note that
|
||||
`nose2 choose to avoid this sys.path/import hackery <https://nose2.readthedocs.io/en/latest/differences.html#test-discovery-and-loading>`_.
|
||||
|
||||
If you place a conftest.py file in the root directory of your project
|
||||
@@ -66,16 +65,34 @@ Unsupported idioms / known issues
|
||||
|
||||
- no nose-configuration is recognized.
|
||||
|
||||
- ``yield``-based methods are unsupported as of pytest 4.1.0. They are
|
||||
- ``yield``-based methods are
|
||||
fundamentally incompatible with pytest because they don't support fixtures
|
||||
properly since collection and test execution are separated.
|
||||
|
||||
Here is a table comparing the default supported naming conventions for both
|
||||
nose and pytest.
|
||||
|
||||
========= ========================== ======= =====
|
||||
what default naming convention pytest nose
|
||||
========= ========================== ======= =====
|
||||
module ``test*.py`` ✅
|
||||
module ``test_*.py`` ✅ ✅
|
||||
module ``*_test.py`` ✅
|
||||
module ``*_tests.py``
|
||||
class ``*(unittest.TestCase)`` ✅ ✅
|
||||
method ``test_*`` ✅ ✅
|
||||
class ``Test*`` ✅
|
||||
method ``test_*`` ✅
|
||||
function ``test_*`` ✅
|
||||
========= ========================== ======= =====
|
||||
|
||||
|
||||
Migrating from nose to pytest
|
||||
------------------------------
|
||||
|
||||
`nose2pytest <https://github.com/pytest-dev/nose2pytest>`_ is a Python script
|
||||
and pytest plugin to help convert Nose-based tests into pytest-based tests.
|
||||
Specifically, the script transforms nose.tools.assert_* function calls into
|
||||
Specifically, the script transforms ``nose.tools.assert_*`` function calls into
|
||||
raw assert statements, while preserving format of original arguments
|
||||
as much as possible.
|
||||
|
||||
|
||||
@@ -478,7 +478,7 @@ integration servers, use this invocation:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --junitxml=path
|
||||
pytest --junit-xml=path
|
||||
|
||||
to create an XML file at ``path``.
|
||||
|
||||
|
||||
@@ -197,6 +197,14 @@ the test. You can also skip based on the version number of a library:
|
||||
The version will be read from the specified
|
||||
module's ``__version__`` attribute.
|
||||
|
||||
Sometimes importing a module can fail due to an exception, if you want to
|
||||
only skip if the module does not exist pass ModuleNotFoundError as the
|
||||
``exc`` kwarg:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
docutils = pytest.importorskip("docutils", exc=ModuleNotFoundError)
|
||||
|
||||
Summary
|
||||
~~~~~~~
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ created in the `base temporary directory`_.
|
||||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
p.write_text(CONTENT, encoding="utf-8")
|
||||
assert p.read_text(encoding="utf-8") == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
assert 0
|
||||
|
||||
@@ -51,8 +51,8 @@ Running this would result in a passed test except for the last
|
||||
d = tmp_path / "sub"
|
||||
d.mkdir()
|
||||
p = d / "hello.txt"
|
||||
p.write_text(CONTENT)
|
||||
assert p.read_text() == CONTENT
|
||||
p.write_text(CONTENT, encoding="utf-8")
|
||||
assert p.read_text(encoding="utf-8") == CONTENT
|
||||
assert len(list(tmp_path.iterdir())) == 1
|
||||
> assert 0
|
||||
E assert 0
|
||||
|
||||
@@ -207,10 +207,10 @@ creation of a per-test temporary directory:
|
||||
@pytest.fixture(autouse=True)
|
||||
def initdir(self, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path) # change to pytest-provided temporary directory
|
||||
tmp_path.joinpath("samplefile.ini").write_text("# testdata")
|
||||
tmp_path.joinpath("samplefile.ini").write_text("# testdata", encoding="utf-8")
|
||||
|
||||
def test_method(self):
|
||||
with open("samplefile.ini") as f:
|
||||
with open("samplefile.ini", encoding="utf-8") as f:
|
||||
s = f.read()
|
||||
assert "testdata" in s
|
||||
|
||||
|
||||
@@ -35,31 +35,43 @@ Pytest supports several ways to run and select tests from the command-line.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest -k "MyClass and not method"
|
||||
pytest -k 'MyClass and not method'
|
||||
|
||||
This will run tests which contain names that match the given *string expression* (case-insensitive),
|
||||
which can include Python operators that use filenames, class names and function names as variables.
|
||||
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
||||
Use ``""`` instead of ``''`` in expression when running this on Windows
|
||||
|
||||
.. _nodeids:
|
||||
|
||||
**Run tests by node ids**
|
||||
**Run tests by collection arguments**
|
||||
|
||||
Each collected test is assigned a unique ``nodeid`` which consist of the module filename followed
|
||||
by specifiers like class names, function names and parameters from parametrization, separated by ``::`` characters.
|
||||
Pass the module filename relative to the working directory, followed by specifiers like the class name and function name
|
||||
separated by ``::`` characters, and parameters from parameterization enclosed in ``[]``.
|
||||
|
||||
To run a specific test within a module:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest test_mod.py::test_func
|
||||
pytest tests/test_mod.py::test_func
|
||||
|
||||
|
||||
Another example specifying a test method in the command line:
|
||||
To run all tests in a class:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest test_mod.py::TestClass::test_method
|
||||
pytest tests/test_mod.py::TestClass
|
||||
|
||||
Specifying a specific test method:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest tests/test_mod.py::TestClass::test_method
|
||||
|
||||
Specifying a specific parametrization of a test:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest tests/test_mod.py::test_func[x1,y2]
|
||||
|
||||
**Run tests by marker expressions**
|
||||
|
||||
@@ -172,7 +184,8 @@ You can invoke ``pytest`` from Python code directly:
|
||||
|
||||
this acts as if you would call "pytest" from the command line.
|
||||
It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead.
|
||||
You can pass in options and arguments:
|
||||
If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable.
|
||||
You can pass in options and arguments explicitly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ The remaining hook functions will not be called in this case.
|
||||
|
||||
.. _`hookwrapper`:
|
||||
|
||||
hookwrapper: executing around other hooks
|
||||
hook wrappers: executing around other hooks
|
||||
-------------------------------------------------
|
||||
|
||||
.. currentmodule:: _pytest.core
|
||||
@@ -69,10 +69,8 @@ which yields exactly once. When pytest invokes hooks it first executes
|
||||
hook wrappers and passes the same arguments as to the regular hooks.
|
||||
|
||||
At the yield point of the hook wrapper pytest will execute the next hook
|
||||
implementations and return their result to the yield point in the form of
|
||||
a :py:class:`Result <pluggy._Result>` instance which encapsulates a result or
|
||||
exception info. The yield point itself will thus typically not raise
|
||||
exceptions (unless there are bugs).
|
||||
implementations and return their result to the yield point, or will
|
||||
propagate an exception if they raised.
|
||||
|
||||
Here is an example definition of a hook wrapper:
|
||||
|
||||
@@ -81,26 +79,35 @@ Here is an example definition of a hook wrapper:
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@pytest.hookimpl(wrapper=True)
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
do_something_before_next_hook_executes()
|
||||
|
||||
outcome = yield
|
||||
# outcome.excinfo may be None or a (cls, val, tb) tuple
|
||||
# If the outcome is an exception, will raise the exception.
|
||||
res = yield
|
||||
|
||||
res = outcome.get_result() # will raise if outcome was exception
|
||||
new_res = post_process_result(res)
|
||||
|
||||
post_process_result(res)
|
||||
# Override the return value to the plugin system.
|
||||
return new_res
|
||||
|
||||
outcome.force_result(new_res) # to override the return value to the plugin system
|
||||
The hook wrapper needs to return a result for the hook, or raise an exception.
|
||||
|
||||
Note that hook wrappers don't return results themselves, they merely
|
||||
perform tracing or other side effects around the actual hook implementations.
|
||||
If the result of the underlying hook is a mutable object, they may modify
|
||||
that result but it's probably better to avoid it.
|
||||
In many cases, the wrapper only needs to perform tracing or other side effects
|
||||
around the actual hook implementations, in which case it can return the result
|
||||
value of the ``yield``. The simplest (though useless) hook wrapper is
|
||||
``return (yield)``.
|
||||
|
||||
In other cases, the wrapper wants the adjust or adapt the result, in which case
|
||||
it can return a new value. If the result of the underlying hook is a mutable
|
||||
object, the wrapper may modify that result, but it's probably better to avoid it.
|
||||
|
||||
If the hook implementation failed with an exception, the wrapper can handle that
|
||||
exception using a ``try-catch-finally`` around the ``yield``, by propagating it,
|
||||
supressing it, or raising a different exception entirely.
|
||||
|
||||
For more information, consult the
|
||||
:ref:`pluggy documentation about hookwrappers <pluggy:hookwrappers>`.
|
||||
:ref:`pluggy documentation about hook wrappers <pluggy:hookwrappers>`.
|
||||
|
||||
.. _plugin-hookorder:
|
||||
|
||||
@@ -130,11 +137,14 @@ after others, i.e. the position in the ``N``-sized list of functions:
|
||||
|
||||
|
||||
# Plugin 3
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@pytest.hookimpl(wrapper=True)
|
||||
def pytest_collection_modifyitems(items):
|
||||
# will execute even before the tryfirst one above!
|
||||
outcome = yield
|
||||
# will execute after all non-hookwrappers executed
|
||||
try:
|
||||
return (yield)
|
||||
finally:
|
||||
# will execute after all non-wrappers executed
|
||||
...
|
||||
|
||||
Here is the order of execution:
|
||||
|
||||
@@ -149,12 +159,11 @@ Here is the order of execution:
|
||||
Plugin1).
|
||||
|
||||
4. Plugin3's pytest_collection_modifyitems then executing the code after the yield
|
||||
point. The yield receives a :py:class:`Result <pluggy._Result>` instance which encapsulates
|
||||
the result from calling the non-wrappers. Wrappers shall not modify the result.
|
||||
point. The yield receives the result from calling the non-wrappers, or raises
|
||||
an exception if the non-wrappers raised.
|
||||
|
||||
It's possible to use ``tryfirst`` and ``trylast`` also in conjunction with
|
||||
``hookwrapper=True`` in which case it will influence the ordering of hookwrappers
|
||||
among each other.
|
||||
It's possible to use ``tryfirst`` and ``trylast`` also on hook wrappers
|
||||
in which case it will influence the ordering of hook wrappers among each other.
|
||||
|
||||
|
||||
Declaring new hooks
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
:orphan:
|
||||
|
||||
..
|
||||
.. sidebar:: Next Open Trainings
|
||||
.. sidebar:: Next Open Trainings
|
||||
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
.. _features:
|
||||
|
||||
@@ -18,7 +17,7 @@ The ``pytest`` framework makes it easy to write small, readable tests, and can
|
||||
scale to support complex functional testing for applications and libraries.
|
||||
|
||||
|
||||
``pytest`` requires: Python 3.7+ or PyPy3.
|
||||
``pytest`` requires: Python 3.8+ or PyPy3.
|
||||
|
||||
**PyPI package name**: :pypi:`pytest`
|
||||
|
||||
@@ -77,7 +76,7 @@ Features
|
||||
|
||||
- Can run :ref:`unittest <unittest>` (including trial) and :ref:`nose <noseintegration>` test suites out of the box
|
||||
|
||||
- Python 3.7+ or PyPy 3
|
||||
- Python 3.8+ or PyPy 3
|
||||
|
||||
- Rich plugin architecture, with over 800+ :ref:`external plugins <plugin-list>` and thriving community
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -82,6 +82,8 @@ pytest.exit
|
||||
pytest.main
|
||||
~~~~~~~~~~~
|
||||
|
||||
**Tutorial**: :ref:`pytest.main-usage`
|
||||
|
||||
.. autofunction:: pytest.main
|
||||
|
||||
pytest.param
|
||||
@@ -235,7 +237,7 @@ pytest.mark.xfail
|
||||
|
||||
Marks a test function as *expected to fail*.
|
||||
|
||||
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=False)
|
||||
.. py:function:: pytest.mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=xfail_strict)
|
||||
|
||||
:type condition: bool or str
|
||||
:param condition:
|
||||
@@ -247,10 +249,10 @@ Marks a test function as *expected to fail*.
|
||||
:keyword Type[Exception] raises:
|
||||
Exception subclass (or tuple of subclasses) expected to be raised by the test function; other exceptions will fail the test.
|
||||
:keyword bool run:
|
||||
If the test function should actually be executed. If ``False``, the function will always xfail and will
|
||||
Whether the test function should actually be executed. If ``False``, the function will always xfail and will
|
||||
not be executed (useful if a function is segfaulting).
|
||||
:keyword bool strict:
|
||||
* If ``False`` (the default) the function will be shown in the terminal output as ``xfailed`` if it fails
|
||||
* If ``False`` the function will be shown in the terminal output as ``xfailed`` if it fails
|
||||
and as ``xpass`` if it passes. In both cases this will not cause the test suite to fail as a whole. This
|
||||
is particularly useful to mark *flaky* tests (tests that fail at random) to be tackled later.
|
||||
* If ``True``, the function will be shown in the terminal output as ``xfailed`` if it fails, but if it
|
||||
@@ -258,6 +260,8 @@ Marks a test function as *expected to fail*.
|
||||
that are always failing and there should be a clear indication if they unexpectedly start to pass (for example
|
||||
a new release of a library fixes a known bug).
|
||||
|
||||
Defaults to :confval:`xfail_strict`, which is ``False`` by default.
|
||||
|
||||
|
||||
Custom marks
|
||||
~~~~~~~~~~~~
|
||||
@@ -783,18 +787,66 @@ reporting or interaction with exceptions:
|
||||
.. autofunction:: pytest_leave_pdb
|
||||
|
||||
|
||||
Objects
|
||||
-------
|
||||
Collection tree objects
|
||||
-----------------------
|
||||
|
||||
Full reference to objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`.
|
||||
These are the collector and item classes (collectively called "nodes") which
|
||||
make up the collection tree.
|
||||
|
||||
Node
|
||||
~~~~
|
||||
|
||||
CallInfo
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.CallInfo()
|
||||
.. autoclass:: _pytest.nodes.Node()
|
||||
:members:
|
||||
|
||||
Collector
|
||||
~~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Collector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Item
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.Item()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
File
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.File()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FSCollector
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.FSCollector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Session
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Session()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Package
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Package()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Module
|
||||
~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Module()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Class
|
||||
~~~~~
|
||||
@@ -803,13 +855,34 @@ Class
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Collector
|
||||
~~~~~~~~~
|
||||
Function
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Collector()
|
||||
.. autoclass:: pytest.Function()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FunctionDefinition
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.python.FunctionDefinition()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
Objects
|
||||
-------
|
||||
|
||||
Objects accessible from :ref:`fixtures <fixture>` or :ref:`hooks <hook-reference>`
|
||||
or importable from ``pytest``.
|
||||
|
||||
|
||||
CallInfo
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.CallInfo()
|
||||
:members:
|
||||
|
||||
CollectReport
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
@@ -837,46 +910,11 @@ ExitCode
|
||||
.. autoclass:: pytest.ExitCode
|
||||
:members:
|
||||
|
||||
File
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.File()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
|
||||
FixtureDef
|
||||
~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.fixtures.FixtureDef()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FSCollector
|
||||
~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.FSCollector()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Function
|
||||
~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Function()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
FunctionDefinition
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: _pytest.python.FunctionDefinition()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Item
|
||||
~~~~
|
||||
|
||||
.. autoclass:: pytest.Item()
|
||||
.. autoclass:: pytest.FixtureDef()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
@@ -907,19 +945,6 @@ Metafunc
|
||||
.. autoclass:: pytest.Metafunc()
|
||||
:members:
|
||||
|
||||
Module
|
||||
~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Module()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
Node
|
||||
~~~~
|
||||
|
||||
.. autoclass:: _pytest.nodes.Node()
|
||||
:members:
|
||||
|
||||
Parser
|
||||
~~~~~~
|
||||
|
||||
@@ -941,13 +966,6 @@ PytestPluginManager
|
||||
:inherited-members:
|
||||
:show-inheritance:
|
||||
|
||||
Session
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.Session()
|
||||
:members:
|
||||
:show-inheritance:
|
||||
|
||||
TestReport
|
||||
~~~~~~~~~~
|
||||
|
||||
@@ -956,10 +974,16 @@ TestReport
|
||||
:show-inheritance:
|
||||
:inherited-members:
|
||||
|
||||
_Result
|
||||
TestShortLogReport
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: pytest.TestShortLogReport()
|
||||
:members:
|
||||
|
||||
Result
|
||||
~~~~~~~
|
||||
|
||||
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`_Result in the pluggy documentation <pluggy._callers._Result>` for more information.
|
||||
Result object used within :ref:`hook wrappers <hookwrapper>`, see :py:class:`Result in the pluggy documentation <pluggy.Result>` for more information.
|
||||
|
||||
Stash
|
||||
~~~~~
|
||||
@@ -1049,11 +1073,11 @@ Environment variables that can be used to change pytest's behavior.
|
||||
|
||||
.. envvar:: CI
|
||||
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to ``BUILD_NUMBER`` variable.
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to ``BUILD_NUMBER`` variable.
|
||||
|
||||
.. envvar:: BUILD_NUMBER
|
||||
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alterative to CI variable.
|
||||
When set (regardless of value), pytest acknowledges that is running in a CI process. Alternative to CI variable.
|
||||
|
||||
.. envvar:: PYTEST_ADDOPTS
|
||||
|
||||
@@ -1147,6 +1171,9 @@ Custom warnings generated in some situations such as improper usage or deprecate
|
||||
.. autoclass:: pytest.PytestRemovedIn8Warning
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: pytest.PytestRemovedIn9Warning
|
||||
:show-inheritance:
|
||||
|
||||
.. autoclass:: pytest.PytestUnhandledCoroutineWarning
|
||||
:show-inheritance:
|
||||
|
||||
@@ -1613,11 +1640,11 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
Additionally, ``pytest`` will attempt to intelligently identify and ignore a
|
||||
virtualenv by the presence of an activation script. Any directory deemed to
|
||||
be the root of a virtual environment will not be considered during test
|
||||
collection unless ``‑‑collect‑in‑virtualenv`` is given. Note also that
|
||||
``norecursedirs`` takes precedence over ``‑‑collect‑in‑virtualenv``; e.g. if
|
||||
collection unless ``--collect-in-virtualenv`` is given. Note also that
|
||||
``norecursedirs`` takes precedence over ``--collect-in-virtualenv``; e.g. if
|
||||
you intend to run tests in a virtualenv with a base directory that matches
|
||||
``'.*'`` you *must* override ``norecursedirs`` in addition to using the
|
||||
``‑‑collect‑in‑virtualenv`` flag.
|
||||
``--collect-in-virtualenv`` flag.
|
||||
|
||||
|
||||
.. confval:: python_classes
|
||||
@@ -1697,6 +1724,11 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
pythonpath = src1 src2
|
||||
|
||||
.. note::
|
||||
|
||||
``pythonpath`` does not affect some imports that happen very early,
|
||||
most notably plugins loaded using the ``-p`` command line option.
|
||||
|
||||
|
||||
.. confval:: required_plugins
|
||||
|
||||
@@ -1713,13 +1745,12 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
.. confval:: testpaths
|
||||
|
||||
|
||||
|
||||
Sets list of directories that should be searched for tests when
|
||||
no specific directories, files or test ids are given in the command line when
|
||||
executing pytest from the :ref:`rootdir <rootdir>` directory.
|
||||
File system paths may use shell-style wildcards, including the recursive
|
||||
``**`` pattern.
|
||||
|
||||
Useful when all project tests are in a known location to speed up
|
||||
test collection and to avoid picking up undesired tests by accident.
|
||||
|
||||
@@ -1728,8 +1759,17 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
testpaths = testing doc
|
||||
|
||||
This tells pytest to only look for tests in ``testing`` and ``doc``
|
||||
directories when executing from the root directory.
|
||||
This configuration means that executing:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pytest
|
||||
|
||||
has the same practical effects as executing:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
pytest testing doc
|
||||
|
||||
|
||||
.. confval:: tmp_path_retention_count
|
||||
@@ -1744,7 +1784,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
tmp_path_retention_count = 3
|
||||
|
||||
Default: 3
|
||||
Default: ``3``
|
||||
|
||||
|
||||
.. confval:: tmp_path_retention_policy
|
||||
@@ -1763,7 +1803,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
[pytest]
|
||||
tmp_path_retention_policy = "all"
|
||||
|
||||
Default: all
|
||||
Default: ``all``
|
||||
|
||||
|
||||
.. confval:: usefixtures
|
||||
@@ -1852,8 +1892,12 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
tests. Optional argument: glob (default: '*').
|
||||
--cache-clear Remove all cache contents at start of test run
|
||||
--lfnf={all,none}, --last-failed-no-failures={all,none}
|
||||
Which tests to run with no previously (known)
|
||||
failures
|
||||
With ``--lf``, determines whether to execute tests
|
||||
when there are no previously (known) failures or
|
||||
when no cached ``lastfailed`` data was found.
|
||||
``all`` (the default) runs the full test suite
|
||||
again. ``none`` just emits a message about no known
|
||||
failures and exits successfully.
|
||||
--sw, --stepwise Exit on test failure and continue from last failing
|
||||
test next time
|
||||
--sw-skip, --stepwise-skip
|
||||
@@ -1904,8 +1948,9 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
--strict-markers Markers not registered in the `markers` section of
|
||||
the configuration file raise errors
|
||||
--strict (Deprecated) alias to --strict-markers
|
||||
-c file Load configuration from `file` instead of trying to
|
||||
locate one of the implicit configuration files
|
||||
-c FILE, --config-file=FILE
|
||||
Load configuration from `FILE` instead of trying to
|
||||
locate one of the implicit configuration files.
|
||||
--continue-on-collection-errors
|
||||
Force test execution even if collection errors occur
|
||||
--rootdir=ROOTDIR Define root directory for tests. Can be relative
|
||||
@@ -1996,7 +2041,7 @@ All the command-line flags can be obtained by running ``pytest --help``::
|
||||
Auto-indent multiline messages passed to the logging
|
||||
module. Accepts true|on, false|off or an integer.
|
||||
--log-disable=LOGGER_DISABLE
|
||||
Disable a logger by name. Can be passed multipe
|
||||
Disable a logger by name. Can be passed multiple
|
||||
times.
|
||||
|
||||
[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg|pyproject.toml file found:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
pallets-sphinx-themes
|
||||
pluggy>=1.0
|
||||
pluggy>=1.2.0
|
||||
pygments-pytest>=2.3.0
|
||||
sphinx-removed-in>=0.2.0
|
||||
sphinx>=5,<6
|
||||
|
||||
@@ -17,7 +17,12 @@ python_classes = ["Test", "Acceptance"]
|
||||
python_functions = ["test"]
|
||||
# NOTE: "doc" is not included here, but gets tested explicitly via "doctesting".
|
||||
testpaths = ["testing"]
|
||||
norecursedirs = ["testing/example_scripts"]
|
||||
norecursedirs = [
|
||||
"testing/example_scripts",
|
||||
".*",
|
||||
"build",
|
||||
"dist",
|
||||
]
|
||||
xfail_strict = true
|
||||
filterwarnings = [
|
||||
"error",
|
||||
@@ -113,7 +118,7 @@ template = "changelog/_template.rst"
|
||||
showcontent = true
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py37']
|
||||
target-version = ['py38']
|
||||
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
[tool.check-wheel-contents]
|
||||
|
||||
@@ -31,10 +31,16 @@ class InvalidFeatureRelease(Exception):
|
||||
SLUG = "pytest-dev/pytest"
|
||||
|
||||
PR_BODY = """\
|
||||
Created automatically from manual trigger.
|
||||
Created by the [prepare release pr](https://github.com/pytest-dev/pytest/actions/workflows/prepare-release-pr.yml)
|
||||
workflow.
|
||||
|
||||
Once all builds pass and it has been **approved** by one or more maintainers, the build
|
||||
can be released by pushing a tag `{version}` to this repository.
|
||||
Once all builds pass and it has been **approved** by one or more maintainers,
|
||||
start the [deploy](https://github.com/pytest-dev/pytest/actions/workflows/deploy.yml) workflow, using these parameters:
|
||||
|
||||
* `Use workflow from`: `release-{version}`.
|
||||
* `Release version`: `{version}`.
|
||||
|
||||
After the `deploy` workflow has been approved by a core maintainer, the package will be uploaded to PyPI automatically.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ def main():
|
||||
Platform agnostic wrapper script for towncrier.
|
||||
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs.
|
||||
"""
|
||||
with open("doc/en/_changelog_towncrier_draft.rst", "w") as draft_file:
|
||||
with open(
|
||||
"doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8"
|
||||
) as draft_file:
|
||||
return call(("towncrier", "--draft"), stdout=draft_file)
|
||||
|
||||
|
||||
|
||||
@@ -5,19 +5,40 @@ from textwrap import dedent
|
||||
from textwrap import indent
|
||||
|
||||
import packaging.version
|
||||
import requests
|
||||
import platformdirs
|
||||
import tabulate
|
||||
import wcwidth
|
||||
from requests_cache import CachedResponse
|
||||
from requests_cache import CachedSession
|
||||
from requests_cache import OriginalResponse
|
||||
from requests_cache import SQLiteCache
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
FILE_HEAD = r"""
|
||||
.. Note this file is autogenerated by scripts/update-plugin-list.py - usually weekly via github action
|
||||
|
||||
.. _plugin-list:
|
||||
|
||||
Plugin List
|
||||
===========
|
||||
Pytest Plugin List
|
||||
==================
|
||||
|
||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||
Packages classified as inactive are excluded.
|
||||
|
||||
For detailed insights into how this list is generated,
|
||||
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||
|
||||
.. warning::
|
||||
|
||||
Please be aware that this list is not a curated collection of projects
|
||||
and does not undergo a systematic review process.
|
||||
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
|
||||
|
||||
Do not presume any endorsement from the ``pytest`` project or its developers,
|
||||
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
|
||||
|
||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||
automatically. Packages classified as inactive are excluded.
|
||||
|
||||
.. The following conditional uses a different format for this list when
|
||||
creating a PDF, because otherwise the table gets far too wide for the
|
||||
@@ -33,6 +54,11 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
||||
"Development Status :: 6 - Mature",
|
||||
"Development Status :: 7 - Inactive",
|
||||
)
|
||||
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||
"logassert",
|
||||
"nuts",
|
||||
"flask_fixture",
|
||||
}
|
||||
|
||||
|
||||
def escape_rst(text: str) -> str:
|
||||
@@ -48,22 +74,50 @@ def escape_rst(text: str) -> str:
|
||||
return text
|
||||
|
||||
|
||||
def iter_plugins():
|
||||
regex = r">([\d\w-]*)</a>"
|
||||
response = requests.get("https://pypi.org/simple")
|
||||
def project_response_with_refresh(
|
||||
session: CachedSession, name: str, last_serial: int
|
||||
) -> OriginalResponse | CachedResponse:
|
||||
"""Get a http cached pypi project
|
||||
|
||||
matches = list(
|
||||
match
|
||||
for match in re.finditer(regex, response.text)
|
||||
if match.groups()[0].startswith("pytest-")
|
||||
force refresh in case of last serial mismatch
|
||||
"""
|
||||
|
||||
response = session.get(f"https://pypi.org/pypi/{name}/json")
|
||||
if int(response.headers.get("X-PyPI-Last-Serial", -1)) != last_serial:
|
||||
response = session.get(f"https://pypi.org/pypi/{name}/json", refresh=True)
|
||||
return response
|
||||
|
||||
|
||||
def get_session() -> CachedSession:
|
||||
"""Configures the requests-cache session"""
|
||||
cache_path = platformdirs.user_cache_path("pytest-plugin-list")
|
||||
cache_path.mkdir(exist_ok=True, parents=True)
|
||||
cache_file = cache_path.joinpath("http_cache.sqlite3")
|
||||
return CachedSession(backend=SQLiteCache(cache_file))
|
||||
|
||||
|
||||
def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
|
||||
response = session.get(
|
||||
"https://pypi.org/simple",
|
||||
headers={"Accept": "application/vnd.pypi.simple.v1+json"},
|
||||
refresh=True,
|
||||
)
|
||||
return {
|
||||
name: p["_last-serial"]
|
||||
for p in response.json()["projects"]
|
||||
if (name := p["name"]).startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||
}
|
||||
|
||||
for match in tqdm(matches, smoothing=0):
|
||||
name = match.groups()[0]
|
||||
response = requests.get(f"https://pypi.org/pypi/{name}/json")
|
||||
|
||||
def iter_plugins():
|
||||
session = get_session()
|
||||
name_2_serial = pytest_plugin_projects_from_pypi(session)
|
||||
|
||||
for name, last_serial in tqdm(name_2_serial.items(), smoothing=0):
|
||||
response = project_response_with_refresh(session, name, last_serial)
|
||||
if response.status_code == 404:
|
||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
|
||||
# return 404 on the JSON API. Skip.
|
||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
|
||||
# but return 404 on the JSON API. Skip.
|
||||
continue
|
||||
response.raise_for_status()
|
||||
info = response.json()["info"]
|
||||
@@ -131,7 +185,7 @@ def plugin_definitions(plugins):
|
||||
|
||||
|
||||
def main():
|
||||
plugins = list(iter_plugins())
|
||||
plugins = [*iter_plugins()]
|
||||
|
||||
reference_dir = pathlib.Path("doc", "en", "reference")
|
||||
|
||||
|
||||
10
setup.cfg
10
setup.cfg
@@ -6,7 +6,7 @@ long_description_content_type = text/x-rst
|
||||
url = https://docs.pytest.org/en/latest/
|
||||
author = Holger Krekel, Bruno Oliveira, Ronny Pfannschmidt, Floris Bruynooghe, Brianna Laugher, Florian Bruhin and others
|
||||
license = MIT
|
||||
license_file = LICENSE
|
||||
license_files = LICENSE
|
||||
platforms = unix, linux, osx, cygwin, win32
|
||||
classifiers =
|
||||
Development Status :: 6 - Mature
|
||||
@@ -17,11 +17,11 @@ classifiers =
|
||||
Operating System :: POSIX
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3 :: Only
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Software Development :: Testing
|
||||
Topic :: Utilities
|
||||
@@ -46,12 +46,11 @@ py_modules = py
|
||||
install_requires =
|
||||
iniconfig
|
||||
packaging
|
||||
pluggy>=0.12,<2.0
|
||||
pluggy>=1.3.0,<2.0
|
||||
colorama;sys_platform=="win32"
|
||||
exceptiongroup>=1.0.0rc8;python_version<"3.11"
|
||||
importlib-metadata>=0.12;python_version<"3.8"
|
||||
tomli>=1.0.0;python_version<"3.11"
|
||||
python_requires = >=3.7
|
||||
python_requires = >=3.8
|
||||
package_dir =
|
||||
=src
|
||||
setup_requires =
|
||||
@@ -73,6 +72,7 @@ testing =
|
||||
nose
|
||||
pygments>=2.7.2
|
||||
requests
|
||||
setuptools
|
||||
xmlschema
|
||||
|
||||
[options.package_data]
|
||||
|
||||
@@ -17,21 +17,23 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import ClassVar
|
||||
from typing import Dict
|
||||
from typing import Final
|
||||
from typing import final
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
from typing import Literal
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Pattern
|
||||
from typing import Sequence
|
||||
from typing import Set
|
||||
from typing import SupportsIndex
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
from weakref import ref
|
||||
|
||||
import pluggy
|
||||
|
||||
@@ -43,22 +45,16 @@ from _pytest._code.source import Source
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest._io.saferepr import safeformat
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import get_real_func
|
||||
from _pytest.deprecated import check_ispytest
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
from typing_extensions import SupportsIndex
|
||||
from weakref import ReferenceType
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||
|
||||
|
||||
class Code:
|
||||
"""Wrapper around Python code objects."""
|
||||
@@ -194,25 +190,25 @@ class Frame:
|
||||
class TracebackEntry:
|
||||
"""A single entry in a Traceback."""
|
||||
|
||||
__slots__ = ("_rawentry", "_excinfo", "_repr_style")
|
||||
__slots__ = ("_rawentry", "_repr_style")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rawentry: TracebackType,
|
||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
||||
repr_style: Optional['Literal["short", "long"]'] = None,
|
||||
) -> None:
|
||||
self._rawentry = rawentry
|
||||
self._excinfo = excinfo
|
||||
self._repr_style: Optional['Literal["short", "long"]'] = None
|
||||
self._rawentry: "Final" = rawentry
|
||||
self._repr_style: "Final" = repr_style
|
||||
|
||||
def with_repr_style(
|
||||
self, repr_style: Optional['Literal["short", "long"]']
|
||||
) -> "TracebackEntry":
|
||||
return TracebackEntry(self._rawentry, repr_style)
|
||||
|
||||
@property
|
||||
def lineno(self) -> int:
|
||||
return self._rawentry.tb_lineno - 1
|
||||
|
||||
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
|
||||
assert mode in ("short", "long")
|
||||
self._repr_style = mode
|
||||
|
||||
@property
|
||||
def frame(self) -> Frame:
|
||||
return Frame(self._rawentry.tb_frame)
|
||||
@@ -272,7 +268,7 @@ class TracebackEntry:
|
||||
|
||||
source = property(getsource)
|
||||
|
||||
def ishidden(self) -> bool:
|
||||
def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
|
||||
"""Return True if the current frame has a var __tracebackhide__
|
||||
resolving to True.
|
||||
|
||||
@@ -296,7 +292,7 @@ class TracebackEntry:
|
||||
else:
|
||||
break
|
||||
if tbh and callable(tbh):
|
||||
return tbh(None if self._excinfo is None else self._excinfo())
|
||||
return tbh(excinfo)
|
||||
return tbh
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -329,16 +325,14 @@ class Traceback(List[TracebackEntry]):
|
||||
def __init__(
|
||||
self,
|
||||
tb: Union[TracebackType, Iterable[TracebackEntry]],
|
||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
||||
) -> None:
|
||||
"""Initialize from given python traceback object and ExceptionInfo."""
|
||||
self._excinfo = excinfo
|
||||
if isinstance(tb, TracebackType):
|
||||
|
||||
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
|
||||
cur_: Optional[TracebackType] = cur
|
||||
while cur_ is not None:
|
||||
yield TracebackEntry(cur_, excinfo=excinfo)
|
||||
yield TracebackEntry(cur_)
|
||||
cur_ = cur_.tb_next
|
||||
|
||||
super().__init__(f(tb))
|
||||
@@ -378,7 +372,7 @@ class Traceback(List[TracebackEntry]):
|
||||
continue
|
||||
if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
|
||||
continue
|
||||
return Traceback(x._rawentry, self._excinfo)
|
||||
return Traceback(x._rawentry)
|
||||
return self
|
||||
|
||||
@overload
|
||||
@@ -398,26 +392,27 @@ class Traceback(List[TracebackEntry]):
|
||||
return super().__getitem__(key)
|
||||
|
||||
def filter(
|
||||
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
|
||||
self,
|
||||
excinfo_or_fn: Union[
|
||||
"ExceptionInfo[BaseException]",
|
||||
Callable[[TracebackEntry], bool],
|
||||
],
|
||||
/,
|
||||
) -> "Traceback":
|
||||
"""Return a Traceback instance with certain items removed
|
||||
"""Return a Traceback instance with certain items removed.
|
||||
|
||||
fn is a function that gets a single argument, a TracebackEntry
|
||||
instance, and should return True when the item should be added
|
||||
to the Traceback, False when not.
|
||||
If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
|
||||
which are hidden (see ishidden() above).
|
||||
|
||||
By default this removes all the TracebackEntries which are hidden
|
||||
(see ishidden() above).
|
||||
Otherwise, the filter is a function that gets a single argument, a
|
||||
``TracebackEntry`` instance, and should return True when the item should
|
||||
be added to the ``Traceback``, False when not.
|
||||
"""
|
||||
return Traceback(filter(fn, self), self._excinfo)
|
||||
|
||||
def getcrashentry(self) -> TracebackEntry:
|
||||
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
|
||||
for i in range(-1, -len(self) - 1, -1):
|
||||
entry = self[i]
|
||||
if not entry.ishidden():
|
||||
return entry
|
||||
return self[-1]
|
||||
if isinstance(excinfo_or_fn, ExceptionInfo):
|
||||
fn = lambda x: not x.ishidden(excinfo_or_fn) # noqa: E731
|
||||
else:
|
||||
fn = excinfo_or_fn
|
||||
return Traceback(filter(fn, self))
|
||||
|
||||
def recursionindex(self) -> Optional[int]:
|
||||
"""Return the index of the frame/TracebackEntry where recursion originates if
|
||||
@@ -469,22 +464,41 @@ class ExceptionInfo(Generic[E]):
|
||||
self._traceback = traceback
|
||||
|
||||
@classmethod
|
||||
def from_exc_info(
|
||||
def from_exception(
|
||||
cls,
|
||||
exc_info: Tuple[Type[E], E, TracebackType],
|
||||
# Ignoring error: "Cannot use a covariant type variable as a parameter".
|
||||
# This is OK to ignore because this class is (conceptually) readonly.
|
||||
# See https://github.com/python/mypy/issues/7049.
|
||||
exception: E, # type: ignore[misc]
|
||||
exprinfo: Optional[str] = None,
|
||||
) -> "ExceptionInfo[E]":
|
||||
"""Return an ExceptionInfo for an existing exc_info tuple.
|
||||
"""Return an ExceptionInfo for an existing exception.
|
||||
|
||||
.. warning::
|
||||
|
||||
Experimental API
|
||||
The exception must have a non-``None`` ``__traceback__`` attribute,
|
||||
otherwise this function fails with an assertion error. This means that
|
||||
the exception must have been raised, or added a traceback with the
|
||||
:py:meth:`~BaseException.with_traceback()` method.
|
||||
|
||||
:param exprinfo:
|
||||
A text string helping to determine if we should strip
|
||||
``AssertionError`` from the output. Defaults to the exception
|
||||
message/``__str__()``.
|
||||
|
||||
.. versionadded:: 7.4
|
||||
"""
|
||||
assert (
|
||||
exception.__traceback__
|
||||
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
|
||||
exc_info = (type(exception), exception, exception.__traceback__)
|
||||
return cls.from_exc_info(exc_info, exprinfo)
|
||||
|
||||
@classmethod
|
||||
def from_exc_info(
|
||||
cls,
|
||||
exc_info: Tuple[Type[E], E, TracebackType],
|
||||
exprinfo: Optional[str] = None,
|
||||
) -> "ExceptionInfo[E]":
|
||||
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
|
||||
_striptext = ""
|
||||
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
||||
exprinfo = getattr(exc_info[1], "msg", None)
|
||||
@@ -563,7 +577,7 @@ class ExceptionInfo(Generic[E]):
|
||||
def traceback(self) -> Traceback:
|
||||
"""The traceback."""
|
||||
if self._traceback is None:
|
||||
self._traceback = Traceback(self.tb, excinfo=ref(self))
|
||||
self._traceback = Traceback(self.tb)
|
||||
return self._traceback
|
||||
|
||||
@traceback.setter
|
||||
@@ -602,18 +616,25 @@ class ExceptionInfo(Generic[E]):
|
||||
"""
|
||||
return isinstance(self.value, exc)
|
||||
|
||||
def _getreprcrash(self) -> "ReprFileLocation":
|
||||
exconly = self.exconly(tryshort=True)
|
||||
entry = self.traceback.getcrashentry()
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
||||
# Find last non-hidden traceback entry that led to the exception of the
|
||||
# traceback, or None if all hidden.
|
||||
for i in range(-1, -len(self.traceback) - 1, -1):
|
||||
entry = self.traceback[i]
|
||||
if not entry.ishidden(self):
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
exconly = self.exconly(tryshort=True)
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
return None
|
||||
|
||||
def getrepr(
|
||||
self,
|
||||
showlocals: bool = False,
|
||||
style: "_TracebackStyle" = "long",
|
||||
style: _TracebackStyle = "long",
|
||||
abspath: bool = False,
|
||||
tbfilter: bool = True,
|
||||
tbfilter: Union[
|
||||
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
||||
] = True,
|
||||
funcargs: bool = False,
|
||||
truncate_locals: bool = True,
|
||||
chain: bool = True,
|
||||
@@ -625,14 +646,20 @@ class ExceptionInfo(Generic[E]):
|
||||
Ignored if ``style=="native"``.
|
||||
|
||||
:param str style:
|
||||
long|short|no|native|value traceback style.
|
||||
long|short|line|no|native|value traceback style.
|
||||
|
||||
:param bool abspath:
|
||||
If paths should be changed to absolute or left unchanged.
|
||||
|
||||
:param bool tbfilter:
|
||||
Hide entries that contain a local variable ``__tracebackhide__==True``.
|
||||
Ignored if ``style=="native"``.
|
||||
:param tbfilter:
|
||||
A filter for traceback entries.
|
||||
|
||||
* If false, don't hide any entries.
|
||||
* If true, hide internal entries and entries that contain a local
|
||||
variable ``__tracebackhide__ = True``.
|
||||
* If a callable, delegates the filtering to the callable.
|
||||
|
||||
Ignored if ``style`` is ``"native"``.
|
||||
|
||||
:param bool funcargs:
|
||||
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
|
||||
@@ -651,7 +678,9 @@ class ExceptionInfo(Generic[E]):
|
||||
return ReprExceptionInfo(
|
||||
reprtraceback=ReprTracebackNative(
|
||||
traceback.format_exception(
|
||||
self.type, self.value, self.traceback[0]._rawentry
|
||||
self.type,
|
||||
self.value,
|
||||
self.traceback[0]._rawentry if self.traceback else None,
|
||||
)
|
||||
),
|
||||
reprcrash=self._getreprcrash(),
|
||||
@@ -668,6 +697,14 @@ class ExceptionInfo(Generic[E]):
|
||||
)
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
def _stringify_exception(self, exc: BaseException) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
str(exc),
|
||||
*getattr(exc, "__notes__", []),
|
||||
]
|
||||
)
|
||||
|
||||
def match(self, regexp: Union[str, Pattern[str]]) -> "Literal[True]":
|
||||
"""Check whether the regular expression `regexp` matches the string
|
||||
representation of the exception using :func:`python:re.search`.
|
||||
@@ -675,7 +712,7 @@ class ExceptionInfo(Generic[E]):
|
||||
If it matches `True` is returned, otherwise an `AssertionError` is raised.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
value = str(self.value)
|
||||
value = self._stringify_exception(self.value)
|
||||
msg = f"Regex pattern did not match.\n Regex: {regexp!r}\n Input: {value!r}"
|
||||
if regexp == value:
|
||||
msg += "\n Did you mean to `re.escape()` the regex?"
|
||||
@@ -683,6 +720,69 @@ class ExceptionInfo(Generic[E]):
|
||||
# Return True to allow for "assert excinfo.match()".
|
||||
return True
|
||||
|
||||
def _group_contains(
|
||||
self,
|
||||
exc_group: BaseExceptionGroup[BaseException],
|
||||
expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]],
|
||||
match: Union[str, Pattern[str], None],
|
||||
target_depth: Optional[int] = None,
|
||||
current_depth: int = 1,
|
||||
) -> bool:
|
||||
"""Return `True` if a `BaseExceptionGroup` contains a matching exception."""
|
||||
if (target_depth is not None) and (current_depth > target_depth):
|
||||
# already descended past the target depth
|
||||
return False
|
||||
for exc in exc_group.exceptions:
|
||||
if isinstance(exc, BaseExceptionGroup):
|
||||
if self._group_contains(
|
||||
exc, expected_exception, match, target_depth, current_depth + 1
|
||||
):
|
||||
return True
|
||||
if (target_depth is not None) and (current_depth != target_depth):
|
||||
# not at the target depth, no match
|
||||
continue
|
||||
if not isinstance(exc, expected_exception):
|
||||
continue
|
||||
if match is not None:
|
||||
value = self._stringify_exception(exc)
|
||||
if not re.search(match, value):
|
||||
continue
|
||||
return True
|
||||
return False
|
||||
|
||||
def group_contains(
|
||||
self,
|
||||
expected_exception: Union[Type[BaseException], Tuple[Type[BaseException], ...]],
|
||||
*,
|
||||
match: Union[str, Pattern[str], None] = None,
|
||||
depth: Optional[int] = None,
|
||||
) -> bool:
|
||||
"""Check whether a captured exception group contains a matching exception.
|
||||
|
||||
:param Type[BaseException] | Tuple[Type[BaseException]] expected_exception:
|
||||
The expected exception type, or a tuple if one of multiple possible
|
||||
exception types are expected.
|
||||
|
||||
:param str | Pattern[str] | None match:
|
||||
If specified, a string containing a regular expression,
|
||||
or a regular expression object, that is tested against the string
|
||||
representation of the exception and its `PEP-678 <https://peps.python.org/pep-0678/>` `__notes__`
|
||||
using :func:`re.search`.
|
||||
|
||||
To match a literal string that may contain :ref:`special characters
|
||||
<re-syntax>`, the pattern can first be escaped with :func:`re.escape`.
|
||||
|
||||
:param Optional[int] depth:
|
||||
If `None`, will search for a matching exception at any nesting depth.
|
||||
If >= 1, will only match an exception if it's at the specified depth (depth = 1 being
|
||||
the exceptions contained within the topmost exception group).
|
||||
"""
|
||||
msg = "Captured exception is not an instance of `BaseExceptionGroup`"
|
||||
assert isinstance(self.value, BaseExceptionGroup), msg
|
||||
msg = "`depth` must be >= 1 if specified"
|
||||
assert (depth is None) or (depth >= 1), msg
|
||||
return self._group_contains(self.value, expected_exception, match, depth)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class FormattedExcinfo:
|
||||
@@ -693,9 +793,9 @@ class FormattedExcinfo:
|
||||
fail_marker: ClassVar = "E"
|
||||
|
||||
showlocals: bool = False
|
||||
style: "_TracebackStyle" = "long"
|
||||
style: _TracebackStyle = "long"
|
||||
abspath: bool = True
|
||||
tbfilter: bool = True
|
||||
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
||||
funcargs: bool = False
|
||||
truncate_locals: bool = True
|
||||
chain: bool = True
|
||||
@@ -807,12 +907,16 @@ class FormattedExcinfo:
|
||||
|
||||
def repr_traceback_entry(
|
||||
self,
|
||||
entry: TracebackEntry,
|
||||
entry: Optional[TracebackEntry],
|
||||
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
||||
) -> "ReprEntry":
|
||||
lines: List[str] = []
|
||||
style = entry._repr_style if entry._repr_style is not None else self.style
|
||||
if style in ("short", "long"):
|
||||
style = (
|
||||
entry._repr_style
|
||||
if entry is not None and entry._repr_style is not None
|
||||
else self.style
|
||||
)
|
||||
if style in ("short", "long") and entry is not None:
|
||||
source = self._getentrysource(entry)
|
||||
if source is None:
|
||||
source = Source("???")
|
||||
@@ -853,25 +957,31 @@ class FormattedExcinfo:
|
||||
|
||||
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
|
||||
traceback = excinfo.traceback
|
||||
if self.tbfilter:
|
||||
traceback = traceback.filter()
|
||||
if callable(self.tbfilter):
|
||||
traceback = self.tbfilter(excinfo)
|
||||
elif self.tbfilter:
|
||||
traceback = traceback.filter(excinfo)
|
||||
|
||||
if isinstance(excinfo.value, RecursionError):
|
||||
traceback, extraline = self._truncate_recursive_traceback(traceback)
|
||||
else:
|
||||
extraline = None
|
||||
|
||||
if not traceback:
|
||||
if extraline is None:
|
||||
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
|
||||
entries = [self.repr_traceback_entry(None, excinfo)]
|
||||
return ReprTraceback(entries, extraline, style=self.style)
|
||||
|
||||
last = traceback[-1]
|
||||
entries = []
|
||||
if self.style == "value":
|
||||
reprentry = self.repr_traceback_entry(last, excinfo)
|
||||
entries.append(reprentry)
|
||||
entries = [self.repr_traceback_entry(last, excinfo)]
|
||||
return ReprTraceback(entries, None, style=self.style)
|
||||
|
||||
for index, entry in enumerate(traceback):
|
||||
einfo = (last == entry) and excinfo or None
|
||||
reprentry = self.repr_traceback_entry(entry, einfo)
|
||||
entries.append(reprentry)
|
||||
entries = [
|
||||
self.repr_traceback_entry(entry, excinfo if last == entry else None)
|
||||
for entry in traceback
|
||||
]
|
||||
return ReprTraceback(entries, extraline, style=self.style)
|
||||
|
||||
def _truncate_recursive_traceback(
|
||||
@@ -928,6 +1038,7 @@ class FormattedExcinfo:
|
||||
seen: Set[int] = set()
|
||||
while e is not None and id(e) not in seen:
|
||||
seen.add(id(e))
|
||||
|
||||
if excinfo_:
|
||||
# Fall back to native traceback as a temporary workaround until
|
||||
# full support for exception groups added to ExceptionInfo.
|
||||
@@ -944,9 +1055,7 @@ class FormattedExcinfo:
|
||||
)
|
||||
else:
|
||||
reprtraceback = self.repr_traceback(excinfo_)
|
||||
reprcrash: Optional[ReprFileLocation] = (
|
||||
excinfo_._getreprcrash() if self.style != "value" else None
|
||||
)
|
||||
reprcrash = excinfo_._getreprcrash()
|
||||
else:
|
||||
# Fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work.
|
||||
@@ -954,25 +1063,17 @@ class FormattedExcinfo:
|
||||
traceback.format_exception(type(e), e, None)
|
||||
)
|
||||
reprcrash = None
|
||||
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
|
||||
if e.__cause__ is not None and self.chain:
|
||||
e = e.__cause__
|
||||
excinfo_ = (
|
||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
||||
if e.__traceback__
|
||||
else None
|
||||
)
|
||||
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||
descr = "The above exception was the direct cause of the following exception:"
|
||||
elif (
|
||||
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
||||
):
|
||||
e = e.__context__
|
||||
excinfo_ = (
|
||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
||||
if e.__traceback__
|
||||
else None
|
||||
)
|
||||
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||
descr = "During handling of the above exception, another exception occurred:"
|
||||
else:
|
||||
e = None
|
||||
@@ -1046,7 +1147,7 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprExceptionInfo(ExceptionRepr):
|
||||
reprtraceback: "ReprTraceback"
|
||||
reprcrash: "ReprFileLocation"
|
||||
reprcrash: Optional["ReprFileLocation"]
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
self.reprtraceback.toterminal(tw)
|
||||
@@ -1057,7 +1158,7 @@ class ReprExceptionInfo(ExceptionRepr):
|
||||
class ReprTraceback(TerminalRepr):
|
||||
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
|
||||
extraline: Optional[str]
|
||||
style: "_TracebackStyle"
|
||||
style: _TracebackStyle
|
||||
|
||||
entrysep: ClassVar = "_ "
|
||||
|
||||
@@ -1091,7 +1192,7 @@ class ReprTracebackNative(ReprTraceback):
|
||||
class ReprEntryNative(TerminalRepr):
|
||||
lines: Sequence[str]
|
||||
|
||||
style: ClassVar["_TracebackStyle"] = "native"
|
||||
style: ClassVar[_TracebackStyle] = "native"
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
tw.write("".join(self.lines))
|
||||
@@ -1103,7 +1204,7 @@ class ReprEntry(TerminalRepr):
|
||||
reprfuncargs: Optional["ReprFuncArgs"]
|
||||
reprlocals: Optional["ReprLocals"]
|
||||
reprfileloc: Optional["ReprFileLocation"]
|
||||
style: "_TracebackStyle"
|
||||
style: _TracebackStyle
|
||||
|
||||
def _write_entry_lines(self, tw: TerminalWriter) -> None:
|
||||
"""Write the source code portions of a list of traceback entries with syntax highlighting.
|
||||
@@ -1151,8 +1252,8 @@ class ReprEntry(TerminalRepr):
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
if self.style == "short":
|
||||
assert self.reprfileloc is not None
|
||||
self.reprfileloc.toterminal(tw)
|
||||
if self.reprfileloc:
|
||||
self.reprfileloc.toterminal(tw)
|
||||
self._write_entry_lines(tw)
|
||||
if self.reprlocals:
|
||||
self.reprlocals.toterminal(tw, indent=" " * 8)
|
||||
|
||||
@@ -149,8 +149,7 @@ def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[i
|
||||
values: List[int] = []
|
||||
for x in ast.walk(node):
|
||||
if isinstance(x, (ast.stmt, ast.ExceptHandler)):
|
||||
# Before Python 3.8, the lineno of a decorated class or function pointed at the decorator.
|
||||
# Since Python 3.8, the lineno points to the class/def, so need to include the decorators.
|
||||
# The lineno points to the class/def, so need to include the decorators.
|
||||
if isinstance(x, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
for d in x.decorator_list:
|
||||
values.append(d.lineno - 1)
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
from typing import final
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import TextIO
|
||||
|
||||
from .wcwidth import wcswidth
|
||||
from _pytest.compat import final
|
||||
|
||||
|
||||
# This code was initially copied from py 1.8.1, file _io/terminalwriter.py.
|
||||
|
||||
@@ -25,14 +25,12 @@ from stat import S_ISREG
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Literal
|
||||
from typing import overload
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import error
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Literal
|
||||
|
||||
# Moved from local.py.
|
||||
iswin32 = sys.platform == "win32" or (getattr(os, "_name", False) == "nt")
|
||||
|
||||
@@ -953,7 +951,7 @@ class LocalPath:
|
||||
else:
|
||||
p.dirpath()._ensuredirs()
|
||||
if not p.check(file=1):
|
||||
p.open("w").close()
|
||||
p.open("wb").close()
|
||||
return p
|
||||
|
||||
@overload
|
||||
|
||||
@@ -112,8 +112,8 @@ def pytest_collection(session: "Session") -> None:
|
||||
assertstate.hook.set_session(session)
|
||||
|
||||
|
||||
@hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||
@hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_runtest_protocol(item: Item) -> Generator[None, object, object]:
|
||||
"""Setup the pytest_assertrepr_compare and pytest_assertion_pass hooks.
|
||||
|
||||
The rewrite module will use util._reprcompare if it exists to use custom
|
||||
@@ -162,10 +162,11 @@ def pytest_runtest_protocol(item: Item) -> Generator[None, None, None]:
|
||||
|
||||
util._assertion_pass = call_assertion_pass_hook
|
||||
|
||||
yield
|
||||
|
||||
util._reprcompare, util._assertion_pass = saved_assert_hooks
|
||||
util._config = None
|
||||
try:
|
||||
return (yield)
|
||||
finally:
|
||||
util._reprcompare, util._assertion_pass = saved_assert_hooks
|
||||
util._config = None
|
||||
|
||||
|
||||
def pytest_sessionfinish(session: "Session") -> None:
|
||||
|
||||
@@ -13,6 +13,7 @@ import struct
|
||||
import sys
|
||||
import tokenize
|
||||
import types
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from pathlib import PurePath
|
||||
from typing import Callable
|
||||
@@ -44,10 +45,9 @@ 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
|
||||
|
||||
class Sentinel:
|
||||
pass
|
||||
|
||||
|
||||
assertstate_key = StashKey["AssertionState"]()
|
||||
@@ -57,6 +57,9 @@ PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
|
||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
|
||||
# Special marker that denotes we have just left a scope definition
|
||||
_SCOPE_END_MARKER = Sentinel()
|
||||
|
||||
|
||||
class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader):
|
||||
"""PEP302/PEP451 import hook which rewrites asserts."""
|
||||
@@ -639,6 +642,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
.push_format_context() and .pop_format_context() which allows
|
||||
to build another %-formatted string while already building one.
|
||||
|
||||
:scope: A tuple containing the current scope used for variables_overwrite.
|
||||
|
||||
:variables_overwrite: A dict filled with references to variables
|
||||
that change value within an assert. This happens when a variable is
|
||||
reassigned with the walrus operator
|
||||
@@ -660,7 +665,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
else:
|
||||
self.enable_assertion_pass_hook = False
|
||||
self.source = source
|
||||
self.variables_overwrite: Dict[str, str] = {}
|
||||
self.scope: tuple[ast.AST, ...] = ()
|
||||
self.variables_overwrite: defaultdict[
|
||||
tuple[ast.AST, ...], Dict[str, str]
|
||||
] = defaultdict(dict)
|
||||
|
||||
def run(self, mod: ast.Module) -> None:
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
@@ -680,9 +688,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if (
|
||||
expect_docstring
|
||||
and isinstance(item, ast.Expr)
|
||||
and isinstance(item.value, ast.Str)
|
||||
and isinstance(item.value, ast.Constant)
|
||||
and isinstance(item.value.value, str)
|
||||
):
|
||||
doc = item.value.s
|
||||
doc = item.value.value
|
||||
if self.is_rewrite_disabled(doc):
|
||||
return
|
||||
expect_docstring = False
|
||||
@@ -723,9 +732,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
mod.body[pos:pos] = imports
|
||||
|
||||
# Collect asserts.
|
||||
nodes: List[ast.AST] = [mod]
|
||||
self.scope = (mod,)
|
||||
nodes: List[Union[ast.AST, Sentinel]] = [mod]
|
||||
while nodes:
|
||||
node = nodes.pop()
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
self.scope = tuple((*self.scope, node))
|
||||
nodes.append(_SCOPE_END_MARKER)
|
||||
if node == _SCOPE_END_MARKER:
|
||||
self.scope = self.scope[:-1]
|
||||
continue
|
||||
assert isinstance(node, ast.AST)
|
||||
for name, field in ast.iter_fields(node):
|
||||
if isinstance(field, list):
|
||||
new: List[ast.AST] = []
|
||||
@@ -814,7 +831,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
current = self.stack.pop()
|
||||
if self.stack:
|
||||
self.explanation_specifiers = self.stack[-1]
|
||||
keys = [ast.Str(key) for key in current.keys()]
|
||||
keys = [ast.Constant(key) for key in current.keys()]
|
||||
format_dict = ast.Dict(keys, list(current.values()))
|
||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||
name = "@py_format" + str(next(self.variable_counter))
|
||||
@@ -868,16 +885,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||
|
||||
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
||||
msg = self.pop_format_context(ast.Str(explanation))
|
||||
msg = self.pop_format_context(ast.Constant(explanation))
|
||||
|
||||
# Failed
|
||||
if assert_.msg:
|
||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||
gluestr = "\n>assert "
|
||||
else:
|
||||
assertmsg = ast.Str("")
|
||||
assertmsg = ast.Constant("")
|
||||
gluestr = "assert "
|
||||
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
|
||||
err_explanation = ast.BinOp(ast.Constant(gluestr), ast.Add(), msg)
|
||||
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
fmt = self.helper("_format_explanation", err_msg)
|
||||
@@ -893,8 +910,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
hook_call_pass = ast.Expr(
|
||||
self.helper(
|
||||
"_call_assertion_pass",
|
||||
ast.Num(assert_.lineno),
|
||||
ast.Str(orig),
|
||||
ast.Constant(assert_.lineno),
|
||||
ast.Constant(orig),
|
||||
fmt_pass,
|
||||
)
|
||||
)
|
||||
@@ -913,7 +930,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
variables = [
|
||||
ast.Name(name, ast.Store()) for name in self.format_variables
|
||||
]
|
||||
clear_format = ast.Assign(variables, ast.NameConstant(None))
|
||||
clear_format = ast.Assign(variables, ast.Constant(None))
|
||||
self.statements.append(clear_format)
|
||||
|
||||
else: # Original assertion rewriting
|
||||
@@ -924,9 +941,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||
explanation = "\n>assert " + explanation
|
||||
else:
|
||||
assertmsg = ast.Str("")
|
||||
assertmsg = ast.Constant("")
|
||||
explanation = "assert " + explanation
|
||||
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
|
||||
template = ast.BinOp(assertmsg, ast.Add(), ast.Constant(explanation))
|
||||
msg = self.pop_format_context(template)
|
||||
fmt = self.helper("_format_explanation", msg)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
@@ -938,7 +955,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# Clear temporary variables by setting them to None.
|
||||
if self.variables:
|
||||
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
||||
clear = ast.Assign(variables, ast.NameConstant(None))
|
||||
clear = ast.Assign(variables, ast.Constant(None))
|
||||
self.statements.append(clear)
|
||||
# Fix locations (line numbers/column offsets).
|
||||
for stmt in self.statements:
|
||||
@@ -946,26 +963,26 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
ast.copy_location(node, assert_)
|
||||
return self.statements
|
||||
|
||||
def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
|
||||
def visit_NamedExpr(self, name: ast.NamedExpr) -> Tuple[ast.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])
|
||||
inlocs = ast.Compare(ast.Constant(target_id), [ast.In()], [locs])
|
||||
dorepr = self.helper("_should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
expr = ast.IfExp(test, self.display(name), ast.Str(target_id))
|
||||
expr = ast.IfExp(test, self.display(name), ast.Constant(target_id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
||||
# Display the repr of the name if it's a local variable or
|
||||
# _should_repr_global_name() thinks it's acceptable.
|
||||
locs = ast.Call(self.builtin("locals"), [], [])
|
||||
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
|
||||
inlocs = ast.Compare(ast.Constant(name.id), [ast.In()], [locs])
|
||||
dorepr = self.helper("_should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
|
||||
expr = ast.IfExp(test, self.display(name), ast.Constant(name.id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
||||
@@ -984,10 +1001,10 @@ 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
|
||||
# Check if the left operand is a ast.NamedExpr and the value has already been visited
|
||||
if (
|
||||
isinstance(v, ast.Compare)
|
||||
and isinstance(v.left, namedExpr)
|
||||
and isinstance(v.left, ast.NamedExpr)
|
||||
and v.left.target.id
|
||||
in [
|
||||
ast_expr.id
|
||||
@@ -996,12 +1013,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
]
|
||||
):
|
||||
pytest_temp = self.variable()
|
||||
self.variables_overwrite[v.left.target.id] = pytest_temp
|
||||
self.variables_overwrite[self.scope][
|
||||
v.left.target.id
|
||||
] = v.left # type:ignore[assignment]
|
||||
v.left.target.id = pytest_temp
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||
expl_format = self.pop_format_context(ast.Str(expl))
|
||||
expl_format = self.pop_format_context(ast.Constant(expl))
|
||||
call = ast.Call(app, [expl_format], [])
|
||||
self.expl_stmts.append(ast.Expr(call))
|
||||
if i < levels:
|
||||
@@ -1013,7 +1032,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
self.statements = body = inner
|
||||
self.statements = save
|
||||
self.expl_stmts = fail_save
|
||||
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
|
||||
expl_template = self.helper("_format_boolop", expl_list, ast.Constant(is_or))
|
||||
expl = self.pop_format_context(expl_template)
|
||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||
|
||||
@@ -1037,10 +1056,22 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
new_args = []
|
||||
new_kwargs = []
|
||||
for arg in call.args:
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||
self.scope, {}
|
||||
):
|
||||
arg = self.variables_overwrite[self.scope][
|
||||
arg.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
for keyword in call.keywords:
|
||||
if isinstance(
|
||||
keyword.value, ast.Name
|
||||
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
||||
keyword.value = self.variables_overwrite[self.scope][
|
||||
keyword.value.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
if keyword.arg:
|
||||
@@ -1074,8 +1105,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Compare(self, comp: ast.Compare) -> Tuple[ast.expr, str]:
|
||||
self.push_format_context()
|
||||
# We first check if we have overwritten a variable in the previous assert
|
||||
if isinstance(comp.left, ast.Name) and comp.left.id in self.variables_overwrite:
|
||||
comp.left.id = self.variables_overwrite[comp.left.id]
|
||||
if isinstance(
|
||||
comp.left, ast.Name
|
||||
) and comp.left.id in self.variables_overwrite.get(self.scope, {}):
|
||||
comp.left = self.variables_overwrite[self.scope][
|
||||
comp.left.id
|
||||
] # type:ignore[assignment]
|
||||
if isinstance(comp.left, ast.NamedExpr):
|
||||
self.variables_overwrite[self.scope][
|
||||
comp.left.target.id
|
||||
] = comp.left # type:ignore[assignment]
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||
left_expl = f"({left_expl})"
|
||||
@@ -1088,20 +1127,22 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
results = [left_res]
|
||||
for i, op, next_operand in it:
|
||||
if (
|
||||
isinstance(next_operand, namedExpr)
|
||||
isinstance(next_operand, ast.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
|
||||
self.variables_overwrite[self.scope][
|
||||
left_res.id
|
||||
] = next_operand # type:ignore[assignment]
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||
next_expl = f"({next_expl})"
|
||||
results.append(next_res)
|
||||
sym = BINOP_MAP[op.__class__]
|
||||
syms.append(ast.Str(sym))
|
||||
syms.append(ast.Constant(sym))
|
||||
expl = f"{left_expl} {sym} {next_expl}"
|
||||
expls.append(ast.Str(expl))
|
||||
expls.append(ast.Constant(expl))
|
||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||
left_res, left_expl = next_res, next_expl
|
||||
@@ -1145,7 +1186,7 @@ def try_makedirs(cache_dir: Path) -> bool:
|
||||
|
||||
def get_cache_dir(file_path: Path) -> Path:
|
||||
"""Return the cache directory to write .pyc files for the given .py file path."""
|
||||
if sys.version_info >= (3, 8) and sys.pycache_prefix:
|
||||
if sys.pycache_prefix:
|
||||
# given:
|
||||
# prefix = '/tmp/pycs'
|
||||
# path = '/home/user/proj/test_app.py'
|
||||
|
||||
@@ -132,7 +132,7 @@ def isiterable(obj: Any) -> bool:
|
||||
try:
|
||||
iter(obj)
|
||||
return not istext(obj)
|
||||
except TypeError:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@@ -193,6 +193,22 @@ def assertrepr_compare(
|
||||
elif op == "not in":
|
||||
if istext(left) and istext(right):
|
||||
explanation = _notin_text(left, right, verbose)
|
||||
elif op == "!=":
|
||||
if isset(left) and isset(right):
|
||||
explanation = ["Both sets are equal"]
|
||||
elif op == ">=":
|
||||
if isset(left) and isset(right):
|
||||
explanation = _compare_gte_set(left, right, verbose)
|
||||
elif op == "<=":
|
||||
if isset(left) and isset(right):
|
||||
explanation = _compare_lte_set(left, right, verbose)
|
||||
elif op == ">":
|
||||
if isset(left) and isset(right):
|
||||
explanation = _compare_gt_set(left, right, verbose)
|
||||
elif op == "<":
|
||||
if isset(left) and isset(right):
|
||||
explanation = _compare_lt_set(left, right, verbose)
|
||||
|
||||
except outcomes.Exit:
|
||||
raise
|
||||
except Exception:
|
||||
@@ -222,7 +238,7 @@ def _compare_eq_any(left: Any, right: Any, verbose: int = 0) -> List[str]:
|
||||
other_side = right if isinstance(left, ApproxBase) else left
|
||||
|
||||
explanation = approx_side._repr_compare(other_side)
|
||||
elif type(left) == type(right) and (
|
||||
elif type(left) is type(right) and (
|
||||
isdatacls(left) or isattrs(left) or isnamedtuple(left)
|
||||
):
|
||||
# Note: unlike dataclasses/attrs, namedtuples compare only the
|
||||
@@ -392,15 +408,49 @@ def _compare_eq_set(
|
||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
explanation = []
|
||||
diff_left = left - right
|
||||
diff_right = right - left
|
||||
if diff_left:
|
||||
explanation.append("Extra items in the left set:")
|
||||
for item in diff_left:
|
||||
explanation.append(saferepr(item))
|
||||
if diff_right:
|
||||
explanation.append("Extra items in the right set:")
|
||||
for item in diff_right:
|
||||
explanation.extend(_set_one_sided_diff("left", left, right))
|
||||
explanation.extend(_set_one_sided_diff("right", right, left))
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_gt_set(
|
||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
explanation = _compare_gte_set(left, right, verbose)
|
||||
if not explanation:
|
||||
return ["Both sets are equal"]
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_lt_set(
|
||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
explanation = _compare_lte_set(left, right, verbose)
|
||||
if not explanation:
|
||||
return ["Both sets are equal"]
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_gte_set(
|
||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
return _set_one_sided_diff("right", right, left)
|
||||
|
||||
|
||||
def _compare_lte_set(
|
||||
left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0
|
||||
) -> List[str]:
|
||||
return _set_one_sided_diff("left", left, right)
|
||||
|
||||
|
||||
def _set_one_sided_diff(
|
||||
posn: str, set1: AbstractSet[Any], set2: AbstractSet[Any]
|
||||
) -> List[str]:
|
||||
explanation = []
|
||||
diff = set1 - set2
|
||||
if diff:
|
||||
explanation.append(f"Extra items in the {posn} set:")
|
||||
for item in diff:
|
||||
explanation.append(saferepr(item))
|
||||
return explanation
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import Generator
|
||||
from typing import Iterable
|
||||
from typing import List
|
||||
@@ -18,7 +19,6 @@ from .pathlib import rm_rf
|
||||
from .reports import CollectReport
|
||||
from _pytest import nodes
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.config import hookimpl
|
||||
@@ -27,7 +27,7 @@ from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import Module
|
||||
from _pytest.nodes import File
|
||||
from _pytest.python import Package
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
@@ -179,16 +179,22 @@ class Cache:
|
||||
else:
|
||||
cache_dir_exists_already = self._cachedir.exists()
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
except OSError:
|
||||
self.warn("could not create cache path {path}", path=path, _ispytest=True)
|
||||
except OSError as exc:
|
||||
self.warn(
|
||||
f"could not create cache path {path}: {exc}",
|
||||
_ispytest=True,
|
||||
)
|
||||
return
|
||||
if not cache_dir_exists_already:
|
||||
self._ensure_supporting_files()
|
||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||
try:
|
||||
f = path.open("w", encoding="UTF-8")
|
||||
except OSError:
|
||||
self.warn("cache could not write path {path}", path=path, _ispytest=True)
|
||||
except OSError as exc:
|
||||
self.warn(
|
||||
f"cache could not write path {path}: {exc}",
|
||||
_ispytest=True,
|
||||
)
|
||||
else:
|
||||
with f:
|
||||
f.write(data)
|
||||
@@ -211,34 +217,34 @@ class LFPluginCollWrapper:
|
||||
self.lfplugin = lfplugin
|
||||
self._collected_at_least_one_failure = False
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector: nodes.Collector):
|
||||
if isinstance(collector, Session):
|
||||
out = yield
|
||||
res: CollectReport = out.get_result()
|
||||
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_make_collect_report(
|
||||
self, collector: nodes.Collector
|
||||
) -> Generator[None, CollectReport, CollectReport]:
|
||||
res = yield
|
||||
if isinstance(collector, (Session, Package)):
|
||||
# Sort any lf-paths to the beginning.
|
||||
lf_paths = self.lfplugin._last_failed_paths
|
||||
|
||||
# Use stable sort to priorize last failed.
|
||||
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
|
||||
return node.path in lf_paths
|
||||
|
||||
res.result = sorted(
|
||||
res.result,
|
||||
# use stable sort to priorize last failed
|
||||
key=lambda x: x.path in lf_paths,
|
||||
key=sort_key,
|
||||
reverse=True,
|
||||
)
|
||||
return
|
||||
|
||||
elif isinstance(collector, Module):
|
||||
elif isinstance(collector, File):
|
||||
if collector.path in self.lfplugin._last_failed_paths:
|
||||
out = yield
|
||||
res = out.get_result()
|
||||
result = res.result
|
||||
lastfailed = self.lfplugin.lastfailed
|
||||
|
||||
# Only filter with known failures.
|
||||
if not self._collected_at_least_one_failure:
|
||||
if not any(x.nodeid in lastfailed for x in result):
|
||||
return
|
||||
return res
|
||||
self.lfplugin.config.pluginmanager.register(
|
||||
LFPluginCollSkipfiles(self.lfplugin), "lfplugin-collskip"
|
||||
)
|
||||
@@ -254,8 +260,8 @@ class LFPluginCollWrapper:
|
||||
# Keep all sub-collectors.
|
||||
or isinstance(x, nodes.Collector)
|
||||
]
|
||||
return
|
||||
yield
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class LFPluginCollSkipfiles:
|
||||
@@ -266,10 +272,7 @@ class LFPluginCollSkipfiles:
|
||||
def pytest_make_collect_report(
|
||||
self, collector: nodes.Collector
|
||||
) -> Optional[CollectReport]:
|
||||
# Packages are Modules, but _last_failed_paths only contains
|
||||
# test-bearing paths and doesn't try to include the paths of their
|
||||
# packages, so don't filter them.
|
||||
if isinstance(collector, Module) and not isinstance(collector, Package):
|
||||
if isinstance(collector, File):
|
||||
if collector.path not in self.lfplugin._last_failed_paths:
|
||||
self.lfplugin._skipped_files += 1
|
||||
|
||||
@@ -299,9 +302,14 @@ class LFPlugin:
|
||||
)
|
||||
|
||||
def get_last_failed_paths(self) -> Set[Path]:
|
||||
"""Return a set with all Paths()s of the previously failed nodeids."""
|
||||
"""Return a set with all Paths of the previously failed nodeids and
|
||||
their parents."""
|
||||
rootpath = self.config.rootpath
|
||||
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
|
||||
result = set()
|
||||
for nodeid in self.lastfailed:
|
||||
path = rootpath / nodeid.split("::")[0]
|
||||
result.add(path)
|
||||
result.update(path.parents)
|
||||
return {x for x in result if x.exists()}
|
||||
|
||||
def pytest_report_collectionfinish(self) -> Optional[str]:
|
||||
@@ -324,14 +332,14 @@ class LFPlugin:
|
||||
else:
|
||||
self.lastfailed[report.nodeid] = True
|
||||
|
||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
||||
@hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_collection_modifyitems(
|
||||
self, config: Config, items: List[nodes.Item]
|
||||
) -> Generator[None, None, None]:
|
||||
yield
|
||||
res = yield
|
||||
|
||||
if not self.active:
|
||||
return
|
||||
return res
|
||||
|
||||
if self.lastfailed:
|
||||
previously_failed = []
|
||||
@@ -376,6 +384,8 @@ class LFPlugin:
|
||||
else:
|
||||
self._report_status += "not deselecting items."
|
||||
|
||||
return res
|
||||
|
||||
def pytest_sessionfinish(self, session: Session) -> None:
|
||||
config = self.config
|
||||
if config.getoption("cacheshow") or hasattr(config, "workerinput"):
|
||||
@@ -396,11 +406,11 @@ class NFPlugin:
|
||||
assert config.cache is not None
|
||||
self.cached_nodeids = set(config.cache.get("cache/nodeids", []))
|
||||
|
||||
@hookimpl(hookwrapper=True, tryfirst=True)
|
||||
@hookimpl(wrapper=True, tryfirst=True)
|
||||
def pytest_collection_modifyitems(
|
||||
self, items: List[nodes.Item]
|
||||
) -> Generator[None, None, None]:
|
||||
yield
|
||||
res = yield
|
||||
|
||||
if self.active:
|
||||
new_items: Dict[str, nodes.Item] = {}
|
||||
@@ -418,6 +428,8 @@ class NFPlugin:
|
||||
else:
|
||||
self.cached_nodeids.update(item.nodeid for item in items)
|
||||
|
||||
return res
|
||||
|
||||
def _get_increasing_order(self, items: Iterable[nodes.Item]) -> List[nodes.Item]:
|
||||
return sorted(items, key=lambda item: item.path.stat().st_mtime, reverse=True) # type: ignore[no-any-return]
|
||||
|
||||
@@ -487,7 +499,11 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
dest="last_failed_no_failures",
|
||||
choices=("all", "none"),
|
||||
default="all",
|
||||
help="Which tests to run with no previously (known) failures",
|
||||
help="With ``--lf``, determines whether to execute tests when there "
|
||||
"are no previously (known) failures or when no "
|
||||
"cached ``lastfailed`` data was found. "
|
||||
"``all`` (the default) runs the full test suite again. "
|
||||
"``none`` just emits a message about no known failures and exits successfully.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,11 +11,14 @@ from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import AnyStr
|
||||
from typing import BinaryIO
|
||||
from typing import Final
|
||||
from typing import final
|
||||
from typing import Generator
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import Literal
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import TextIO
|
||||
@@ -24,7 +27,6 @@ from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from _pytest.compat import final
|
||||
from _pytest.config import Config
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.config.argparsing import Parser
|
||||
@@ -34,12 +36,9 @@ from _pytest.fixtures import SubRequest
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import File
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.reports import CollectReport
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
from typing_extensions import Literal
|
||||
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
|
||||
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
@@ -132,8 +131,8 @@ def _windowsconsoleio_workaround(stream: TextIO) -> None:
|
||||
sys.stderr = _reopen_stdio(sys.stderr, "wb")
|
||||
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_load_initial_conftests(early_config: Config):
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_load_initial_conftests(early_config: Config) -> Generator[None, None, None]:
|
||||
ns = early_config.known_args_namespace
|
||||
if ns.capture == "fd":
|
||||
_windowsconsoleio_workaround(sys.stdout)
|
||||
@@ -147,12 +146,16 @@ def pytest_load_initial_conftests(early_config: Config):
|
||||
|
||||
# Finally trigger conftest loading but while capturing (issue #93).
|
||||
capman.start_global_capturing()
|
||||
outcome = yield
|
||||
capman.suspend_global_capture()
|
||||
if outcome.excinfo is not None:
|
||||
try:
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
capman.suspend_global_capture()
|
||||
except BaseException:
|
||||
out, err = capman.read_global_capture()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
raise
|
||||
|
||||
|
||||
# IO Helpers.
|
||||
@@ -241,7 +244,7 @@ class DontReadFromInput(TextIO):
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
||||
|
||||
def truncate(self, size: Optional[int] = None) -> int:
|
||||
raise UnsupportedOperation("cannont truncate stdin")
|
||||
raise UnsupportedOperation("cannot truncate stdin")
|
||||
|
||||
def write(self, data: str) -> int:
|
||||
raise UnsupportedOperation("cannot write to stdin")
|
||||
@@ -687,7 +690,7 @@ class MultiCapture(Generic[AnyStr]):
|
||||
return CaptureResult(out, err) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
|
||||
def _get_multicapture(method: _CaptureMethod) -> MultiCapture[str]:
|
||||
if method == "fd":
|
||||
return MultiCapture(in_=FDCapture(0), out=FDCapture(1), err=FDCapture(2))
|
||||
elif method == "sys":
|
||||
@@ -723,7 +726,7 @@ class CaptureManager:
|
||||
needed to ensure the fixtures take precedence over the global capture.
|
||||
"""
|
||||
|
||||
def __init__(self, method: "_CaptureMethod") -> None:
|
||||
def __init__(self, method: _CaptureMethod) -> None:
|
||||
self._method: Final = method
|
||||
self._global_capturing: Optional[MultiCapture[str]] = None
|
||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||
@@ -843,41 +846,45 @@ class CaptureManager:
|
||||
self.deactivate_fixture()
|
||||
self.suspend_global_capture(in_=False)
|
||||
|
||||
out, err = self.read_global_capture()
|
||||
item.add_report_section(when, "stdout", out)
|
||||
item.add_report_section(when, "stderr", err)
|
||||
out, err = self.read_global_capture()
|
||||
item.add_report_section(when, "stdout", out)
|
||||
item.add_report_section(when, "stderr", err)
|
||||
|
||||
# Hooks
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector: Collector):
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_make_collect_report(
|
||||
self, collector: Collector
|
||||
) -> Generator[None, CollectReport, CollectReport]:
|
||||
if isinstance(collector, File):
|
||||
self.resume_global_capture()
|
||||
outcome = yield
|
||||
self.suspend_global_capture()
|
||||
try:
|
||||
rep = yield
|
||||
finally:
|
||||
self.suspend_global_capture()
|
||||
out, err = self.read_global_capture()
|
||||
rep = outcome.get_result()
|
||||
if out:
|
||||
rep.sections.append(("Captured stdout", out))
|
||||
if err:
|
||||
rep.sections.append(("Captured stderr", err))
|
||||
else:
|
||||
yield
|
||||
rep = yield
|
||||
return rep
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_runtest_setup(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("setup", item):
|
||||
yield
|
||||
return (yield)
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_runtest_call(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("call", item):
|
||||
yield
|
||||
return (yield)
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_runtest_teardown(self, item: Item) -> Generator[None, None, None]:
|
||||
with self.item_capture("teardown", item):
|
||||
yield
|
||||
return (yield)
|
||||
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_keyboard_interrupt(self) -> None:
|
||||
|
||||
@@ -12,26 +12,12 @@ from inspect import signature
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generic
|
||||
from typing import Final
|
||||
from typing import NoReturn
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
|
||||
import py
|
||||
|
||||
# fmt: off
|
||||
# Workaround for https://github.com/sphinx-doc/sphinx/issues/10351.
|
||||
# If `overload` is imported from `compat` instead of from `typing`,
|
||||
# Sphinx doesn't recognize it as `overload` and the API docs for
|
||||
# overloaded functions look good again. But type checkers handle
|
||||
# it fine.
|
||||
# fmt: on
|
||||
if True:
|
||||
from typing import overload as overload
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
|
||||
|
||||
_T = TypeVar("_T")
|
||||
_S = TypeVar("_S")
|
||||
@@ -58,17 +44,6 @@ class NotSetType(enum.Enum):
|
||||
NOTSET: Final = NotSetType.token # noqa: E305
|
||||
# fmt: on
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
import importlib.metadata
|
||||
|
||||
importlib_metadata = importlib.metadata
|
||||
else:
|
||||
import importlib_metadata as importlib_metadata # noqa: F401
|
||||
|
||||
|
||||
def _format_args(func: Callable[..., Any]) -> str:
|
||||
return str(signature(func))
|
||||
|
||||
|
||||
def is_generator(func: object) -> bool:
|
||||
genfunc = inspect.isgeneratorfunction(func)
|
||||
@@ -93,7 +68,7 @@ def is_async_function(func: object) -> bool:
|
||||
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
||||
|
||||
|
||||
def getlocation(function, curdir: str | None = None) -> str:
|
||||
def getlocation(function, curdir: str | os.PathLike[str] | None = None) -> str:
|
||||
function = get_real_func(function)
|
||||
fn = Path(inspect.getfile(function))
|
||||
lineno = function.__code__.co_firstlineno
|
||||
@@ -127,7 +102,7 @@ def num_mock_patch_args(function) -> int:
|
||||
|
||||
|
||||
def getfuncargnames(
|
||||
function: Callable[..., Any],
|
||||
function: Callable[..., object],
|
||||
*,
|
||||
name: str = "",
|
||||
is_method: bool = False,
|
||||
@@ -338,57 +313,25 @@ def safe_isclass(obj: object) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
if sys.version_info >= (3, 8):
|
||||
from typing import final as final
|
||||
else:
|
||||
from typing_extensions import final as final
|
||||
elif sys.version_info >= (3, 8):
|
||||
from typing import final as final
|
||||
else:
|
||||
|
||||
def final(f):
|
||||
return f
|
||||
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from functools import cached_property as cached_property
|
||||
else:
|
||||
|
||||
class cached_property(Generic[_S, _T]):
|
||||
__slots__ = ("func", "__doc__")
|
||||
|
||||
def __init__(self, func: Callable[[_S], _T]) -> None:
|
||||
self.func = func
|
||||
self.__doc__ = func.__doc__
|
||||
|
||||
@overload
|
||||
def __get__(
|
||||
self, instance: None, owner: type[_S] | None = ...
|
||||
) -> cached_property[_S, _T]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
|
||||
...
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
if instance is None:
|
||||
return self
|
||||
value = instance.__dict__[self.func.__name__] = self.func(instance)
|
||||
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 the current process's real user id or None if it could not be
|
||||
determined.
|
||||
|
||||
:return: The user id or None if it could not be determined.
|
||||
"""
|
||||
# mypy follows the version and platform checking expectation of PEP 484:
|
||||
# https://mypy.readthedocs.io/en/stable/common_issues.html?highlight=platform#python-version-and-system-platform-checks
|
||||
# Containment checks are too complex for mypy v1.5.0 and cause failure.
|
||||
if sys.platform == "win32" or sys.platform == "emscripten":
|
||||
# win32 does not have a getuid() function.
|
||||
# Emscripten has a return 0 stub.
|
||||
return None
|
||||
# getuid shouldn't fail, but cpython defines such a case.
|
||||
# Let's hope for the best.
|
||||
uid = os.getuid()
|
||||
return uid if uid != -1 else None
|
||||
else:
|
||||
# On other platforms, a return value of -1 is assumed to indicate that
|
||||
# the current process's real user id could not be determined.
|
||||
ERROR = -1
|
||||
uid = os.getuid()
|
||||
return uid if uid != ERROR else None
|
||||
|
||||
|
||||
# Perform exhaustiveness checking.
|
||||
|
||||
@@ -5,6 +5,7 @@ import copy
|
||||
import dataclasses
|
||||
import enum
|
||||
import glob
|
||||
import importlib.metadata
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
@@ -21,6 +22,7 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import Generator
|
||||
from typing import IO
|
||||
from typing import Iterable
|
||||
@@ -35,21 +37,23 @@ from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import pluggy
|
||||
from pluggy import HookimplMarker
|
||||
from pluggy import HookimplOpts
|
||||
from pluggy import HookspecMarker
|
||||
from pluggy import HookspecOpts
|
||||
from pluggy import PluginManager
|
||||
|
||||
import _pytest._code
|
||||
import _pytest.deprecated
|
||||
import _pytest.hookspec
|
||||
from .compat import PathAwareHookProxy
|
||||
from .exceptions import PrintHelp as PrintHelp
|
||||
from .exceptions import UsageError as UsageError
|
||||
from .findpaths import determine_setup
|
||||
from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import importlib_metadata
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.pathlib import absolutepath
|
||||
@@ -57,6 +61,7 @@ from _pytest.pathlib import bestrelpath
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.pathlib import ImportMode
|
||||
from _pytest.pathlib import resolve_package_path
|
||||
from _pytest.pathlib import safe_exists
|
||||
from _pytest.stash import Stash
|
||||
from _pytest.warning_types import PytestConfigWarning
|
||||
from _pytest.warning_types import warn_explicit_for
|
||||
@@ -137,7 +142,9 @@ def main(
|
||||
) -> Union[int, ExitCode]:
|
||||
"""Perform an in-process test run.
|
||||
|
||||
:param args: List of command line arguments.
|
||||
:param args:
|
||||
List of command line arguments. If `None` or not given, defaults to reading
|
||||
arguments directly from the process command line (:data:`sys.argv`).
|
||||
:param plugins: List of plugin objects to be auto-registered during initialization.
|
||||
|
||||
:returns: An exit code.
|
||||
@@ -257,7 +264,8 @@ default_plugins = essential_plugins + (
|
||||
"logging",
|
||||
"reports",
|
||||
"python_path",
|
||||
*(["unraisableexception", "threadexception"] if sys.version_info >= (3, 8) else []),
|
||||
"unraisableexception",
|
||||
"threadexception",
|
||||
"faulthandler",
|
||||
)
|
||||
|
||||
@@ -350,9 +358,9 @@ def _get_legacy_hook_marks(
|
||||
if TYPE_CHECKING:
|
||||
# abuse typeguard from importlib to avoid massive method type union thats lacking a alias
|
||||
assert inspect.isroutine(method)
|
||||
known_marks: set[str] = {m.name for m in getattr(method, "pytestmark", [])}
|
||||
must_warn: list[str] = []
|
||||
opts: dict[str, bool] = {}
|
||||
known_marks: Set[str] = {m.name for m in getattr(method, "pytestmark", [])}
|
||||
must_warn: List[str] = []
|
||||
opts: Dict[str, bool] = {}
|
||||
for opt_name in opt_names:
|
||||
opt_attr = getattr(method, opt_name, AttributeError)
|
||||
if opt_attr is not AttributeError:
|
||||
@@ -437,15 +445,17 @@ class PytestPluginManager(PluginManager):
|
||||
# Used to know when we are importing conftests after the pytest_configure stage.
|
||||
self._configured = False
|
||||
|
||||
def parse_hookimpl_opts(self, plugin: _PluggyPlugin, name: str):
|
||||
def parse_hookimpl_opts(
|
||||
self, plugin: _PluggyPlugin, name: str
|
||||
) -> Optional[HookimplOpts]:
|
||||
# pytest hooks are always prefixed with "pytest_",
|
||||
# so we avoid accessing possibly non-readable attributes
|
||||
# (see issue #1073).
|
||||
if not name.startswith("pytest_"):
|
||||
return
|
||||
return None
|
||||
# Ignore names which can not be hooks.
|
||||
if name == "pytest_plugins":
|
||||
return
|
||||
return None
|
||||
|
||||
opts = super().parse_hookimpl_opts(plugin, name)
|
||||
if opts is not None:
|
||||
@@ -454,18 +464,18 @@ class PytestPluginManager(PluginManager):
|
||||
method = getattr(plugin, name)
|
||||
# Consider only actual functions for hooks (#3775).
|
||||
if not inspect.isroutine(method):
|
||||
return
|
||||
return None
|
||||
# Collect unmarked hooks as long as they have the `pytest_' prefix.
|
||||
return _get_legacy_hook_marks(
|
||||
return _get_legacy_hook_marks( # type: ignore[return-value]
|
||||
method, "impl", ("tryfirst", "trylast", "optionalhook", "hookwrapper")
|
||||
)
|
||||
|
||||
def parse_hookspec_opts(self, module_or_class, name: str):
|
||||
def parse_hookspec_opts(self, module_or_class, name: str) -> Optional[HookspecOpts]:
|
||||
opts = super().parse_hookspec_opts(module_or_class, name)
|
||||
if opts is None:
|
||||
method = getattr(module_or_class, name)
|
||||
if name.startswith("pytest_"):
|
||||
opts = _get_legacy_hook_marks(
|
||||
opts = _get_legacy_hook_marks( # type: ignore[assignment]
|
||||
method,
|
||||
"spec",
|
||||
("firstresult", "historic"),
|
||||
@@ -526,7 +536,13 @@ class PytestPluginManager(PluginManager):
|
||||
# Internal API for local conftest plugin handling.
|
||||
#
|
||||
def _set_initial_conftests(
|
||||
self, namespace: argparse.Namespace, rootpath: Path
|
||||
self,
|
||||
args: Sequence[Union[str, Path]],
|
||||
pyargs: bool,
|
||||
noconftest: bool,
|
||||
rootpath: Path,
|
||||
confcutdir: Optional[Path],
|
||||
importmode: Union[ImportMode, str],
|
||||
) -> None:
|
||||
"""Load initial conftest files given a preparsed "namespace".
|
||||
|
||||
@@ -536,27 +552,25 @@ class PytestPluginManager(PluginManager):
|
||||
common options will not confuse our logic here.
|
||||
"""
|
||||
current = Path.cwd()
|
||||
self._confcutdir = (
|
||||
absolutepath(current / namespace.confcutdir)
|
||||
if namespace.confcutdir
|
||||
else None
|
||||
)
|
||||
self._noconftest = namespace.noconftest
|
||||
self._using_pyargs = namespace.pyargs
|
||||
testpaths = namespace.file_or_dir
|
||||
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
|
||||
self._noconftest = noconftest
|
||||
self._using_pyargs = pyargs
|
||||
foundanchor = False
|
||||
for testpath in testpaths:
|
||||
path = str(testpath)
|
||||
for intitial_path in args:
|
||||
path = str(intitial_path)
|
||||
# remove node-id syntax
|
||||
i = path.find("::")
|
||||
if i != -1:
|
||||
path = path[:i]
|
||||
anchor = absolutepath(current / path)
|
||||
if anchor.exists(): # we found some file object
|
||||
self._try_load_conftest(anchor, namespace.importmode, rootpath)
|
||||
|
||||
# Ensure we do not break if what appears to be an anchor
|
||||
# is in fact a very long option (#10169, #11394).
|
||||
if safe_exists(anchor):
|
||||
self._try_load_conftest(anchor, importmode, rootpath)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
self._try_load_conftest(current, namespace.importmode, rootpath)
|
||||
self._try_load_conftest(current, importmode, rootpath)
|
||||
|
||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||
"""Whether a path is within the confcutdir.
|
||||
@@ -570,26 +584,25 @@ class PytestPluginManager(PluginManager):
|
||||
def _try_load_conftest(
|
||||
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> None:
|
||||
self._getconftestmodules(anchor, importmode, rootpath)
|
||||
self._loadconftestmodules(anchor, importmode, rootpath)
|
||||
# let's also consider test* subdirs
|
||||
if anchor.is_dir():
|
||||
for x in anchor.glob("test*"):
|
||||
if x.is_dir():
|
||||
self._getconftestmodules(x, importmode, rootpath)
|
||||
self._loadconftestmodules(x, importmode, rootpath)
|
||||
|
||||
def _getconftestmodules(
|
||||
def _loadconftestmodules(
|
||||
self, path: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
) -> Sequence[types.ModuleType]:
|
||||
) -> None:
|
||||
if self._noconftest:
|
||||
return []
|
||||
return
|
||||
|
||||
directory = self._get_directory(path)
|
||||
|
||||
# Optimization: avoid repeated searches in the same directory.
|
||||
# Assumes always called with same importmode and rootpath.
|
||||
existing_clist = self._dirpath2confmods.get(directory)
|
||||
if existing_clist is not None:
|
||||
return existing_clist
|
||||
if directory in self._dirpath2confmods:
|
||||
return
|
||||
|
||||
# XXX these days we may rather want to use config.rootpath
|
||||
# and allow users to opt into looking into the rootdir parent
|
||||
@@ -602,16 +615,17 @@ class PytestPluginManager(PluginManager):
|
||||
mod = self._importconftest(conftestpath, importmode, rootpath)
|
||||
clist.append(mod)
|
||||
self._dirpath2confmods[directory] = clist
|
||||
return clist
|
||||
|
||||
def _getconftestmodules(self, path: Path) -> Sequence[types.ModuleType]:
|
||||
directory = self._get_directory(path)
|
||||
return self._dirpath2confmods.get(directory, ())
|
||||
|
||||
def _rget_with_confmod(
|
||||
self,
|
||||
name: str,
|
||||
path: Path,
|
||||
importmode: Union[str, ImportMode],
|
||||
rootpath: Path,
|
||||
) -> Tuple[types.ModuleType, Any]:
|
||||
modules = self._getconftestmodules(path, importmode, rootpath=rootpath)
|
||||
modules = self._getconftestmodules(path)
|
||||
for mod in reversed(modules):
|
||||
try:
|
||||
return mod, getattr(mod, name)
|
||||
@@ -942,7 +956,8 @@ class Config:
|
||||
#: Command line arguments.
|
||||
ARGS = enum.auto()
|
||||
#: Invocation directory.
|
||||
INCOVATION_DIR = enum.auto()
|
||||
INVOCATION_DIR = enum.auto()
|
||||
INCOVATION_DIR = INVOCATION_DIR # backwards compatibility alias
|
||||
#: 'testpaths' configuration value.
|
||||
TESTPATHS = enum.auto()
|
||||
|
||||
@@ -992,10 +1007,8 @@ class Config:
|
||||
# Deprecated alias. Was never public. Can be removed in a few releases.
|
||||
self._store = self.stash
|
||||
|
||||
from .compat import PathAwareHookProxy
|
||||
|
||||
self.trace = self.pluginmanager.trace.root.get("config")
|
||||
self.hook = PathAwareHookProxy(self.pluginmanager.hook)
|
||||
self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment]
|
||||
self._inicache: Dict[str, Any] = {}
|
||||
self._override_ini: Sequence[str] = ()
|
||||
self._opt2dest: Dict[str, str] = {}
|
||||
@@ -1055,9 +1068,10 @@ class Config:
|
||||
fin()
|
||||
|
||||
def get_terminal_writer(self) -> TerminalWriter:
|
||||
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
|
||||
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
|
||||
"terminalreporter"
|
||||
)
|
||||
assert terminalreporter is not None
|
||||
return terminalreporter._tw
|
||||
|
||||
def pytest_cmdline_parse(
|
||||
@@ -1130,8 +1144,25 @@ class Config:
|
||||
|
||||
@hookimpl(trylast=True)
|
||||
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
||||
# We haven't fully parsed the command line arguments yet, so
|
||||
# early_config.args it not set yet. But we need it for
|
||||
# discovering the initial conftests. So "pre-run" the logic here.
|
||||
# It will be done for real in `parse()`.
|
||||
args, args_source = early_config._decide_args(
|
||||
args=early_config.known_args_namespace.file_or_dir,
|
||||
pyargs=early_config.known_args_namespace.pyargs,
|
||||
testpaths=early_config.getini("testpaths"),
|
||||
invocation_dir=early_config.invocation_params.dir,
|
||||
rootpath=early_config.rootpath,
|
||||
warn=False,
|
||||
)
|
||||
self.pluginmanager._set_initial_conftests(
|
||||
early_config.known_args_namespace, rootpath=early_config.rootpath
|
||||
args=args,
|
||||
pyargs=early_config.known_args_namespace.pyargs,
|
||||
noconftest=early_config.known_args_namespace.noconftest,
|
||||
rootpath=early_config.rootpath,
|
||||
confcutdir=early_config.known_args_namespace.confcutdir,
|
||||
importmode=early_config.known_args_namespace.importmode,
|
||||
)
|
||||
|
||||
def _initini(self, args: Sequence[str]) -> None:
|
||||
@@ -1142,7 +1173,7 @@ class Config:
|
||||
ns.inifilename,
|
||||
ns.file_or_dir + unknown_args,
|
||||
rootdir_cmd_arg=ns.rootdir or None,
|
||||
config=self,
|
||||
invocation_dir=self.invocation_params.dir,
|
||||
)
|
||||
self._rootpath = rootpath
|
||||
self._inipath = inipath
|
||||
@@ -1191,7 +1222,7 @@ class Config:
|
||||
|
||||
package_files = (
|
||||
str(file)
|
||||
for dist in importlib_metadata.distributions()
|
||||
for dist in importlib.metadata.distributions()
|
||||
if any(ep.group == "pytest11" for ep in dist.entry_points)
|
||||
for file in dist.files or []
|
||||
)
|
||||
@@ -1211,6 +1242,49 @@ class Config:
|
||||
|
||||
return args
|
||||
|
||||
def _decide_args(
|
||||
self,
|
||||
*,
|
||||
args: List[str],
|
||||
pyargs: bool,
|
||||
testpaths: List[str],
|
||||
invocation_dir: Path,
|
||||
rootpath: Path,
|
||||
warn: bool,
|
||||
) -> Tuple[List[str], ArgsSource]:
|
||||
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
|
||||
|
||||
:param warn: Whether can issue warnings.
|
||||
"""
|
||||
if args:
|
||||
source = Config.ArgsSource.ARGS
|
||||
result = args
|
||||
else:
|
||||
if invocation_dir == rootpath:
|
||||
source = Config.ArgsSource.TESTPATHS
|
||||
if pyargs:
|
||||
result = testpaths
|
||||
else:
|
||||
result = []
|
||||
for path in testpaths:
|
||||
result.extend(sorted(glob.iglob(path, recursive=True)))
|
||||
if testpaths and not result:
|
||||
if warn:
|
||||
warning_text = (
|
||||
"No files were found in testpaths; "
|
||||
"consider removing or adjusting your testpaths configuration. "
|
||||
"Searching recursively from the current directory instead."
|
||||
)
|
||||
self.issue_config_time_warning(
|
||||
PytestConfigWarning(warning_text), stacklevel=3
|
||||
)
|
||||
else:
|
||||
result = []
|
||||
if not result:
|
||||
source = Config.ArgsSource.INVOCATION_DIR
|
||||
result = [str(invocation_dir)]
|
||||
return result, source
|
||||
|
||||
def _preparse(self, args: List[str], addopts: bool = True) -> None:
|
||||
if addopts:
|
||||
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
|
||||
@@ -1249,8 +1323,11 @@ class Config:
|
||||
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
||||
)
|
||||
|
||||
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
if self.known_args_namespace.confcutdir is None:
|
||||
if self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
else:
|
||||
confcutdir = str(self.rootpath)
|
||||
self.known_args_namespace.confcutdir = confcutdir
|
||||
try:
|
||||
self.hook.pytest_load_initial_conftests(
|
||||
@@ -1267,12 +1344,14 @@ class Config:
|
||||
else:
|
||||
raise
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_collection(self) -> Generator[None, None, None]:
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_collection(self) -> Generator[None, object, object]:
|
||||
# Validate invalid ini keys after collection is done so we take in account
|
||||
# options added by late-loading conftest files.
|
||||
yield
|
||||
self._validate_config_options()
|
||||
try:
|
||||
return (yield)
|
||||
finally:
|
||||
self._validate_config_options()
|
||||
|
||||
def _checkversion(self) -> None:
|
||||
import pytest
|
||||
@@ -1356,25 +1435,17 @@ class Config:
|
||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||
self._parser.after_preparse = True # type: ignore
|
||||
try:
|
||||
source = Config.ArgsSource.ARGS
|
||||
args = self._parser.parse_setoption(
|
||||
args, self.option, namespace=self.option
|
||||
)
|
||||
if not args:
|
||||
if self.invocation_params.dir == self.rootpath:
|
||||
source = Config.ArgsSource.TESTPATHS
|
||||
testpaths: List[str] = self.getini("testpaths")
|
||||
if self.known_args_namespace.pyargs:
|
||||
args = testpaths
|
||||
else:
|
||||
args = []
|
||||
for path in testpaths:
|
||||
args.extend(sorted(glob.iglob(path, recursive=True)))
|
||||
if not args:
|
||||
source = Config.ArgsSource.INCOVATION_DIR
|
||||
args = [str(self.invocation_params.dir)]
|
||||
self.args = args
|
||||
self.args_source = source
|
||||
self.args, self.args_source = self._decide_args(
|
||||
args=args,
|
||||
pyargs=self.known_args_namespace.pyargs,
|
||||
testpaths=self.getini("testpaths"),
|
||||
invocation_dir=self.invocation_params.dir,
|
||||
rootpath=self.rootpath,
|
||||
warn=True,
|
||||
)
|
||||
except PrintHelp:
|
||||
pass
|
||||
|
||||
@@ -1382,7 +1453,7 @@ class Config:
|
||||
"""Issue and handle a warning during the "configure" stage.
|
||||
|
||||
During ``pytest_configure`` we can't capture warnings using the ``catch_warnings_for_item``
|
||||
function because it is not possible to have hookwrappers around ``pytest_configure``.
|
||||
function because it is not possible to have hook wrappers around ``pytest_configure``.
|
||||
|
||||
This function is mainly intended for plugins that need to issue warnings during
|
||||
``pytest_configure`` (or similar stages).
|
||||
@@ -1494,13 +1565,9 @@ class Config:
|
||||
else:
|
||||
return self._getini_unknown_type(name, type, value)
|
||||
|
||||
def _getconftest_pathlist(
|
||||
self, name: str, path: Path, rootpath: Path
|
||||
) -> Optional[List[Path]]:
|
||||
def _getconftest_pathlist(self, name: str, path: Path) -> Optional[List[Path]]:
|
||||
try:
|
||||
mod, relroots = self.pluginmanager._rget_with_confmod(
|
||||
name, path, self.getoption("importmode"), rootpath
|
||||
)
|
||||
mod, relroots = self.pluginmanager._rget_with_confmod(name, path)
|
||||
except KeyError:
|
||||
return None
|
||||
assert mod.__file__ is not None
|
||||
|
||||
@@ -7,26 +7,23 @@ from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
from typing import Dict
|
||||
from typing import final
|
||||
from typing import List
|
||||
from typing import Literal
|
||||
from typing import Mapping
|
||||
from typing import NoReturn
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import _pytest._io
|
||||
from _pytest.compat import final
|
||||
from _pytest.config.exceptions import UsageError
|
||||
from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT
|
||||
from _pytest.deprecated import ARGUMENT_TYPE_STR
|
||||
from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
|
||||
from _pytest.deprecated import check_ispytest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
||||
FILE_OR_DIR = "file_or_dir"
|
||||
|
||||
|
||||
@@ -177,7 +174,7 @@ class Parser:
|
||||
name: str,
|
||||
help: str,
|
||||
type: Optional[
|
||||
"Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']"
|
||||
Literal["string", "paths", "pathlist", "args", "linelist", "bool"]
|
||||
] = None,
|
||||
default: Any = None,
|
||||
) -> None:
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from typing import Mapping
|
||||
|
||||
import pluggy
|
||||
|
||||
from ..compat import LEGACY_PATH
|
||||
from ..compat import legacy_path
|
||||
from ..deprecated import HOOK_LEGACY_PATH_ARG
|
||||
from _pytest.nodes import _check_path
|
||||
|
||||
# hookname: (Path, LEGACY_PATH)
|
||||
imply_paths_hooks = {
|
||||
imply_paths_hooks: Mapping[str, tuple[str, str]] = {
|
||||
"pytest_ignore_collect": ("collection_path", "path"),
|
||||
"pytest_collect_file": ("file_path", "path"),
|
||||
"pytest_pycollect_makemodule": ("module_path", "path"),
|
||||
@@ -18,6 +21,14 @@ imply_paths_hooks = {
|
||||
}
|
||||
|
||||
|
||||
def _check_path(path: Path, fspath: LEGACY_PATH) -> None:
|
||||
if Path(fspath) != path:
|
||||
raise ValueError(
|
||||
f"Path({fspath!r}) != {path!r}\n"
|
||||
"if both path and fspath are given they need to be equal"
|
||||
)
|
||||
|
||||
|
||||
class PathAwareHookProxy:
|
||||
"""
|
||||
this helper wraps around hook callers
|
||||
@@ -27,24 +38,24 @@ class PathAwareHookProxy:
|
||||
this may have to be changed later depending on bugs
|
||||
"""
|
||||
|
||||
def __init__(self, hook_caller):
|
||||
self.__hook_caller = hook_caller
|
||||
def __init__(self, hook_relay: pluggy.HookRelay) -> None:
|
||||
self._hook_relay = hook_relay
|
||||
|
||||
def __dir__(self):
|
||||
return dir(self.__hook_caller)
|
||||
def __dir__(self) -> list[str]:
|
||||
return dir(self._hook_relay)
|
||||
|
||||
def __getattr__(self, key, _wraps=functools.wraps):
|
||||
hook = getattr(self.__hook_caller, key)
|
||||
def __getattr__(self, key: str) -> pluggy.HookCaller:
|
||||
hook: pluggy.HookCaller = getattr(self._hook_relay, key)
|
||||
if key not in imply_paths_hooks:
|
||||
self.__dict__[key] = hook
|
||||
return hook
|
||||
else:
|
||||
path_var, fspath_var = imply_paths_hooks[key]
|
||||
|
||||
@_wraps(hook)
|
||||
@functools.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)
|
||||
path_value: Path | None = kw.pop(path_var, None)
|
||||
fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None)
|
||||
if fspath_value is not None:
|
||||
warnings.warn(
|
||||
HOOK_LEGACY_PATH_ARG.format(
|
||||
@@ -65,6 +76,8 @@ class PathAwareHookProxy:
|
||||
kw[fspath_var] = fspath_value
|
||||
return hook(**kw)
|
||||
|
||||
fixed_hook.name = hook.name # type: ignore[attr-defined]
|
||||
fixed_hook.spec = hook.spec # type: ignore[attr-defined]
|
||||
fixed_hook.__name__ = key
|
||||
self.__dict__[key] = fixed_hook
|
||||
return fixed_hook
|
||||
return fixed_hook # type: ignore[return-value]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from _pytest.compat import final
|
||||
from typing import final
|
||||
|
||||
|
||||
@final
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import List
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import iniconfig
|
||||
@@ -16,9 +15,7 @@ from .exceptions import UsageError
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import commonpath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Config
|
||||
from _pytest.pathlib import safe_exists
|
||||
|
||||
|
||||
def _parse_ini_config(path: Path) -> iniconfig.IniConfig:
|
||||
@@ -151,14 +148,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
|
||||
return path
|
||||
return path.parent
|
||||
|
||||
def safe_exists(path: Path) -> bool:
|
||||
# This can throw on paths that contain characters unrepresentable at the OS level,
|
||||
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
|
||||
try:
|
||||
return path.exists()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# These look like paths but may not exist
|
||||
possible_paths = (
|
||||
absolutepath(get_file_part_from_node_id(arg))
|
||||
@@ -176,8 +165,21 @@ def determine_setup(
|
||||
inifile: Optional[str],
|
||||
args: Sequence[str],
|
||||
rootdir_cmd_arg: Optional[str] = None,
|
||||
config: Optional["Config"] = None,
|
||||
invocation_dir: Optional[Path] = None,
|
||||
) -> Tuple[Path, Optional[Path], Dict[str, Union[str, List[str]]]]:
|
||||
"""Determine the rootdir, inifile and ini configuration values from the
|
||||
command line arguments.
|
||||
|
||||
:param inifile:
|
||||
The `--inifile` command line argument, if given.
|
||||
:param args:
|
||||
The free command line arguments.
|
||||
:param rootdir_cmd_arg:
|
||||
The `--rootdir` command line argument, if given.
|
||||
:param invocation_dir:
|
||||
The working directory when pytest was invoked, if known.
|
||||
If not known, the current working directory is used.
|
||||
"""
|
||||
rootdir = None
|
||||
dirs = get_dirs_from_args(args)
|
||||
if inifile:
|
||||
@@ -198,8 +200,8 @@ def determine_setup(
|
||||
if dirs != [ancestor]:
|
||||
rootdir, inipath, inicfg = locate_config(dirs)
|
||||
if rootdir is None:
|
||||
if config is not None:
|
||||
cwd = config.invocation_params.dir
|
||||
if invocation_dir is not None:
|
||||
cwd = invocation_dir
|
||||
else:
|
||||
cwd = Path.cwd()
|
||||
rootdir = get_common_ancestor([cwd, ancestor])
|
||||
|
||||
@@ -304,10 +304,10 @@ class PdbInvoke:
|
||||
|
||||
|
||||
class PdbTrace:
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, None, None]:
|
||||
@hookimpl(wrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem) -> Generator[None, object, object]:
|
||||
wrap_pytest_function_for_tracing(pyfuncitem)
|
||||
yield
|
||||
return (yield)
|
||||
|
||||
|
||||
def wrap_pytest_function_for_tracing(pyfuncitem):
|
||||
|
||||
@@ -122,6 +122,11 @@ HOOK_LEGACY_MARKING = UnformattedWarning(
|
||||
"#configuring-hook-specs-impls-using-markers",
|
||||
)
|
||||
|
||||
MARKED_FIXTURE = PytestRemovedIn8Warning(
|
||||
"Marks applied to fixtures have no effect\n"
|
||||
"See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function"
|
||||
)
|
||||
|
||||
# You want to make some `__init__` or function "private".
|
||||
#
|
||||
# def my_private_function(some, args):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user