Compare commits
750 Commits
7.2.0.dev0
...
7.4.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2390610696 | ||
|
|
a0714aa007 | ||
|
|
44ad1c9811 | ||
|
|
5dc77253d4 | ||
|
|
a517827318 | ||
|
|
21fe071d79 | ||
|
|
f8bb8572fe | ||
|
|
1944dc06d3 | ||
|
|
946634c84c | ||
|
|
d849a3ed64 | ||
|
|
721a0881fb | ||
|
|
5341b9cd67 | ||
|
|
c39bdf6190 | ||
|
|
b0c4775a28 | ||
|
|
45f34dfb8d | ||
|
|
e4f022f0d8 | ||
|
|
63b0c6f75f | ||
|
|
884b911a9c | ||
|
|
6e49a74089 | ||
|
|
79c2012d40 | ||
|
|
de69883e3a | ||
|
|
1de00e9830 | ||
|
|
7f5d9b9df4 | ||
|
|
82eb86f707 | ||
|
|
0319a0d4fd | ||
|
|
7855a72d2c | ||
|
|
7a0a0e8b08 | ||
|
|
fbcfd3a52e | ||
|
|
b170081788 | ||
|
|
7a5f2feefb | ||
|
|
69140717d4 | ||
|
|
5c7c3f6329 | ||
|
|
ba40975bb7 | ||
|
|
e3fe7286f8 | ||
|
|
34c73944e1 | ||
|
|
350122abb2 | ||
|
|
06ff7ca13b | ||
|
|
6dfe498c77 | ||
|
|
a566b78730 | ||
|
|
511adf85be | ||
|
|
c71b5df734 | ||
|
|
d53951836d | ||
|
|
a4d7254d18 | ||
|
|
b6c55787fe | ||
|
|
fb03d1388b | ||
|
|
d9bf9dbec1 | ||
|
|
64319dbc01 | ||
|
|
1e8135df16 | ||
|
|
1e32a4b570 | ||
|
|
faa1f9d2ad | ||
|
|
14890329dc | ||
|
|
d97d44a97a | ||
|
|
f6b995e9d5 | ||
|
|
661b938fca | ||
|
|
7e510769b4 | ||
|
|
a704605cf1 | ||
|
|
797b924fc4 | ||
|
|
b5ec092525 | ||
|
|
5b35518389 | ||
|
|
8528052a95 | ||
|
|
1eb83706b6 | ||
|
|
fa09f6dcc6 | ||
|
|
b55c02c3c9 | ||
|
|
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 | ||
|
|
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 | ||
|
|
3683722bcb | ||
|
|
31d0b51039 | ||
|
|
2d2f69dab5 | ||
|
|
2a39ed3461 | ||
|
|
f1c7585184 | ||
|
|
a3b39069bc | ||
|
|
172c832cbd | ||
|
|
839b90db45 | ||
|
|
549cc512f7 | ||
|
|
2369bed1db | ||
|
|
54864f0c9b | ||
|
|
ba969d2ae7 | ||
|
|
407b330fe1 | ||
|
|
431ec6d34e | ||
|
|
eada68b2b3 | ||
|
|
ab069247cd | ||
|
|
7af1e4e4ed | ||
|
|
0ae04ae629 | ||
|
|
723035be7f | ||
|
|
6e478b0947 | ||
|
|
a869141b3d | ||
|
|
5e98aefc92 | ||
|
|
5f47e423b2 | ||
|
|
5a61ec3d4a | ||
|
|
b3b44ea814 | ||
|
|
1d48b3021d | ||
|
|
d5dda84ef3 | ||
|
|
517e02e59e | ||
|
|
4e259590c9 | ||
|
|
97a2761d72 | ||
|
|
88c9e92258 | ||
|
|
72ad32411f | ||
|
|
cb9e8be301 | ||
|
|
d72da480c4 | ||
|
|
07e7deb4a7 | ||
|
|
572b5657d7 | ||
|
|
44afed9b13 | ||
|
|
13ea4780b8 | ||
|
|
135600fca3 | ||
|
|
c237297b3d | ||
|
|
9ccae9a8e3 | ||
|
|
77152d26e7 | ||
|
|
da626e7186 | ||
|
|
051f8f1f0f | ||
|
|
31ad577325 | ||
|
|
835cac8d8b | ||
|
|
464f29901f | ||
|
|
aa72496d24 | ||
|
|
e9f3a01392 | ||
|
|
00c94ab01b | ||
|
|
9048621002 | ||
|
|
27165cf8db | ||
|
|
7a829cb57d | ||
|
|
5e1c3d2477 | ||
|
|
59e7d2bbc9 | ||
|
|
af99040123 | ||
|
|
a2b7db7655 | ||
|
|
9c93c96b14 | ||
|
|
4a46ee8bc9 | ||
|
|
5dbfb8e108 | ||
|
|
86a1beba07 | ||
|
|
ca40380e99 | ||
|
|
05eee78aaa | ||
|
|
02893139f9 | ||
|
|
8c53dbf9d7 | ||
|
|
54b8b40f83 | ||
|
|
54911acf8d | ||
|
|
c746d2b016 | ||
|
|
a3693ce503 | ||
|
|
af4143729f | ||
|
|
bd7919e03d | ||
|
|
7d4b40337b | ||
|
|
6a714d7b70 | ||
|
|
5a23eeff7a | ||
|
|
310b67b227 | ||
|
|
4d4ed42c34 | ||
|
|
096b942ec4 | ||
|
|
61cfaacec6 | ||
|
|
95c62eb527 | ||
|
|
03b19945fb | ||
|
|
b2ac31cc9f | ||
|
|
1a96f16401 | ||
|
|
7421f3bb94 | ||
|
|
5e0583f4b9 | ||
|
|
8efb4bb9c1 | ||
|
|
3ad4344656 | ||
|
|
6bf7f55555 | ||
|
|
61f70a5a75 | ||
|
|
326ae0cd88 | ||
|
|
10220d3f31 | ||
|
|
a98b00cd09 | ||
|
|
215ea7fd03 | ||
|
|
b31db4809b | ||
|
|
f6adebb990 | ||
|
|
b90e7b84d0 | ||
|
|
19807ab79a | ||
|
|
3e52124185 | ||
|
|
1eca228bd5 | ||
|
|
cab02e67d7 | ||
|
|
64dbc7a0a1 | ||
|
|
60d992677d | ||
|
|
0079decf29 | ||
|
|
3a58fc2d44 | ||
|
|
39b6bb551c | ||
|
|
9fbd67dd4b | ||
|
|
eca93db05b | ||
|
|
fb701b538c | ||
|
|
314e623304 | ||
|
|
62e75c7d55 | ||
|
|
fd30759d94 | ||
|
|
eb984a717a | ||
|
|
54f0fb3c63 | ||
|
|
49a4ed14cf | ||
|
|
f513d33d5a | ||
|
|
857e34ef85 | ||
|
|
99dfc19fe6 | ||
|
|
56544c11b5 | ||
|
|
7710e18b4c | ||
|
|
791b51d0fa | ||
|
|
bc4e70e048 | ||
|
|
b817aa457c | ||
|
|
66b28912ac | ||
|
|
cca029d55e | ||
|
|
d5466b3917 | ||
|
|
4fce29f15d | ||
|
|
69e3973d86 | ||
|
|
c842893b02 | ||
|
|
506b10d295 | ||
|
|
05061493cb | ||
|
|
f97f3dc3a3 | ||
|
|
3c31b0132f | ||
|
|
593178d909 | ||
|
|
54d5a63d14 | ||
|
|
b55e264a67 | ||
|
|
13d6114c0a | ||
|
|
b635e16d30 | ||
|
|
a092b3ab36 | ||
|
|
a006dabf6e | ||
|
|
aa7e9de91d | ||
|
|
6aec32163d | ||
|
|
2f33ea87c8 | ||
|
|
1ada62e237 | ||
|
|
50b232b0cb | ||
|
|
496196b15c | ||
|
|
0314b50c52 | ||
|
|
8e2de91bf8 | ||
|
|
692ab1160b | ||
|
|
549839bac5 | ||
|
|
646a46e5f4 | ||
|
|
f07017f91b | ||
|
|
a17d3b0c44 | ||
|
|
bbec1ce67f | ||
|
|
5a040aef97 | ||
|
|
c1d2168df6 | ||
|
|
bbe7cbae4a | ||
|
|
deae8f47f6 | ||
|
|
10f55f79af | ||
|
|
a6d244343f | ||
|
|
2b552c2240 | ||
|
|
54d7b9a08e | ||
|
|
6afc02abca | ||
|
|
e75e2d66a0 | ||
|
|
66db0b7522 | ||
|
|
3a68c08426 | ||
|
|
9e1804a6ee | ||
|
|
bc2c3b66aa | ||
|
|
d84ed48f39 | ||
|
|
ffe49ac17c | ||
|
|
d352098261 | ||
|
|
c5c562b645 | ||
|
|
d543a45a68 | ||
|
|
f341a5c559 | ||
|
|
1027dc8c09 | ||
|
|
6b905ee6dc | ||
|
|
508be0b2bf | ||
|
|
02a9371259 | ||
|
|
dc0cb0d149 | ||
|
|
82344ba4f8 | ||
|
|
59d8f8a223 | ||
|
|
ed4c18f686 | ||
|
|
6660d45521 | ||
|
|
af078f3a96 | ||
|
|
73349ef3e1 | ||
|
|
63c4d45c59 | ||
|
|
eebbfc65c9 | ||
|
|
382209d9e9 | ||
|
|
00e2f1c15c | ||
|
|
8a151774b8 | ||
|
|
a7c1fc204b | ||
|
|
965e942dfb | ||
|
|
349f4bffa0 | ||
|
|
49abbf2485 | ||
|
|
19dda7c9bd | ||
|
|
baada535a3 | ||
|
|
c477ecd146 | ||
|
|
36d19f2135 | ||
|
|
8490ff5277 | ||
|
|
3dac833a52 | ||
|
|
36b6384ff2 | ||
|
|
31df38f6a7 | ||
|
|
2a33e6ab61 | ||
|
|
7fada7127e | ||
|
|
3a8d401ac7 | ||
|
|
2b6622fdd3 | ||
|
|
0bc9ffcc87 | ||
|
|
f3d7e984ab | ||
|
|
452617686c | ||
|
|
6e7917c1a0 | ||
|
|
15ac0349b2 | ||
|
|
86602ce6e0 | ||
|
|
c543e0c4e8 | ||
|
|
f13f4360d3 | ||
|
|
b7d4de1ea9 | ||
|
|
a9c66172d4 | ||
|
|
3de63f9a9b | ||
|
|
f5797abb62 | ||
|
|
b735f9377a | ||
|
|
d8c5e8805f | ||
|
|
07b380bb3a | ||
|
|
78c2c48c67 | ||
|
|
8a40fc5315 | ||
|
|
196f01965e | ||
|
|
3bf2bc55b1 | ||
|
|
571dc6b220 | ||
|
|
d0abfb1c2a | ||
|
|
81113c1a90 | ||
|
|
784ba85b17 | ||
|
|
a28d595b46 | ||
|
|
c42bb36009 | ||
|
|
13e594a314 | ||
|
|
4e7486d3fb | ||
|
|
d8c783268c | ||
|
|
3c1534944c | ||
|
|
a501518a26 | ||
|
|
680f3e2097 | ||
|
|
ed5d4e1269 | ||
|
|
8e7ce60c7d | ||
|
|
2bc036e50a | ||
|
|
79c2e92bb8 | ||
|
|
4da0689653 | ||
|
|
9be1e3fac8 | ||
|
|
0c21d60349 | ||
|
|
4545fcff05 | ||
|
|
24ef7c98e8 | ||
|
|
c4981f5a1b | ||
|
|
7a15bad89b | ||
|
|
72cac9b1af | ||
|
|
2df0d9656c | ||
|
|
4677580cae | ||
|
|
14919c4bbb | ||
|
|
06007de7ba | ||
|
|
4ee6e900a0 | ||
|
|
eab875509d | ||
|
|
2be1b8f355 | ||
|
|
594413e8f0 | ||
|
|
ce3e2e922b | ||
|
|
59b8ec3ce1 | ||
|
|
ae9dbf5006 | ||
|
|
7e8a4849d8 | ||
|
|
e3294398d6 | ||
|
|
b1fb9a9c8d | ||
|
|
8c52dc5b7e | ||
|
|
0fdacb6db5 | ||
|
|
256ab097a2 | ||
|
|
5bd41befa8 | ||
|
|
246129089c | ||
|
|
34765f6b2e | ||
|
|
33a083e844 | ||
|
|
77d2c9631a | ||
|
|
d416fbab91 | ||
|
|
f53b49c8c4 | ||
|
|
85da6cad11 | ||
|
|
c326bd5669 | ||
|
|
a734dab00d | ||
|
|
1e0aa16908 | ||
|
|
b34eb0860c | ||
|
|
77e34526e8 | ||
|
|
abf4941f3e | ||
|
|
b4ae5e4a35 | ||
|
|
649b2e7f30 | ||
|
|
6ad32a9c5c | ||
|
|
50f390f015 | ||
|
|
4ed2b3a733 | ||
|
|
7f4b63b143 | ||
|
|
245a8c23dd | ||
|
|
a9bbfb87d6 | ||
|
|
1caa099e42 | ||
|
|
4b77638ba8 | ||
|
|
0fc3a977a9 | ||
|
|
adaa463ee3 | ||
|
|
42776c0949 | ||
|
|
059bc2443a | ||
|
|
932264d8fd | ||
|
|
d9d890ee15 | ||
|
|
71aa1388e0 | ||
|
|
69f2855cc8 | ||
|
|
3039391b83 | ||
|
|
832100955a | ||
|
|
bec2b8ee3a | ||
|
|
7378f35b03 | ||
|
|
b6eaf319d1 | ||
|
|
2e7c718373 | ||
|
|
08dfd3124c | ||
|
|
56862c03cb | ||
|
|
67e29d2548 | ||
|
|
cb65f64572 | ||
|
|
f425dfc158 | ||
|
|
63d2f7f7f8 | ||
|
|
739322af03 | ||
|
|
beae7fd0ba | ||
|
|
7431750bb6 | ||
|
|
cb7f5ed3b1 | ||
|
|
c72d202317 | ||
|
|
cc0092b9d8 | ||
|
|
433efaeaa9 | ||
|
|
4d430ea6ff | ||
|
|
abd5df4b3c | ||
|
|
58cf20edf0 | ||
|
|
f43ddd8acd | ||
|
|
1c31a7e659 | ||
|
|
c0e3071fdf | ||
|
|
b4ab2f0942 | ||
|
|
dbc1017b08 | ||
|
|
70659b8c34 | ||
|
|
3bf15135e4 | ||
|
|
6c57cc1727 | ||
|
|
c0843f94e4 | ||
|
|
8c5487fd6b | ||
|
|
4696ba74f9 | ||
|
|
34a285986e | ||
|
|
b8e2f42ec1 | ||
|
|
70cac3d464 | ||
|
|
c1d134172c | ||
|
|
cbcb3a356e | ||
|
|
09b2c95320 | ||
|
|
c2b1d5b368 | ||
|
|
5c513a5fb3 | ||
|
|
7b9f71d84f | ||
|
|
334d6514c6 | ||
|
|
f6dc71c8de | ||
|
|
8c60fa1ef9 | ||
|
|
b83a74a303 | ||
|
|
9c5b6fbab2 | ||
|
|
b1948c60ac | ||
|
|
da68c9d5bc | ||
|
|
29041e1e1f | ||
|
|
dda7673f57 | ||
|
|
7dc540f258 | ||
|
|
4cd0322ca1 | ||
|
|
966d4fb3e4 | ||
|
|
8726597321 | ||
|
|
aa83fa353c | ||
|
|
4449bdc068 | ||
|
|
929e7d5ae0 | ||
|
|
6d7973942f | ||
|
|
4414c4adae | ||
|
|
b99541e744 | ||
|
|
5f57481563 | ||
|
|
04de180f41 | ||
|
|
b08c1dcf0e | ||
|
|
3e590fe79e | ||
|
|
7dcabc1f49 | ||
|
|
2941da0f2b | ||
|
|
1b47de7d7f | ||
|
|
85000f037f | ||
|
|
c34eaaaa1c | ||
|
|
06738e36df | ||
|
|
94bc31b07d | ||
|
|
b961ef5b79 | ||
|
|
bd642fac70 | ||
|
|
da9a2b584e | ||
|
|
8bbd35aa69 | ||
|
|
0b1e9ba270 | ||
|
|
f92c4a77ad | ||
|
|
9a037a5713 | ||
|
|
11fb5cd331 | ||
|
|
62d975a69e | ||
|
|
aafac5578c | ||
|
|
2b70e7f4df | ||
|
|
d2a206683c | ||
|
|
f29f79e39f | ||
|
|
c7e784f95d | ||
|
|
0a7b8722e5 | ||
|
|
aa55975c7d | ||
|
|
141c5e5a89 | ||
|
|
cba65e74b3 | ||
|
|
74691346bd | ||
|
|
6db715c205 | ||
|
|
1e6a587363 | ||
|
|
052da7128b | ||
|
|
cdc287d21a | ||
|
|
5a9536cf42 | ||
|
|
17c385180d | ||
|
|
fab696dcd1 | ||
|
|
2cd41651bb | ||
|
|
5ef064b602 | ||
|
|
d6b837e2d3 | ||
|
|
96412d19ab | ||
|
|
bb94e83b49 | ||
|
|
1a50e5222f | ||
|
|
27c94f573c | ||
|
|
04be900d06 | ||
|
|
fe2c74cb1a | ||
|
|
083285948f | ||
|
|
433c7376c5 | ||
|
|
5adfb0e187 | ||
|
|
c2f684fcd6 | ||
|
|
130c954bb1 | ||
|
|
e54c6a1362 | ||
|
|
9a8f5dd73e | ||
|
|
6017666fe1 | ||
|
|
a72e8b3d3e | ||
|
|
0efd7a6ec6 | ||
|
|
c533015c56 | ||
|
|
e547cd565d | ||
|
|
a47e91e077 | ||
|
|
420dc7824b | ||
|
|
c988e49af6 | ||
|
|
31f9e5bcdd | ||
|
|
9a9acf13f8 | ||
|
|
8ac6dce2c7 | ||
|
|
0e62861e84 | ||
|
|
611b579d21 | ||
|
|
8469f44401 | ||
|
|
a1ed6839e3 | ||
|
|
08c0eb661e | ||
|
|
1feb857fcc | ||
|
|
296b642eb3 | ||
|
|
ee10ecdf7e | ||
|
|
73c2d1c6e3 | ||
|
|
7b977f4beb | ||
|
|
5d28a58640 | ||
|
|
5d20e28538 | ||
|
|
cc0fbbefa0 | ||
|
|
56c266640e | ||
|
|
69fb79e741 | ||
|
|
90d2adf0e8 | ||
|
|
c5432a18e7 | ||
|
|
16781f9b8c | ||
|
|
a88c0f3bb0 | ||
|
|
ccdee08ddd | ||
|
|
a29f4aff98 | ||
|
|
58af76122f | ||
|
|
29462b1277 | ||
|
|
37316ed0de | ||
|
|
231e220635 | ||
|
|
93fdb3ecb7 | ||
|
|
a4a8763ebf | ||
|
|
31a9c5c667 | ||
|
|
db139307ce | ||
|
|
644a81af37 | ||
|
|
f6aa9d661d | ||
|
|
933156b296 | ||
|
|
23c8f3d234 | ||
|
|
5f9d68c8d9 | ||
|
|
2f62e6ec96 | ||
|
|
4ddf48b0a3 | ||
|
|
f25b9f619b | ||
|
|
33bf91482e | ||
|
|
2ba8fd5bc5 | ||
|
|
306b33e755 | ||
|
|
3444d35c54 | ||
|
|
97383d2d5d | ||
|
|
e580534df0 | ||
|
|
f6db7ae749 | ||
|
|
eb8b3ad929 | ||
|
|
21091a45d0 | ||
|
|
d949b3f7d9 | ||
|
|
246815de13 | ||
|
|
e598429c73 | ||
|
|
0347957aa7 | ||
|
|
1e8e46d456 | ||
|
|
7df405747a | ||
|
|
28e8c8582e | ||
|
|
1285caadf2 | ||
|
|
12971a37e1 | ||
|
|
715d0ca9d0 | ||
|
|
2b9f8f2f9b | ||
|
|
ac9b65d890 | ||
|
|
8514fab409 | ||
|
|
988197c795 | ||
|
|
1ea7081752 | ||
|
|
6a3168e770 | ||
|
|
5800d39053 | ||
|
|
045dbc895d | ||
|
|
a60a6a3fea | ||
|
|
4e3ec26487 | ||
|
|
86b62067b7 | ||
|
|
ace71037ab | ||
|
|
2e8a319828 | ||
|
|
61095e4ba6 | ||
|
|
752a059cc2 | ||
|
|
99e20713fd | ||
|
|
26c62e1793 | ||
|
|
61dfdfcc3f | ||
|
|
9c2ee32069 | ||
|
|
765583dc34 | ||
|
|
f0cf63a84a | ||
|
|
8fa775bcee | ||
|
|
539a5d70f2 | ||
|
|
1527130476 | ||
|
|
238b25ffa9 | ||
|
|
e2269f8a5c | ||
|
|
16345bf689 | ||
|
|
79725c9102 | ||
|
|
387ec02021 | ||
|
|
3fcdbaf0c5 | ||
|
|
141b301d3d | ||
|
|
31595e8733 | ||
|
|
63c7338028 | ||
|
|
0eae340e13 | ||
|
|
1f10c59cd3 | ||
|
|
00ad12b9db | ||
|
|
592fee2263 | ||
|
|
176d2d7b4e | ||
|
|
05dcb22365 | ||
|
|
cccb9d8530 | ||
|
|
67ab712480 | ||
|
|
250a0344ad | ||
|
|
3c359455e2 | ||
|
|
d5ce9f5a16 | ||
|
|
0ebfa1d76d | ||
|
|
e9dd3dffab | ||
|
|
3297bb24a9 | ||
|
|
6a6a32ceca | ||
|
|
e503faeb6e | ||
|
|
4b6b91fda3 | ||
|
|
4c16b5af03 | ||
|
|
b75cbee290 | ||
|
|
eb22339dc3 | ||
|
|
63126643b9 | ||
|
|
8f64a39d0a | ||
|
|
a18655f729 | ||
|
|
22756c28e5 | ||
|
|
f196701cc1 | ||
|
|
1d70b022f0 | ||
|
|
d52a6e6074 | ||
|
|
2bec21ef5f | ||
|
|
5c1756343f | ||
|
|
a854ab97b9 | ||
|
|
abfae83130 | ||
|
|
37fa0fb4d3 | ||
|
|
e75cdae2bc | ||
|
|
61f2b265a3 | ||
|
|
f3053017b9 | ||
|
|
bb6155adfa | ||
|
|
5fefd7de96 | ||
|
|
750ce30392 | ||
|
|
de1f378b60 | ||
|
|
14d5b4ca6c | ||
|
|
307dbf15c4 | ||
|
|
a8697601ad | ||
|
|
0ed1b0ac12 | ||
|
|
26b0702b98 |
6
.github/dependabot.yml
vendored
6
.github/dependabot.yml
vendored
@@ -9,3 +9,9 @@ updates:
|
||||
allow:
|
||||
- dependency-type: direct
|
||||
- dependency-type: indirect
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
time: "03:00"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
|
||||
90
.github/workflows/deploy.yml
vendored
Normal file
90
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Release version'
|
||||
required: true
|
||||
default: '1.2.3'
|
||||
|
||||
|
||||
# Set permissions at the job level.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SETUPTOOLS_SCM_PRETEND_VERSION: ${{ github.event.inputs.version }}
|
||||
timeout-minutes: 10
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
deploy:
|
||||
if: github.repository == 'pytest-dev/pytest'
|
||||
needs: [package]
|
||||
runs-on: ubuntu-latest
|
||||
environment: deploy
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Download Package
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: Packages
|
||||
path: dist
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@v1.8.5
|
||||
|
||||
- name: Push tag
|
||||
run: |
|
||||
git config user.name "pytest bot"
|
||||
git config user.email "pytestbot@gmail.com"
|
||||
git tag --annotate --message=v${{ github.event.inputs.version }} v${{ github.event.inputs.version }} ${{ github.sha }}
|
||||
git push origin v${{ github.event.inputs.version }}
|
||||
|
||||
release-notes:
|
||||
|
||||
# todo: generate the content in the build job
|
||||
# the goal being of using a github action script to push the release data
|
||||
# after success instead of creating a complete python/tox env
|
||||
needs: [deploy]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade tox
|
||||
|
||||
- name: Publish GitHub release notes
|
||||
env:
|
||||
GH_RELEASE_NOTES_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
sudo apt-get install pandoc
|
||||
tox -e publish-gh-release-notes
|
||||
4
.github/workflows/prepare-release-pr.yml
vendored
4
.github/workflows/prepare-release-pr.yml
vendored
@@ -27,12 +27,12 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.8"
|
||||
|
||||
|
||||
23
.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
|
||||
@@ -1,4 +1,4 @@
|
||||
name: main
|
||||
name: test
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -18,11 +18,28 @@ on:
|
||||
env:
|
||||
PYTEST_ADDOPTS: "--color=yes"
|
||||
|
||||
# Cancel running jobs for the same workflow and branch.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Set permissions at the job level.
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- name: Build and Check Package
|
||||
uses: hynek/build-and-inspect-python-package@v1.5
|
||||
|
||||
build:
|
||||
needs: [package]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 45
|
||||
permissions:
|
||||
@@ -33,27 +50,28 @@ jobs:
|
||||
matrix:
|
||||
name: [
|
||||
"windows-py37",
|
||||
"windows-py37-pluggy",
|
||||
"windows-py38",
|
||||
"windows-py38-pluggy",
|
||||
"windows-py39",
|
||||
"windows-py310",
|
||||
"windows-py311",
|
||||
"windows-py312",
|
||||
|
||||
"ubuntu-py37",
|
||||
"ubuntu-py37-pluggy",
|
||||
"ubuntu-py37-freeze",
|
||||
"ubuntu-py38",
|
||||
"ubuntu-py38-pluggy",
|
||||
"ubuntu-py39",
|
||||
"ubuntu-py310",
|
||||
"ubuntu-py311",
|
||||
"ubuntu-py312",
|
||||
"ubuntu-pypy3",
|
||||
|
||||
"macos-py37",
|
||||
"macos-py38",
|
||||
"macos-py39",
|
||||
"macos-py310",
|
||||
"macos-py312",
|
||||
|
||||
"docs",
|
||||
"doctesting",
|
||||
"plugins",
|
||||
]
|
||||
@@ -63,15 +81,15 @@ jobs:
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-numpy"
|
||||
- name: "windows-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: windows-latest
|
||||
tox_env: "py37-pluggymain-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
|
||||
@@ -81,19 +99,19 @@ jobs:
|
||||
os: windows-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "windows-py311"
|
||||
python: "3.11-dev"
|
||||
python: "3.11"
|
||||
os: windows-latest
|
||||
tox_env: "py311"
|
||||
- name: "windows-py312"
|
||||
python: "3.12-dev"
|
||||
os: windows-latest
|
||||
tox_env: "py312"
|
||||
|
||||
- name: "ubuntu-py37"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-lsof-numpy-pexpect"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py37-pluggy"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py37-pluggymain-xdist"
|
||||
- name: "ubuntu-py37-freeze"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -102,6 +120,10 @@ jobs:
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-xdist"
|
||||
- name: "ubuntu-py38-pluggy"
|
||||
python: "3.8"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py38-pluggymain-pylib-xdist"
|
||||
- name: "ubuntu-py39"
|
||||
python: "3.9"
|
||||
os: ubuntu-latest
|
||||
@@ -111,9 +133,15 @@ jobs:
|
||||
os: ubuntu-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "ubuntu-py311"
|
||||
python: "3.11-dev"
|
||||
python: "3.11"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py311"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-py312"
|
||||
python: "3.12-dev"
|
||||
os: ubuntu-latest
|
||||
tox_env: "py312"
|
||||
use_coverage: true
|
||||
- name: "ubuntu-pypy3"
|
||||
python: "pypy-3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -123,29 +151,25 @@ jobs:
|
||||
python: "3.7"
|
||||
os: macos-latest
|
||||
tox_env: "py37-xdist"
|
||||
- name: "macos-py38"
|
||||
python: "3.8"
|
||||
os: macos-latest
|
||||
tox_env: "py38-xdist"
|
||||
use_coverage: true
|
||||
- name: "macos-py39"
|
||||
python: "3.9"
|
||||
os: macos-latest
|
||||
tox_env: "py39-xdist"
|
||||
use_coverage: true
|
||||
- name: "macos-py310"
|
||||
python: "3.10"
|
||||
os: macos-latest
|
||||
tox_env: "py310-xdist"
|
||||
- name: "macos-py312"
|
||||
python: "3.12-dev"
|
||||
os: macos-latest
|
||||
tox_env: "py312-xdist"
|
||||
|
||||
- name: "plugins"
|
||||
python: "3.9"
|
||||
os: ubuntu-latest
|
||||
tox_env: "plugins"
|
||||
|
||||
- name: "docs"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
tox_env: "docs"
|
||||
- name: "doctesting"
|
||||
python: "3.7"
|
||||
os: ubuntu-latest
|
||||
@@ -153,15 +177,22 @@ jobs:
|
||||
use_coverage: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
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@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python }}
|
||||
check-latest: ${{ endsWith(matrix.python, '-dev') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -170,11 +201,13 @@ jobs:
|
||||
|
||||
- name: Test without coverage
|
||||
if: "! matrix.use_coverage"
|
||||
run: "tox -e ${{ matrix.tox_env }}"
|
||||
shell: bash
|
||||
run: tox run -e ${{ matrix.tox_env }} --installpkg `find dist/*.tar.gz`
|
||||
|
||||
- name: Test with coverage
|
||||
if: "matrix.use_coverage"
|
||||
run: "tox -e ${{ matrix.tox_env }}-coverage"
|
||||
shell: bash
|
||||
run: tox run -e ${{ matrix.tox_env }}-coverage --installpkg `find dist/*.tar.gz`
|
||||
|
||||
- name: Generate coverage report
|
||||
if: "matrix.use_coverage"
|
||||
@@ -182,51 +215,9 @@ jobs:
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
if: "matrix.use_coverage"
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v3
|
||||
continue-on-error: true
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
files: ./coverage.xml
|
||||
verbose: true
|
||||
|
||||
deploy:
|
||||
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pytest'
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
needs: [build]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.7"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade build tox
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
- name: Publish package to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@master
|
||||
with:
|
||||
user: __token__
|
||||
password: ${{ secrets.pypi_token }}
|
||||
|
||||
- name: Publish GitHub release notes
|
||||
env:
|
||||
GH_RELEASE_NOTES_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
sudo apt-get install pandoc
|
||||
tox -e publish-gh-release-notes
|
||||
8
.github/workflows/update-plugin-list.yml
vendored
8
.github/workflows/update-plugin-list.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
createPullRequest:
|
||||
update-plugin-list:
|
||||
if: github.repository_owner == 'pytest-dev'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -20,12 +20,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.8
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
run: python scripts/update-plugin-list.py
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@2455e1596942c2902952003bbb574afbbe2ab2e6
|
||||
uses: peter-evans/create-pull-request@153407881ec5c347639a548ade7d8ad1d6740e38
|
||||
with:
|
||||
commit-message: '[automated] Update plugin list'
|
||||
author: 'pytest bot <pytestbot@users.noreply.github.com>'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -50,6 +50,7 @@ coverage.xml
|
||||
.project
|
||||
.settings
|
||||
.vscode
|
||||
__pycache__/
|
||||
|
||||
# generated by pip
|
||||
pip-wheel-metadata/
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.1.0
|
||||
rev: 23.3.0
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: v1.12.1
|
||||
rev: 1.14.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==20.8b1]
|
||||
additional_dependencies: [black==23.1.0]
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.1.0
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
@@ -20,8 +20,8 @@ repos:
|
||||
- id: debug-statements
|
||||
exclude: _pytest/(debugging|hookspec).py
|
||||
language_version: python3
|
||||
- repo: https://github.com/myint/autoflake
|
||||
rev: v1.4
|
||||
- repo: https://github.com/PyCQA/autoflake
|
||||
rev: v2.1.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
name: autoflake
|
||||
@@ -29,46 +29,47 @@ repos:
|
||||
language: python
|
||||
files: \.py$
|
||||
- repo: https://github.com/PyCQA/flake8
|
||||
rev: 4.0.1
|
||||
rev: 6.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
language_version: python3
|
||||
additional_dependencies:
|
||||
- flake8-typing-imports==1.12.0
|
||||
- flake8-docstrings==1.5.0
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v2.7.1
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.10.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: ['--application-directories=.:src', --py37-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.31.0
|
||||
rev: v3.7.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py37-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.20.0
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
args: [--max-py-version=3.10]
|
||||
args: ["--max-py-version=3.12", "--include-version-classifiers"]
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.9.0
|
||||
rev: v1.10.0
|
||||
hooks:
|
||||
- id: python-use-type-annotations
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v0.931
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
files: ^(src/|testing/)
|
||||
args: []
|
||||
additional_dependencies:
|
||||
- iniconfig>=1.1.0
|
||||
- py>=1.8.2
|
||||
- attrs>=19.2.0
|
||||
- packaging
|
||||
- tomli
|
||||
- types-atomicwrites
|
||||
- types-pkg_resources
|
||||
# for mypy running on python>=3.11 since exceptiongroup is only a dependency
|
||||
# on <3.11
|
||||
- exceptiongroup>=1.0.0rc8
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: rst
|
||||
@@ -101,7 +102,7 @@ repos:
|
||||
types: [python]
|
||||
- id: py-path-deprecated
|
||||
name: py.path usage is deprecated
|
||||
exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py
|
||||
exclude: docs|src/_pytest/deprecated.py|testing/deprecated_test.py|src/_pytest/legacypath.py
|
||||
language: pygrep
|
||||
entry: \bpy\.path\.local
|
||||
types: [python]
|
||||
|
||||
@@ -2,9 +2,16 @@ version: 2
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: doc/en/requirements.txt
|
||||
- method: pip
|
||||
path: .
|
||||
# Install pytest first, then doc/en/requirements.txt.
|
||||
# This order is important to honor any pins in doc/en/requirements.txt
|
||||
# when the pinned library is also a dependency of pytest.
|
||||
- method: pip
|
||||
path: .
|
||||
- requirements: doc/en/requirements.txt
|
||||
|
||||
sphinx:
|
||||
configuration: doc/en/conf.py
|
||||
fail_on_warning: true
|
||||
|
||||
build:
|
||||
os: ubuntu-20.04
|
||||
|
||||
63
AUTHORS
63
AUTHORS
@@ -8,13 +8,18 @@ Abdeali JK
|
||||
Abdelrahman Elbehery
|
||||
Abhijeet Kasurde
|
||||
Adam Johnson
|
||||
Adam Stewart
|
||||
Adam Uhlir
|
||||
Ahn Ki-Wook
|
||||
Akiomi Kamakura
|
||||
Alan Velasco
|
||||
Alessio Izzo
|
||||
Alex Jones
|
||||
Alex Lambson
|
||||
Alexander Johnson
|
||||
Alexander King
|
||||
Alexei Kozlenok
|
||||
Alice Purcell
|
||||
Allan Feldman
|
||||
Aly Sivji
|
||||
Amir Elkess
|
||||
@@ -42,8 +47,10 @@ Ariel Pillemer
|
||||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
Ashish Kurmi
|
||||
Aviral Verma
|
||||
Aviv Palivoda
|
||||
Babak Keyvani
|
||||
Barney Gale
|
||||
Ben Gartner
|
||||
Ben Webb
|
||||
@@ -51,10 +58,12 @@ Benjamin Peterson
|
||||
Bernard Pratz
|
||||
Bob Ippolito
|
||||
Brian Dorsey
|
||||
Brian Larsen
|
||||
Brian Maissy
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
Bruno Oliveira
|
||||
Cal Jacobson
|
||||
Cal Leeming
|
||||
Carl Friedrich Bolz
|
||||
Carlos Jenkins
|
||||
@@ -62,9 +71,12 @@ Ceridwen
|
||||
Charles Cloud
|
||||
Charles Machalow
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Cheuk Ting Ho
|
||||
Chris Mahoney
|
||||
Chris Lamb
|
||||
Chris NeJame
|
||||
Chris Rose
|
||||
Chris Wheeler
|
||||
Christian Boelsen
|
||||
Christian Fetzer
|
||||
Christian Neumüller
|
||||
@@ -83,6 +95,8 @@ Damian Skrzypczak
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Nuri
|
||||
Daniel Sánchez Castelló
|
||||
Daniel Valenzuela Zenteno
|
||||
Daniel Wandschneider
|
||||
Daniele Procida
|
||||
Danielle Jenkins
|
||||
@@ -117,13 +131,16 @@ Eric Siegerman
|
||||
Erik Aronesty
|
||||
Erik M. Bray
|
||||
Evan Kepner
|
||||
Evgeny Seliverstov
|
||||
Fabien Zarifian
|
||||
Fabio Zadrozny
|
||||
Felix Hofstätter
|
||||
Felix Nieuwenhuizen
|
||||
Feng Ma
|
||||
Florian Bruhin
|
||||
Florian Dahlitz
|
||||
Floris Bruynooghe
|
||||
Gabriel Landau
|
||||
Gabriel Reis
|
||||
Garvit Shubham
|
||||
Gene Wood
|
||||
@@ -149,8 +166,12 @@ Ian Bicking
|
||||
Ian Lesperance
|
||||
Ilya Konstantinov
|
||||
Ionuț Turturică
|
||||
Isaac Virshup
|
||||
Israel Fruchter
|
||||
Itxaso Aizpurua
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
Jake VanderPlas
|
||||
Jakob van Santen
|
||||
Jakub Mitoraj
|
||||
James Bourbeau
|
||||
@@ -163,7 +184,9 @@ Jeff Rackauckas
|
||||
Jeff Widman
|
||||
Jenni Rinker
|
||||
John Eddie Ayson
|
||||
John Litborn
|
||||
John Towler
|
||||
Jon Parise
|
||||
Jon Sonesen
|
||||
Jonas Obrist
|
||||
Jordan Guymon
|
||||
@@ -173,18 +196,22 @@ Joseph Hunkeler
|
||||
Josh Karpel
|
||||
Joshua Bronson
|
||||
Jurko Gospodnetić
|
||||
Justyna Janczyszyn
|
||||
Justice Ndou
|
||||
Justyna Janczyszyn
|
||||
Kale Kundert
|
||||
Kamran Ahmad
|
||||
Kenny Y
|
||||
Karl O. Pinc
|
||||
Karthikeyan Singaravelan
|
||||
Katarzyna Jachim
|
||||
Katarzyna Król
|
||||
Katerina Koukiou
|
||||
Keri Volans
|
||||
Kevin C
|
||||
Kevin Cox
|
||||
Kevin Hierro Carrasco
|
||||
Kevin J. Foley
|
||||
Kian Eliasi
|
||||
Kian-Meng Ang
|
||||
Kodi B. Arfer
|
||||
Kojo Idrissa
|
||||
@@ -204,12 +231,14 @@ Maho
|
||||
Maik Figura
|
||||
Mandeep Bhutani
|
||||
Manuel Krebber
|
||||
Marc Mueller
|
||||
Marc Schlaich
|
||||
Marcelo Duarte Trevisani
|
||||
Marcin Bachry
|
||||
Marco Gorelli
|
||||
Mark Abramowitz
|
||||
Mark Dickinson
|
||||
Marko Pacak
|
||||
Markus Unterwaditzer
|
||||
Martijn Faassen
|
||||
Martin Altmayer
|
||||
@@ -223,7 +252,6 @@ Matthias Hafner
|
||||
Maxim Filipenko
|
||||
Maximilian Cosmo Sitter
|
||||
mbyt
|
||||
Mickey Pashov
|
||||
Michael Aquilina
|
||||
Michael Birtwell
|
||||
Michael Droettboom
|
||||
@@ -232,6 +260,7 @@ Michael Krebs
|
||||
Michael Seifert
|
||||
Michal Wajszczuk
|
||||
Michał Zięba
|
||||
Mickey Pashov
|
||||
Mihai Capotă
|
||||
Mike Hoyle (hoylemd)
|
||||
Mike Lundy
|
||||
@@ -245,9 +274,10 @@ Nicholas Murphy
|
||||
Niclas Olofsson
|
||||
Nicolas Delaby
|
||||
Nikolay Kondratyev
|
||||
Olga Matoula
|
||||
Nipunn Koorapati
|
||||
Oleg Pidsadnyi
|
||||
Oleg Sushchenko
|
||||
Olga Matoula
|
||||
Oliver Bestwalter
|
||||
Omar Kohl
|
||||
Omer Hadari
|
||||
@@ -255,12 +285,15 @@ Ondřej Súkup
|
||||
Oscar Benjamin
|
||||
Parth Patel
|
||||
Patrick Hayes
|
||||
Paul Müller
|
||||
Paul Reece
|
||||
Pauli Virtanen
|
||||
Pavel Karateev
|
||||
Paweł Adamczak
|
||||
Pedro Algarvio
|
||||
Petter Strandmark
|
||||
Philipp Loose
|
||||
Pierre Sassoulas
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Piotr Helm
|
||||
@@ -270,15 +303,18 @@ Prashant Sharma
|
||||
Pulkit Goyal
|
||||
Punyashloka Biswal
|
||||
Quentin Pradet
|
||||
q0w
|
||||
Ralf Schmitt
|
||||
Ram Rachum
|
||||
Ralph Giles
|
||||
Ram Rachum
|
||||
Ran Benita
|
||||
Raphael Castaneda
|
||||
Raphael Pierzina
|
||||
Rafal Semik
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
Robert Holt
|
||||
Roberto Aldera
|
||||
Roberto Polli
|
||||
Roland Puntaier
|
||||
Romain Dorgueil
|
||||
@@ -289,23 +325,29 @@ Ruaridh Williamson
|
||||
Russel Winder
|
||||
Ryan Wooden
|
||||
Saiprasad Kale
|
||||
Samuel Colvin
|
||||
Samuel Dion-Girardeau
|
||||
Samuel Searles-Bryant
|
||||
Samuele Pedroni
|
||||
Sanket Duthade
|
||||
Sankt Petersbug
|
||||
Saravanan Padmanaban
|
||||
Segev Finer
|
||||
Serhii Mozghovyi
|
||||
Seth Junot
|
||||
Shantanu Jain
|
||||
Shubham Adep
|
||||
Simon Blanchard
|
||||
Simon Gomizelj
|
||||
Simon Holesch
|
||||
Simon Kerr
|
||||
Skylar Downes
|
||||
Srinivas Reddy Thatiparthy
|
||||
Stefaan Lippens
|
||||
Stefan Farmbauer
|
||||
Stefan Scherfke
|
||||
Stefan Zimmermann
|
||||
Stefanie Molin
|
||||
Stefano Taschini
|
||||
Steffen Allner
|
||||
Stephan Obermann
|
||||
@@ -317,26 +359,34 @@ Taneli Hukkinen
|
||||
Tanvi Mehta
|
||||
Tarcisio Fischer
|
||||
Tareq Alayan
|
||||
Tatiana Ovary
|
||||
Ted Xiao
|
||||
Terje Runde
|
||||
Thomas Grainger
|
||||
Thomas Hisch
|
||||
Tim Hoffmann
|
||||
Tim Strazny
|
||||
TJ Bruno
|
||||
Tobias Diez
|
||||
Tom Dalton
|
||||
Tom Viner
|
||||
Tomáš Gavenčiak
|
||||
Tomer Keren
|
||||
Tony Narlock
|
||||
Tor Colvin
|
||||
Trevor Bekolay
|
||||
Tyler Goodlet
|
||||
Tyler Smart
|
||||
Tzu-ping Chung
|
||||
Vasily Kuznetsov
|
||||
Victor Maryama
|
||||
Victor Rodriguez
|
||||
Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
Vijay Arora
|
||||
Virgil Dupras
|
||||
Vitaly Lashmanov
|
||||
Vivaan Verma
|
||||
Vlad Dragos
|
||||
Vlad Radziuk
|
||||
Vladyslav Rachek
|
||||
@@ -349,9 +399,14 @@ Wouter van Ackooy
|
||||
Xixi Zhao
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Yannick Péroux
|
||||
Yoav Caspi
|
||||
Yuliang Shao
|
||||
Yusuke Kadowaki
|
||||
Yuval Shimon
|
||||
Zac Hatfield-Dodds
|
||||
Zachary Kneupper
|
||||
Zachary OBrien
|
||||
Zhouxin Qiu
|
||||
Zoltán Máté
|
||||
Zsolt Cserna
|
||||
|
||||
@@ -50,6 +50,8 @@ Fix bugs
|
||||
--------
|
||||
|
||||
Look through the `GitHub issues for bugs <https://github.com/pytest-dev/pytest/labels/type:%20bug>`_.
|
||||
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
|
||||
to work on a particular issue, add a comment to that effect on the specific issue.
|
||||
@@ -221,7 +223,7 @@ changes you want to review and merge. Pull requests are stored on
|
||||
Once you send a pull request, we can discuss its potential modifications and
|
||||
even add more commits to it later on. There's an excellent tutorial on how Pull
|
||||
Requests work in the
|
||||
`GitHub Help Center <https://help.github.com/articles/using-pull-requests/>`_.
|
||||
`GitHub Help Center <https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests>`_.
|
||||
|
||||
Here is a simple overview, with pytest-specific bits:
|
||||
|
||||
@@ -242,6 +244,11 @@ Here is a simple overview, with pytest-specific bits:
|
||||
be released in micro releases whereas features will be released in
|
||||
minor releases and incompatible changes in major releases.
|
||||
|
||||
You will need the tags to test locally, so be sure you have the tags from the main repository. If you suspect you don't, set the main repository as upstream and fetch the tags::
|
||||
|
||||
$ git remote add upstream https://github.com/pytest-dev/pytest
|
||||
$ git fetch upstream --tags
|
||||
|
||||
If you need some help with Git, follow this quick start
|
||||
guide: https://git.wiki.kernel.org/index.php/QuickStart
|
||||
|
||||
@@ -378,7 +385,7 @@ them.
|
||||
Backporting bug fixes for the next patch release
|
||||
------------------------------------------------
|
||||
|
||||
Pytest makes feature release every few weeks or months. In between, patch releases
|
||||
Pytest makes a feature release every few weeks or months. In between, patch releases
|
||||
are made to the previous feature release, containing bug fixes only. The bug fixes
|
||||
usually fix regressions, but may be any change that should reach users before the
|
||||
next feature release.
|
||||
@@ -387,7 +394,7 @@ Suppose for example that the latest release was 1.2.3, and you want to include
|
||||
a bug fix in 1.2.4 (check https://github.com/pytest-dev/pytest/releases for the
|
||||
actual latest release). The procedure for this is:
|
||||
|
||||
#. First, make sure the bug is fixed the ``main`` branch, with a regular pull
|
||||
#. First, make sure the bug is fixed in the ``main`` branch, with a regular pull
|
||||
request, as described above. An exception to this is if the bug fix is not
|
||||
applicable to ``main`` anymore.
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
:target: https://codecov.io/gh/pytest-dev/pytest
|
||||
:alt: Code coverage Status
|
||||
|
||||
.. image:: https://github.com/pytest-dev/pytest/workflows/main/badge.svg
|
||||
:target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Amain
|
||||
.. image:: https://github.com/pytest-dev/pytest/workflows/test/badge.svg
|
||||
:target: https://github.com/pytest-dev/pytest/actions?query=workflow%3Atest
|
||||
|
||||
.. image:: https://results.pre-commit.ci/badge/github/pytest-dev/pytest/main.svg
|
||||
:target: https://results.pre-commit.ci/latest/github/pytest-dev/pytest/main
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
<li><a href="{{ pathto('changelog') }}">Changelog</a></li>
|
||||
<li><a href="{{ pathto('contributing') }}">Contributing</a></li>
|
||||
<li><a href="{{ pathto('backwards-compatibility') }}">Backwards Compatibility</a></li>
|
||||
<li><a href="{{ pathto('py27-py34-deprecation') }}">Python 2.7 and 3.4 Support</a></li>
|
||||
<li><a href="{{ pathto('sponsor') }}">Sponsor</a></li>
|
||||
<li><a href="{{ pathto('tidelift') }}">pytest for Enterprise</a></li>
|
||||
<li><a href="{{ pathto('license') }}">License</a></li>
|
||||
@@ -30,5 +29,3 @@
|
||||
{%- endif %}
|
||||
|
||||
<hr>
|
||||
<a href="{{ pathto('genindex') }}">Index</a>
|
||||
<hr>
|
||||
|
||||
@@ -6,6 +6,19 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-7.4.3
|
||||
release-7.4.2
|
||||
release-7.4.1
|
||||
release-7.4.0
|
||||
release-7.3.2
|
||||
release-7.3.1
|
||||
release-7.3.0
|
||||
release-7.2.2
|
||||
release-7.2.1
|
||||
release-7.2.0
|
||||
release-7.1.3
|
||||
release-7.1.2
|
||||
release-7.1.1
|
||||
release-7.1.0
|
||||
release-7.0.1
|
||||
release-7.0.0
|
||||
|
||||
18
doc/en/announce/release-7.1.1.rst
Normal file
18
doc/en/announce/release-7.1.1.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-7.1.1
|
||||
=======================================
|
||||
|
||||
pytest 7.1.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
23
doc/en/announce/release-7.1.2.rst
Normal file
23
doc/en/announce/release-7.1.2.rst
Normal file
@@ -0,0 +1,23 @@
|
||||
pytest-7.1.2
|
||||
=======================================
|
||||
|
||||
pytest 7.1.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:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Hugo van Kemenade
|
||||
* Kian Eliasi
|
||||
* Ran Benita
|
||||
* Zac Hatfield-Dodds
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
28
doc/en/announce/release-7.1.3.rst
Normal file
28
doc/en/announce/release-7.1.3.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pytest-7.1.3
|
||||
=======================================
|
||||
|
||||
pytest 7.1.3 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Gergely Kalmár
|
||||
* Nipunn Koorapati
|
||||
* Pax
|
||||
* Sviatoslav Sydorenko
|
||||
* Tim Hoffmann
|
||||
* Tony Narlock
|
||||
* Wolfremium
|
||||
* Zach OBrien
|
||||
* aizpurua23a
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
93
doc/en/announce/release-7.2.0.rst
Normal file
93
doc/en/announce/release-7.2.0.rst
Normal file
@@ -0,0 +1,93 @@
|
||||
pytest-7.2.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 7.2.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Aaron Berdy
|
||||
* Adam Turner
|
||||
* Albert Villanova del Moral
|
||||
* Alice Purcell
|
||||
* Anthony Sottile
|
||||
* Anton Yakutovich
|
||||
* Babak Keyvani
|
||||
* Brandon Chinn
|
||||
* Bruno Oliveira
|
||||
* Chanvin Xiao
|
||||
* Cheuk Ting Ho
|
||||
* Chris Wheeler
|
||||
* EmptyRabbit
|
||||
* Ezio Melotti
|
||||
* Florian Best
|
||||
* Florian Bruhin
|
||||
* Fredrik Berndtsson
|
||||
* Gabriel Landau
|
||||
* Gergely Kalmár
|
||||
* Hugo van Kemenade
|
||||
* James Gerity
|
||||
* John Litborn
|
||||
* Jon Parise
|
||||
* Kevin C
|
||||
* Kian Eliasi
|
||||
* MatthewFlamm
|
||||
* Miro Hrončok
|
||||
* Nate Meyvis
|
||||
* Neil Girdhar
|
||||
* Nhieuvu1802
|
||||
* Nipunn Koorapati
|
||||
* Ofek Lev
|
||||
* Paul Müller
|
||||
* Paul Reece
|
||||
* Pax
|
||||
* Pete Baughman
|
||||
* Peyman Salehi
|
||||
* Philipp A
|
||||
* Ran Benita
|
||||
* Robert O'Shea
|
||||
* Ronny Pfannschmidt
|
||||
* Rowin
|
||||
* Ruth Comer
|
||||
* Samuel Colvin
|
||||
* Samuel Gaist
|
||||
* Sandro Tosi
|
||||
* Shantanu
|
||||
* Simon K
|
||||
* Stephen Rosen
|
||||
* Sviatoslav Sydorenko
|
||||
* Tatiana Ovary
|
||||
* Thierry Moisan
|
||||
* Thomas Grainger
|
||||
* Tim Hoffmann
|
||||
* Tobias Diez
|
||||
* Tony Narlock
|
||||
* Vivaan Verma
|
||||
* Wolfremium
|
||||
* Zac Hatfield-Dodds
|
||||
* Zach OBrien
|
||||
* aizpurua23a
|
||||
* gresm
|
||||
* holesch
|
||||
* itxasos23
|
||||
* johnkangw
|
||||
* skhomuti
|
||||
* sommersoft
|
||||
* wodny
|
||||
* zx.qiu
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
25
doc/en/announce/release-7.2.1.rst
Normal file
25
doc/en/announce/release-7.2.1.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-7.2.1
|
||||
=======================================
|
||||
|
||||
pytest 7.2.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Valenzuela
|
||||
* Kadino
|
||||
* Prerak Patel
|
||||
* Ronny Pfannschmidt
|
||||
* Santiago Castro
|
||||
* s-padmanaban
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
25
doc/en/announce/release-7.2.2.rst
Normal file
25
doc/en/announce/release-7.2.2.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-7.2.2
|
||||
=======================================
|
||||
|
||||
pytest 7.2.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Garvit Shubham
|
||||
* Mahesh Vashishtha
|
||||
* Ramsey
|
||||
* Ronny Pfannschmidt
|
||||
* Teejay
|
||||
* q0w
|
||||
* vin01
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
130
doc/en/announce/release-7.3.0.rst
Normal file
130
doc/en/announce/release-7.3.0.rst
Normal file
@@ -0,0 +1,130 @@
|
||||
pytest-7.3.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 7.3.0 release!
|
||||
|
||||
This release contains new features, improvements, and bug fixes,
|
||||
the full list of changes is available in the changelog:
|
||||
|
||||
https://docs.pytest.org/en/stable/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/stable/
|
||||
|
||||
As usual, you can upgrade from PyPI via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Aaron Berdy
|
||||
* Adam Turner
|
||||
* Albert Villanova del Moral
|
||||
* Alessio Izzo
|
||||
* Alex Hadley
|
||||
* Alice Purcell
|
||||
* Anthony Sottile
|
||||
* Anton Yakutovich
|
||||
* Ashish Kurmi
|
||||
* Babak Keyvani
|
||||
* Billy
|
||||
* Brandon Chinn
|
||||
* Bruno Oliveira
|
||||
* Cal Jacobson
|
||||
* Chanvin Xiao
|
||||
* Cheuk Ting Ho
|
||||
* Chris Wheeler
|
||||
* Daniel Garcia Moreno
|
||||
* Daniel Scheffler
|
||||
* Daniel Valenzuela
|
||||
* EmptyRabbit
|
||||
* Ezio Melotti
|
||||
* Felix Hofstätter
|
||||
* Florian Best
|
||||
* Florian Bruhin
|
||||
* Fredrik Berndtsson
|
||||
* Gabriel Landau
|
||||
* Garvit Shubham
|
||||
* Gergely Kalmár
|
||||
* HTRafal
|
||||
* Hugo van Kemenade
|
||||
* Ilya Konstantinov
|
||||
* Itxaso Aizpurua
|
||||
* James Gerity
|
||||
* Jay
|
||||
* John Litborn
|
||||
* Jon Parise
|
||||
* Jouke Witteveen
|
||||
* Kadino
|
||||
* Kevin C
|
||||
* Kian Eliasi
|
||||
* Klaus Rettinghaus
|
||||
* Kodi Arfer
|
||||
* Mahesh Vashishtha
|
||||
* Manuel Jacob
|
||||
* Marko Pacak
|
||||
* MatthewFlamm
|
||||
* Miro Hrončok
|
||||
* Nate Meyvis
|
||||
* Neil Girdhar
|
||||
* Nhieuvu1802
|
||||
* Nipunn Koorapati
|
||||
* Ofek Lev
|
||||
* Paul Kehrer
|
||||
* Paul Müller
|
||||
* Paul Reece
|
||||
* Pax
|
||||
* Pete Baughman
|
||||
* Peyman Salehi
|
||||
* Philipp A
|
||||
* Pierre Sassoulas
|
||||
* Prerak Patel
|
||||
* Ramsey
|
||||
* Ran Benita
|
||||
* Robert O'Shea
|
||||
* Ronny Pfannschmidt
|
||||
* Rowin
|
||||
* Ruth Comer
|
||||
* Samuel Colvin
|
||||
* Samuel Gaist
|
||||
* Sandro Tosi
|
||||
* Santiago Castro
|
||||
* Shantanu
|
||||
* Simon K
|
||||
* Stefanie Molin
|
||||
* Stephen Rosen
|
||||
* Sviatoslav Sydorenko
|
||||
* Tatiana Ovary
|
||||
* Teejay
|
||||
* Thierry Moisan
|
||||
* Thomas Grainger
|
||||
* Tim Hoffmann
|
||||
* Tobias Diez
|
||||
* Tony Narlock
|
||||
* Vivaan Verma
|
||||
* Wolfremium
|
||||
* Yannick PÉROUX
|
||||
* Yusuke Kadowaki
|
||||
* Zac Hatfield-Dodds
|
||||
* Zach OBrien
|
||||
* aizpurua23a
|
||||
* bitzge
|
||||
* bluthej
|
||||
* gresm
|
||||
* holesch
|
||||
* itxasos23
|
||||
* johnkangw
|
||||
* q0w
|
||||
* rdb
|
||||
* s-padmanaban
|
||||
* skhomuti
|
||||
* sommersoft
|
||||
* vin01
|
||||
* wim glenn
|
||||
* wodny
|
||||
* zx.qiu
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-7.3.1.rst
Normal file
18
doc/en/announce/release-7.3.1.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-7.3.1
|
||||
=======================================
|
||||
|
||||
pytest 7.3.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Ran Benita
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
21
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
|
||||
19
doc/en/announce/release-7.4.3.rst
Normal file
19
doc/en/announce/release-7.4.3.rst
Normal file
@@ -0,0 +1,19 @@
|
||||
pytest-7.4.3
|
||||
=======================================
|
||||
|
||||
pytest 7.4.3 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at https://docs.pytest.org/en/stable/changelog.html.
|
||||
|
||||
Thanks to all of the contributors to this release:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Marc Mueller
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -77,3 +77,20 @@ Deprecation Roadmap
|
||||
Features currently deprecated and removed in previous releases can be found in :ref:`deprecations`.
|
||||
|
||||
We track future deprecation and removal of features using milestones and the `deprecation <https://github.com/pytest-dev/pytest/issues?q=label%3A%22type%3A+deprecation%22>`_ and `removal <https://github.com/pytest-dev/pytest/labels/type%3A%20removal>`_ labels on GitHub.
|
||||
|
||||
|
||||
Python version support
|
||||
======================
|
||||
|
||||
Released pytest versions support all Python versions that are actively maintained at the time of the release:
|
||||
|
||||
============== ===================
|
||||
pytest version min. Python version
|
||||
============== ===================
|
||||
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)
|
||||
@@ -33,39 +33,93 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
Values can be any object handled by the json stdlib module.
|
||||
|
||||
capsys -- .../_pytest/capture.py:878
|
||||
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsys.readouterr()`` method
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
|
||||
capsysbinary -- .../_pytest/capture.py:895
|
||||
capsysbinary -- .../_pytest/capture.py:1001
|
||||
Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsysbinary.readouterr()``
|
||||
method calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``bytes`` objects.
|
||||
|
||||
capfd -- .../_pytest/capture.py:912
|
||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsysbinary):
|
||||
print("hello")
|
||||
captured = capsysbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capfd -- .../_pytest/capture.py:1029
|
||||
Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
|
||||
capfdbinary -- .../_pytest/capture.py:929
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfd):
|
||||
os.system('echo "hello"')
|
||||
captured = capfd.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
capfdbinary -- .../_pytest/capture.py:1057
|
||||
Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``byte`` objects.
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:731
|
||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfdbinary):
|
||||
os.system('echo "hello"')
|
||||
captured = capfdbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
capsys -- .../_pytest/capture.py:973
|
||||
Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsys.readouterr()`` method
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsys):
|
||||
print("hello")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
|
||||
doctest_namespace [session scope] -- .../_pytest/doctest.py:757
|
||||
Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1334
|
||||
Usually this fixture is used in conjunction with another ``autouse`` fixture:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def add_np(doctest_namespace):
|
||||
doctest_namespace["np"] = numpy
|
||||
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
|
||||
pytestconfig [session scope] -- .../_pytest/fixtures.py:1353
|
||||
Session-scoped fixture that returns the session's :class:`pytest.Config`
|
||||
object.
|
||||
|
||||
@@ -109,7 +163,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
record_testsuite_property("ARCH", "PPC")
|
||||
record_testsuite_property("STORAGE_TYPE", "CEPH")
|
||||
|
||||
``name`` must be a string, ``value`` will be converted to a string and properly xml-escaped.
|
||||
:param name:
|
||||
The property name.
|
||||
:param value:
|
||||
The property value. Will be converted to a string.
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -117,10 +174,10 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
`pytest-xdist <https://github.com/pytest-dev/pytest-xdist>`__ plugin. See
|
||||
:issue:`7767` for details.
|
||||
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:295
|
||||
tmpdir_factory [session scope] -- .../_pytest/legacypath.py:302
|
||||
Return a :class:`pytest.TempdirFactory` instance for the test session.
|
||||
|
||||
tmpdir -- .../_pytest/legacypath.py:302
|
||||
tmpdir -- .../_pytest/legacypath.py:309
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
@@ -132,9 +189,14 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a
|
||||
|
||||
The returned object is a `legacy_path`_ object.
|
||||
|
||||
.. note::
|
||||
These days, it is preferred to use ``tmp_path``.
|
||||
|
||||
:ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`.
|
||||
|
||||
.. _legacy_path: https://py.readthedocs.io/en/latest/path.html
|
||||
|
||||
caplog -- .../_pytest/logging.py:487
|
||||
caplog -- .../_pytest/logging.py:570
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following properties/methods::
|
||||
@@ -145,42 +207,49 @@ 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
|
||||
os.environ::
|
||||
The fixture provides these methods to modify objects, dictionaries, or
|
||||
:data:`os.environ`:
|
||||
|
||||
monkeypatch.setattr(obj, name, value, raising=True)
|
||||
monkeypatch.delattr(obj, name, raising=True)
|
||||
monkeypatch.setitem(mapping, name, value)
|
||||
monkeypatch.delitem(obj, name, raising=True)
|
||||
monkeypatch.setenv(name, value, prepend=None)
|
||||
monkeypatch.delenv(name, raising=True)
|
||||
monkeypatch.syspath_prepend(path)
|
||||
monkeypatch.chdir(path)
|
||||
* :meth:`monkeypatch.setattr(obj, name, value, raising=True) <pytest.MonkeyPatch.setattr>`
|
||||
* :meth:`monkeypatch.delattr(obj, name, raising=True) <pytest.MonkeyPatch.delattr>`
|
||||
* :meth:`monkeypatch.setitem(mapping, name, value) <pytest.MonkeyPatch.setitem>`
|
||||
* :meth:`monkeypatch.delitem(obj, name, raising=True) <pytest.MonkeyPatch.delitem>`
|
||||
* :meth:`monkeypatch.setenv(name, value, prepend=None) <pytest.MonkeyPatch.setenv>`
|
||||
* :meth:`monkeypatch.delenv(name, raising=True) <pytest.MonkeyPatch.delenv>`
|
||||
* :meth:`monkeypatch.syspath_prepend(path) <pytest.MonkeyPatch.syspath_prepend>`
|
||||
* :meth:`monkeypatch.chdir(path) <pytest.MonkeyPatch.chdir>`
|
||||
* :meth:`monkeypatch.context() <pytest.MonkeyPatch.context>`
|
||||
|
||||
All modifications will be undone after the requesting test function or
|
||||
fixture has finished. The ``raising`` parameter determines if a KeyError
|
||||
or AttributeError will be raised if the set/deletion operation has no target.
|
||||
fixture has finished. The ``raising`` parameter determines if a :class:`KeyError`
|
||||
or :class:`AttributeError` will be raised if the set/deletion operation does not have the
|
||||
specified target.
|
||||
|
||||
recwarn -- .../_pytest/recwarn.py:29
|
||||
To undo modifications done by the fixture in a contained scope,
|
||||
use :meth:`context() <pytest.MonkeyPatch.context>`.
|
||||
|
||||
recwarn -- .../_pytest/recwarn.py:30
|
||||
Return a :class:`WarningsRecorder` instance that records all warnings emitted by test functions.
|
||||
|
||||
See https://docs.python.org/library/how-to/capture-warnings.html for information
|
||||
See https://docs.pytest.org/en/latest/how-to/capture-warnings.html for information
|
||||
on warning categories.
|
||||
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:183
|
||||
tmp_path_factory [session scope] -- .../_pytest/tmpdir.py:245
|
||||
Return a :class:`pytest.TempPathFactory` instance for the test session.
|
||||
|
||||
tmp_path -- .../_pytest/tmpdir.py:198
|
||||
tmp_path -- .../_pytest/tmpdir.py:260
|
||||
Return a temporary directory path object which is unique to each test
|
||||
function invocation, created as a sub directory of the base temporary
|
||||
directory.
|
||||
|
||||
By default, a new base temporary directory is created each test session,
|
||||
and old bases are removed after 3 sessions, to aid in debugging. If
|
||||
``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
and old bases are removed after 3 sessions, to aid in debugging.
|
||||
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
If ``--basetemp`` is used then it is cleared each session. See :ref:`base
|
||||
temporary directory`.
|
||||
|
||||
The returned object is a :class:`pathlib.Path` object.
|
||||
|
||||
@@ -28,6 +28,548 @@ with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 7.4.3 (2023-10-24)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10447 <https://github.com/pytest-dev/pytest/issues/10447>`_: Markers are now considered in the reverse mro order to ensure base class markers are considered first -- this resolves a regression.
|
||||
|
||||
|
||||
- `#11239 <https://github.com/pytest-dev/pytest/issues/11239>`_: Fixed ``:=`` in asserts impacting unrelated test cases.
|
||||
|
||||
|
||||
- `#11439 <https://github.com/pytest-dev/pytest/issues/11439>`_: Handled an edge case where :data:`sys.stderr` might already be closed when :ref:`faulthandler` is tearing down.
|
||||
|
||||
|
||||
pytest 7.4.2 (2023-09-07)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#11237 <https://github.com/pytest-dev/pytest/issues/11237>`_: Fix doctest collection of `functools.cached_property` objects.
|
||||
|
||||
|
||||
- `#11306 <https://github.com/pytest-dev/pytest/issues/11306>`_: Fixed bug using ``--importmode=importlib`` which would cause package ``__init__.py`` files to be imported more than once in some cases.
|
||||
|
||||
|
||||
- `#11367 <https://github.com/pytest-dev/pytest/issues/11367>`_: Fixed bug where `user_properties` where not being saved in the JUnit XML file if a fixture failed during teardown.
|
||||
|
||||
|
||||
- `#11394 <https://github.com/pytest-dev/pytest/issues/11394>`_: Fixed crash when parsing long command line arguments that might be interpreted as files.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#11391 <https://github.com/pytest-dev/pytest/issues/11391>`_: Improved disclaimer on pytest plugin reference page to better indicate this is an automated, non-curated listing.
|
||||
|
||||
|
||||
pytest 7.4.1 (2023-09-02)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10337 <https://github.com/pytest-dev/pytest/issues/10337>`_: Fixed bug where fake intermediate modules generated by ``--import-mode=importlib`` would not include the
|
||||
child modules as attributes of the parent modules.
|
||||
|
||||
|
||||
- `#10702 <https://github.com/pytest-dev/pytest/issues/10702>`_: Fixed error assertion handling in :func:`pytest.approx` when ``None`` is an expected or received value when comparing dictionaries.
|
||||
|
||||
|
||||
- `#10811 <https://github.com/pytest-dev/pytest/issues/10811>`_: Fixed issue when using ``--import-mode=importlib`` together with ``--doctest-modules`` that caused modules
|
||||
to be imported more than once, causing problems with modules that have import side effects.
|
||||
|
||||
|
||||
pytest 7.4.0 (2023-06-23)
|
||||
=========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#10901 <https://github.com/pytest-dev/pytest/issues/10901>`_: Added :func:`ExceptionInfo.from_exception() <pytest.ExceptionInfo.from_exception>`, a simpler way to create an :class:`~pytest.ExceptionInfo` from an exception.
|
||||
This can replace :func:`ExceptionInfo.from_exc_info() <pytest.ExceptionInfo.from_exc_info()>` for most uses.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10872 <https://github.com/pytest-dev/pytest/issues/10872>`_: Update test log report annotation to named tuple and fixed inconsistency in docs for :hook:`pytest_report_teststatus` hook.
|
||||
|
||||
|
||||
- `#10907 <https://github.com/pytest-dev/pytest/issues/10907>`_: When an exception traceback to be displayed is completely filtered out (by mechanisms such as ``__tracebackhide__``, internal frames, and similar), now only the exception string and the following message are shown:
|
||||
|
||||
"All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames.".
|
||||
|
||||
Previously, the last frame of the traceback was shown, even though it was hidden.
|
||||
|
||||
|
||||
- `#10940 <https://github.com/pytest-dev/pytest/issues/10940>`_: Improved verbose output (``-vv``) of ``skip`` and ``xfail`` reasons by performing text wrapping while leaving a clear margin for progress output.
|
||||
|
||||
Added ``TerminalReporter.wrap_write()`` as a helper for that.
|
||||
|
||||
|
||||
- `#10991 <https://github.com/pytest-dev/pytest/issues/10991>`_: Added handling of ``%f`` directive to print microseconds in log format options, such as ``log-date-format``.
|
||||
|
||||
|
||||
- `#11005 <https://github.com/pytest-dev/pytest/issues/11005>`_: Added the underlying exception to the cache provider's path creation and write warning messages.
|
||||
|
||||
|
||||
- `#11013 <https://github.com/pytest-dev/pytest/issues/11013>`_: Added warning when :confval:`testpaths` is set, but paths are not found by glob. In this case, pytest will fall back to searching from the current directory.
|
||||
|
||||
|
||||
- `#11043 <https://github.com/pytest-dev/pytest/issues/11043>`_: When `--confcutdir` is not specified, and there is no config file present, the conftest cutoff directory (`--confcutdir`) is now set to the :ref:`rootdir <rootdir>`.
|
||||
Previously in such cases, `conftest.py` files would be probed all the way to the root directory of the filesystem.
|
||||
If you are badly affected by this change, consider adding an empty config file to your desired cutoff directory, or explicitly set `--confcutdir`.
|
||||
|
||||
|
||||
- `#11081 <https://github.com/pytest-dev/pytest/issues/11081>`_: The :confval:`norecursedirs` check is now performed in a :hook:`pytest_ignore_collect` implementation, so plugins can affect it.
|
||||
|
||||
If after updating to this version you see that your `norecursedirs` setting is not being respected,
|
||||
it means that a conftest or a plugin you use has a bad `pytest_ignore_collect` implementation.
|
||||
Most likely, your hook returns `False` for paths it does not want to ignore,
|
||||
which ends the processing and doesn't allow other plugins, including pytest itself, to ignore the path.
|
||||
The fix is to return `None` instead of `False` for paths your hook doesn't want to ignore.
|
||||
|
||||
|
||||
- `#8711 <https://github.com/pytest-dev/pytest/issues/8711>`_: :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>` and :func:`caplog.at_level() <pytest.LogCaptureFixture.at_level>`
|
||||
will temporarily enable the requested ``level`` if ``level`` was disabled globally via
|
||||
``logging.disable(LEVEL)``.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10831 <https://github.com/pytest-dev/pytest/issues/10831>`_: Terminal Reporting: Fixed bug when running in ``--tb=line`` mode where ``pytest.fail(pytrace=False)`` tests report ``None``.
|
||||
|
||||
|
||||
- `#11068 <https://github.com/pytest-dev/pytest/issues/11068>`_: Fixed the ``--last-failed`` whole-file skipping functionality ("skipped N files") for :ref:`non-python test files <non-python tests>`.
|
||||
|
||||
|
||||
- `#11104 <https://github.com/pytest-dev/pytest/issues/11104>`_: Fixed a regression in pytest 7.3.2 which caused to :confval:`testpaths` to be considered for loading initial conftests,
|
||||
even when it was not utilized (e.g. when explicit paths were given on the command line).
|
||||
Now the ``testpaths`` are only considered when they are in use.
|
||||
|
||||
|
||||
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Fixed traceback entries hidden with ``__tracebackhide__ = True`` still being shown for chained exceptions (parts after "... the above exception ..." message).
|
||||
|
||||
|
||||
- `#7781 <https://github.com/pytest-dev/pytest/issues/7781>`_: Fix writing non-encodable text to log file when using ``--debug``.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#9146 <https://github.com/pytest-dev/pytest/issues/9146>`_: Improved documentation for :func:`caplog.set_level() <pytest.LogCaptureFixture.set_level>`.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#11031 <https://github.com/pytest-dev/pytest/issues/11031>`_: Enhanced the CLI flag for ``-c`` to now include ``--config-file`` to make it clear that this flag applies to the usage of a custom config file.
|
||||
|
||||
|
||||
pytest 7.3.2 (2023-06-10)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10169 <https://github.com/pytest-dev/pytest/issues/10169>`_: Fix bug where very long option names could cause pytest to break with ``OSError: [Errno 36] File name too long`` on some systems.
|
||||
|
||||
|
||||
- `#10894 <https://github.com/pytest-dev/pytest/issues/10894>`_: Support for Python 3.12 (beta at the time of writing).
|
||||
|
||||
|
||||
- `#10987 <https://github.com/pytest-dev/pytest/issues/10987>`_: :confval:`testpaths` is now honored to load root ``conftests``.
|
||||
|
||||
|
||||
- `#10999 <https://github.com/pytest-dev/pytest/issues/10999>`_: The `monkeypatch` `setitem`/`delitem` type annotations now allow `TypedDict` arguments.
|
||||
|
||||
|
||||
- `#11028 <https://github.com/pytest-dev/pytest/issues/11028>`_: Fixed bug in assertion rewriting where a variable assigned with the walrus operator could not be used later in a function call.
|
||||
|
||||
|
||||
- `#11054 <https://github.com/pytest-dev/pytest/issues/11054>`_: Fixed ``--last-failed``'s "(skipped N files)" functionality for files inside of packages (directories with `__init__.py` files).
|
||||
|
||||
|
||||
pytest 7.3.1 (2023-04-14)
|
||||
=========================
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10875 <https://github.com/pytest-dev/pytest/issues/10875>`_: Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests.
|
||||
|
||||
|
||||
- `#10890 <https://github.com/pytest-dev/pytest/issues/10890>`_: Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10896 <https://github.com/pytest-dev/pytest/issues/10896>`_: Fixed performance regression related to :fixture:`tmp_path` and the new :confval:`tmp_path_retention_policy` option.
|
||||
|
||||
|
||||
- `#10903 <https://github.com/pytest-dev/pytest/issues/10903>`_: Fix crash ``INTERNALERROR IndexError: list index out of range`` which happens when displaying an exception where all entries are hidden.
|
||||
This reverts the change "Correctly handle ``__tracebackhide__`` for chained exceptions." introduced in version 7.3.0.
|
||||
|
||||
|
||||
pytest 7.3.0 (2023-04-08)
|
||||
=========================
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#10525 <https://github.com/pytest-dev/pytest/issues/10525>`_: Test methods decorated with ``@classmethod`` can now be discovered as tests, following the same rules as normal methods. This fills the gap that static methods were discoverable as tests but not class methods.
|
||||
|
||||
|
||||
- `#10755 <https://github.com/pytest-dev/pytest/issues/10755>`_: :confval:`console_output_style` now supports ``progress-even-when-capture-no`` to force the use of the progress output even when capture is disabled. This is useful in large test suites where capture may have significant performance impact.
|
||||
|
||||
|
||||
- `#7431 <https://github.com/pytest-dev/pytest/issues/7431>`_: ``--log-disable`` CLI option added to disable individual loggers.
|
||||
|
||||
|
||||
- `#8141 <https://github.com/pytest-dev/pytest/issues/8141>`_: Added :confval:`tmp_path_retention_count` and :confval:`tmp_path_retention_policy` configuration options to control how directories created by the :fixture:`tmp_path` fixture are kept.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10226 <https://github.com/pytest-dev/pytest/issues/10226>`_: If multiple errors are raised in teardown, we now re-raise an ``ExceptionGroup`` of them instead of discarding all but the last.
|
||||
|
||||
|
||||
- `#10658 <https://github.com/pytest-dev/pytest/issues/10658>`_: Allow ``-p`` arguments to include spaces (eg: ``-p no:logging`` instead of
|
||||
``-pno:logging``). Mostly useful in the ``addopts`` section of the configuration
|
||||
file.
|
||||
|
||||
|
||||
- `#10710 <https://github.com/pytest-dev/pytest/issues/10710>`_: Added ``start`` and ``stop`` timestamps to ``TestReport`` objects.
|
||||
|
||||
|
||||
- `#10727 <https://github.com/pytest-dev/pytest/issues/10727>`_: Split the report header for ``rootdir``, ``config file`` and ``testpaths`` so each has its own line.
|
||||
|
||||
|
||||
- `#10840 <https://github.com/pytest-dev/pytest/issues/10840>`_: pytest should no longer crash on AST with pathological position attributes, for example testing AST produced by `Hylang <https://github.com/hylang/hy>__`.
|
||||
|
||||
|
||||
- `#6267 <https://github.com/pytest-dev/pytest/issues/6267>`_: The full output of a test is no longer truncated if the truncation message would be longer than
|
||||
the hidden text. The line number shown has also been fixed.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10743 <https://github.com/pytest-dev/pytest/issues/10743>`_: The assertion rewriting mechanism now works correctly when assertion expressions contain the walrus operator.
|
||||
|
||||
|
||||
- `#10765 <https://github.com/pytest-dev/pytest/issues/10765>`_: Fixed :fixture:`tmp_path` fixture always raising :class:`OSError` on ``emscripten`` platform due to missing :func:`os.getuid`.
|
||||
|
||||
|
||||
- `#1904 <https://github.com/pytest-dev/pytest/issues/1904>`_: Correctly handle ``__tracebackhide__`` for chained exceptions.
|
||||
NOTE: This change was reverted in version 7.3.1.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#10782 <https://github.com/pytest-dev/pytest/issues/10782>`_: Fixed the minimal example in :ref:`goodpractices`: ``pip install -e .`` requires a ``version`` entry in ``pyproject.toml`` to run successfully.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#10669 <https://github.com/pytest-dev/pytest/issues/10669>`_: pytest no longer directly depends on the `attrs <https://www.attrs.org/en/stable/>`__ package. While
|
||||
we at pytest all love the package dearly and would like to thank the ``attrs`` team for many years of cooperation and support,
|
||||
it makes sense for ``pytest`` to have as little external dependencies as possible, as this helps downstream projects.
|
||||
With that in mind, we have replaced the pytest's limited internal usage to use the standard library's ``dataclasses`` instead.
|
||||
|
||||
Nice diffs for ``attrs`` classes are still supported though.
|
||||
|
||||
|
||||
pytest 7.2.2 (2023-03-03)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10533 <https://github.com/pytest-dev/pytest/issues/10533>`_: Fixed :func:`pytest.approx` handling of dictionaries containing one or more values of `0.0`.
|
||||
|
||||
|
||||
- `#10592 <https://github.com/pytest-dev/pytest/issues/10592>`_: Fixed crash if `--cache-show` and `--help` are passed at the same time.
|
||||
|
||||
|
||||
- `#10597 <https://github.com/pytest-dev/pytest/issues/10597>`_: Fixed bug where a fixture method named ``teardown`` would be called as part of ``nose`` teardown stage.
|
||||
|
||||
|
||||
- `#10626 <https://github.com/pytest-dev/pytest/issues/10626>`_: Fixed crash if ``--fixtures`` and ``--help`` are passed at the same time.
|
||||
|
||||
|
||||
- `#10660 <https://github.com/pytest-dev/pytest/issues/10660>`_: Fixed :py:func:`pytest.raises` to return a 'ContextManager' so that type-checkers could narrow
|
||||
:code:`pytest.raises(...) if ... else nullcontext()` down to 'ContextManager' rather than 'object'.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#10690 <https://github.com/pytest-dev/pytest/issues/10690>`_: Added `CI` and `BUILD_NUMBER` environment variables to the documentation.
|
||||
|
||||
|
||||
- `#10721 <https://github.com/pytest-dev/pytest/issues/10721>`_: Fixed entry-points declaration in the documentation example using Hatch.
|
||||
|
||||
|
||||
- `#10753 <https://github.com/pytest-dev/pytest/issues/10753>`_: Changed wording of the module level skip to be very explicit
|
||||
about not collecting tests and not executing the rest of the module.
|
||||
|
||||
|
||||
pytest 7.2.1 (2023-01-13)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10452 <https://github.com/pytest-dev/pytest/issues/10452>`_: Fix 'importlib.abc.TraversableResources' deprecation warning in Python 3.12.
|
||||
|
||||
|
||||
- `#10457 <https://github.com/pytest-dev/pytest/issues/10457>`_: If a test is skipped from inside a fixture, the test summary now shows the test location instead of the fixture location.
|
||||
|
||||
|
||||
- `#10506 <https://github.com/pytest-dev/pytest/issues/10506>`_: Fix bug where sometimes pytest would use the file system root directory as :ref:`rootdir <rootdir>` on Windows.
|
||||
|
||||
|
||||
- `#10607 <https://github.com/pytest-dev/pytest/issues/10607>`_: Fix a race condition when creating junitxml reports, which could occur when multiple instances of pytest execute in parallel.
|
||||
|
||||
|
||||
- `#10641 <https://github.com/pytest-dev/pytest/issues/10641>`_: Fix a race condition when creating or updating the stepwise plugin's cache, which could occur when multiple xdist worker nodes try to simultaneously update the stepwise plugin's cache.
|
||||
|
||||
|
||||
pytest 7.2.0 (2022-10-23)
|
||||
=========================
|
||||
|
||||
Deprecations
|
||||
------------
|
||||
|
||||
- `#10012 <https://github.com/pytest-dev/pytest/issues/10012>`_: Update :class:`pytest.PytestUnhandledCoroutineWarning` to a deprecation; it will raise an error in pytest 8.
|
||||
|
||||
|
||||
- `#10396 <https://github.com/pytest-dev/pytest/issues/10396>`_: pytest no longer depends on the ``py`` library. ``pytest`` provides a vendored copy of ``py.error`` and ``py.path`` modules but will use the ``py`` library if it is installed. If you need other ``py.*`` modules, continue to install the deprecated ``py`` library separately, otherwise it can usually be removed as a dependency.
|
||||
|
||||
|
||||
- `#4562 <https://github.com/pytest-dev/pytest/issues/4562>`_: Deprecate configuring hook specs/impls using attributes/marks.
|
||||
|
||||
Instead use :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec`.
|
||||
For more details, see the :ref:`docs <legacy-path-hooks-deprecated>`.
|
||||
|
||||
|
||||
- `#9886 <https://github.com/pytest-dev/pytest/issues/9886>`_: The functionality for running tests written for ``nose`` has been officially deprecated.
|
||||
|
||||
This includes:
|
||||
|
||||
* Plain ``setup`` and ``teardown`` functions and methods: this might catch users by surprise, as ``setup()`` and ``teardown()`` are not pytest idioms, but part of the ``nose`` support.
|
||||
* Setup/teardown using the `@with_setup <with-setup-nose>`_ decorator.
|
||||
|
||||
For more details, consult the :ref:`deprecation docs <nose-deprecation>`.
|
||||
|
||||
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
||||
|
||||
- `#7337 <https://github.com/pytest-dev/pytest/issues/7337>`_: A deprecation warning is now emitted if a test function returns something other than `None`. This prevents a common mistake among beginners that expect that returning a `bool` (for example `return foo(a, b) == result`) would cause a test to pass or fail, instead of using `assert`. The plan is to make returning non-`None` from tests an error in the future.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#9897 <https://github.com/pytest-dev/pytest/issues/9897>`_: Added shell-style wildcard support to ``testpaths``.
|
||||
|
||||
|
||||
|
||||
Improvements
|
||||
------------
|
||||
|
||||
- `#10218 <https://github.com/pytest-dev/pytest/issues/10218>`_: ``@pytest.mark.parametrize()`` (and similar functions) now accepts any ``Sequence[str]`` for the argument names,
|
||||
instead of just ``list[str]`` and ``tuple[str, ...]``.
|
||||
|
||||
(Note that ``str``, which is itself a ``Sequence[str]``, is still treated as a
|
||||
comma-delimited name list, as before).
|
||||
|
||||
|
||||
- `#10381 <https://github.com/pytest-dev/pytest/issues/10381>`_: The ``--no-showlocals`` flag has been added. This can be passed directly to tests to override ``--showlocals`` declared through ``addopts``.
|
||||
|
||||
|
||||
- `#3426 <https://github.com/pytest-dev/pytest/issues/3426>`_: Assertion failures with strings in NFC and NFD forms that normalize to the same string now have a dedicated error message detailing the issue, and their utf-8 representation is expressed instead.
|
||||
|
||||
|
||||
- `#8508 <https://github.com/pytest-dev/pytest/issues/8508>`_: Introduce multiline display for warning matching via :py:func:`pytest.warns` and
|
||||
enhance match comparison for :py:func:`_pytest._code.ExceptionInfo.match` as returned by :py:func:`pytest.raises`.
|
||||
|
||||
|
||||
- `#8646 <https://github.com/pytest-dev/pytest/issues/8646>`_: Improve :py:func:`pytest.raises`. Previously passing an empty tuple would give a confusing
|
||||
error. We now raise immediately with a more helpful message.
|
||||
|
||||
|
||||
- `#9741 <https://github.com/pytest-dev/pytest/issues/9741>`_: On Python 3.11, use the standard library's :mod:`tomllib` to parse TOML.
|
||||
|
||||
:mod:`tomli` is no longer a dependency on Python 3.11.
|
||||
|
||||
|
||||
- `#9742 <https://github.com/pytest-dev/pytest/issues/9742>`_: Display assertion message without escaped newline characters with ``-vv``.
|
||||
|
||||
|
||||
- `#9823 <https://github.com/pytest-dev/pytest/issues/9823>`_: Improved error message that is shown when no collector is found for a given file.
|
||||
|
||||
|
||||
- `#9873 <https://github.com/pytest-dev/pytest/issues/9873>`_: Some coloring has been added to the short test summary.
|
||||
|
||||
|
||||
- `#9883 <https://github.com/pytest-dev/pytest/issues/9883>`_: Normalize the help description of all command-line options.
|
||||
|
||||
|
||||
- `#9920 <https://github.com/pytest-dev/pytest/issues/9920>`_: Display full crash messages in ``short test summary info``, when running in a CI environment.
|
||||
|
||||
|
||||
- `#9987 <https://github.com/pytest-dev/pytest/issues/9987>`_: Added support for hidden configuration file by allowing ``.pytest.ini`` as an alternative to ``pytest.ini``.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10150 <https://github.com/pytest-dev/pytest/issues/10150>`_: :data:`sys.stdin` now contains all expected methods of a file-like object when capture is enabled.
|
||||
|
||||
|
||||
- `#10382 <https://github.com/pytest-dev/pytest/issues/10382>`_: Do not break into pdb when ``raise unittest.SkipTest()`` appears top-level in a file.
|
||||
|
||||
|
||||
- `#7792 <https://github.com/pytest-dev/pytest/issues/7792>`_: Marks are now inherited according to the full MRO in test classes. Previously, if a test class inherited from two or more classes, only marks from the first super-class would apply.
|
||||
|
||||
When inheriting marks from super-classes, marks from the sub-classes are now ordered before marks from the super-classes, in MRO order. Previously it was the reverse.
|
||||
|
||||
When inheriting marks from super-classes, the `pytestmark` attribute of the sub-class now only contains the marks directly applied to it. Previously, it also contained marks from its super-classes. Please note that this attribute should not normally be accessed directly; use :func:`pytest.Node.iter_markers` instead.
|
||||
|
||||
|
||||
- `#9159 <https://github.com/pytest-dev/pytest/issues/9159>`_: Showing inner exceptions by forcing native display in ``ExceptionGroups`` even when using display options other than ``--tb=native``. A temporary step before full implementation of pytest-native display for inner exceptions in ``ExceptionGroups``.
|
||||
|
||||
|
||||
- `#9877 <https://github.com/pytest-dev/pytest/issues/9877>`_: Ensure ``caplog.get_records(when)`` returns current/correct data after invoking ``caplog.clear()``.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#10344 <https://github.com/pytest-dev/pytest/issues/10344>`_: Update information on writing plugins to use ``pyproject.toml`` instead of ``setup.py``.
|
||||
|
||||
|
||||
- `#9248 <https://github.com/pytest-dev/pytest/issues/9248>`_: The documentation is now built using Sphinx 5.x (up from 3.x previously).
|
||||
|
||||
|
||||
- `#9291 <https://github.com/pytest-dev/pytest/issues/9291>`_: Update documentation on how :func:`pytest.warns` affects :class:`DeprecationWarning`.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#10313 <https://github.com/pytest-dev/pytest/issues/10313>`_: Made ``_pytest.doctest.DoctestItem`` export ``pytest.DoctestItem`` for
|
||||
type check and runtime purposes. Made `_pytest.doctest` use internal APIs
|
||||
to avoid circular imports.
|
||||
|
||||
|
||||
- `#9906 <https://github.com/pytest-dev/pytest/issues/9906>`_: Made ``_pytest.compat`` re-export ``importlib_metadata`` in the eyes of type checkers.
|
||||
|
||||
|
||||
- `#9910 <https://github.com/pytest-dev/pytest/issues/9910>`_: Fix default encoding warning (``EncodingWarning``) in ``cacheprovider``
|
||||
|
||||
|
||||
- `#9984 <https://github.com/pytest-dev/pytest/issues/9984>`_: Improve the error message when we attempt to access a fixture that has been
|
||||
torn down.
|
||||
Add an additional sentence to the docstring explaining when it's not a good
|
||||
idea to call ``getfixturevalue``.
|
||||
|
||||
|
||||
pytest 7.1.3 (2022-08-31)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#10060 <https://github.com/pytest-dev/pytest/issues/10060>`_: When running with ``--pdb``, ``TestCase.tearDown`` is no longer called for tests when the *class* has been skipped via ``unittest.skip`` or ``pytest.mark.skip``.
|
||||
|
||||
|
||||
- `#10190 <https://github.com/pytest-dev/pytest/issues/10190>`_: Invalid XML characters in setup or teardown error messages are now properly escaped for JUnit XML reports.
|
||||
|
||||
|
||||
- `#10230 <https://github.com/pytest-dev/pytest/issues/10230>`_: Ignore ``.py`` files created by ``pyproject.toml``-based editable builds introduced in `pip 21.3 <https://pip.pypa.io/en/stable/news/#v21-3>`__.
|
||||
|
||||
|
||||
- `#3396 <https://github.com/pytest-dev/pytest/issues/3396>`_: Doctests now respect the ``--import-mode`` flag.
|
||||
|
||||
|
||||
- `#9514 <https://github.com/pytest-dev/pytest/issues/9514>`_: Type-annotate ``FixtureRequest.param`` as ``Any`` as a stop gap measure until :issue:`8073` is fixed.
|
||||
|
||||
|
||||
- `#9791 <https://github.com/pytest-dev/pytest/issues/9791>`_: Fixed a path handling code in ``rewrite.py`` that seems to work fine, but was incorrect and fails in some systems.
|
||||
|
||||
|
||||
- `#9917 <https://github.com/pytest-dev/pytest/issues/9917>`_: Fixed string representation for :func:`pytest.approx` when used to compare tuples.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#9937 <https://github.com/pytest-dev/pytest/issues/9937>`_: Explicit note that :fixture:`tmpdir` fixture is discouraged in favour of :fixture:`tmp_path`.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#10114 <https://github.com/pytest-dev/pytest/issues/10114>`_: Replace `atomicwrites <https://github.com/untitaker/python-atomicwrites>`__ dependency on windows with `os.replace`.
|
||||
|
||||
|
||||
pytest 7.1.2 (2022-04-23)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#9726 <https://github.com/pytest-dev/pytest/issues/9726>`_: An unnecessary ``numpy`` import inside :func:`pytest.approx` was removed.
|
||||
|
||||
|
||||
- `#9820 <https://github.com/pytest-dev/pytest/issues/9820>`_: Fix comparison of ``dataclasses`` with ``InitVar``.
|
||||
|
||||
|
||||
- `#9869 <https://github.com/pytest-dev/pytest/issues/9869>`_: Increase ``stacklevel`` for the ``NODE_CTOR_FSPATH_ARG`` deprecation to point to the
|
||||
user's code, not pytest.
|
||||
|
||||
|
||||
- `#9871 <https://github.com/pytest-dev/pytest/issues/9871>`_: Fix a bizarre (and fortunately rare) bug where the `temp_path` fixture could raise
|
||||
an internal error while attempting to get the current user's username.
|
||||
|
||||
|
||||
pytest 7.1.1 (2022-03-17)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#9767 <https://github.com/pytest-dev/pytest/issues/9767>`_: Fixed a regression in pytest 7.1.0 where some conftest.py files outside of the source tree (e.g. in the `site-packages` directory) were not picked up.
|
||||
|
||||
|
||||
pytest 7.1.0 (2022-03-13)
|
||||
=========================
|
||||
|
||||
@@ -198,7 +740,7 @@ Breaking Changes
|
||||
- `#7259 <https://github.com/pytest-dev/pytest/issues/7259>`_: The :ref:`Node.reportinfo() <non-python tests>` function first return value type has been expanded from `py.path.local | str` to `os.PathLike[str] | str`.
|
||||
|
||||
Most plugins which refer to `reportinfo()` only define it as part of a custom :class:`pytest.Item` implementation.
|
||||
Since `py.path.local` is a `os.PathLike[str]`, these plugins are unaffacted.
|
||||
Since `py.path.local` is an `os.PathLike[str]`, these plugins are unaffacted.
|
||||
|
||||
Plugins and users which call `reportinfo()`, use the first return value and interact with it as a `py.path.local`, would need to adjust by calling `py.path.local(fspath)`.
|
||||
Although preferably, avoid the legacy `py.path.local` and use `pathlib.Path`, or use `item.location` or `item.path`, instead.
|
||||
@@ -2589,7 +3131,8 @@ Important
|
||||
|
||||
This release is a Python3.5+ only release.
|
||||
|
||||
For more details, see our :std:doc:`Python 2.7 and 3.4 support plan <py27-py34-deprecation>`.
|
||||
For more details, see our `Python 2.7 and 3.4 support plan
|
||||
<https://docs.pytest.org/en/7.0.x/py27-py34-deprecation.html>`_.
|
||||
|
||||
Removals
|
||||
--------
|
||||
@@ -2813,7 +3356,11 @@ Features
|
||||
|
||||
- :issue:`6870`: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``.
|
||||
|
||||
Remark: while this is technically a new feature and according to our :ref:`policy <what goes into 4.6.x releases>` it should not have been backported, we have opened an exception in this particular case because it fixes a serious interaction with ``pytest-xdist``, so it can also be considered a bugfix.
|
||||
Remark: while this is technically a new feature and according to our
|
||||
`policy <https://docs.pytest.org/en/7.0.x/py27-py34-deprecation.html#what-goes-into-4-6-x-releases>`_
|
||||
it should not have been backported, we have opened an exception in this
|
||||
particular case because it fixes a serious interaction with ``pytest-xdist``,
|
||||
so it can also be considered a bugfix.
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
@@ -2985,7 +3532,8 @@ Important
|
||||
|
||||
The ``4.6.X`` series will be the last series to support **Python 2 and Python 3.4**.
|
||||
|
||||
For more details, see our :std:doc:`Python 2.7 and 3.4 support plan <py27-py34-deprecation>`.
|
||||
For more details, see our `Python 2.7 and 3.4 support plan
|
||||
<https://docs.pytest.org/en/7.0.x/py27-py34-deprecation.html>`_.
|
||||
|
||||
|
||||
Features
|
||||
@@ -3692,7 +4240,7 @@ Removals
|
||||
See our :ref:`docs <calling fixtures directly deprecated>` on information on how to update your code.
|
||||
|
||||
|
||||
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than a existence check.
|
||||
- :issue:`4546`: Remove ``Node.get_marker(name)`` the return value was not usable for more than an existence check.
|
||||
|
||||
Use ``Node.get_closest_marker(name)`` as a replacement.
|
||||
|
||||
@@ -6153,7 +6701,7 @@ Bug Fixes
|
||||
Thanks :user:`adborden` for the report and :user:`nicoddemus` for the PR.
|
||||
|
||||
* Clean up unittest TestCase objects after tests are complete (:issue:`1649`).
|
||||
Thanks :user:`d_b_w` for the report and PR.
|
||||
Thanks :user:`d-b-w` for the report and PR.
|
||||
|
||||
|
||||
3.0.3 (2016-09-28)
|
||||
@@ -6168,7 +6716,7 @@ Bug Fixes
|
||||
Thanks :user:`nicoddemus` for the PR.
|
||||
|
||||
* Fix pkg_resources import error in Jython projects (:issue:`1853`).
|
||||
Thanks :user:`raquel-ucl` for the PR.
|
||||
Thanks :user:`raquelalegre` for the PR.
|
||||
|
||||
* Got rid of ``AttributeError: 'Module' object has no attribute '_obj'`` exception
|
||||
in Python 3 (:issue:`1944`).
|
||||
|
||||
@@ -38,6 +38,7 @@ release = ".".join(version.split(".")[:2])
|
||||
|
||||
autodoc_member_order = "bysource"
|
||||
autodoc_typehints = "description"
|
||||
autodoc_typehints_description_target = "documented"
|
||||
todo_include_todos = 1
|
||||
|
||||
latex_engine = "lualatex"
|
||||
@@ -162,11 +163,11 @@ linkcheck_workers = 5
|
||||
|
||||
_repo = "https://github.com/pytest-dev/pytest"
|
||||
extlinks = {
|
||||
"bpo": ("https://bugs.python.org/issue%s", "bpo-"),
|
||||
"pypi": ("https://pypi.org/project/%s/", ""),
|
||||
"issue": (f"{_repo}/issues/%s", "issue #"),
|
||||
"pull": (f"{_repo}/pull/%s", "pull request #"),
|
||||
"user": ("https://github.com/%s", "@"),
|
||||
"bpo": ("https://bugs.python.org/issue%s", "bpo-%s"),
|
||||
"pypi": ("https://pypi.org/project/%s/", "%s"),
|
||||
"issue": (f"{_repo}/issues/%s", "issue #%s"),
|
||||
"pull": (f"{_repo}/pull/%s", "pull request #%s"),
|
||||
"user": ("https://github.com/%s", "@%s"),
|
||||
}
|
||||
|
||||
|
||||
@@ -247,7 +248,7 @@ html_sidebars = {
|
||||
html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
html_use_index = True
|
||||
html_use_index = False
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
@@ -320,7 +321,9 @@ latex_domain_indices = False
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [("usage", "pytest", "pytest usage", ["holger krekel at merlinux eu"], 1)]
|
||||
man_pages = [
|
||||
("how-to/usage", "pytest", "pytest usage", ["holger krekel at merlinux eu"], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for Epub output ---------------------------------------------------
|
||||
@@ -338,7 +341,7 @@ epub_copyright = "2013, holger krekel et alii"
|
||||
# The scheme of the identifier. Typical schemes are ISBN or URL.
|
||||
# epub_scheme = ''
|
||||
|
||||
# The unique identifier of the text. This can be a ISBN number
|
||||
# The unique identifier of the text. This can be an ISBN number
|
||||
# or the project homepage.
|
||||
# epub_identifier = ''
|
||||
|
||||
@@ -390,6 +393,7 @@ intersphinx_mapping = {
|
||||
"tox": ("https://tox.wiki/en/stable", None),
|
||||
"virtualenv": ("https://virtualenv.pypa.io/en/stable", None),
|
||||
"setuptools": ("https://setuptools.pypa.io/en/stable", None),
|
||||
"packaging": ("https://packaging.python.org/en/latest", None),
|
||||
}
|
||||
|
||||
|
||||
@@ -417,8 +421,6 @@ def configure_logging(app: "sphinx.application.Sphinx") -> None:
|
||||
|
||||
|
||||
def setup(app: "sphinx.application.Sphinx") -> None:
|
||||
# from sphinx.ext.autodoc import cut_lines
|
||||
# app.connect('autodoc-process-docstring', cut_lines(4, what=['module']))
|
||||
app.add_crossref_type(
|
||||
"fixture",
|
||||
"fixture",
|
||||
|
||||
@@ -85,7 +85,6 @@ Further topics
|
||||
|
||||
backwards-compatibility
|
||||
deprecations
|
||||
py27-py34-deprecation
|
||||
|
||||
contributing
|
||||
development_guide
|
||||
|
||||
@@ -18,6 +18,113 @@ Deprecated Features
|
||||
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
|
||||
:class:`~pytest.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters <warnings>`.
|
||||
|
||||
|
||||
.. _nose-deprecation:
|
||||
|
||||
Support for tests written for nose
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.2
|
||||
|
||||
Support for running tests written for `nose <https://nose.readthedocs.io/en/latest/>`__ is now deprecated.
|
||||
|
||||
``nose`` has been in maintenance mode-only for years, and maintaining the plugin is not trivial as it spills
|
||||
over the code base (see :issue:`9886` for more details).
|
||||
|
||||
setup/teardown
|
||||
^^^^^^^^^^^^^^
|
||||
|
||||
One thing that might catch users by surprise is that plain ``setup`` and ``teardown`` methods are not pytest native,
|
||||
they are in fact part of the ``nose`` support.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Test:
|
||||
def setup(self):
|
||||
self.resource = make_resource()
|
||||
|
||||
def teardown(self):
|
||||
self.resource.close()
|
||||
|
||||
def test_foo(self):
|
||||
...
|
||||
|
||||
def test_bar(self):
|
||||
...
|
||||
|
||||
|
||||
|
||||
Native pytest support uses ``setup_method`` and ``teardown_method`` (see :ref:`xunit-method-setup`), so the above should be changed to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class Test:
|
||||
def setup_method(self):
|
||||
self.resource = make_resource()
|
||||
|
||||
def teardown_method(self):
|
||||
self.resource.close()
|
||||
|
||||
def test_foo(self):
|
||||
...
|
||||
|
||||
def test_bar(self):
|
||||
...
|
||||
|
||||
|
||||
This is easy to do in an entire code base by doing a simple find/replace.
|
||||
|
||||
@with_setup
|
||||
^^^^^^^^^^^
|
||||
|
||||
Code using `@with_setup <with-setup-nose>`_ such as this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from nose.tools import with_setup
|
||||
|
||||
|
||||
def setup_some_resource():
|
||||
...
|
||||
|
||||
|
||||
def teardown_some_resource():
|
||||
...
|
||||
|
||||
|
||||
@with_setup(setup_some_resource, teardown_some_resource)
|
||||
def test_foo():
|
||||
...
|
||||
|
||||
Will also need to be ported to a supported pytest style. One way to do it is using a fixture:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def setup_some_resource():
|
||||
...
|
||||
|
||||
|
||||
def teardown_some_resource():
|
||||
...
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def some_resource():
|
||||
setup_some_resource()
|
||||
yield
|
||||
teardown_some_resource()
|
||||
|
||||
|
||||
def test_foo(some_resource):
|
||||
...
|
||||
|
||||
|
||||
.. _`with-setup-nose`: https://nose.readthedocs.io/en/latest/testing_tools.html?highlight=with_setup#nose.tools.with_setup
|
||||
|
||||
.. _instance-collector-deprecation:
|
||||
|
||||
The ``pytest.Instance`` collector
|
||||
@@ -78,6 +185,50 @@ no matter what argument was used in the constructor. We expect to deprecate the
|
||||
|
||||
.. _legacy-path-hooks-deprecated:
|
||||
|
||||
Configuring hook specs/impls using markers
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Before pluggy, pytest's plugin library, was its own package and had a clear API,
|
||||
pytest just used ``pytest.mark`` to configure hooks.
|
||||
|
||||
The :py:func:`pytest.hookimpl` and :py:func:`pytest.hookspec` decorators
|
||||
have been available since years and should be used instead.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_runtest_call():
|
||||
...
|
||||
|
||||
|
||||
# or
|
||||
def pytest_runtest_call():
|
||||
...
|
||||
|
||||
|
||||
pytest_runtest_call.tryfirst = True
|
||||
|
||||
should be changed to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_runtest_call():
|
||||
...
|
||||
|
||||
Changed ``hookimpl`` attributes:
|
||||
|
||||
* ``tryfirst``
|
||||
* ``trylast``
|
||||
* ``optionalhook``
|
||||
* ``hookwrapper``
|
||||
|
||||
Changed ``hookwrapper`` attributes:
|
||||
|
||||
* ``firstresult``
|
||||
* ``historic``
|
||||
|
||||
|
||||
``py.path.local`` arguments for hooks replaced with ``pathlib.Path``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -252,6 +403,47 @@ or ``pytest.warns(Warning)``.
|
||||
|
||||
See :ref:`warns use cases` for examples.
|
||||
|
||||
|
||||
Returning non-None value in test functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 7.2
|
||||
|
||||
A :class:`pytest.PytestReturnNotNoneWarning` is now emitted if a test function returns something other than `None`.
|
||||
|
||||
This prevents a common mistake among beginners that expect that returning a `bool` would cause a test to pass or fail, for example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["a", "b", "result"],
|
||||
[
|
||||
[1, 2, 5],
|
||||
[2, 3, 8],
|
||||
[5, 3, 18],
|
||||
],
|
||||
)
|
||||
def test_foo(a, b, result):
|
||||
return foo(a, b) == result
|
||||
|
||||
Given that pytest ignores the return value, this might be surprising that it will never fail.
|
||||
|
||||
The proper fix is to change the `return` to an `assert`:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
["a", "b", "result"],
|
||||
[
|
||||
[1, 2, 5],
|
||||
[2, 3, 8],
|
||||
[5, 3, 18],
|
||||
],
|
||||
)
|
||||
def test_foo(a, b, result):
|
||||
assert foo(a, b) == result
|
||||
|
||||
|
||||
The ``--strict`` command-line option
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -860,7 +1052,7 @@ that are then turned into proper test methods. Example:
|
||||
.. code-block:: python
|
||||
|
||||
def check(x, y):
|
||||
assert x ** x == y
|
||||
assert x**x == y
|
||||
|
||||
|
||||
def test_squared():
|
||||
@@ -875,7 +1067,7 @@ This form of test function doesn't support fixtures properly, and users should s
|
||||
|
||||
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
|
||||
def test_squared(x, y):
|
||||
assert x ** x == y
|
||||
assert x**x == y
|
||||
|
||||
.. _internal classes accessed through node deprecated:
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ example: specifying and selecting acceptance tests
|
||||
self.tmpdir = request.config.mktemp(request.function.__name__, numbered=True)
|
||||
|
||||
def run(self, *cmd):
|
||||
""" called by test code to execute an acceptance test. """
|
||||
"""called by test code to execute an acceptance test."""
|
||||
self.tmpdir.chdir()
|
||||
return subprocess.check_output(cmd).decode()
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ def b(a, order):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def c(a, b, order):
|
||||
def c(b, order):
|
||||
order.append("c")
|
||||
|
||||
|
||||
|
||||
@@ -246,9 +246,9 @@ You can ask which markers exist for your test suite - the list includes our just
|
||||
|
||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures
|
||||
|
||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
|
||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.
|
||||
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead.
|
||||
|
||||
|
||||
For an example on how to add and work with markers from a plugin, see
|
||||
@@ -346,7 +346,7 @@ Custom marker and command line option to control test runs
|
||||
Plugins can provide custom markers and implement specific behaviour
|
||||
based on it. This is a self-contained example which adds a command
|
||||
line option and a parametrized test function marker to run tests
|
||||
specifies via named environments:
|
||||
specified via named environments:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -375,7 +375,7 @@ specifies via named environments:
|
||||
envnames = [mark.args[0] for mark in item.iter_markers(name="env")]
|
||||
if envnames:
|
||||
if item.config.getoption("-E") not in envnames:
|
||||
pytest.skip("test requires env in {!r}".format(envnames))
|
||||
pytest.skip(f"test requires env in {envnames!r}")
|
||||
|
||||
A test file using this local plugin:
|
||||
|
||||
@@ -438,9 +438,9 @@ The ``--markers`` option always gives you a list of available markers:
|
||||
|
||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/stable/explanation/fixtures.html#usefixtures
|
||||
|
||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
|
||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible. DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.
|
||||
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible. DEPRECATED, use @pytest.hookimpl(trylast=True) instead.
|
||||
|
||||
|
||||
.. _`passing callables to custom markers`:
|
||||
@@ -528,7 +528,7 @@ test function. From a conftest file we can read it like this:
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
for mark in item.iter_markers(name="glob"):
|
||||
print("glob args={} kwargs={}".format(mark.args, mark.kwargs))
|
||||
print(f"glob args={mark.args} kwargs={mark.kwargs}")
|
||||
sys.stdout.flush()
|
||||
|
||||
Let's run this without capturing output and see what we get:
|
||||
@@ -558,6 +558,7 @@ for your particular platform, you could use the following plugin:
|
||||
# content of conftest.py
|
||||
#
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
ALL = set("darwin linux win32".split())
|
||||
@@ -567,7 +568,7 @@ for your particular platform, you could use the following plugin:
|
||||
supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers())
|
||||
plat = sys.platform
|
||||
if supported_platforms and plat not in supported_platforms:
|
||||
pytest.skip("cannot run on platform {}".format(plat))
|
||||
pytest.skip(f"cannot run on platform {plat}")
|
||||
|
||||
then tests will be skipped if they were specified for a different platform.
|
||||
Let's do a little test file to show how this looks like:
|
||||
@@ -610,7 +611,7 @@ then you will see two tests skipped and two executed tests as expected:
|
||||
test_plat.py s.s. [100%]
|
||||
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [2] conftest.py:12: cannot run on platform linux
|
||||
SKIPPED [2] conftest.py:13: cannot run on platform linux
|
||||
======================= 2 passed, 2 skipped in 0.12s =======================
|
||||
|
||||
Note that if you specify a platform via the marker-command line option like this:
|
||||
|
||||
@@ -9,7 +9,7 @@ Working with non-python tests
|
||||
A basic example for specifying tests in Yaml files
|
||||
--------------------------------------------------------------
|
||||
|
||||
.. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py
|
||||
.. _`pytest-yamlwsgi`: https://pypi.org/project/pytest-yamlwsgi/
|
||||
|
||||
Here is an example ``conftest.py`` (extracted from Ali Afshar's special purpose `pytest-yamlwsgi`_ plugin). This ``conftest.py`` will collect ``test*.yaml`` files and will execute the yaml-formatted content as custom tests:
|
||||
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -504,9 +504,9 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||
. $ pytest -rs -q multipython.py
|
||||
sssssssssssssssssssssssssss [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [9] multipython.py:29: 'python3.5' not found
|
||||
SKIPPED [9] multipython.py:29: 'python3.6' not found
|
||||
SKIPPED [9] multipython.py:29: 'python3.7' not found
|
||||
SKIPPED [9] multipython.py:69: 'python3.5' not found
|
||||
SKIPPED [9] multipython.py:69: 'python3.6' not found
|
||||
SKIPPED [9] multipython.py:69: 'python3.7' not found
|
||||
27 skipped in 0.12s
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
@@ -574,7 +574,7 @@ If you run this with reporting for skips enabled:
|
||||
test_module.py .s [100%]
|
||||
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [1] conftest.py:12: could not import 'opt2': No module named 'opt2'
|
||||
SKIPPED [1] test_module.py:3: could not import 'opt2': No module named 'opt2'
|
||||
======================= 1 passed, 1 skipped in 0.12s =======================
|
||||
|
||||
You'll see that we don't have an ``opt2`` module and thus the second test run
|
||||
@@ -657,20 +657,17 @@ 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 is helpful to define a no-op context manager ``does_not_raise`` to serve
|
||||
as a complement to ``raises``. For example:
|
||||
It may be helpful to use ``nullcontext`` as a complement to ``raises``.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextlib import contextmanager
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@contextmanager
|
||||
def does_not_raise():
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"example_input,expectation",
|
||||
[
|
||||
@@ -687,22 +684,3 @@ as a complement to ``raises``. For example:
|
||||
|
||||
In the example above, the first three test cases should run unexceptionally,
|
||||
while the fourth should raise ``ZeroDivisionError``.
|
||||
|
||||
If you're only supporting Python 3.7+, you can simply use ``nullcontext``
|
||||
to define ``does_not_raise``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextlib import nullcontext as does_not_raise
|
||||
|
||||
Or, if you're supporting Python 3.3+ you can use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextlib import ExitStack as does_not_raise
|
||||
|
||||
Or, if desired, you can ``pip install contextlib2`` and use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from contextlib2 import nullcontext as does_not_raise
|
||||
|
||||
@@ -148,7 +148,8 @@ The test collection would look like this:
|
||||
$ pytest --collect-only
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||
rootdir: /home/sweet/project, configfile: pytest.ini
|
||||
rootdir: /home/sweet/project
|
||||
configfile: pytest.ini
|
||||
collected 2 items
|
||||
|
||||
<Module check_myapp.py>
|
||||
@@ -209,7 +210,8 @@ You can always peek at the collection tree without running tests like this:
|
||||
. $ pytest --collect-only pythoncollection.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||
rootdir: /home/sweet/project, configfile: pytest.ini
|
||||
rootdir: /home/sweet/project
|
||||
configfile: pytest.ini
|
||||
collected 3 items
|
||||
|
||||
<Module CWD/pythoncollection.py>
|
||||
@@ -290,7 +292,8 @@ file will be left out:
|
||||
$ pytest --collect-only
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||
rootdir: /home/sweet/project, configfile: pytest.ini
|
||||
rootdir: /home/sweet/project
|
||||
configfile: pytest.ini
|
||||
collected 0 items
|
||||
|
||||
======================= no tests collected in 0.12s ========================
|
||||
|
||||
@@ -144,7 +144,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
E 1
|
||||
E 1...
|
||||
E
|
||||
E ...Full output truncated (7 lines hidden), use '-vv' to show
|
||||
E ...Full output truncated (6 lines hidden), use '-vv' to show
|
||||
|
||||
failure_demo.py:60: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||
@@ -184,9 +184,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
E Left contains 1 more item:
|
||||
E {'c': 0}
|
||||
E Right contains 1 more item:
|
||||
E {'d': 0}...
|
||||
E
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
E {'d': 0}
|
||||
E Use -v to get more diff
|
||||
|
||||
failure_demo.py:71: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||
@@ -195,16 +194,15 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
|
||||
def test_eq_set(self):
|
||||
> assert {0, 10, 11, 12} == {0, 20, 21}
|
||||
E AssertionError: assert {0, 10, 11, 12} == {0, 20, 21}
|
||||
E assert {0, 10, 11, 12} == {0, 20, 21}
|
||||
E Extra items in the left set:
|
||||
E 10
|
||||
E 11
|
||||
E 12
|
||||
E Extra items in the right set:
|
||||
E 20
|
||||
E 21...
|
||||
E
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
E 21
|
||||
E Use -v to get more diff
|
||||
|
||||
failure_demo.py:74: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
||||
@@ -241,9 +239,8 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
E which
|
||||
E includes foo
|
||||
E ? +++
|
||||
E and a...
|
||||
E
|
||||
E ...Full output truncated (2 lines hidden), use '-vv' to show
|
||||
E and a
|
||||
E tail
|
||||
|
||||
failure_demo.py:84: AssertionError
|
||||
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
||||
@@ -307,9 +304,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
E ['b']
|
||||
E
|
||||
E Drill down into differing attribute b:
|
||||
E b: 'b' != 'c'...
|
||||
E
|
||||
E ...Full output truncated (3 lines hidden), use '-vv' to show
|
||||
E b: 'b' != 'c'
|
||||
E - c
|
||||
E + b
|
||||
|
||||
failure_demo.py:108: AssertionError
|
||||
________________ TestSpecialisedExplanations.test_eq_attrs _________________
|
||||
@@ -334,9 +331,9 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
E ['b']
|
||||
E
|
||||
E Drill down into differing attribute b:
|
||||
E b: 'b' != 'c'...
|
||||
E
|
||||
E ...Full output truncated (3 lines hidden), use '-vv' to show
|
||||
E b: 'b' != 'c'
|
||||
E - c
|
||||
E + b
|
||||
|
||||
failure_demo.py:120: AssertionError
|
||||
______________________________ test_attribute ______________________________
|
||||
@@ -673,7 +670,7 @@ Here is a nice run of several failures and how ``pytest`` presents things:
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list - asser...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_list_long - ...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_dict - Asser...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - Assert...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_set - assert...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_eq_longer_list
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_in_list - asser...
|
||||
FAILED failure_demo.py::TestSpecialisedExplanations::test_not_in_text_multiline
|
||||
|
||||
@@ -342,7 +342,7 @@ Example:
|
||||
def checkconfig(x):
|
||||
__tracebackhide__ = True
|
||||
if not hasattr(x, "config"):
|
||||
pytest.fail("not configured: {}".format(x))
|
||||
pytest.fail(f"not configured: {x}")
|
||||
|
||||
|
||||
def test_something():
|
||||
@@ -376,6 +376,7 @@ this to make sure unexpected exception types aren't hidden:
|
||||
.. code-block:: python
|
||||
|
||||
import operator
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -386,7 +387,7 @@ this to make sure unexpected exception types aren't hidden:
|
||||
def checkconfig(x):
|
||||
__tracebackhide__ = operator.methodcaller("errisinstance", ConfigException)
|
||||
if not hasattr(x, "config"):
|
||||
raise ConfigException("not configured: {}".format(x))
|
||||
raise ConfigException(f"not configured: {x}")
|
||||
|
||||
|
||||
def test_something():
|
||||
@@ -565,6 +566,7 @@ an ``incremental`` marker which is to be used on classes:
|
||||
# content of conftest.py
|
||||
|
||||
from typing import Dict, Tuple
|
||||
|
||||
import pytest
|
||||
|
||||
# store history of failures per test class name and per index in parametrize (if parametrize used)
|
||||
@@ -608,7 +610,7 @@ an ``incremental`` marker which is to be used on classes:
|
||||
test_name = _test_failed_incremental[cls_name].get(parametrize_index, None)
|
||||
# if name found, test has failed for the combination of class name & test name
|
||||
if test_name is not None:
|
||||
pytest.xfail("previous test failed ({})".format(test_name))
|
||||
pytest.xfail(f"previous test failed ({test_name})")
|
||||
|
||||
|
||||
These two hook implementations work together to abort incremental-marked
|
||||
@@ -659,8 +661,7 @@ If we run this:
|
||||
|
||||
test_step.py:11: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
XFAIL test_step.py::TestUserHandling::test_deletion
|
||||
reason: previous test failed (test_modification)
|
||||
XFAIL test_step.py::TestUserHandling::test_deletion - reason: previous test failed (test_modification)
|
||||
================== 1 failed, 2 passed, 1 xfailed in 0.12s ==================
|
||||
|
||||
We'll see that ``test_deletion`` was not executed because ``test_modification``
|
||||
@@ -690,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()
|
||||
|
||||
@@ -802,9 +803,10 @@ case we just write some information out to a ``failures`` file:
|
||||
|
||||
# content of conftest.py
|
||||
|
||||
import pytest
|
||||
import os.path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
@@ -815,7 +817,7 @@ case we just write some information out to a ``failures`` file:
|
||||
# we only look at actual failing test calls, not setup/teardown
|
||||
if rep.when == "call" and rep.failed:
|
||||
mode = "a" if os.path.exists("failures") else "w"
|
||||
with open("failures", mode) as f:
|
||||
with open("failures", mode, encoding="utf-8") as f:
|
||||
# let's also access a fixture for the fun of it
|
||||
if "tmp_path" in item.fixturenames:
|
||||
extra = " ({})".format(item.funcargs["tmp_path"])
|
||||
@@ -890,8 +892,11 @@ here is a little example implemented via a local plugin:
|
||||
.. code-block:: python
|
||||
|
||||
# content of conftest.py
|
||||
|
||||
from typing import Dict
|
||||
import pytest
|
||||
from pytest import StashKey, CollectReport
|
||||
|
||||
phase_report_key = StashKey[Dict[str, CollectReport]]()
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
|
||||
@@ -900,10 +905,9 @@ here is a little example implemented via a local plugin:
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
|
||||
# set a report attribute for each phase of a call, which can
|
||||
# store test results for each phase of a call, which can
|
||||
# be "setup", "call", "teardown"
|
||||
|
||||
setattr(item, "rep_" + rep.when, rep)
|
||||
item.stash.setdefault(phase_report_key, {})[rep.when] = rep
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -911,11 +915,11 @@ here is a little example implemented via a local plugin:
|
||||
yield
|
||||
# request.node is an "item" because we use the default
|
||||
# "function" scope
|
||||
if request.node.rep_setup.failed:
|
||||
print("setting up a test failed!", request.node.nodeid)
|
||||
elif request.node.rep_setup.passed:
|
||||
if request.node.rep_call.failed:
|
||||
print("executing test failed", request.node.nodeid)
|
||||
report = request.node.stash[phase_report_key]
|
||||
if report["setup"].failed:
|
||||
print("setting up a test failed or skipped", request.node.nodeid)
|
||||
elif ("call" not in report) or report["call"].failed:
|
||||
print("executing test failed or skipped", request.node.nodeid)
|
||||
|
||||
|
||||
if you then have failing tests:
|
||||
@@ -953,8 +957,8 @@ and run it:
|
||||
rootdir: /home/sweet/project
|
||||
collected 3 items
|
||||
|
||||
test_module.py Esetting up a test failed! test_module.py::test_setup_fails
|
||||
Fexecuting test failed test_module.py::test_call_fails
|
||||
test_module.py Esetting up a test failed or skipped test_module.py::test_setup_fails
|
||||
Fexecuting test failed or skipped test_module.py::test_call_fails
|
||||
F
|
||||
|
||||
================================== ERRORS ==================================
|
||||
@@ -1066,6 +1070,7 @@ like ``pytest-timeout`` they must be imported explicitly and passed on to pytest
|
||||
|
||||
# contents of app_main.py
|
||||
import sys
|
||||
|
||||
import pytest_timeout # Third party plugin
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "--pytest":
|
||||
|
||||
@@ -94,7 +94,7 @@ Mark Lapierre discusses the `Pros and Cons of Quarantined Tests <https://dev.to/
|
||||
CI tools that rerun on failure
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Azure Pipelines (the Azure cloud CI/CD tool, formerly Visual Studio Team Services or VSTS) has a feature to `identify flaky tests <https://docs.microsoft.com/en-us/azure/devops/release-notes/2017/dec-11-vsts#identify-flaky-tests>`_ and rerun failed tests.
|
||||
Azure Pipelines (the Azure cloud CI/CD tool, formerly Visual Studio Team Services or VSTS) has a feature to `identify flaky tests <https://docs.microsoft.com/en-us/previous-versions/azure/devops/2017/dec-11-vsts?view=tfs-2017#identify-flaky-tests>`_ and rerun failed tests.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -12,41 +12,27 @@ For development, we recommend you use :mod:`venv` for virtual environments and
|
||||
as well as the ``pytest`` package itself.
|
||||
This ensures your code and dependencies are isolated from your system Python installation.
|
||||
|
||||
Next, place a ``pyproject.toml`` file in the root of your package:
|
||||
Create a ``pyproject.toml`` file in the root of your repository as described in
|
||||
:doc:`packaging:tutorials/packaging-projects`.
|
||||
The first few lines should look like this:
|
||||
|
||||
.. code-block:: toml
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=42", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
and a ``setup.cfg`` file containing your package's metadata with the following minimum content:
|
||||
[project]
|
||||
name = "PACKAGENAME"
|
||||
version = "PACKAGEVERSION"
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[metadata]
|
||||
name = PACKAGENAME
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
|
||||
where ``PACKAGENAME`` is the name of your package.
|
||||
|
||||
.. note::
|
||||
|
||||
If your pip version is older than ``21.3``, you'll also need a ``setup.py`` file:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
setup()
|
||||
where ``PACKAGENAME`` and ``PACKAGEVERSION`` are the name and version of your package respectively.
|
||||
|
||||
You can then install your package in "editable" mode by running from the same directory:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install -e .
|
||||
pip install -e .
|
||||
|
||||
which lets you change your source code (both tests and application) and rerun tests at will.
|
||||
|
||||
@@ -65,8 +51,8 @@ Conventions for Python test discovery
|
||||
* In those directories, search for ``test_*.py`` or ``*_test.py`` files, imported by their `test package name`_.
|
||||
* From those files, collect test items:
|
||||
|
||||
* ``test`` prefixed test functions or methods outside of class
|
||||
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method)
|
||||
* ``test`` prefixed test functions or methods outside of class.
|
||||
* ``test`` prefixed test functions or methods inside ``Test`` prefixed test classes (without an ``__init__`` method). Methods decorated with ``@staticmethod`` and ``@classmethods`` are also considered.
|
||||
|
||||
For examples of how to customize your test discovery :doc:`/example/pythoncollection`.
|
||||
|
||||
@@ -89,11 +75,11 @@ to keep tests separate from actual application code (often a good idea):
|
||||
.. code-block:: text
|
||||
|
||||
pyproject.toml
|
||||
setup.cfg
|
||||
mypkg/
|
||||
__init__.py
|
||||
app.py
|
||||
view.py
|
||||
src/
|
||||
mypkg/
|
||||
__init__.py
|
||||
app.py
|
||||
view.py
|
||||
tests/
|
||||
test_app.py
|
||||
test_view.py
|
||||
@@ -103,84 +89,57 @@ This has the following benefits:
|
||||
|
||||
* Your tests can run against an installed version after executing ``pip install .``.
|
||||
* Your tests can run against the local copy with an editable install after executing ``pip install --editable .``.
|
||||
* If you don't use an editable install and are relying on the fact that Python by default puts the current
|
||||
directory in ``sys.path`` to import your package, you can execute ``python -m pytest`` to execute the tests against the
|
||||
local copy directly, without using ``pip``.
|
||||
|
||||
For new projects, we recommend to use ``importlib`` :ref:`import mode <import-modes>`
|
||||
(see which-import-mode_ for a detailed explanation).
|
||||
To this end, add the following to your ``pyproject.toml``:
|
||||
|
||||
.. code-block:: toml
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = [
|
||||
"--import-mode=importlib",
|
||||
]
|
||||
|
||||
.. _src-layout:
|
||||
|
||||
Generally, but especially if you use the default import mode ``prepend``,
|
||||
it is **strongly** suggested to use a ``src`` layout.
|
||||
Here, your application root package resides in a sub-directory of your root,
|
||||
i.e. ``src/mypkg/`` instead of ``mypkg``.
|
||||
|
||||
This layout prevents a lot of common pitfalls and has many benefits,
|
||||
which are better explained in this excellent `blog post`_ by Ionel Cristian Mărieș.
|
||||
|
||||
.. _blog post: https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure>
|
||||
|
||||
.. note::
|
||||
|
||||
If you do not use an editable install and use the ``src`` layout as above you need to extend the Python's
|
||||
search path for module files to execute the tests against the local copy directly. You can do it in an
|
||||
ad-hoc manner by setting the ``PYTHONPATH`` environment variable:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
PYTHONPATH=src pytest
|
||||
|
||||
or in a permanent manner by using the :confval:`pythonpath` configuration variable and adding the
|
||||
following to your ``pyproject.toml``:
|
||||
|
||||
.. code-block:: toml
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
pythonpath = "src"
|
||||
|
||||
.. note::
|
||||
|
||||
If you do not use an editable install and not use the ``src`` layout (``mypkg`` directly in the root
|
||||
directory) you can rely on the fact that Python by default puts the current directory in ``sys.path`` to
|
||||
import your package and run ``python -m pytest`` to execute the tests against the local copy directly.
|
||||
|
||||
See :ref:`pytest vs python -m pytest` for more information about the difference between calling ``pytest`` and
|
||||
``python -m pytest``.
|
||||
|
||||
Note that this scheme has a drawback if you are using ``prepend`` :ref:`import mode <import-modes>`
|
||||
(which is the default): your test files must have **unique names**, because
|
||||
``pytest`` will import them as *top-level* modules since there are no packages
|
||||
to derive a full package name from. In other words, the test files in the example above will
|
||||
be imported as ``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to
|
||||
``sys.path``.
|
||||
|
||||
If you need to have test modules with the same name, you might add ``__init__.py`` files to your
|
||||
``tests`` folder and subfolders, changing them to packages:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pyproject.toml
|
||||
setup.cfg
|
||||
mypkg/
|
||||
...
|
||||
tests/
|
||||
__init__.py
|
||||
foo/
|
||||
__init__.py
|
||||
test_view.py
|
||||
bar/
|
||||
__init__.py
|
||||
test_view.py
|
||||
|
||||
Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test_view``, allowing
|
||||
you to have modules with the same name. But now this introduces a subtle problem: in order to load
|
||||
the test modules from the ``tests`` directory, pytest prepends the root of the repository to
|
||||
``sys.path``, which adds the side-effect that now ``mypkg`` is also importable.
|
||||
|
||||
This is problematic if you are using a tool like `tox`_ to test your package in a virtual environment,
|
||||
because you want to test the *installed* version of your package, not the local code from the repository.
|
||||
|
||||
.. _`src-layout`:
|
||||
|
||||
In this situation, it is **strongly** suggested to use a ``src`` layout where application root package resides in a
|
||||
sub-directory of your root:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pyproject.toml
|
||||
setup.cfg
|
||||
src/
|
||||
mypkg/
|
||||
__init__.py
|
||||
app.py
|
||||
view.py
|
||||
tests/
|
||||
__init__.py
|
||||
foo/
|
||||
__init__.py
|
||||
test_view.py
|
||||
bar/
|
||||
__init__.py
|
||||
test_view.py
|
||||
|
||||
|
||||
This layout prevents a lot of common pitfalls and has many benefits, which are better explained in this excellent
|
||||
`blog post by Ionel Cristian Mărieș <https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure>`_.
|
||||
|
||||
.. note::
|
||||
The new ``--import-mode=importlib`` (see :ref:`import-modes`) doesn't have
|
||||
any of the drawbacks above because ``sys.path`` is not changed when importing
|
||||
test modules, so users that run
|
||||
into this issue are strongly encouraged to try it and report if the new option works well for them.
|
||||
|
||||
The ``src`` directory layout is still strongly recommended however.
|
||||
|
||||
|
||||
Tests as part of application code
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
@@ -191,12 +150,11 @@ want to distribute them along with your application:
|
||||
.. code-block:: text
|
||||
|
||||
pyproject.toml
|
||||
setup.cfg
|
||||
mypkg/
|
||||
[src/]mypkg/
|
||||
__init__.py
|
||||
app.py
|
||||
view.py
|
||||
test/
|
||||
tests/
|
||||
__init__.py
|
||||
test_app.py
|
||||
test_view.py
|
||||
@@ -254,6 +212,56 @@ Note that this layout also works in conjunction with the ``src`` layout mentione
|
||||
much less surprising.
|
||||
|
||||
|
||||
.. _which-import-mode:
|
||||
|
||||
Choosing an import mode
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
For historical reasons, pytest defaults to the ``prepend`` :ref:`import mode <import-modes>`
|
||||
instead of the ``importlib`` import mode we recommend for new projects.
|
||||
The reason lies in the way the ``prepend`` mode works:
|
||||
|
||||
Since there are no packages to derive a full package name from,
|
||||
``pytest`` will import your test files as *top-level* modules.
|
||||
The test files in the first example (:ref:`src layout <src-layout>`) would be imported as
|
||||
``test_app`` and ``test_view`` top-level modules by adding ``tests/`` to ``sys.path``.
|
||||
|
||||
This results in a drawback compared to the import mode ``importlib``:
|
||||
your test files must have **unique names**.
|
||||
|
||||
If you need to have test modules with the same name,
|
||||
as a workaround you might add ``__init__.py`` files to your ``tests`` folder and subfolders,
|
||||
changing them to packages:
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
pyproject.toml
|
||||
mypkg/
|
||||
...
|
||||
tests/
|
||||
__init__.py
|
||||
foo/
|
||||
__init__.py
|
||||
test_view.py
|
||||
bar/
|
||||
__init__.py
|
||||
test_view.py
|
||||
|
||||
Now pytest will load the modules as ``tests.foo.test_view`` and ``tests.bar.test_view``,
|
||||
allowing you to have modules with the same name.
|
||||
But now this introduces a subtle problem:
|
||||
in order to load the test modules from the ``tests`` directory,
|
||||
pytest prepends the root of the repository to ``sys.path``,
|
||||
which adds the side-effect that now ``mypkg`` is also importable.
|
||||
|
||||
This is problematic if you are using a tool like tox_ to test your package in a virtual environment,
|
||||
because you want to test the *installed* version of your package,
|
||||
not the local code from the repository.
|
||||
|
||||
The ``importlib`` import mode does not have any of the drawbacks above,
|
||||
because ``sys.path`` is not changed when importing test modules.
|
||||
|
||||
|
||||
.. _`buildout`: http://www.buildout.org/en/latest/
|
||||
|
||||
.. _`use tox`:
|
||||
@@ -263,8 +271,8 @@ tox
|
||||
|
||||
Once you are done with your work and want to make sure that your actual
|
||||
package passes all tests you may want to look into :doc:`tox <tox:index>`, the
|
||||
virtualenv test automation tool and its :doc:`pytest support <tox:example/pytest>`.
|
||||
tox helps you to setup virtualenv environments with pre-defined
|
||||
virtualenv test automation tool.
|
||||
``tox`` helps you to setup virtualenv environments with pre-defined
|
||||
dependencies and then executing a pre-configured test command with
|
||||
options. It will run tests against the installed package and not
|
||||
against your source code checkout, helping to detect packaging
|
||||
@@ -286,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.
|
||||
|
||||
@@ -16,7 +16,7 @@ import process can be controlled through the ``--import-mode`` command-line flag
|
||||
these values:
|
||||
|
||||
* ``prepend`` (default): the directory path containing each module will be inserted into the *beginning*
|
||||
of :py:data:`sys.path` if not already there, and then imported with the :func:`__import__ <__import__>` builtin.
|
||||
of :py:data:`sys.path` if not already there, and then imported with the :func:`importlib.import_module <importlib.import_module>` function.
|
||||
|
||||
This requires test module names to be unique when the test directory tree is not arranged in
|
||||
packages, because the modules will put in :py:data:`sys.modules` after importing.
|
||||
@@ -24,7 +24,7 @@ these values:
|
||||
This is the classic mechanism, dating back from the time Python 2 was still supported.
|
||||
|
||||
* ``append``: the directory containing each module is appended to the end of :py:data:`sys.path` if not already
|
||||
there, and imported with ``__import__``.
|
||||
there, and imported with :func:`importlib.import_module <importlib.import_module>`.
|
||||
|
||||
This better allows to run test modules against installed versions of a package even if the
|
||||
package under test has the same import root. For example:
|
||||
@@ -43,12 +43,21 @@ these values:
|
||||
Same as ``prepend``, requires test module names to be unique when the test directory tree is
|
||||
not arranged in packages, because the modules will put in :py:data:`sys.modules` after importing.
|
||||
|
||||
* ``importlib``: new in pytest-6.0, this mode uses :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
|
||||
* ``importlib``: new in pytest-6.0, this mode uses more fine control mechanisms provided by :mod:`importlib` to import test modules. This gives full control over the import process, and doesn't require changing :py:data:`sys.path`.
|
||||
|
||||
For this reason this doesn't require test module names to be unique, but also makes test
|
||||
modules non-importable by each other.
|
||||
For this reason this doesn't require test module names to be unique.
|
||||
|
||||
One drawback however is that test modules are non-importable by each other. Also, utility
|
||||
modules in the tests directories are not automatically importable because the tests directory is no longer
|
||||
added to :py:data:`sys.path`.
|
||||
|
||||
Initially we intended to make ``importlib`` the default in future releases, however it is clear now that
|
||||
it has its own set of drawbacks so the default will remain ``prepend`` for the foreseeable future.
|
||||
|
||||
.. seealso::
|
||||
|
||||
The :confval:`pythonpath` configuration variable.
|
||||
|
||||
We intend to make ``importlib`` the default in future releases, depending on feedback.
|
||||
|
||||
``prepend`` and ``append`` import modes scenarios
|
||||
-------------------------------------------------
|
||||
|
||||
@@ -22,7 +22,7 @@ Install ``pytest``
|
||||
.. code-block:: bash
|
||||
|
||||
$ pytest --version
|
||||
pytest 7.1.0
|
||||
pytest 7.4.3
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -238,7 +237,7 @@ file which provides an alternative explanation for ``Foo`` objects:
|
||||
if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
|
||||
return [
|
||||
"Comparing Foo instances:",
|
||||
" vals: {} != {}".format(left.val, right.val),
|
||||
f" vals: {left.val} != {right.val}",
|
||||
]
|
||||
|
||||
now, given this test module:
|
||||
|
||||
@@ -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
|
||||
--------------------------------
|
||||
@@ -199,7 +206,6 @@ across pytest invocations:
|
||||
|
||||
# content of test_caching.py
|
||||
import pytest
|
||||
import time
|
||||
|
||||
|
||||
def expensive_computation():
|
||||
@@ -234,7 +240,7 @@ If you run this command for the first time, you can see the print statement:
|
||||
> assert mydata == 23
|
||||
E assert 42 == 23
|
||||
|
||||
test_caching.py:20: AssertionError
|
||||
test_caching.py:19: AssertionError
|
||||
-------------------------- Captured stdout setup ---------------------------
|
||||
running expensive computation...
|
||||
========================= short test summary info ==========================
|
||||
@@ -257,7 +263,7 @@ the cache and nothing will be printed:
|
||||
> assert mydata == 23
|
||||
E assert 42 == 23
|
||||
|
||||
test_caching.py:20: AssertionError
|
||||
test_caching.py:19: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_caching.py::test_function - assert 42 == 23
|
||||
1 failed in 0.12s
|
||||
|
||||
@@ -42,6 +42,8 @@ Running pytest now produces this output:
|
||||
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
|
||||
======================= 1 passed, 1 warning in 0.12s =======================
|
||||
|
||||
.. _`controlling-warnings`:
|
||||
|
||||
Controlling warnings
|
||||
--------------------
|
||||
|
||||
@@ -107,6 +109,18 @@ When a warning matches more than one option in the list, the action for the last
|
||||
is performed.
|
||||
|
||||
|
||||
.. note::
|
||||
|
||||
The ``-W`` flag and the ``filterwarnings`` ini option use warning filters that are
|
||||
similar in structure, but each configuration option interprets its filter
|
||||
differently. For example, *message* in ``filterwarnings`` is a string containing a
|
||||
regular expression that the start of the warning message must match,
|
||||
case-insensitively, while *message* in ``-W`` is a literal string that the start of
|
||||
the warning message must contain (case-insensitively), ignoring any whitespace at
|
||||
the start or end of message. Consult the `warning filter`_ documentation for more
|
||||
details.
|
||||
|
||||
|
||||
.. _`filterwarnings`:
|
||||
|
||||
``@pytest.mark.filterwarnings``
|
||||
@@ -176,11 +190,14 @@ using an external system.
|
||||
DeprecationWarning and PendingDeprecationWarning
|
||||
------------------------------------------------
|
||||
|
||||
|
||||
By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings from
|
||||
user code and third-party libraries, as recommended by :pep:`565`.
|
||||
This helps users keep their code modern and avoid breakages when deprecated warnings are effectively removed.
|
||||
|
||||
However, in the specific case where users capture any type of warnings in their test, either with
|
||||
:func:`pytest.warns`, :func:`pytest.deprecated_call` or using the :ref:`recwarn <recwarn>` fixture,
|
||||
no warning will be displayed at all.
|
||||
|
||||
Sometimes it is useful to hide some specific deprecation warnings that happen in code that you have no control over
|
||||
(such as third-party libraries), in which case you might use the warning filters options (ini or marks) to ignore
|
||||
those warnings.
|
||||
@@ -197,6 +214,9 @@ For example:
|
||||
This will ignore all warnings of type ``DeprecationWarning`` where the start of the message matches
|
||||
the regular expression ``".*U.*mode is deprecated"``.
|
||||
|
||||
See :ref:`@pytest.mark.filterwarnings <filterwarnings>` and
|
||||
:ref:`Controlling warnings <controlling-warnings>` for more examples.
|
||||
|
||||
.. note::
|
||||
|
||||
If warnings are configured at the interpreter level, using
|
||||
@@ -245,14 +265,15 @@ when called with a ``17`` argument.
|
||||
Asserting warnings with the warns function
|
||||
------------------------------------------
|
||||
|
||||
|
||||
|
||||
You can check that code raises a particular warning using :func:`pytest.warns`,
|
||||
which works in a similar manner to :ref:`raises <assertraises>`:
|
||||
which works in a similar manner to :ref:`raises <assertraises>` (except that
|
||||
:ref:`raises <assertraises>` does not capture all exceptions, only the
|
||||
``expected_exception``):
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import warnings
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -260,21 +281,35 @@ which works in a similar manner to :ref:`raises <assertraises>`:
|
||||
with pytest.warns(UserWarning):
|
||||
warnings.warn("my warning", UserWarning)
|
||||
|
||||
The test will fail if the warning in question is not raised. The keyword
|
||||
argument ``match`` to assert that the exception matches a text or regex::
|
||||
The test will fail if the warning in question is not raised. Use the keyword
|
||||
argument ``match`` to assert that the warning matches a text or regex.
|
||||
To match a literal string that may contain regular expression metacharacters like ``(`` or ``.``, the pattern can
|
||||
first be escaped with ``re.escape``.
|
||||
|
||||
>>> with warns(UserWarning, match='must be 0 or None'):
|
||||
Some examples:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
|
||||
>>> with warns(UserWarning, match="must be 0 or None"):
|
||||
... warnings.warn("value must be 0 or None", UserWarning)
|
||||
...
|
||||
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
>>> with warns(UserWarning, match=r"must be \d+$"):
|
||||
... warnings.warn("value must be 42", UserWarning)
|
||||
...
|
||||
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
>>> with warns(UserWarning, match=r"must be \d+$"):
|
||||
... warnings.warn("this is not here", UserWarning)
|
||||
...
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Failed: DID NOT WARN. No warnings of type ...UserWarning... were emitted...
|
||||
|
||||
>>> with warns(UserWarning, match=re.escape("issue with foo() func")):
|
||||
... warnings.warn("issue with foo() func")
|
||||
...
|
||||
|
||||
You can also call :func:`pytest.warns` on a function or code string:
|
||||
|
||||
.. code-block:: python
|
||||
@@ -358,20 +393,32 @@ Additional use cases of warnings in tests
|
||||
|
||||
Here are some use cases involving warnings that often come up in tests, and suggestions on how to deal with them:
|
||||
|
||||
- To ensure that **at least one** warning is emitted, use:
|
||||
- To ensure that **at least one** of the indicated warnings is issued, use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with pytest.warns():
|
||||
def test_warning():
|
||||
with pytest.warns((RuntimeWarning, UserWarning)):
|
||||
...
|
||||
|
||||
- To ensure that **only** certain warnings are issued, use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_warning(recwarn):
|
||||
...
|
||||
assert len(recwarn) == 1
|
||||
user_warning = recwarn.pop(UserWarning)
|
||||
assert issubclass(user_warning.category, UserWarning)
|
||||
|
||||
- To ensure that **no** warnings are emitted, use:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
...
|
||||
def test_warning():
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter("error")
|
||||
...
|
||||
|
||||
- To suppress warnings, use:
|
||||
|
||||
|
||||
@@ -126,14 +126,17 @@ pytest also introduces new options:
|
||||
in expected doctest output.
|
||||
|
||||
* ``NUMBER``: when enabled, floating-point numbers only need to match as far as
|
||||
the precision you have written in the expected doctest output. For example,
|
||||
the following output would only need to match to 2 decimal places::
|
||||
the precision you have written in the expected doctest output. The numbers are
|
||||
compared using :func:`pytest.approx` with relative tolerance equal to the
|
||||
precision. For example, the following output would only need to match to 2
|
||||
decimal places when comparing ``3.14`` to
|
||||
``pytest.approx(math.pi, rel=10**-2)``::
|
||||
|
||||
>>> math.pi
|
||||
3.14
|
||||
|
||||
If you wrote ``3.1416`` then the actual output would need to match to 4
|
||||
decimal places; and so on.
|
||||
If you wrote ``3.1416`` then the actual output would need to match to
|
||||
approximately 4 decimal places; and so on.
|
||||
|
||||
This avoids false positives caused by limited floating-point precision, like
|
||||
this::
|
||||
@@ -239,7 +242,6 @@ which can then be used in your doctests directly:
|
||||
>>> len(a)
|
||||
10
|
||||
"""
|
||||
pass
|
||||
|
||||
Note that like the normal ``conftest.py``, the fixtures are discovered in the directory tree conftest is in.
|
||||
Meaning that if you put your doctest with your source code, the relevant conftest.py needs to be in the same directory tree.
|
||||
|
||||
@@ -398,9 +398,10 @@ access the fixture function:
|
||||
.. code-block:: python
|
||||
|
||||
# content of conftest.py
|
||||
import pytest
|
||||
import smtplib
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def smtp_connection():
|
||||
@@ -609,10 +610,10 @@ Here's what that might look like:
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_emaillib.py
|
||||
import pytest
|
||||
|
||||
from emaillib import Email, MailAdminClient
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mail_admin():
|
||||
@@ -630,6 +631,7 @@ Here's what that might look like:
|
||||
def receiving_user(mail_admin):
|
||||
user = mail_admin.create_user()
|
||||
yield user
|
||||
user.clear_mailbox()
|
||||
mail_admin.delete_user(user)
|
||||
|
||||
|
||||
@@ -683,10 +685,10 @@ Here's how the previous example would look using the ``addfinalizer`` method:
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_emaillib.py
|
||||
import pytest
|
||||
|
||||
from emaillib import Email, MailAdminClient
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mail_admin():
|
||||
@@ -736,6 +738,87 @@ does offer some nuances for when you're in a pinch.
|
||||
. [100%]
|
||||
1 passed in 0.12s
|
||||
|
||||
Note on finalizer order
|
||||
""""""""""""""""""""""""
|
||||
|
||||
Finalizers are executed in a first-in-last-out order.
|
||||
For yield fixtures, the first teardown code to run is from the right-most fixture, i.e. the last test parameter.
|
||||
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_finalizers.py
|
||||
import pytest
|
||||
|
||||
|
||||
def test_bar(fix_w_yield1, fix_w_yield2):
|
||||
print("test_bar")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fix_w_yield1():
|
||||
yield
|
||||
print("after_yield_1")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fix_w_yield2():
|
||||
yield
|
||||
print("after_yield_2")
|
||||
|
||||
|
||||
.. code-block:: pytest
|
||||
|
||||
$ pytest -s test_finalizers.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||
rootdir: /home/sweet/project
|
||||
collected 1 item
|
||||
|
||||
test_finalizers.py test_bar
|
||||
.after_yield_2
|
||||
after_yield_1
|
||||
|
||||
|
||||
============================ 1 passed in 0.12s =============================
|
||||
|
||||
For finalizers, the first fixture to run is last call to `request.addfinalizer`.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_finalizers.py
|
||||
from functools import partial
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fix_w_finalizers(request):
|
||||
request.addfinalizer(partial(print, "finalizer_2"))
|
||||
request.addfinalizer(partial(print, "finalizer_1"))
|
||||
|
||||
|
||||
def test_bar(fix_w_finalizers):
|
||||
print("test_bar")
|
||||
|
||||
|
||||
.. code-block:: pytest
|
||||
|
||||
$ pytest -s test_finalizers.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||
rootdir: /home/sweet/project
|
||||
collected 1 item
|
||||
|
||||
test_finalizers.py test_bar
|
||||
.finalizer_1
|
||||
finalizer_2
|
||||
|
||||
|
||||
============================ 1 passed in 0.12s =============================
|
||||
|
||||
This is so because yield fixtures use `addfinalizer` behind the scenes: when the fixture executes, `addfinalizer` registers a function that resumes the generator, which in turn calls the teardown code.
|
||||
|
||||
|
||||
.. _`safe teardowns`:
|
||||
|
||||
Safe teardowns
|
||||
@@ -752,10 +835,10 @@ above):
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_emaillib.py
|
||||
import pytest
|
||||
|
||||
from emaillib import Email, MailAdminClient
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup():
|
||||
@@ -1030,16 +1113,17 @@ read an optional server URL from the test module which uses our fixture:
|
||||
.. code-block:: python
|
||||
|
||||
# content of conftest.py
|
||||
import pytest
|
||||
import smtplib
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def smtp_connection(request):
|
||||
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
|
||||
smtp_connection = smtplib.SMTP(server, 587, timeout=5)
|
||||
yield smtp_connection
|
||||
print("finalizing {} ({})".format(smtp_connection, server))
|
||||
print(f"finalizing {smtp_connection} ({server})")
|
||||
smtp_connection.close()
|
||||
|
||||
We use the ``request.module`` attribute to optionally obtain an
|
||||
@@ -1153,7 +1237,6 @@ If the data created by the factory requires managing, the fixture can take care
|
||||
|
||||
@pytest.fixture
|
||||
def make_customer_record():
|
||||
|
||||
created_records = []
|
||||
|
||||
def _make_customer_record(name):
|
||||
@@ -1193,15 +1276,16 @@ through the special :py:class:`request <FixtureRequest>` object:
|
||||
.. code-block:: python
|
||||
|
||||
# content of conftest.py
|
||||
import pytest
|
||||
import smtplib
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="module", params=["smtp.gmail.com", "mail.python.org"])
|
||||
def smtp_connection(request):
|
||||
smtp_connection = smtplib.SMTP(request.param, 587, timeout=5)
|
||||
yield smtp_connection
|
||||
print("finalizing {}".format(smtp_connection))
|
||||
print(f"finalizing {smtp_connection}")
|
||||
smtp_connection.close()
|
||||
|
||||
The main change is the declaration of ``params`` with
|
||||
@@ -1332,13 +1416,15 @@ Running the above tests results in the following test IDs being used:
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||
rootdir: /home/sweet/project
|
||||
collected 11 items
|
||||
collected 12 items
|
||||
|
||||
<Module test_anothersmtp.py>
|
||||
<Function test_showhelo[smtp.gmail.com]>
|
||||
<Function test_showhelo[mail.python.org]>
|
||||
<Module test_emaillib.py>
|
||||
<Function test_email_received>
|
||||
<Module test_finalizers.py>
|
||||
<Function test_bar>
|
||||
<Module test_ids.py>
|
||||
<Function test_a[spam]>
|
||||
<Function test_a[ham]>
|
||||
@@ -1350,7 +1436,7 @@ Running the above tests results in the following test IDs being used:
|
||||
<Function test_ehlo[mail.python.org]>
|
||||
<Function test_noop[mail.python.org]>
|
||||
|
||||
======================= 11 tests collected in 0.12s ========================
|
||||
======================= 12 tests collected in 0.12s ========================
|
||||
|
||||
.. _`fixture-parametrize-marks`:
|
||||
|
||||
@@ -1503,7 +1589,7 @@ to show the setup/teardown flow:
|
||||
|
||||
|
||||
def test_2(otherarg, modarg):
|
||||
print(" RUN test2 with otherarg {} and modarg {}".format(otherarg, modarg))
|
||||
print(f" RUN test2 with otherarg {otherarg} and modarg {modarg}")
|
||||
|
||||
|
||||
Let's run the tests in verbose mode and with looking at the print-output:
|
||||
@@ -1604,6 +1690,7 @@ and declare its use in a test module via a ``usefixtures`` marker:
|
||||
|
||||
# content of test_setenv.py
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -1611,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):
|
||||
@@ -1684,8 +1771,6 @@ Given the tests file structure is:
|
||||
::
|
||||
|
||||
tests/
|
||||
__init__.py
|
||||
|
||||
conftest.py
|
||||
# content of tests/conftest.py
|
||||
import pytest
|
||||
@@ -1700,8 +1785,6 @@ Given the tests file structure is:
|
||||
assert username == 'username'
|
||||
|
||||
subfolder/
|
||||
__init__.py
|
||||
|
||||
conftest.py
|
||||
# content of tests/subfolder/conftest.py
|
||||
import pytest
|
||||
@@ -1710,8 +1793,8 @@ Given the tests file structure is:
|
||||
def username(username):
|
||||
return 'overridden-' + username
|
||||
|
||||
test_something.py
|
||||
# content of tests/subfolder/test_something.py
|
||||
test_something_else.py
|
||||
# content of tests/subfolder/test_something_else.py
|
||||
def test_username(username):
|
||||
assert username == 'overridden-username'
|
||||
|
||||
@@ -1727,8 +1810,6 @@ Given the tests file structure is:
|
||||
::
|
||||
|
||||
tests/
|
||||
__init__.py
|
||||
|
||||
conftest.py
|
||||
# content of tests/conftest.py
|
||||
import pytest
|
||||
@@ -1770,8 +1851,6 @@ Given the tests file structure is:
|
||||
::
|
||||
|
||||
tests/
|
||||
__init__.py
|
||||
|
||||
conftest.py
|
||||
# content of tests/conftest.py
|
||||
import pytest
|
||||
@@ -1808,8 +1887,6 @@ Given the tests file structure is:
|
||||
::
|
||||
|
||||
tests/
|
||||
__init__.py
|
||||
|
||||
conftest.py
|
||||
# content of tests/conftest.py
|
||||
import pytest
|
||||
|
||||
@@ -55,6 +55,13 @@ These options can also be customized through ``pytest.ini`` file:
|
||||
log_format = %(asctime)s %(levelname)s %(message)s
|
||||
log_date_format = %Y-%m-%d %H:%M:%S
|
||||
|
||||
Specific loggers can be disabled via ``--log-disable={logger_name}``.
|
||||
This argument can be passed multiple times:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --log-disable=main --log-disable=testing
|
||||
|
||||
Further it is possible to disable reporting of captured content (stdout,
|
||||
stderr and logs) on failed tests completely with:
|
||||
|
||||
@@ -73,7 +80,6 @@ messages. This is supported by the ``caplog`` fixture:
|
||||
|
||||
def test_foo(caplog):
|
||||
caplog.set_level(logging.INFO)
|
||||
pass
|
||||
|
||||
By default the level is set on the root logger,
|
||||
however as a convenience it is also possible to set the log level of any
|
||||
@@ -83,7 +89,6 @@ logger:
|
||||
|
||||
def test_foo(caplog):
|
||||
caplog.set_level(logging.CRITICAL, logger="root.baz")
|
||||
pass
|
||||
|
||||
The log levels set are restored automatically at the end of the test.
|
||||
|
||||
@@ -161,9 +166,7 @@ the records for the ``setup`` and ``call`` stages during teardown like so:
|
||||
x.message for x in caplog.get_records(when) if x.levelno == logging.WARNING
|
||||
]
|
||||
if messages:
|
||||
pytest.fail(
|
||||
"warning messages encountered during testing: {}".format(messages)
|
||||
)
|
||||
pytest.fail(f"warning messages encountered during testing: {messages}")
|
||||
|
||||
|
||||
|
||||
@@ -180,8 +183,8 @@ logging records as they are emitted directly into the console.
|
||||
|
||||
You can specify the logging level for which log records with equal or higher
|
||||
level are printed to the console by passing ``--log-cli-level``. This setting
|
||||
accepts the logging level names as seen in python's documentation or an integer
|
||||
as the logging level num.
|
||||
accepts the logging level names or numeric values as seen in
|
||||
:ref:`logging's documentation <python:levels>`.
|
||||
|
||||
Additionally, you can also specify ``--log-cli-format`` and
|
||||
``--log-cli-date-format`` which mirror and default to ``--log-format`` and
|
||||
@@ -198,11 +201,12 @@ option names are:
|
||||
If you need to record the whole test suite logging calls to a file, you can pass
|
||||
``--log-file=/path/to/log/file``. This log file is opened in write mode which
|
||||
means that it will be overwritten at each run tests session.
|
||||
Note that relative paths for the log-file location, whether passed on the CLI or declared in a
|
||||
config file, are always resolved relative to the current working directory.
|
||||
|
||||
You can also specify the logging level for the log file by passing
|
||||
``--log-file-level``. This setting accepts the logging level names as seen in
|
||||
python's documentation(ie, uppercased level names) or an integer as the logging
|
||||
level num.
|
||||
``--log-file-level``. This setting accepts the logging level names or numeric
|
||||
values as seen in :ref:`logging's documentation <python:levels>`.
|
||||
|
||||
Additionally, you can also specify ``--log-file-format`` and
|
||||
``--log-file-date-format`` which are equal to ``--log-format`` and
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
How to monkeypatch/mock modules and environments
|
||||
================================================================
|
||||
|
||||
.. currentmodule:: _pytest.monkeypatch
|
||||
.. currentmodule:: pytest
|
||||
|
||||
Sometimes tests need to invoke functionality which depends
|
||||
on global settings or which invokes code which cannot be easily
|
||||
@@ -14,17 +14,16 @@ environment variable, or to modify ``sys.path`` for importing.
|
||||
The ``monkeypatch`` fixture provides these helper methods for safely patching and mocking
|
||||
functionality in tests:
|
||||
|
||||
.. code-block:: python
|
||||
* :meth:`monkeypatch.setattr(obj, name, value, raising=True) <pytest.MonkeyPatch.setattr>`
|
||||
* :meth:`monkeypatch.delattr(obj, name, raising=True) <pytest.MonkeyPatch.delattr>`
|
||||
* :meth:`monkeypatch.setitem(mapping, name, value) <pytest.MonkeyPatch.setitem>`
|
||||
* :meth:`monkeypatch.delitem(obj, name, raising=True) <pytest.MonkeyPatch.delitem>`
|
||||
* :meth:`monkeypatch.setenv(name, value, prepend=None) <pytest.MonkeyPatch.setenv>`
|
||||
* :meth:`monkeypatch.delenv(name, raising=True) <pytest.MonkeyPatch.delenv>`
|
||||
* :meth:`monkeypatch.syspath_prepend(path) <pytest.MonkeyPatch.syspath_prepend>`
|
||||
* :meth:`monkeypatch.chdir(path) <pytest.MonkeyPatch.chdir>`
|
||||
* :meth:`monkeypatch.context() <pytest.MonkeyPatch.context>`
|
||||
|
||||
monkeypatch.setattr(obj, name, value, raising=True)
|
||||
monkeypatch.setattr("somemodule.obj.name", value, raising=True)
|
||||
monkeypatch.delattr(obj, name, raising=True)
|
||||
monkeypatch.setitem(mapping, name, value)
|
||||
monkeypatch.delitem(obj, name, raising=True)
|
||||
monkeypatch.setenv(name, value, prepend=None)
|
||||
monkeypatch.delenv(name, raising=True)
|
||||
monkeypatch.syspath_prepend(path)
|
||||
monkeypatch.chdir(path)
|
||||
|
||||
All modifications will be undone after the requesting
|
||||
test function or fixture has finished. The ``raising``
|
||||
@@ -55,13 +54,16 @@ during a test.
|
||||
5. Use :py:meth:`monkeypatch.syspath_prepend <MonkeyPatch.syspath_prepend>` to modify ``sys.path`` which will also
|
||||
call ``pkg_resources.fixup_namespace_packages`` and :py:func:`importlib.invalidate_caches`.
|
||||
|
||||
6. Use :py:meth:`monkeypatch.context <MonkeyPatch.context>` to apply patches only in a specific scope, which can help
|
||||
control teardown of complex fixtures or patches to the stdlib.
|
||||
|
||||
See the `monkeypatch blog post`_ for some introduction material
|
||||
and a discussion of its motivation.
|
||||
|
||||
.. _`monkeypatch blog post`: https://tetamap.wordpress.com//2009/03/03/monkeypatching-in-unit-tests-done-right/
|
||||
|
||||
Simple example: monkeypatching functions
|
||||
----------------------------------------
|
||||
Monkeypatching functions
|
||||
------------------------
|
||||
|
||||
Consider a scenario where you are working with user directories. In the context of
|
||||
testing, you do not want your test to depend on the running user. ``monkeypatch``
|
||||
@@ -133,10 +135,10 @@ This can be done in our test file by defining a class to represent ``r``.
|
||||
# this is the previous code block example
|
||||
import app
|
||||
|
||||
|
||||
# custom class to be the mock return value
|
||||
# will override the requests.Response returned from requests.get
|
||||
class MockResponse:
|
||||
|
||||
# mock json() method always returns a specific testing dictionary
|
||||
@staticmethod
|
||||
def json():
|
||||
@@ -144,7 +146,6 @@ This can be done in our test file by defining a class to represent ``r``.
|
||||
|
||||
|
||||
def test_get_json(monkeypatch):
|
||||
|
||||
# Any arguments may be passed and mock_get() will always return our
|
||||
# mocked object, which only has the .json() method.
|
||||
def mock_get(*args, **kwargs):
|
||||
@@ -179,6 +180,7 @@ This mock can be shared across tests using a ``fixture``:
|
||||
# app.py that includes the get_json() function
|
||||
import app
|
||||
|
||||
|
||||
# custom class to be the mock return value of requests.get()
|
||||
class MockResponse:
|
||||
@staticmethod
|
||||
@@ -356,7 +358,6 @@ For testing purposes we can patch the ``DEFAULT_CONFIG`` dictionary to specific
|
||||
|
||||
|
||||
def test_connection(monkeypatch):
|
||||
|
||||
# Patch the values of DEFAULT_CONFIG to specific
|
||||
# testing values only for this test.
|
||||
monkeypatch.setitem(app.DEFAULT_CONFIG, "user", "test_user")
|
||||
@@ -381,7 +382,6 @@ You can use the :py:meth:`monkeypatch.delitem <MonkeyPatch.delitem>` to remove v
|
||||
|
||||
|
||||
def test_missing_user(monkeypatch):
|
||||
|
||||
# patch the DEFAULT_CONFIG t be missing the 'user' key
|
||||
monkeypatch.delitem(app.DEFAULT_CONFIG, "user", raising=False)
|
||||
|
||||
@@ -402,6 +402,7 @@ separate fixtures for each potential mock and reference them in the needed tests
|
||||
# app.py with the connection string function
|
||||
import app
|
||||
|
||||
|
||||
# all of the mocks are moved into separated fixtures
|
||||
@pytest.fixture
|
||||
def mock_test_user(monkeypatch):
|
||||
@@ -423,7 +424,6 @@ separate fixtures for each potential mock and reference them in the needed tests
|
||||
|
||||
# tests reference only the fixture mocks that are needed
|
||||
def test_connection(mock_test_user, mock_test_database):
|
||||
|
||||
expected = "User Id=test_user; Location=test_db;"
|
||||
|
||||
result = app.create_connection_string()
|
||||
@@ -431,12 +431,11 @@ separate fixtures for each potential mock and reference them in the needed tests
|
||||
|
||||
|
||||
def test_missing_user(mock_missing_default_user):
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
_ = app.create_connection_string()
|
||||
|
||||
|
||||
.. currentmodule:: _pytest.monkeypatch
|
||||
.. currentmodule:: pytest
|
||||
|
||||
API Reference
|
||||
-------------
|
||||
|
||||
@@ -5,6 +5,9 @@ How to run tests written for nose
|
||||
|
||||
``pytest`` has basic support for running tests written for nose_.
|
||||
|
||||
.. warning::
|
||||
This functionality has been deprecated and is likely to be removed in ``pytest 8.x``.
|
||||
|
||||
.. _nosestyle:
|
||||
|
||||
Usage
|
||||
@@ -23,8 +26,8 @@ make use of pytest's capabilities.
|
||||
Supported nose Idioms
|
||||
----------------------
|
||||
|
||||
* setup and teardown at module/class/method level
|
||||
* SkipTest exceptions and markers
|
||||
* ``setup()`` and ``teardown()`` at module/class/method level: any function or method called ``setup`` will be called during the setup phase for each test, same for ``teardown``.
|
||||
* ``SkipTest`` exceptions and markers
|
||||
* setup/teardown decorators
|
||||
* ``__test__`` attribute on modules/classes/functions
|
||||
* general usage of nose utilities
|
||||
|
||||
@@ -12,8 +12,9 @@ Examples for modifying traceback printing:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest --showlocals # show local variables in tracebacks
|
||||
pytest -l # show local variables (shortcut)
|
||||
pytest --showlocals # show local variables in tracebacks
|
||||
pytest -l # show local variables (shortcut)
|
||||
pytest --no-showlocals # hide local variables (if addopts enables them)
|
||||
|
||||
pytest --tb=auto # (default) 'long' tracebacks for the first and last
|
||||
# entry, but 'short' style for the other entries
|
||||
@@ -166,9 +167,9 @@ Now we can increase pytest's verbosity:
|
||||
E Right contains 4 more items:
|
||||
E {'10': 10, '20': 20, '30': 30, '40': 40}
|
||||
E Full diff:
|
||||
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}...
|
||||
E
|
||||
E ...Full output truncated (3 lines hidden), use '-vv' to show
|
||||
E - {'0': 0, '10': 10, '20': 20, '30': 30, '40': 40}
|
||||
E ? - - - - - - - -
|
||||
E + {'0': 0, '1': 1, '2': 2, '3': 3, '4': 4}
|
||||
|
||||
test_verbosity_example.py:14: AssertionError
|
||||
___________________________ test_long_text_fail ____________________________
|
||||
@@ -348,8 +349,7 @@ Example:
|
||||
test_example.py:14: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
SKIPPED [1] test_example.py:22: skipping this test
|
||||
XFAIL test_example.py::test_xfail
|
||||
reason: xfailing this test
|
||||
XFAIL test_example.py::test_xfail - reason: xfailing this test
|
||||
XPASS test_example.py::test_xpass always xfail
|
||||
ERROR test_example.py::test_error - assert 0
|
||||
FAILED test_example.py::test_fail - assert 0
|
||||
|
||||
@@ -51,6 +51,9 @@ Here is a little annotated list for some popular plugins:
|
||||
* :pypi:`pytest-flakes`:
|
||||
check source code with pyflakes.
|
||||
|
||||
* :pypi:`allure-pytest`:
|
||||
report test results via `allure-framework <https://github.com/allure-framework/>`_.
|
||||
|
||||
To see a complete list of all plugins with their latest testing
|
||||
status against different pytest and Python versions, please visit
|
||||
:ref:`plugin-list`.
|
||||
|
||||
@@ -69,6 +69,7 @@ It is also possible to skip the whole module using
|
||||
.. code-block:: python
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
if not sys.platform.startswith("win"):
|
||||
@@ -409,6 +410,7 @@ test instances when using parametrize:
|
||||
.. code-block:: python
|
||||
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -104,8 +104,21 @@ The ``tmpdir`` and ``tmpdir_factory`` fixtures
|
||||
|
||||
The ``tmpdir`` and ``tmpdir_factory`` fixtures are similar to ``tmp_path``
|
||||
and ``tmp_path_factory``, but use/return legacy `py.path.local`_ objects
|
||||
rather than standard :class:`pathlib.Path` objects. These days, prefer to
|
||||
use ``tmp_path`` and ``tmp_path_factory``.
|
||||
rather than standard :class:`pathlib.Path` objects.
|
||||
|
||||
.. note::
|
||||
These days, it is preferred to use ``tmp_path`` and ``tmp_path_factory``.
|
||||
|
||||
In order to help modernize old code bases, one can run pytest with the legacypath
|
||||
plugin disabled:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest -p no:legacypath
|
||||
|
||||
This will trigger errors on tests using the legacy paths.
|
||||
It can also be permanently set as part of the :confval:`addopts` parameter in the
|
||||
config file.
|
||||
|
||||
See :fixture:`tmpdir <tmpdir>` :fixture:`tmpdir_factory <tmpdir_factory>`
|
||||
API for details.
|
||||
@@ -118,10 +131,12 @@ The default base temporary directory
|
||||
|
||||
Temporary directories are by default created as sub-directories of
|
||||
the system temporary directory. The base name will be ``pytest-NUM`` where
|
||||
``NUM`` will be incremented with each test run. Moreover, entries older
|
||||
than 3 temporary directories will be removed.
|
||||
``NUM`` will be incremented with each test run.
|
||||
By default, entries older than 3 temporary directories will be removed.
|
||||
This behavior can be configured with :confval:`tmp_path_retention_count` and
|
||||
:confval:`tmp_path_retention_policy`.
|
||||
|
||||
The number of entries currently cannot be changed, but using the ``--basetemp``
|
||||
Using the ``--basetemp``
|
||||
option will remove the directory before every run, effectively meaning the temporary directories
|
||||
of only the most recent run will be kept.
|
||||
|
||||
|
||||
@@ -27,12 +27,15 @@ Almost all ``unittest`` features are supported:
|
||||
* ``setUpClass/tearDownClass``;
|
||||
* ``setUpModule/tearDownModule``;
|
||||
|
||||
.. _`pytest-subtests`: https://github.com/pytest-dev/pytest-subtests
|
||||
.. _`load_tests protocol`: https://docs.python.org/3/library/unittest.html#load-tests-protocol
|
||||
|
||||
Additionally, :ref:`subtests <python:subtests>` are supported by the
|
||||
`pytest-subtests`_ plugin.
|
||||
|
||||
Up to this point pytest does not have support for the following features:
|
||||
|
||||
* `load_tests protocol`_;
|
||||
* :ref:`subtests <python:subtests>`;
|
||||
|
||||
Benefits out of the box
|
||||
-----------------------
|
||||
@@ -115,6 +118,7 @@ fixture definition:
|
||||
# content of test_unittest_db.py
|
||||
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@@ -153,7 +157,7 @@ the ``self.db`` values in the traceback:
|
||||
E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
|
||||
E assert 0
|
||||
|
||||
test_unittest_db.py:10: AssertionError
|
||||
test_unittest_db.py:11: AssertionError
|
||||
___________________________ MyTest.test_method2 ____________________________
|
||||
|
||||
self = <test_unittest_db.MyTest testMethod=test_method2>
|
||||
@@ -163,7 +167,7 @@ the ``self.db`` values in the traceback:
|
||||
E AssertionError: <conftest.db_class.<locals>.DummyDB object at 0xdeadbeef0001>
|
||||
E assert 0
|
||||
|
||||
test_unittest_db.py:13: AssertionError
|
||||
test_unittest_db.py:14: AssertionError
|
||||
========================= short test summary info ==========================
|
||||
FAILED test_unittest_db.py::MyTest::test_method1 - AssertionError: <conft...
|
||||
FAILED test_unittest_db.py::MyTest::test_method2 - AssertionError: <conft...
|
||||
@@ -194,19 +198,19 @@ creation of a per-test temporary directory:
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_unittest_cleandir.py
|
||||
import os
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class MyTest(unittest.TestCase):
|
||||
@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,11 +35,12 @@ Pytest supports several ways to run and select tests from the command-line.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pytest -k "MyClass and not method"
|
||||
pytest -k 'MyClass and not method'
|
||||
|
||||
This will run tests which contain names that match the given *string expression* (case-insensitive),
|
||||
which can include Python operators that use filenames, class names and function names as variables.
|
||||
The example above will run ``TestMyClass.test_something`` but not ``TestMyClass.test_method_simple``.
|
||||
Use ``""`` instead of ``''`` in expression when running this on Windows
|
||||
|
||||
.. _nodeids:
|
||||
|
||||
@@ -172,7 +173,8 @@ You can invoke ``pytest`` from Python code directly:
|
||||
|
||||
this acts as if you would call "pytest" from the command line.
|
||||
It will not raise :class:`SystemExit` but return the :ref:`exit code <exit-codes>` instead.
|
||||
You can pass in options and arguments:
|
||||
If you don't pass it any arguments, ``main`` reads the arguments from the command line arguments of the process (:data:`sys.argv`), which may be undesirable.
|
||||
You can pass in options and arguments explicitly:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -183,9 +185,10 @@ You can specify additional plugins to ``pytest.main``:
|
||||
.. code-block:: python
|
||||
|
||||
# content of myinvoke.py
|
||||
import pytest
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
class MyPlugin:
|
||||
def pytest_sessionfinish(self):
|
||||
|
||||
@@ -194,7 +194,7 @@ class or module can then be passed to the ``pluginmanager`` using the ``pytest_a
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_addhooks(pluginmanager):
|
||||
""" This example assumes the hooks are grouped in the 'sample_hook' module. """
|
||||
"""This example assumes the hooks are grouped in the 'sample_hook' module."""
|
||||
from my_app.tests import sample_hook
|
||||
|
||||
pluginmanager.add_hookspecs(sample_hook)
|
||||
@@ -249,18 +249,19 @@ and use pytest_addoption as follows:
|
||||
|
||||
# contents of hooks.py
|
||||
|
||||
|
||||
# Use firstresult=True because we only want one plugin to define this
|
||||
# default value
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_config_file_default_value():
|
||||
""" Return the default value for the config file command line option. """
|
||||
"""Return the default value for the config file command line option."""
|
||||
|
||||
|
||||
# contents of myplugin.py
|
||||
|
||||
|
||||
def pytest_addhooks(pluginmanager):
|
||||
""" This example assumes the hooks are grouped in the 'hooks' module. """
|
||||
"""This example assumes the hooks are grouped in the 'hooks' module."""
|
||||
from . import hooks
|
||||
|
||||
pluginmanager.add_hookspecs(hooks)
|
||||
|
||||
@@ -147,29 +147,32 @@ Making your plugin installable by others
|
||||
|
||||
If you want to make your plugin externally available, you
|
||||
may define a so-called entry point for your distribution so
|
||||
that ``pytest`` finds your plugin module. Entry points are
|
||||
a feature that is provided by :std:doc:`setuptools:index`. pytest looks up
|
||||
the ``pytest11`` entrypoint to discover its
|
||||
plugins and you can thus make your plugin available by defining
|
||||
it in your setuptools-invocation:
|
||||
that ``pytest`` finds your plugin module. Entry points are
|
||||
a feature that is provided by :std:doc:`setuptools <setuptools:index>`.
|
||||
|
||||
.. sourcecode:: python
|
||||
pytest looks up the ``pytest11`` entrypoint to discover its
|
||||
plugins, thus you can make your plugin available by defining
|
||||
it in your ``pyproject.toml`` file.
|
||||
|
||||
# sample ./setup.py file
|
||||
from setuptools import setup
|
||||
.. sourcecode:: toml
|
||||
|
||||
setup(
|
||||
name="myproject",
|
||||
packages=["myproject"],
|
||||
# the following makes a plugin available to pytest
|
||||
entry_points={"pytest11": ["name_of_plugin = myproject.pluginmodule"]},
|
||||
# custom PyPI classifier for pytest plugins
|
||||
classifiers=["Framework :: Pytest"],
|
||||
)
|
||||
# sample ./pyproject.toml file
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "myproject"
|
||||
classifiers = [
|
||||
"Framework :: Pytest",
|
||||
]
|
||||
|
||||
[project.entry-points.pytest11]
|
||||
myproject = "myproject.pluginmodule"
|
||||
|
||||
If a package is installed this way, ``pytest`` will load
|
||||
``myproject.pluginmodule`` as a plugin which can define
|
||||
:ref:`hooks <hook-reference>`.
|
||||
:ref:`hooks <hook-reference>`. Confirm registration with ``pytest --trace-config``
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -367,7 +370,7 @@ string value of ``Hello World!`` if we do not supply a value or ``Hello
|
||||
def _hello(name=None):
|
||||
if not name:
|
||||
name = request.config.getoption("name")
|
||||
return "Hello {name}!".format(name=name)
|
||||
return f"Hello {name}!"
|
||||
|
||||
return _hello
|
||||
|
||||
@@ -446,7 +449,8 @@ in our ``pytest.ini`` to tell pytest where to look for example files.
|
||||
$ pytest
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-7.x.y, pluggy-1.x.y
|
||||
rootdir: /home/sweet/project, configfile: pytest.ini
|
||||
rootdir: /home/sweet/project
|
||||
configfile: pytest.ini
|
||||
collected 2 items
|
||||
|
||||
test_example.py .. [100%]
|
||||
|
||||
@@ -32,7 +32,7 @@ which will usually be called once for all the functions:
|
||||
.. code-block:: python
|
||||
|
||||
def setup_module(module):
|
||||
""" setup any state specific to the execution of the given module."""
|
||||
"""setup any state specific to the execution of the given module."""
|
||||
|
||||
|
||||
def teardown_module(module):
|
||||
@@ -63,6 +63,8 @@ and after all test methods of the class are called:
|
||||
setup_class.
|
||||
"""
|
||||
|
||||
.. _xunit-method-setup:
|
||||
|
||||
Method and function level setup/teardown
|
||||
-----------------------------------------------
|
||||
|
||||
|
||||
@@ -2,16 +2,10 @@
|
||||
|
||||
.. sidebar:: Next Open Trainings
|
||||
|
||||
- `PyConDE <https://2022.pycon.de/program/W93DBJ/>`__, April 11th 2022 (3h), Berlin, Germany
|
||||
- `PyConIT <https://pycon.it/en/talk/pytest-simple-rapid-and-fun-testing-with-python>`__, June 3rd 2022 (4h), Florence, Italy
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, March 7th to 9th 2023 (3 day in-depth training), Remote and Leipzig, Germany
|
||||
- `Professional Testing with Python <https://python-academy.com/courses/python_course_testing.html>`_, via `Python Academy <https://www.python-academy.com/>`_, **March 5th to 7th 2024** (3 day in-depth training), **Leipzig, Germany / Remote**
|
||||
|
||||
Also see :doc:`previous talks and blogposts <talks>`.
|
||||
|
||||
..
|
||||
- `Europython <https://ep2022.europython.eu/>`__, July 11th to 17th (3h), Dublin, Ireland
|
||||
- `CH Open Workshoptage <https://workshoptage.ch/>`__ (German), September 6th to 8th (1 day), Bern, Switzerland
|
||||
|
||||
.. _features:
|
||||
|
||||
pytest: helps you write better programs
|
||||
@@ -27,8 +21,6 @@ scale to support complex functional testing for applications and libraries.
|
||||
|
||||
**PyPI package name**: :pypi:`pytest`
|
||||
|
||||
**Documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
|
||||
|
||||
|
||||
A quick example
|
||||
---------------
|
||||
@@ -104,11 +96,6 @@ Bugs/Requests
|
||||
Please use the `GitHub issue tracker <https://github.com/pytest-dev/pytest/issues>`_ to submit bugs or request features.
|
||||
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
Consult the :ref:`Changelog <changelog>` page for fixes and enhancements of each version.
|
||||
|
||||
Support pytest
|
||||
--------------
|
||||
|
||||
@@ -141,13 +128,3 @@ Security
|
||||
pytest has never been associated with a security vulnerability, but in any case, to report a
|
||||
security vulnerability please use the `Tidelift security contact <https://tidelift.com/security>`_.
|
||||
Tidelift will coordinate the fix and disclosure.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Copyright Holger Krekel and others, 2004.
|
||||
|
||||
Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
|
||||
|
||||
.. _`MIT`: https://github.com/pytest-dev/pytest/blob/main/LICENSE
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
Python 2.7 and 3.4 support
|
||||
==========================
|
||||
|
||||
It is demanding on the maintainers of an open source project to support many Python versions, as
|
||||
there's extra cost of keeping code compatible between all versions, while holding back on
|
||||
features only made possible on newer Python versions.
|
||||
|
||||
In case of Python 2 and 3, the difference between the languages makes it even more prominent,
|
||||
because many new Python 3 features cannot be used in a Python 2/3 compatible code base.
|
||||
|
||||
Python 2.7 EOL has been reached :pep:`in 2020 <0373#maintenance-releases>`, with
|
||||
the last release made in April, 2020.
|
||||
|
||||
Python 3.4 EOL has been reached :pep:`in 2019 <0429#release-schedule>`, with the last release made in March, 2019.
|
||||
|
||||
For those reasons, in Jun 2019 it was decided that **pytest 4.6** series will be the last to support Python 2.7 and 3.4.
|
||||
|
||||
What this means for general users
|
||||
---------------------------------
|
||||
|
||||
Thanks to the `python_requires`_ setuptools option,
|
||||
Python 2.7 and Python 3.4 users using a modern pip version
|
||||
will install the last pytest 4.6.X version automatically even if 5.0 or later versions
|
||||
are available on PyPI.
|
||||
|
||||
Users should ensure they are using the latest pip and setuptools versions for this to work.
|
||||
|
||||
Maintenance of 4.6.X versions
|
||||
-----------------------------
|
||||
|
||||
Until January 2020, the pytest core team ported many bug-fixes from the main release into the
|
||||
``4.6.x`` branch, with several 4.6.X releases being made along the year.
|
||||
|
||||
From now on, the core team will **no longer actively backport patches**, but the ``4.6.x``
|
||||
branch will continue to exist so the community itself can contribute patches.
|
||||
|
||||
The core team will be happy to accept those patches, and make new 4.6.X releases **until mid-2020**
|
||||
(but consider that date as a ballpark, after that date the team might still decide to make new releases
|
||||
for critical bugs).
|
||||
|
||||
.. _`python_requires`: https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
|
||||
|
||||
Technical aspects
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
(This section is a transcript from :issue:`5275`).
|
||||
|
||||
In this section we describe the technical aspects of the Python 2.7 and 3.4 support plan.
|
||||
|
||||
.. _what goes into 4.6.x releases:
|
||||
|
||||
What goes into 4.6.X releases
|
||||
+++++++++++++++++++++++++++++
|
||||
|
||||
New 4.6.X releases will contain bug fixes only.
|
||||
|
||||
When will 4.6.X releases happen
|
||||
+++++++++++++++++++++++++++++++
|
||||
|
||||
New 4.6.X releases will happen after we have a few bugs in place to release, or if a few weeks have
|
||||
passed (say a single bug has been fixed a month after the latest 4.6.X release).
|
||||
|
||||
No hard rules here, just ballpark.
|
||||
|
||||
Who will handle applying bug fixes
|
||||
++++++++++++++++++++++++++++++++++
|
||||
|
||||
We core maintainers expect that people still using Python 2.7/3.4 and being affected by
|
||||
bugs to step up and provide patches and/or port bug fixes from the active branches.
|
||||
|
||||
We will be happy to guide users interested in doing so, so please don't hesitate to ask.
|
||||
|
||||
**Backporting changes into 4.6**
|
||||
|
||||
Please follow these instructions:
|
||||
|
||||
#. ``git fetch --all --prune``
|
||||
|
||||
#. ``git checkout origin/4.6.x -b backport-XXXX`` # use the PR number here
|
||||
|
||||
#. Locate the merge commit on the PR, in the *merged* message, for example:
|
||||
|
||||
nicoddemus merged commit 0f8b462 into pytest-dev:features
|
||||
|
||||
#. ``git cherry-pick -m1 REVISION`` # use the revision you found above (``0f8b462``).
|
||||
|
||||
#. Open a PR targeting ``4.6.x``:
|
||||
|
||||
* Prefix the message with ``[4.6]`` so it is an obvious backport
|
||||
* Delete the PR body, it usually contains a duplicate commit message.
|
||||
|
||||
**Providing new PRs to 4.6**
|
||||
|
||||
Fresh pull requests to ``4.6.x`` will be accepted provided that
|
||||
the equivalent code in the active branches does not contain that bug (for example, a bug is specific
|
||||
to Python 2 only).
|
||||
|
||||
Bug fixes that also happen in the mainstream version should be first fixed
|
||||
there, and then backported as per instructions above.
|
||||
@@ -29,9 +29,11 @@ pytest.ini
|
||||
|
||||
``pytest.ini`` files take precedence over other files, even when empty.
|
||||
|
||||
Alternatively, the hidden version ``.pytest.ini`` can be used.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# pytest.ini
|
||||
# pytest.ini or .pytest.ini
|
||||
[pytest]
|
||||
minversion = 6.0
|
||||
addopts = -ra -q
|
||||
@@ -88,7 +90,7 @@ and can also be used to hold pytest configuration if they have a ``[pytest]`` se
|
||||
setup.cfg
|
||||
~~~~~~~~~
|
||||
|
||||
``setup.cfg`` files are general purpose configuration files, used originally by :doc:`distutils <distutils/configfile>`, and can also be used to hold pytest configuration
|
||||
``setup.cfg`` files are general purpose configuration files, used originally by ``distutils`` (now deprecated) and `setuptools <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`__, and can also be used to hold pytest configuration
|
||||
if they have a ``[tool:pytest]`` section.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
@@ -335,7 +335,7 @@ For example:
|
||||
|
||||
.. literalinclude:: /example/fixtures/test_fixtures_order_dependencies.py
|
||||
|
||||
If we map out what depends on what, we get something that look like this:
|
||||
If we map out what depends on what, we get something that looks like this:
|
||||
|
||||
.. image:: /example/fixtures/test_fixtures_order_dependencies.*
|
||||
:align: center
|
||||
|
||||
@@ -8,8 +8,8 @@ Reference guides
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
fixtures
|
||||
plugin_list
|
||||
customize
|
||||
reference
|
||||
fixtures
|
||||
customize
|
||||
exit-codes
|
||||
plugin_list
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,11 @@
|
||||
pallets-sphinx-themes
|
||||
pluggy>=1.0
|
||||
pygments-pytest>=2.2.0
|
||||
pygments-pytest>=2.3.0
|
||||
sphinx-removed-in>=0.2.0
|
||||
sphinx>=3.1,<4
|
||||
sphinx>=5,<6
|
||||
sphinxcontrib-trio
|
||||
sphinxcontrib-svg2pdfconverter
|
||||
# Pin packaging because it no longer handles 'latest' version, which
|
||||
# is the version that is assigned to the docs.
|
||||
# See https://github.com/pytest-dev/pytest/pull/10578#issuecomment-1348249045.
|
||||
packaging <22
|
||||
|
||||
@@ -17,6 +17,8 @@ Books
|
||||
Talks and blog postings
|
||||
---------------------------------------------
|
||||
|
||||
- Training: `pytest - simple, rapid and fun testing with Python <https://www.youtube.com/watch?v=ofPHJrAOaTE>`_, Florian Bruhin, PyConDE 2022
|
||||
|
||||
- `pytest: Simple, rapid and fun testing with Python, <https://youtu.be/cSJ-X3TbQ1c?t=15752>`_ (@ 4:22:32), Florian Bruhin, WeAreDevelopers World Congress 2021
|
||||
|
||||
- Webinar: `pytest: Test Driven Development für Python (German) <https://bruhin.software/ins-pytest/>`_, Florian Bruhin, via mylearning.ch, 2020
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import sys
|
||||
from distutils.core import setup
|
||||
|
||||
if __name__ == "__main__":
|
||||
if "sdist" not in sys.argv[1:]:
|
||||
raise ValueError("please use 'pytest' pypi package instead of 'py.test'")
|
||||
setup(
|
||||
name="py.test",
|
||||
version="0.0",
|
||||
description="please use 'pytest' for installation",
|
||||
)
|
||||
@@ -3,7 +3,6 @@ requires = [
|
||||
# sync with setup.py until we discard non-pep-517/518
|
||||
"setuptools>=45.0",
|
||||
"setuptools-scm[toml]>=6.2.3",
|
||||
"wheel",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
@@ -38,6 +37,9 @@ filterwarnings = [
|
||||
# Those are caught/handled by pyupgrade, and not easy to filter with the
|
||||
# module being the filename (with .py removed).
|
||||
"default:invalid escape sequence:DeprecationWarning",
|
||||
# ignore not yet fixed warnings for hook markers
|
||||
"default:.*not marked using pytest.hook.*",
|
||||
"ignore:.*not marked using pytest.hook.*::xdist.*",
|
||||
# ignore use of unregistered marks, because we use many to test the implementation
|
||||
"ignore::_pytest.warning_types.PytestUnknownMarkWarning",
|
||||
# https://github.com/benjaminp/six/issues/341
|
||||
@@ -112,3 +114,8 @@ template = "changelog/_template.rst"
|
||||
|
||||
[tool.black]
|
||||
target-version = ['py37']
|
||||
|
||||
# check-wheel-contents is executed by the build-and-inspect-python-package action.
|
||||
[tool.check-wheel-contents]
|
||||
# W009: Wheel contains multiple toplevel library entries
|
||||
ignore = "W009"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -13,11 +13,25 @@ from tqdm import tqdm
|
||||
FILE_HEAD = r"""
|
||||
.. _plugin-list:
|
||||
|
||||
Plugin List
|
||||
===========
|
||||
Pytest Plugin List
|
||||
==================
|
||||
|
||||
Below is an automated compilation of ``pytest``` plugins available on `PyPI <https://pypi.org>`_.
|
||||
It includes PyPI projects whose names begin with "pytest-" and a handful of manually selected projects.
|
||||
Packages classified as inactive are excluded.
|
||||
|
||||
For detailed insights into how this list is generated,
|
||||
please refer to `the update script <https://github.com/pytest-dev/pytest/blob/main/scripts/update-plugin-list.py>`_.
|
||||
|
||||
.. warning::
|
||||
|
||||
Please be aware that this list is not a curated collection of projects
|
||||
and does not undergo a systematic review process.
|
||||
It serves purely as an informational resource to aid in the discovery of ``pytest`` plugins.
|
||||
|
||||
Do not presume any endorsement from the ``pytest`` project or its developers,
|
||||
and always conduct your own quality assessment before incorporating any of these plugins into your own projects.
|
||||
|
||||
PyPI projects that match "pytest-\*" are considered plugins and are listed
|
||||
automatically. Packages classified as inactive are excluded.
|
||||
|
||||
.. The following conditional uses a different format for this list when
|
||||
creating a PDF, because otherwise the table gets far too wide for the
|
||||
@@ -33,6 +47,9 @@ DEVELOPMENT_STATUS_CLASSIFIERS = (
|
||||
"Development Status :: 6 - Mature",
|
||||
"Development Status :: 7 - Inactive",
|
||||
)
|
||||
ADDITIONAL_PROJECTS = { # set of additional projects to consider as plugins
|
||||
"logassert",
|
||||
}
|
||||
|
||||
|
||||
def escape_rst(text: str) -> str:
|
||||
@@ -52,18 +69,18 @@ def iter_plugins():
|
||||
regex = r">([\d\w-]*)</a>"
|
||||
response = requests.get("https://pypi.org/simple")
|
||||
|
||||
matches = list(
|
||||
match
|
||||
for match in re.finditer(regex, response.text)
|
||||
if match.groups()[0].startswith("pytest-")
|
||||
)
|
||||
match_names = (match.groups()[0] for match in re.finditer(regex, response.text))
|
||||
plugin_names = [
|
||||
name
|
||||
for name in match_names
|
||||
if name.startswith("pytest-") or name in ADDITIONAL_PROJECTS
|
||||
]
|
||||
|
||||
for match in tqdm(matches, smoothing=0):
|
||||
name = match.groups()[0]
|
||||
for name in tqdm(plugin_names, smoothing=0):
|
||||
response = requests.get(f"https://pypi.org/pypi/{name}/json")
|
||||
if response.status_code == 404:
|
||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple but
|
||||
# return 404 on the JSON API. Skip.
|
||||
# Some packages, like pytest-azurepipelines42, are included in https://pypi.org/simple
|
||||
# but return 404 on the JSON API. Skip.
|
||||
continue
|
||||
response.raise_for_status()
|
||||
info = response.json()["info"]
|
||||
@@ -78,11 +95,23 @@ def iter_plugins():
|
||||
requires = "N/A"
|
||||
if info["requires_dist"]:
|
||||
for requirement in info["requires_dist"]:
|
||||
if requirement == "pytest" or "pytest " in requirement:
|
||||
if re.match(r"pytest(?![-.\w])", requirement):
|
||||
requires = requirement
|
||||
break
|
||||
|
||||
def version_sort_key(version_string):
|
||||
"""
|
||||
Return the sort key for the given version string
|
||||
returned by the API.
|
||||
"""
|
||||
try:
|
||||
return packaging.version.parse(version_string)
|
||||
except packaging.version.InvalidVersion:
|
||||
# Use a hard-coded pre-release version.
|
||||
return packaging.version.Version("0.0.0alpha")
|
||||
|
||||
releases = response.json()["releases"]
|
||||
for release in sorted(releases, key=packaging.version.parse, reverse=True):
|
||||
for release in sorted(releases, key=version_sort_key, reverse=True):
|
||||
if releases[release]:
|
||||
release_date = datetime.date.fromisoformat(
|
||||
releases[release][-1]["upload_time_iso_8601"].split("T")[0]
|
||||
@@ -90,7 +119,9 @@ def iter_plugins():
|
||||
last_release = release_date.strftime("%b %d, %Y")
|
||||
break
|
||||
name = f':pypi:`{info["name"]}`'
|
||||
summary = escape_rst(info["summary"].replace("\n", ""))
|
||||
summary = ""
|
||||
if info["summary"]:
|
||||
summary = escape_rst(info["summary"].replace("\n", ""))
|
||||
yield {
|
||||
"name": name,
|
||||
"summary": summary.strip(),
|
||||
@@ -122,7 +153,7 @@ def main():
|
||||
reference_dir = pathlib.Path("doc", "en", "reference")
|
||||
|
||||
plugin_list = reference_dir / "plugin_list.rst"
|
||||
with plugin_list.open("w") as f:
|
||||
with plugin_list.open("w", encoding="UTF-8") as f:
|
||||
f.write(FILE_HEAD)
|
||||
f.write(f"This list contains {len(plugins)} plugins.\n\n")
|
||||
f.write(".. only:: not latex\n\n")
|
||||
|
||||
15
setup.cfg
15
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
|
||||
@@ -21,6 +21,8 @@ classifiers =
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
Programming Language :: Python :: 3.11
|
||||
Programming Language :: Python :: 3.12
|
||||
Topic :: Software Development :: Libraries
|
||||
Topic :: Software Development :: Testing
|
||||
Topic :: Utilities
|
||||
@@ -36,20 +38,20 @@ packages =
|
||||
_pytest
|
||||
_pytest._code
|
||||
_pytest._io
|
||||
_pytest._py
|
||||
_pytest.assertion
|
||||
_pytest.config
|
||||
_pytest.mark
|
||||
pytest
|
||||
py_modules = py
|
||||
install_requires =
|
||||
attrs>=19.2.0
|
||||
iniconfig
|
||||
packaging
|
||||
pluggy>=0.12,<2.0
|
||||
py>=1.8.2
|
||||
tomli>=1.0.0
|
||||
atomicwrites>=1.0;sys_platform=="win32"
|
||||
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
|
||||
package_dir =
|
||||
=src
|
||||
@@ -66,11 +68,13 @@ console_scripts =
|
||||
[options.extras_require]
|
||||
testing =
|
||||
argcomplete
|
||||
attrs>=19.2.0
|
||||
hypothesis>=3.56
|
||||
mock
|
||||
nose
|
||||
pygments>=2.7.2
|
||||
requests
|
||||
setuptools
|
||||
xmlschema
|
||||
|
||||
[options.package_data]
|
||||
@@ -94,7 +98,6 @@ mypy_path = src
|
||||
check_untyped_defs = True
|
||||
disallow_any_generics = True
|
||||
ignore_missing_imports = True
|
||||
no_implicit_optional = True
|
||||
show_error_codes = True
|
||||
strict_equality = True
|
||||
warn_redundant_casts = True
|
||||
|
||||
@@ -78,15 +78,15 @@ class FastFilesCompleter:
|
||||
|
||||
def __call__(self, prefix: str, **kwargs: Any) -> List[str]:
|
||||
# Only called on non option completions.
|
||||
if os.path.sep in prefix[1:]:
|
||||
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
||||
if os.sep in prefix[1:]:
|
||||
prefix_dir = len(os.path.dirname(prefix) + os.sep)
|
||||
else:
|
||||
prefix_dir = 0
|
||||
completion = []
|
||||
globbed = []
|
||||
if "*" not in prefix and "?" not in prefix:
|
||||
# We are on unix, otherwise no bash.
|
||||
if not prefix or prefix[-1] == os.path.sep:
|
||||
if not prefix or prefix[-1] == os.sep:
|
||||
globbed.extend(glob(prefix + ".*"))
|
||||
prefix += "*"
|
||||
globbed.extend(glob(prefix))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ast
|
||||
import dataclasses
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
@@ -30,9 +31,7 @@ from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
from weakref import ref
|
||||
|
||||
import attr
|
||||
import pluggy
|
||||
|
||||
import _pytest
|
||||
@@ -50,12 +49,15 @@ from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import bestrelpath
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
from typing_extensions import Literal
|
||||
from typing_extensions import SupportsIndex
|
||||
from weakref import ReferenceType
|
||||
|
||||
_TracebackStyle = Literal["long", "short", "line", "no", "native", "value", "auto"]
|
||||
|
||||
if sys.version_info[:2] < (3, 11):
|
||||
from exceptiongroup import BaseExceptionGroup
|
||||
|
||||
|
||||
class Code:
|
||||
"""Wrapper around Python code objects."""
|
||||
@@ -191,25 +193,25 @@ class Frame:
|
||||
class TracebackEntry:
|
||||
"""A single entry in a Traceback."""
|
||||
|
||||
__slots__ = ("_rawentry", "_excinfo", "_repr_style")
|
||||
__slots__ = ("_rawentry", "_repr_style")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
rawentry: TracebackType,
|
||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
||||
repr_style: Optional['Literal["short", "long"]'] = None,
|
||||
) -> None:
|
||||
self._rawentry = rawentry
|
||||
self._excinfo = excinfo
|
||||
self._repr_style: Optional['Literal["short", "long"]'] = None
|
||||
self._rawentry: "Final" = rawentry
|
||||
self._repr_style: "Final" = repr_style
|
||||
|
||||
def with_repr_style(
|
||||
self, repr_style: Optional['Literal["short", "long"]']
|
||||
) -> "TracebackEntry":
|
||||
return TracebackEntry(self._rawentry, repr_style)
|
||||
|
||||
@property
|
||||
def lineno(self) -> int:
|
||||
return self._rawentry.tb_lineno - 1
|
||||
|
||||
def set_repr_style(self, mode: "Literal['short', 'long']") -> None:
|
||||
assert mode in ("short", "long")
|
||||
self._repr_style = mode
|
||||
|
||||
@property
|
||||
def frame(self) -> Frame:
|
||||
return Frame(self._rawentry.tb_frame)
|
||||
@@ -269,7 +271,7 @@ class TracebackEntry:
|
||||
|
||||
source = property(getsource)
|
||||
|
||||
def ishidden(self) -> bool:
|
||||
def ishidden(self, excinfo: Optional["ExceptionInfo[BaseException]"]) -> bool:
|
||||
"""Return True if the current frame has a var __tracebackhide__
|
||||
resolving to True.
|
||||
|
||||
@@ -293,7 +295,7 @@ class TracebackEntry:
|
||||
else:
|
||||
break
|
||||
if tbh and callable(tbh):
|
||||
return tbh(None if self._excinfo is None else self._excinfo())
|
||||
return tbh(excinfo)
|
||||
return tbh
|
||||
|
||||
def __str__(self) -> str:
|
||||
@@ -326,16 +328,14 @@ class Traceback(List[TracebackEntry]):
|
||||
def __init__(
|
||||
self,
|
||||
tb: Union[TracebackType, Iterable[TracebackEntry]],
|
||||
excinfo: Optional["ReferenceType[ExceptionInfo[BaseException]]"] = None,
|
||||
) -> None:
|
||||
"""Initialize from given python traceback object and ExceptionInfo."""
|
||||
self._excinfo = excinfo
|
||||
if isinstance(tb, TracebackType):
|
||||
|
||||
def f(cur: TracebackType) -> Iterable[TracebackEntry]:
|
||||
cur_: Optional[TracebackType] = cur
|
||||
while cur_ is not None:
|
||||
yield TracebackEntry(cur_, excinfo=excinfo)
|
||||
yield TracebackEntry(cur_)
|
||||
cur_ = cur_.tb_next
|
||||
|
||||
super().__init__(f(tb))
|
||||
@@ -375,7 +375,7 @@ class Traceback(List[TracebackEntry]):
|
||||
continue
|
||||
if firstlineno is not None and x.frame.code.firstlineno != firstlineno:
|
||||
continue
|
||||
return Traceback(x._rawentry, self._excinfo)
|
||||
return Traceback(x._rawentry)
|
||||
return self
|
||||
|
||||
@overload
|
||||
@@ -395,26 +395,27 @@ class Traceback(List[TracebackEntry]):
|
||||
return super().__getitem__(key)
|
||||
|
||||
def filter(
|
||||
self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden()
|
||||
self,
|
||||
# TODO(py38): change to positional only.
|
||||
_excinfo_or_fn: Union[
|
||||
"ExceptionInfo[BaseException]",
|
||||
Callable[[TracebackEntry], bool],
|
||||
],
|
||||
) -> "Traceback":
|
||||
"""Return a Traceback instance with certain items removed
|
||||
"""Return a Traceback instance with certain items removed.
|
||||
|
||||
fn is a function that gets a single argument, a TracebackEntry
|
||||
instance, and should return True when the item should be added
|
||||
to the Traceback, False when not.
|
||||
If the filter is an `ExceptionInfo`, removes all the ``TracebackEntry``s
|
||||
which are hidden (see ishidden() above).
|
||||
|
||||
By default this removes all the TracebackEntries which are hidden
|
||||
(see ishidden() above).
|
||||
Otherwise, the filter is a function that gets a single argument, a
|
||||
``TracebackEntry`` instance, and should return True when the item should
|
||||
be added to the ``Traceback``, False when not.
|
||||
"""
|
||||
return Traceback(filter(fn, self), self._excinfo)
|
||||
|
||||
def getcrashentry(self) -> TracebackEntry:
|
||||
"""Return last non-hidden traceback entry that lead to the exception of a traceback."""
|
||||
for i in range(-1, -len(self) - 1, -1):
|
||||
entry = self[i]
|
||||
if not entry.ishidden():
|
||||
return entry
|
||||
return self[-1]
|
||||
if isinstance(_excinfo_or_fn, ExceptionInfo):
|
||||
fn = lambda x: not x.ishidden(_excinfo_or_fn) # noqa: E731
|
||||
else:
|
||||
fn = _excinfo_or_fn
|
||||
return Traceback(filter(fn, self))
|
||||
|
||||
def recursionindex(self) -> Optional[int]:
|
||||
"""Return the index of the frame/TracebackEntry where recursion originates if
|
||||
@@ -442,7 +443,7 @@ E = TypeVar("E", bound=BaseException, covariant=True)
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(repr=False, init=False, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class ExceptionInfo(Generic[E]):
|
||||
"""Wraps sys.exc_info() objects and offers help for navigating the traceback."""
|
||||
|
||||
@@ -466,22 +467,41 @@ class ExceptionInfo(Generic[E]):
|
||||
self._traceback = traceback
|
||||
|
||||
@classmethod
|
||||
def from_exc_info(
|
||||
def from_exception(
|
||||
cls,
|
||||
exc_info: Tuple[Type[E], E, TracebackType],
|
||||
# Ignoring error: "Cannot use a covariant type variable as a parameter".
|
||||
# This is OK to ignore because this class is (conceptually) readonly.
|
||||
# See https://github.com/python/mypy/issues/7049.
|
||||
exception: E, # type: ignore[misc]
|
||||
exprinfo: Optional[str] = None,
|
||||
) -> "ExceptionInfo[E]":
|
||||
"""Return an ExceptionInfo for an existing exc_info tuple.
|
||||
"""Return an ExceptionInfo for an existing exception.
|
||||
|
||||
.. warning::
|
||||
|
||||
Experimental API
|
||||
The exception must have a non-``None`` ``__traceback__`` attribute,
|
||||
otherwise this function fails with an assertion error. This means that
|
||||
the exception must have been raised, or added a traceback with the
|
||||
:py:meth:`~BaseException.with_traceback()` method.
|
||||
|
||||
:param exprinfo:
|
||||
A text string helping to determine if we should strip
|
||||
``AssertionError`` from the output. Defaults to the exception
|
||||
message/``__str__()``.
|
||||
|
||||
.. versionadded:: 7.4
|
||||
"""
|
||||
assert (
|
||||
exception.__traceback__
|
||||
), "Exceptions passed to ExcInfo.from_exception(...) must have a non-None __traceback__."
|
||||
exc_info = (type(exception), exception, exception.__traceback__)
|
||||
return cls.from_exc_info(exc_info, exprinfo)
|
||||
|
||||
@classmethod
|
||||
def from_exc_info(
|
||||
cls,
|
||||
exc_info: Tuple[Type[E], E, TracebackType],
|
||||
exprinfo: Optional[str] = None,
|
||||
) -> "ExceptionInfo[E]":
|
||||
"""Like :func:`from_exception`, but using old-style exc_info tuple."""
|
||||
_striptext = ""
|
||||
if exprinfo is None and isinstance(exc_info[1], AssertionError):
|
||||
exprinfo = getattr(exc_info[1], "msg", None)
|
||||
@@ -560,7 +580,7 @@ class ExceptionInfo(Generic[E]):
|
||||
def traceback(self) -> Traceback:
|
||||
"""The traceback."""
|
||||
if self._traceback is None:
|
||||
self._traceback = Traceback(self.tb, excinfo=ref(self))
|
||||
self._traceback = Traceback(self.tb)
|
||||
return self._traceback
|
||||
|
||||
@traceback.setter
|
||||
@@ -599,18 +619,25 @@ class ExceptionInfo(Generic[E]):
|
||||
"""
|
||||
return isinstance(self.value, exc)
|
||||
|
||||
def _getreprcrash(self) -> "ReprFileLocation":
|
||||
exconly = self.exconly(tryshort=True)
|
||||
entry = self.traceback.getcrashentry()
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
def _getreprcrash(self) -> Optional["ReprFileLocation"]:
|
||||
# Find last non-hidden traceback entry that led to the exception of the
|
||||
# traceback, or None if all hidden.
|
||||
for i in range(-1, -len(self.traceback) - 1, -1):
|
||||
entry = self.traceback[i]
|
||||
if not entry.ishidden(self):
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
exconly = self.exconly(tryshort=True)
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
return None
|
||||
|
||||
def getrepr(
|
||||
self,
|
||||
showlocals: bool = False,
|
||||
style: "_TracebackStyle" = "long",
|
||||
abspath: bool = False,
|
||||
tbfilter: bool = True,
|
||||
tbfilter: Union[
|
||||
bool, Callable[["ExceptionInfo[BaseException]"], Traceback]
|
||||
] = True,
|
||||
funcargs: bool = False,
|
||||
truncate_locals: bool = True,
|
||||
chain: bool = True,
|
||||
@@ -622,14 +649,20 @@ class ExceptionInfo(Generic[E]):
|
||||
Ignored if ``style=="native"``.
|
||||
|
||||
:param str style:
|
||||
long|short|no|native|value traceback style.
|
||||
long|short|line|no|native|value traceback style.
|
||||
|
||||
:param bool abspath:
|
||||
If paths should be changed to absolute or left unchanged.
|
||||
|
||||
:param bool tbfilter:
|
||||
Hide entries that contain a local variable ``__tracebackhide__==True``.
|
||||
Ignored if ``style=="native"``.
|
||||
:param tbfilter:
|
||||
A filter for traceback entries.
|
||||
|
||||
* If false, don't hide any entries.
|
||||
* If true, hide internal entries and entries that contain a local
|
||||
variable ``__tracebackhide__ = True``.
|
||||
* If a callable, delegates the filtering to the callable.
|
||||
|
||||
Ignored if ``style`` is ``"native"``.
|
||||
|
||||
:param bool funcargs:
|
||||
Show fixtures ("funcargs" for legacy purposes) per traceback entry.
|
||||
@@ -646,12 +679,14 @@ class ExceptionInfo(Generic[E]):
|
||||
"""
|
||||
if style == "native":
|
||||
return ReprExceptionInfo(
|
||||
ReprTracebackNative(
|
||||
reprtraceback=ReprTracebackNative(
|
||||
traceback.format_exception(
|
||||
self.type, self.value, self.traceback[0]._rawentry
|
||||
self.type,
|
||||
self.value,
|
||||
self.traceback[0]._rawentry if self.traceback else None,
|
||||
)
|
||||
),
|
||||
self._getreprcrash(),
|
||||
reprcrash=self._getreprcrash(),
|
||||
)
|
||||
|
||||
fmt = FormattedExcinfo(
|
||||
@@ -672,15 +707,16 @@ class ExceptionInfo(Generic[E]):
|
||||
If it matches `True` is returned, otherwise an `AssertionError` is raised.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
msg = "Regex pattern {!r} does not match {!r}."
|
||||
if regexp == str(self.value):
|
||||
msg += " Did you mean to `re.escape()` the regex?"
|
||||
assert re.search(regexp, str(self.value)), msg.format(regexp, str(self.value))
|
||||
value = str(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?"
|
||||
assert re.search(regexp, value), msg
|
||||
# Return True to allow for "assert excinfo.match()".
|
||||
return True
|
||||
|
||||
|
||||
@attr.s(auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class FormattedExcinfo:
|
||||
"""Presenting information about failing Functions and Generators."""
|
||||
|
||||
@@ -691,12 +727,12 @@ class FormattedExcinfo:
|
||||
showlocals: bool = False
|
||||
style: "_TracebackStyle" = "long"
|
||||
abspath: bool = True
|
||||
tbfilter: bool = True
|
||||
tbfilter: Union[bool, Callable[[ExceptionInfo[BaseException]], Traceback]] = True
|
||||
funcargs: bool = False
|
||||
truncate_locals: bool = True
|
||||
chain: bool = True
|
||||
astcache: Dict[Union[str, Path], ast.AST] = attr.ib(
|
||||
factory=dict, init=False, repr=False
|
||||
astcache: Dict[Union[str, Path], ast.AST] = dataclasses.field(
|
||||
default_factory=dict, init=False, repr=False
|
||||
)
|
||||
|
||||
def _getindent(self, source: "Source") -> int:
|
||||
@@ -737,11 +773,13 @@ class FormattedExcinfo:
|
||||
) -> List[str]:
|
||||
"""Return formatted and marked up source lines."""
|
||||
lines = []
|
||||
if source is None or line_index >= len(source.lines):
|
||||
if source is not None and line_index < 0:
|
||||
line_index += len(source)
|
||||
if source is None or line_index >= len(source.lines) or line_index < 0:
|
||||
# `line_index` could still be outside `range(len(source.lines))` if
|
||||
# we're processing AST with pathological position attributes.
|
||||
source = Source("???")
|
||||
line_index = 0
|
||||
if line_index < 0:
|
||||
line_index += len(source)
|
||||
space_prefix = " "
|
||||
if short:
|
||||
lines.append(space_prefix + source.lines[line_index].strip())
|
||||
@@ -801,12 +839,16 @@ class FormattedExcinfo:
|
||||
|
||||
def repr_traceback_entry(
|
||||
self,
|
||||
entry: TracebackEntry,
|
||||
entry: Optional[TracebackEntry],
|
||||
excinfo: Optional[ExceptionInfo[BaseException]] = None,
|
||||
) -> "ReprEntry":
|
||||
lines: List[str] = []
|
||||
style = entry._repr_style if entry._repr_style is not None else self.style
|
||||
if style in ("short", "long"):
|
||||
style = (
|
||||
entry._repr_style
|
||||
if entry is not None and entry._repr_style is not None
|
||||
else self.style
|
||||
)
|
||||
if style in ("short", "long") and entry is not None:
|
||||
source = self._getentrysource(entry)
|
||||
if source is None:
|
||||
source = Source("???")
|
||||
@@ -847,25 +889,31 @@ class FormattedExcinfo:
|
||||
|
||||
def repr_traceback(self, excinfo: ExceptionInfo[BaseException]) -> "ReprTraceback":
|
||||
traceback = excinfo.traceback
|
||||
if self.tbfilter:
|
||||
traceback = traceback.filter()
|
||||
if callable(self.tbfilter):
|
||||
traceback = self.tbfilter(excinfo)
|
||||
elif self.tbfilter:
|
||||
traceback = traceback.filter(excinfo)
|
||||
|
||||
if isinstance(excinfo.value, RecursionError):
|
||||
traceback, extraline = self._truncate_recursive_traceback(traceback)
|
||||
else:
|
||||
extraline = None
|
||||
|
||||
if not traceback:
|
||||
if extraline is None:
|
||||
extraline = "All traceback entries are hidden. Pass `--full-trace` to see hidden and internal frames."
|
||||
entries = [self.repr_traceback_entry(None, excinfo)]
|
||||
return ReprTraceback(entries, extraline, style=self.style)
|
||||
|
||||
last = traceback[-1]
|
||||
entries = []
|
||||
if self.style == "value":
|
||||
reprentry = self.repr_traceback_entry(last, excinfo)
|
||||
entries.append(reprentry)
|
||||
entries = [self.repr_traceback_entry(last, excinfo)]
|
||||
return ReprTraceback(entries, None, style=self.style)
|
||||
|
||||
for index, entry in enumerate(traceback):
|
||||
einfo = (last == entry) and excinfo or None
|
||||
reprentry = self.repr_traceback_entry(entry, einfo)
|
||||
entries.append(reprentry)
|
||||
entries = [
|
||||
self.repr_traceback_entry(entry, excinfo if last == entry else None)
|
||||
for entry in traceback
|
||||
]
|
||||
return ReprTraceback(entries, extraline, style=self.style)
|
||||
|
||||
def _truncate_recursive_traceback(
|
||||
@@ -922,11 +970,24 @@ class FormattedExcinfo:
|
||||
seen: Set[int] = set()
|
||||
while e is not None and id(e) not in seen:
|
||||
seen.add(id(e))
|
||||
|
||||
if excinfo_:
|
||||
reprtraceback = self.repr_traceback(excinfo_)
|
||||
reprcrash: Optional[ReprFileLocation] = (
|
||||
excinfo_._getreprcrash() if self.style != "value" else None
|
||||
)
|
||||
# Fall back to native traceback as a temporary workaround until
|
||||
# full support for exception groups added to ExceptionInfo.
|
||||
# See https://github.com/pytest-dev/pytest/issues/9159
|
||||
if isinstance(e, BaseExceptionGroup):
|
||||
reprtraceback: Union[
|
||||
ReprTracebackNative, ReprTraceback
|
||||
] = ReprTracebackNative(
|
||||
traceback.format_exception(
|
||||
type(excinfo_.value),
|
||||
excinfo_.value,
|
||||
excinfo_.traceback[0]._rawentry,
|
||||
)
|
||||
)
|
||||
else:
|
||||
reprtraceback = self.repr_traceback(excinfo_)
|
||||
reprcrash = excinfo_._getreprcrash()
|
||||
else:
|
||||
# Fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work.
|
||||
@@ -934,25 +995,17 @@ class FormattedExcinfo:
|
||||
traceback.format_exception(type(e), e, None)
|
||||
)
|
||||
reprcrash = None
|
||||
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
|
||||
if e.__cause__ is not None and self.chain:
|
||||
e = e.__cause__
|
||||
excinfo_ = (
|
||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
||||
if e.__traceback__
|
||||
else None
|
||||
)
|
||||
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||
descr = "The above exception was the direct cause of the following exception:"
|
||||
elif (
|
||||
e.__context__ is not None and not e.__suppress_context__ and self.chain
|
||||
):
|
||||
e = e.__context__
|
||||
excinfo_ = (
|
||||
ExceptionInfo.from_exc_info((type(e), e, e.__traceback__))
|
||||
if e.__traceback__
|
||||
else None
|
||||
)
|
||||
excinfo_ = ExceptionInfo.from_exception(e) if e.__traceback__ else None
|
||||
descr = "During handling of the above exception, another exception occurred:"
|
||||
else:
|
||||
e = None
|
||||
@@ -960,7 +1013,7 @@ class FormattedExcinfo:
|
||||
return ExceptionChainRepr(repr_chain)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class TerminalRepr:
|
||||
def __str__(self) -> str:
|
||||
# FYI this is called from pytest-xdist's serialization of exception
|
||||
@@ -978,14 +1031,14 @@ class TerminalRepr:
|
||||
|
||||
|
||||
# This class is abstract -- only subclasses are instantiated.
|
||||
@attr.s(eq=False)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ExceptionRepr(TerminalRepr):
|
||||
# Provided by subclasses.
|
||||
reprcrash: Optional["ReprFileLocation"]
|
||||
reprtraceback: "ReprTraceback"
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
self.sections: List[Tuple[str, str, str]] = []
|
||||
reprcrash: Optional["ReprFileLocation"]
|
||||
sections: List[Tuple[str, str, str]] = dataclasses.field(
|
||||
init=False, default_factory=list
|
||||
)
|
||||
|
||||
def addsection(self, name: str, content: str, sep: str = "-") -> None:
|
||||
self.sections.append((name, content, sep))
|
||||
@@ -996,16 +1049,23 @@ class ExceptionRepr(TerminalRepr):
|
||||
tw.line(content)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ExceptionChainRepr(ExceptionRepr):
|
||||
chain: Sequence[Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]]
|
||||
|
||||
def __attrs_post_init__(self) -> None:
|
||||
super().__attrs_post_init__()
|
||||
def __init__(
|
||||
self,
|
||||
chain: Sequence[
|
||||
Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]]
|
||||
],
|
||||
) -> None:
|
||||
# reprcrash and reprtraceback of the outermost (the newest) exception
|
||||
# in the chain.
|
||||
self.reprtraceback = self.chain[-1][0]
|
||||
self.reprcrash = self.chain[-1][1]
|
||||
super().__init__(
|
||||
reprtraceback=chain[-1][0],
|
||||
reprcrash=chain[-1][1],
|
||||
)
|
||||
self.chain = chain
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
for element in self.chain:
|
||||
@@ -1016,17 +1076,17 @@ class ExceptionChainRepr(ExceptionRepr):
|
||||
super().toterminal(tw)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprExceptionInfo(ExceptionRepr):
|
||||
reprtraceback: "ReprTraceback"
|
||||
reprcrash: "ReprFileLocation"
|
||||
reprcrash: Optional["ReprFileLocation"]
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
self.reprtraceback.toterminal(tw)
|
||||
super().toterminal(tw)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprTraceback(TerminalRepr):
|
||||
reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]]
|
||||
extraline: Optional[str]
|
||||
@@ -1055,12 +1115,12 @@ class ReprTraceback(TerminalRepr):
|
||||
|
||||
class ReprTracebackNative(ReprTraceback):
|
||||
def __init__(self, tblines: Sequence[str]) -> None:
|
||||
self.style = "native"
|
||||
self.reprentries = [ReprEntryNative(tblines)]
|
||||
self.extraline = None
|
||||
self.style = "native"
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprEntryNative(TerminalRepr):
|
||||
lines: Sequence[str]
|
||||
|
||||
@@ -1070,7 +1130,7 @@ class ReprEntryNative(TerminalRepr):
|
||||
tw.write("".join(self.lines))
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprEntry(TerminalRepr):
|
||||
lines: Sequence[str]
|
||||
reprfuncargs: Optional["ReprFuncArgs"]
|
||||
@@ -1124,8 +1184,8 @@ class ReprEntry(TerminalRepr):
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
if self.style == "short":
|
||||
assert self.reprfileloc is not None
|
||||
self.reprfileloc.toterminal(tw)
|
||||
if self.reprfileloc:
|
||||
self.reprfileloc.toterminal(tw)
|
||||
self._write_entry_lines(tw)
|
||||
if self.reprlocals:
|
||||
self.reprlocals.toterminal(tw, indent=" " * 8)
|
||||
@@ -1150,12 +1210,15 @@ class ReprEntry(TerminalRepr):
|
||||
)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprFileLocation(TerminalRepr):
|
||||
path: str = attr.ib(converter=str)
|
||||
path: str
|
||||
lineno: int
|
||||
message: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.path = str(self.path)
|
||||
|
||||
def toterminal(self, tw: TerminalWriter) -> None:
|
||||
# Filename and lineno output for each entry, using an output format
|
||||
# that most editors understand.
|
||||
@@ -1167,7 +1230,7 @@ class ReprFileLocation(TerminalRepr):
|
||||
tw.line(f":{self.lineno}: {msg}")
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprLocals(TerminalRepr):
|
||||
lines: Sequence[str]
|
||||
|
||||
@@ -1176,7 +1239,7 @@ class ReprLocals(TerminalRepr):
|
||||
tw.line(indent + line)
|
||||
|
||||
|
||||
@attr.s(eq=False, auto_attribs=True)
|
||||
@dataclasses.dataclass(eq=False)
|
||||
class ReprFuncArgs(TerminalRepr):
|
||||
args: Sequence[Tuple[str, object]]
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class SafeRepr(reprlib.Repr):
|
||||
information on exceptions raised during the call.
|
||||
"""
|
||||
|
||||
def __init__(self, maxsize: Optional[int]) -> None:
|
||||
def __init__(self, maxsize: Optional[int], use_ascii: bool = False) -> None:
|
||||
"""
|
||||
:param maxsize:
|
||||
If not None, will truncate the resulting repr to that specific size, using ellipsis
|
||||
@@ -54,10 +54,15 @@ class SafeRepr(reprlib.Repr):
|
||||
# truncation.
|
||||
self.maxstring = maxsize if maxsize is not None else 1_000_000_000
|
||||
self.maxsize = maxsize
|
||||
self.use_ascii = use_ascii
|
||||
|
||||
def repr(self, x: object) -> str:
|
||||
try:
|
||||
s = super().repr(x)
|
||||
if self.use_ascii:
|
||||
s = ascii(x)
|
||||
else:
|
||||
s = super().repr(x)
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except BaseException as exc:
|
||||
@@ -94,7 +99,9 @@ def safeformat(obj: object) -> str:
|
||||
DEFAULT_REPR_MAX_SIZE = 240
|
||||
|
||||
|
||||
def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str:
|
||||
def saferepr(
|
||||
obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE, use_ascii: bool = False
|
||||
) -> str:
|
||||
"""Return a size-limited safe repr-string for the given object.
|
||||
|
||||
Failing __repr__ functions of user instances will be represented
|
||||
@@ -104,7 +111,27 @@ def saferepr(obj: object, maxsize: Optional[int] = DEFAULT_REPR_MAX_SIZE) -> str
|
||||
This function is a wrapper around the Repr/reprlib functionality of the
|
||||
stdlib.
|
||||
"""
|
||||
return SafeRepr(maxsize).repr(obj)
|
||||
|
||||
return SafeRepr(maxsize, use_ascii).repr(obj)
|
||||
|
||||
|
||||
def saferepr_unlimited(obj: object, use_ascii: bool = True) -> str:
|
||||
"""Return an unlimited-size safe repr-string for the given object.
|
||||
|
||||
As with saferepr, failing __repr__ functions of user instances
|
||||
will be represented with a short exception info.
|
||||
|
||||
This function is a wrapper around simple repr.
|
||||
|
||||
Note: a cleaner solution would be to alter ``saferepr``this way
|
||||
when maxsize=None, but that might affect some other code.
|
||||
"""
|
||||
try:
|
||||
if use_ascii:
|
||||
return ascii(obj)
|
||||
return repr(obj)
|
||||
except Exception as exc:
|
||||
return _format_repr_exception(exc, obj)
|
||||
|
||||
|
||||
class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter):
|
||||
|
||||
0
src/_pytest/_py/__init__.py
Normal file
0
src/_pytest/_py/__init__.py
Normal file
109
src/_pytest/_py/error.py
Normal file
109
src/_pytest/_py/error.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""create errno-specific classes for IO or os calls."""
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
from typing import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import ParamSpec
|
||||
|
||||
P = ParamSpec("P")
|
||||
|
||||
R = TypeVar("R")
|
||||
|
||||
|
||||
class Error(EnvironmentError):
|
||||
def __repr__(self) -> str:
|
||||
return "{}.{} {!r}: {} ".format(
|
||||
self.__class__.__module__,
|
||||
self.__class__.__name__,
|
||||
self.__class__.__doc__,
|
||||
" ".join(map(str, self.args)),
|
||||
# repr(self.args)
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
s = "[{}]: {}".format(
|
||||
self.__class__.__doc__,
|
||||
" ".join(map(str, self.args)),
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
_winerrnomap = {
|
||||
2: errno.ENOENT,
|
||||
3: errno.ENOENT,
|
||||
17: errno.EEXIST,
|
||||
18: errno.EXDEV,
|
||||
13: errno.EBUSY, # empty cd drive, but ENOMEDIUM seems unavailiable
|
||||
22: errno.ENOTDIR,
|
||||
20: errno.ENOTDIR,
|
||||
267: errno.ENOTDIR,
|
||||
5: errno.EACCES, # anything better?
|
||||
}
|
||||
|
||||
|
||||
class ErrorMaker:
|
||||
"""lazily provides Exception classes for each possible POSIX errno
|
||||
(as defined per the 'errno' module). All such instances
|
||||
subclass EnvironmentError.
|
||||
"""
|
||||
|
||||
_errno2class: dict[int, type[Error]] = {}
|
||||
|
||||
def __getattr__(self, name: str) -> type[Error]:
|
||||
if name[0] == "_":
|
||||
raise AttributeError(name)
|
||||
eno = getattr(errno, name)
|
||||
cls = self._geterrnoclass(eno)
|
||||
setattr(self, name, cls)
|
||||
return cls
|
||||
|
||||
def _geterrnoclass(self, eno: int) -> type[Error]:
|
||||
try:
|
||||
return self._errno2class[eno]
|
||||
except KeyError:
|
||||
clsname = errno.errorcode.get(eno, "UnknownErrno%d" % (eno,))
|
||||
errorcls = type(
|
||||
clsname,
|
||||
(Error,),
|
||||
{"__module__": "py.error", "__doc__": os.strerror(eno)},
|
||||
)
|
||||
self._errno2class[eno] = errorcls
|
||||
return errorcls
|
||||
|
||||
def checked_call(
|
||||
self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs
|
||||
) -> R:
|
||||
"""Call a function and raise an errno-exception if applicable."""
|
||||
__tracebackhide__ = True
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Error:
|
||||
raise
|
||||
except OSError as value:
|
||||
if not hasattr(value, "errno"):
|
||||
raise
|
||||
errno = value.errno
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
cls = self._geterrnoclass(_winerrnomap[errno])
|
||||
except KeyError:
|
||||
raise value
|
||||
else:
|
||||
# we are not on Windows, or we got a proper OSError
|
||||
cls = self._geterrnoclass(errno)
|
||||
|
||||
raise cls(f"{func.__name__}{args!r}")
|
||||
|
||||
|
||||
_error_maker = ErrorMaker()
|
||||
checked_call = _error_maker.checked_call
|
||||
|
||||
|
||||
def __getattr__(attr: str) -> type[Error]:
|
||||
return getattr(_error_maker, attr) # type: ignore[no-any-return]
|
||||
1475
src/_pytest/_py/path.py
Normal file
1475
src/_pytest/_py/path.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -39,7 +39,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"enable_assertion_pass_hook",
|
||||
type="bool",
|
||||
default=False,
|
||||
help="Enables the pytest_assertion_pass hook."
|
||||
help="Enables the pytest_assertion_pass hook. "
|
||||
"Make sure to delete any previously generated pyc cache files.",
|
||||
)
|
||||
|
||||
@@ -53,7 +53,7 @@ def register_assert_rewrite(*names: str) -> None:
|
||||
actually imported, usually in your __init__.py if you are a plugin
|
||||
using a package.
|
||||
|
||||
:raises TypeError: If the given module names are not strings.
|
||||
:param names: The module names to register.
|
||||
"""
|
||||
for name in names:
|
||||
if not isinstance(name, str):
|
||||
|
||||
@@ -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,15 +45,32 @@ from _pytest.stash import StashKey
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.assertion import AssertionState
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
namedExpr = ast.NamedExpr
|
||||
astNameConstant = ast.Constant
|
||||
astStr = ast.Constant
|
||||
astNum = ast.Constant
|
||||
else:
|
||||
namedExpr = ast.Expr
|
||||
astNameConstant = ast.NameConstant
|
||||
astStr = ast.Str
|
||||
astNum = ast.Num
|
||||
|
||||
|
||||
class Sentinel:
|
||||
pass
|
||||
|
||||
|
||||
assertstate_key = StashKey["AssertionState"]()
|
||||
|
||||
|
||||
# pytest caches rewritten pycs in pycache dirs
|
||||
PYTEST_TAG = f"{sys.implementation.cache_tag}-pytest-{version}"
|
||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||
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."""
|
||||
@@ -180,7 +198,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
for initial_path in self.session._initialpaths:
|
||||
# Make something as c:/projects/my_project/path.py ->
|
||||
# ['c:', 'projects', 'my_project', 'path.py']
|
||||
parts = str(initial_path).split(os.path.sep)
|
||||
parts = str(initial_path).split(os.sep)
|
||||
# add 'path' to basenames to be checked.
|
||||
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
|
||||
|
||||
@@ -190,7 +208,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
return False
|
||||
|
||||
# For matching the name it must be as if it was a filename.
|
||||
path = PurePath(os.path.sep.join(parts) + ".py")
|
||||
path = PurePath(*parts).with_suffix(".py")
|
||||
|
||||
for pat in self.fnpats:
|
||||
# if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
|
||||
@@ -274,14 +292,20 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder, importlib.abc.Loader)
|
||||
return f.read()
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
if sys.version_info >= (3, 12):
|
||||
from importlib.resources.abc import TraversableResources
|
||||
else:
|
||||
from importlib.abc import TraversableResources
|
||||
|
||||
def get_resource_reader(self, name: str) -> importlib.abc.TraversableResources: # type: ignore
|
||||
def get_resource_reader(self, name: str) -> TraversableResources: # type: ignore
|
||||
if sys.version_info < (3, 11):
|
||||
from importlib.readers import FileReader
|
||||
else:
|
||||
from importlib.resources.readers import FileReader
|
||||
|
||||
return FileReader(types.SimpleNamespace(path=self._rewritten_names[name]))
|
||||
return FileReader( # type:ignore[no-any-return]
|
||||
types.SimpleNamespace(path=self._rewritten_names[name])
|
||||
)
|
||||
|
||||
|
||||
def _write_pyc_fp(
|
||||
@@ -302,53 +326,29 @@ def _write_pyc_fp(
|
||||
fp.write(marshal.dumps(co))
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
from atomicwrites import atomic_write
|
||||
|
||||
def _write_pyc(
|
||||
state: "AssertionState",
|
||||
co: types.CodeType,
|
||||
source_stat: os.stat_result,
|
||||
pyc: Path,
|
||||
) -> bool:
|
||||
try:
|
||||
with atomic_write(os.fspath(pyc), mode="wb", overwrite=True) as fp:
|
||||
_write_pyc_fp(fp, source_stat, co)
|
||||
except OSError as e:
|
||||
state.trace(f"error writing pyc file at {pyc}: {e}")
|
||||
# we ignore any failure to write the cache file
|
||||
# there are many reasons, permission-denied, pycache dir being a
|
||||
# file etc.
|
||||
return False
|
||||
return True
|
||||
|
||||
else:
|
||||
|
||||
def _write_pyc(
|
||||
state: "AssertionState",
|
||||
co: types.CodeType,
|
||||
source_stat: os.stat_result,
|
||||
pyc: Path,
|
||||
) -> bool:
|
||||
proc_pyc = f"{pyc}.{os.getpid()}"
|
||||
try:
|
||||
fp = open(proc_pyc, "wb")
|
||||
except OSError as e:
|
||||
state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
|
||||
return False
|
||||
|
||||
try:
|
||||
def _write_pyc(
|
||||
state: "AssertionState",
|
||||
co: types.CodeType,
|
||||
source_stat: os.stat_result,
|
||||
pyc: Path,
|
||||
) -> bool:
|
||||
proc_pyc = f"{pyc}.{os.getpid()}"
|
||||
try:
|
||||
with open(proc_pyc, "wb") as fp:
|
||||
_write_pyc_fp(fp, source_stat, co)
|
||||
os.rename(proc_pyc, pyc)
|
||||
except OSError as e:
|
||||
state.trace(f"error writing pyc file at {pyc}: {e}")
|
||||
# we ignore any failure to write the cache file
|
||||
# there are many reasons, permission-denied, pycache dir being a
|
||||
# file etc.
|
||||
return False
|
||||
finally:
|
||||
fp.close()
|
||||
return True
|
||||
except OSError as e:
|
||||
state.trace(f"error writing pyc file at {proc_pyc}: errno={e.errno}")
|
||||
return False
|
||||
|
||||
try:
|
||||
os.replace(proc_pyc, pyc)
|
||||
except OSError as e:
|
||||
state.trace(f"error writing pyc file at {pyc}: {e}")
|
||||
# we ignore any failure to write the cache file
|
||||
# there are many reasons, permission-denied, pycache dir being a
|
||||
# file etc.
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _rewrite_test(fn: Path, config: Config) -> Tuple[os.stat_result, types.CodeType]:
|
||||
@@ -653,8 +653,14 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
.push_format_context() and .pop_format_context() which allows
|
||||
to build another %-formatted string while already building one.
|
||||
|
||||
This state is reset on every new assert statement visited and used
|
||||
by the other visitors.
|
||||
: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
|
||||
|
||||
This state, except the variables_overwrite, is reset on every new assert
|
||||
statement visited and used by the other visitors.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -670,6 +676,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
else:
|
||||
self.enable_assertion_pass_hook = False
|
||||
self.source = source
|
||||
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."""
|
||||
@@ -684,14 +694,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if doc is not None and self.is_rewrite_disabled(doc):
|
||||
return
|
||||
pos = 0
|
||||
lineno = 1
|
||||
item = None
|
||||
for item in mod.body:
|
||||
if (
|
||||
expect_docstring
|
||||
and isinstance(item, ast.Expr)
|
||||
and isinstance(item.value, ast.Str)
|
||||
and isinstance(item.value, astStr)
|
||||
):
|
||||
doc = item.value.s
|
||||
if sys.version_info >= (3, 8):
|
||||
doc = item.value.value
|
||||
else:
|
||||
doc = item.value.s
|
||||
if self.is_rewrite_disabled(doc):
|
||||
return
|
||||
expect_docstring = False
|
||||
@@ -732,9 +745,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
mod.body[pos:pos] = imports
|
||||
|
||||
# Collect asserts.
|
||||
nodes: List[ast.AST] = [mod]
|
||||
self.scope = (mod,)
|
||||
nodes: List[Union[ast.AST, Sentinel]] = [mod]
|
||||
while nodes:
|
||||
node = nodes.pop()
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
||||
self.scope = tuple((*self.scope, node))
|
||||
nodes.append(_SCOPE_END_MARKER)
|
||||
if node == _SCOPE_END_MARKER:
|
||||
self.scope = self.scope[:-1]
|
||||
continue
|
||||
assert isinstance(node, ast.AST)
|
||||
for name, field in ast.iter_fields(node):
|
||||
if isinstance(field, list):
|
||||
new: List[ast.AST] = []
|
||||
@@ -823,7 +844,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
current = self.stack.pop()
|
||||
if self.stack:
|
||||
self.explanation_specifiers = self.stack[-1]
|
||||
keys = [ast.Str(key) for key in current.keys()]
|
||||
keys = [astStr(key) for key in current.keys()]
|
||||
format_dict = ast.Dict(keys, list(current.values()))
|
||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||
name = "@py_format" + str(next(self.variable_counter))
|
||||
@@ -877,16 +898,16 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||
|
||||
if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook
|
||||
msg = self.pop_format_context(ast.Str(explanation))
|
||||
msg = self.pop_format_context(astStr(explanation))
|
||||
|
||||
# Failed
|
||||
if assert_.msg:
|
||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||
gluestr = "\n>assert "
|
||||
else:
|
||||
assertmsg = ast.Str("")
|
||||
assertmsg = astStr("")
|
||||
gluestr = "assert "
|
||||
err_explanation = ast.BinOp(ast.Str(gluestr), ast.Add(), msg)
|
||||
err_explanation = ast.BinOp(astStr(gluestr), ast.Add(), msg)
|
||||
err_msg = ast.BinOp(assertmsg, ast.Add(), err_explanation)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
fmt = self.helper("_format_explanation", err_msg)
|
||||
@@ -902,8 +923,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
hook_call_pass = ast.Expr(
|
||||
self.helper(
|
||||
"_call_assertion_pass",
|
||||
ast.Num(assert_.lineno),
|
||||
ast.Str(orig),
|
||||
astNum(assert_.lineno),
|
||||
astStr(orig),
|
||||
fmt_pass,
|
||||
)
|
||||
)
|
||||
@@ -922,7 +943,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
variables = [
|
||||
ast.Name(name, ast.Store()) for name in self.format_variables
|
||||
]
|
||||
clear_format = ast.Assign(variables, ast.NameConstant(None))
|
||||
clear_format = ast.Assign(variables, astNameConstant(None))
|
||||
self.statements.append(clear_format)
|
||||
|
||||
else: # Original assertion rewriting
|
||||
@@ -933,9 +954,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
assertmsg = self.helper("_format_assertmsg", assert_.msg)
|
||||
explanation = "\n>assert " + explanation
|
||||
else:
|
||||
assertmsg = ast.Str("")
|
||||
assertmsg = astStr("")
|
||||
explanation = "assert " + explanation
|
||||
template = ast.BinOp(assertmsg, ast.Add(), ast.Str(explanation))
|
||||
template = ast.BinOp(assertmsg, ast.Add(), astStr(explanation))
|
||||
msg = self.pop_format_context(template)
|
||||
fmt = self.helper("_format_explanation", msg)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
@@ -947,7 +968,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# Clear temporary variables by setting them to None.
|
||||
if self.variables:
|
||||
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
||||
clear = ast.Assign(variables, ast.NameConstant(None))
|
||||
clear = ast.Assign(variables, astNameConstant(None))
|
||||
self.statements.append(clear)
|
||||
# Fix locations (line numbers/column offsets).
|
||||
for stmt in self.statements:
|
||||
@@ -955,14 +976,26 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
ast.copy_location(node, assert_)
|
||||
return self.statements
|
||||
|
||||
def visit_NamedExpr(self, name: namedExpr) -> Tuple[namedExpr, str]:
|
||||
# This method handles the 'walrus operator' repr of the target
|
||||
# name if it's a local variable or _should_repr_global_name()
|
||||
# thinks it's acceptable.
|
||||
locs = ast.Call(self.builtin("locals"), [], [])
|
||||
target_id = name.target.id # type: ignore[attr-defined]
|
||||
inlocs = ast.Compare(astStr(target_id), [ast.In()], [locs])
|
||||
dorepr = self.helper("_should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
expr = ast.IfExp(test, self.display(name), astStr(target_id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_Name(self, name: ast.Name) -> Tuple[ast.Name, str]:
|
||||
# Display the repr of the name if it's a local variable or
|
||||
# _should_repr_global_name() thinks it's acceptable.
|
||||
locs = ast.Call(self.builtin("locals"), [], [])
|
||||
inlocs = ast.Compare(ast.Str(name.id), [ast.In()], [locs])
|
||||
inlocs = ast.Compare(astStr(name.id), [ast.In()], [locs])
|
||||
dorepr = self.helper("_should_repr_global_name", name)
|
||||
test = ast.BoolOp(ast.Or(), [inlocs, dorepr])
|
||||
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
|
||||
expr = ast.IfExp(test, self.display(name), astStr(name.id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_BoolOp(self, boolop: ast.BoolOp) -> Tuple[ast.Name, str]:
|
||||
@@ -981,10 +1014,26 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# cond is set in a prior loop iteration below
|
||||
self.expl_stmts.append(ast.If(cond, fail_inner, [])) # noqa
|
||||
self.expl_stmts = fail_inner
|
||||
# Check if the left operand is a namedExpr and the value has already been visited
|
||||
if (
|
||||
isinstance(v, ast.Compare)
|
||||
and isinstance(v.left, namedExpr)
|
||||
and v.left.target.id
|
||||
in [
|
||||
ast_expr.id
|
||||
for ast_expr in boolop.values[:i]
|
||||
if hasattr(ast_expr, "id")
|
||||
]
|
||||
):
|
||||
pytest_temp = self.variable()
|
||||
self.variables_overwrite[self.scope][
|
||||
v.left.target.id
|
||||
] = v.left # type:ignore[assignment]
|
||||
v.left.target.id = pytest_temp
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||
expl_format = self.pop_format_context(ast.Str(expl))
|
||||
expl_format = self.pop_format_context(astStr(expl))
|
||||
call = ast.Call(app, [expl_format], [])
|
||||
self.expl_stmts.append(ast.Expr(call))
|
||||
if i < levels:
|
||||
@@ -996,7 +1045,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
self.statements = body = inner
|
||||
self.statements = save
|
||||
self.expl_stmts = fail_save
|
||||
expl_template = self.helper("_format_boolop", expl_list, ast.Num(is_or))
|
||||
expl_template = self.helper("_format_boolop", expl_list, astNum(is_or))
|
||||
expl = self.pop_format_context(expl_template)
|
||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||
|
||||
@@ -1020,10 +1069,22 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
new_args = []
|
||||
new_kwargs = []
|
||||
for arg in call.args:
|
||||
if isinstance(arg, ast.Name) and arg.id in self.variables_overwrite.get(
|
||||
self.scope, {}
|
||||
):
|
||||
arg = self.variables_overwrite[self.scope][
|
||||
arg.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(arg)
|
||||
arg_expls.append(expl)
|
||||
new_args.append(res)
|
||||
for keyword in call.keywords:
|
||||
if isinstance(
|
||||
keyword.value, ast.Name
|
||||
) and keyword.value.id in self.variables_overwrite.get(self.scope, {}):
|
||||
keyword.value = self.variables_overwrite[self.scope][
|
||||
keyword.value.id
|
||||
] # type:ignore[assignment]
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
if keyword.arg:
|
||||
@@ -1056,6 +1117,17 @@ 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.get(self.scope, {}):
|
||||
comp.left = self.variables_overwrite[self.scope][
|
||||
comp.left.id
|
||||
] # type:ignore[assignment]
|
||||
if isinstance(comp.left, namedExpr):
|
||||
self.variables_overwrite[self.scope][
|
||||
comp.left.target.id
|
||||
] = comp.left # type:ignore[assignment]
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||
left_expl = f"({left_expl})"
|
||||
@@ -1067,14 +1139,23 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
syms = []
|
||||
results = [left_res]
|
||||
for i, op, next_operand in it:
|
||||
if (
|
||||
isinstance(next_operand, namedExpr)
|
||||
and isinstance(left_res, ast.Name)
|
||||
and next_operand.target.id == left_res.id
|
||||
):
|
||||
next_operand.target.id = self.variable()
|
||||
self.variables_overwrite[self.scope][
|
||||
left_res.id
|
||||
] = next_operand # type:ignore[assignment]
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||
next_expl = f"({next_expl})"
|
||||
results.append(next_res)
|
||||
sym = BINOP_MAP[op.__class__]
|
||||
syms.append(ast.Str(sym))
|
||||
syms.append(astStr(sym))
|
||||
expl = f"{left_expl} {sym} {next_expl}"
|
||||
expls.append(ast.Str(expl))
|
||||
expls.append(astStr(expl))
|
||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||
left_res, left_expl = next_res, next_expl
|
||||
@@ -1090,6 +1171,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
res: ast.expr = ast.BoolOp(ast.And(), load_names)
|
||||
else:
|
||||
res = load_names[0]
|
||||
|
||||
return res, self.explanation_param(self.pop_format_context(expl_call))
|
||||
|
||||
|
||||
|
||||
@@ -38,9 +38,9 @@ def _truncate_explanation(
|
||||
"""Truncate given list of strings that makes up the assertion explanation.
|
||||
|
||||
Truncates to either 8 lines, or 640 characters - whichever the input reaches
|
||||
first. The remaining lines will be replaced by a usage message.
|
||||
first, taking the truncation explanation into account. The remaining lines
|
||||
will be replaced by a usage message.
|
||||
"""
|
||||
|
||||
if max_lines is None:
|
||||
max_lines = DEFAULT_MAX_LINES
|
||||
if max_chars is None:
|
||||
@@ -48,35 +48,56 @@ def _truncate_explanation(
|
||||
|
||||
# Check if truncation required
|
||||
input_char_count = len("".join(input_lines))
|
||||
if len(input_lines) <= max_lines and input_char_count <= max_chars:
|
||||
# The length of the truncation explanation depends on the number of lines
|
||||
# removed but is at least 68 characters:
|
||||
# The real value is
|
||||
# 64 (for the base message:
|
||||
# '...\n...Full output truncated (1 line hidden), use '-vv' to show")'
|
||||
# )
|
||||
# + 1 (for plural)
|
||||
# + int(math.log10(len(input_lines) - max_lines)) (number of hidden line, at least 1)
|
||||
# + 3 for the '...' added to the truncated line
|
||||
# But if there's more than 100 lines it's very likely that we're going to
|
||||
# truncate, so we don't need the exact value using log10.
|
||||
tolerable_max_chars = (
|
||||
max_chars + 70 # 64 + 1 (for plural) + 2 (for '99') + 3 for '...'
|
||||
)
|
||||
# The truncation explanation add two lines to the output
|
||||
tolerable_max_lines = max_lines + 2
|
||||
if (
|
||||
len(input_lines) <= tolerable_max_lines
|
||||
and input_char_count <= tolerable_max_chars
|
||||
):
|
||||
return input_lines
|
||||
|
||||
# Truncate first to max_lines, and then truncate to max_chars if max_chars
|
||||
# is exceeded.
|
||||
# Truncate first to max_lines, and then truncate to max_chars if necessary
|
||||
truncated_explanation = input_lines[:max_lines]
|
||||
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
|
||||
|
||||
# Add ellipsis to final line
|
||||
truncated_explanation[-1] = truncated_explanation[-1] + "..."
|
||||
|
||||
# Append useful message to explanation
|
||||
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
||||
truncated_line_count += 1 # Account for the part-truncated final line
|
||||
msg = "...Full output truncated"
|
||||
if truncated_line_count == 1:
|
||||
msg += f" ({truncated_line_count} line hidden)"
|
||||
truncated_char = True
|
||||
# We reevaluate the need to truncate chars following removal of some lines
|
||||
if len("".join(truncated_explanation)) > tolerable_max_chars:
|
||||
truncated_explanation = _truncate_by_char_count(
|
||||
truncated_explanation, max_chars
|
||||
)
|
||||
else:
|
||||
msg += f" ({truncated_line_count} lines hidden)"
|
||||
msg += f", {USAGE_MSG}"
|
||||
truncated_explanation.extend(["", str(msg)])
|
||||
return truncated_explanation
|
||||
truncated_char = False
|
||||
|
||||
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
||||
if truncated_explanation[-1]:
|
||||
# Add ellipsis and take into account part-truncated final line
|
||||
truncated_explanation[-1] = truncated_explanation[-1] + "..."
|
||||
if truncated_char:
|
||||
# It's possible that we did not remove any char from this line
|
||||
truncated_line_count += 1
|
||||
else:
|
||||
# Add proper ellipsis when we were able to fit a full line exactly
|
||||
truncated_explanation[-1] = "..."
|
||||
return truncated_explanation + [
|
||||
"",
|
||||
f"...Full output truncated ({truncated_line_count} line"
|
||||
f"{'' if truncated_line_count == 1 else 's'} hidden), {USAGE_MSG}",
|
||||
]
|
||||
|
||||
|
||||
def _truncate_by_char_count(input_lines: List[str], max_chars: int) -> List[str]:
|
||||
# Check if truncation required
|
||||
if len("".join(input_lines)) <= max_chars:
|
||||
return input_lines
|
||||
|
||||
# Find point at which input length exceeds total allowed length
|
||||
iterated_char_count = 0
|
||||
for iterated_index, input_line in enumerate(input_lines):
|
||||
|
||||
@@ -10,12 +10,13 @@ from typing import List
|
||||
from typing import Mapping
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from unicodedata import normalize
|
||||
|
||||
import _pytest._code
|
||||
from _pytest import outcomes
|
||||
from _pytest._io.saferepr import _pformat_dispatch
|
||||
from _pytest._io.saferepr import safeformat
|
||||
from _pytest._io.saferepr import saferepr
|
||||
from _pytest._io.saferepr import saferepr_unlimited
|
||||
from _pytest.config import Config
|
||||
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
@@ -156,20 +157,32 @@ def has_default_eq(
|
||||
return True
|
||||
|
||||
|
||||
def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]:
|
||||
def assertrepr_compare(
|
||||
config, op: str, left: Any, right: Any, use_ascii: bool = False
|
||||
) -> Optional[List[str]]:
|
||||
"""Return specialised explanations for some operators/operands."""
|
||||
verbose = config.getoption("verbose")
|
||||
|
||||
# Strings which normalize equal are often hard to distinguish when printed; use ascii() to make this easier.
|
||||
# See issue #3246.
|
||||
use_ascii = (
|
||||
isinstance(left, str)
|
||||
and isinstance(right, str)
|
||||
and normalize("NFD", left) == normalize("NFD", right)
|
||||
)
|
||||
|
||||
if verbose > 1:
|
||||
left_repr = safeformat(left)
|
||||
right_repr = safeformat(right)
|
||||
left_repr = saferepr_unlimited(left, use_ascii=use_ascii)
|
||||
right_repr = saferepr_unlimited(right, use_ascii=use_ascii)
|
||||
else:
|
||||
# XXX: "15 chars indentation" is wrong
|
||||
# ("E AssertionError: assert "); should use term width.
|
||||
maxsize = (
|
||||
80 - 15 - len(op) - 2
|
||||
) // 2 # 15 chars indentation, 1 space around op
|
||||
left_repr = saferepr(left, maxsize=maxsize)
|
||||
right_repr = saferepr(right, maxsize=maxsize)
|
||||
|
||||
left_repr = saferepr(left, maxsize=maxsize, use_ascii=use_ascii)
|
||||
right_repr = saferepr(right, maxsize=maxsize, use_ascii=use_ascii)
|
||||
|
||||
summary = f"{left_repr} {op} {right_repr}"
|
||||
|
||||
@@ -437,8 +450,10 @@ def _compare_eq_cls(left: Any, right: Any, verbose: int) -> List[str]:
|
||||
if not has_default_eq(left):
|
||||
return []
|
||||
if isdatacls(left):
|
||||
all_fields = left.__dataclass_fields__
|
||||
fields_to_check = [field for field, info in all_fields.items() if info.compare]
|
||||
import dataclasses
|
||||
|
||||
all_fields = dataclasses.fields(left)
|
||||
fields_to_check = [info.name for info in all_fields if info.compare]
|
||||
elif isattrs(left):
|
||||
all_fields = left.__attrs_attrs__
|
||||
fields_to_check = [field.name for field in all_fields if getattr(field, "eq")]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Implementation of the cache provider."""
|
||||
# This plugin was not named "cache" to avoid conflicts with the external
|
||||
# pytest-cache version.
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -12,8 +13,6 @@ from typing import Optional
|
||||
from typing import Set
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
|
||||
from .pathlib import resolve_from_str
|
||||
from .pathlib import rm_rf
|
||||
from .reports import CollectReport
|
||||
@@ -28,11 +27,10 @@ from _pytest.deprecated import check_ispytest
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.main import Session
|
||||
from _pytest.python import Module
|
||||
from _pytest.nodes import File
|
||||
from _pytest.python import Package
|
||||
from _pytest.reports import TestReport
|
||||
|
||||
|
||||
README_CONTENT = """\
|
||||
# pytest cache directory #
|
||||
|
||||
@@ -53,10 +51,12 @@ Signature: 8a477f597d28d172789f06886806bc55
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(init=False, auto_attribs=True)
|
||||
@dataclasses.dataclass
|
||||
class Cache:
|
||||
_cachedir: Path = attr.ib(repr=False)
|
||||
_config: Config = attr.ib(repr=False)
|
||||
"""Instance of the `cache` fixture."""
|
||||
|
||||
_cachedir: Path = dataclasses.field(repr=False)
|
||||
_config: Config = dataclasses.field(repr=False)
|
||||
|
||||
# Sub-directory under cache-dir for directories created by `mkdir()`.
|
||||
_CACHE_PREFIX_DIRS = "d"
|
||||
@@ -157,7 +157,7 @@ class Cache:
|
||||
"""
|
||||
path = self._getvaluepath(key)
|
||||
try:
|
||||
with path.open("r") as f:
|
||||
with path.open("r", encoding="UTF-8") as f:
|
||||
return json.load(f)
|
||||
except (ValueError, OSError):
|
||||
return default
|
||||
@@ -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, indent=2)
|
||||
data = json.dumps(value, ensure_ascii=False, indent=2)
|
||||
try:
|
||||
f = path.open("w")
|
||||
except OSError:
|
||||
self.warn("cache could not write path {path}", path=path, _ispytest=True)
|
||||
f = path.open("w", encoding="UTF-8")
|
||||
except OSError as exc:
|
||||
self.warn(
|
||||
f"cache could not write path {path}: {exc}",
|
||||
_ispytest=True,
|
||||
)
|
||||
else:
|
||||
with f:
|
||||
f.write(data)
|
||||
@@ -196,7 +202,7 @@ class Cache:
|
||||
def _ensure_supporting_files(self) -> None:
|
||||
"""Create supporting files in the cache dir that are not really part of the cache."""
|
||||
readme_path = self._cachedir / "README.md"
|
||||
readme_path.write_text(README_CONTENT)
|
||||
readme_path.write_text(README_CONTENT, encoding="UTF-8")
|
||||
|
||||
gitignore_path = self._cachedir.joinpath(".gitignore")
|
||||
msg = "# Created by pytest automatically.\n*\n"
|
||||
@@ -213,22 +219,30 @@ class LFPluginCollWrapper:
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector: nodes.Collector):
|
||||
if isinstance(collector, Session):
|
||||
if isinstance(collector, (Session, Package)):
|
||||
out = yield
|
||||
res: CollectReport = out.get_result()
|
||||
|
||||
# Sort any lf-paths to the beginning.
|
||||
lf_paths = self.lfplugin._last_failed_paths
|
||||
|
||||
# Use stable sort to priorize last failed.
|
||||
def sort_key(node: Union[nodes.Item, nodes.Collector]) -> bool:
|
||||
# Package.path is the __init__.py file, we need the directory.
|
||||
if isinstance(node, Package):
|
||||
path = node.path.parent
|
||||
else:
|
||||
path = node.path
|
||||
return path in lf_paths
|
||||
|
||||
res.result = sorted(
|
||||
res.result,
|
||||
# use stable sort to priorize last failed
|
||||
key=lambda x: x.path in lf_paths,
|
||||
key=sort_key,
|
||||
reverse=True,
|
||||
)
|
||||
return
|
||||
|
||||
elif isinstance(collector, Module):
|
||||
elif isinstance(collector, File):
|
||||
if collector.path in self.lfplugin._last_failed_paths:
|
||||
out = yield
|
||||
res = out.get_result()
|
||||
@@ -266,10 +280,9 @@ class LFPluginCollSkipfiles:
|
||||
def pytest_make_collect_report(
|
||||
self, collector: nodes.Collector
|
||||
) -> Optional[CollectReport]:
|
||||
# Packages are Modules, but _last_failed_paths only contains
|
||||
# test-bearing paths and doesn't try to include the paths of their
|
||||
# packages, so don't filter them.
|
||||
if isinstance(collector, Module) and not isinstance(collector, Package):
|
||||
# Packages are Files, but we only want to skip test-bearing Files,
|
||||
# so don't filter Packages.
|
||||
if isinstance(collector, File) and not isinstance(collector, Package):
|
||||
if collector.path not in self.lfplugin._last_failed_paths:
|
||||
self.lfplugin._skipped_files += 1
|
||||
|
||||
@@ -299,9 +312,14 @@ class LFPlugin:
|
||||
)
|
||||
|
||||
def get_last_failed_paths(self) -> Set[Path]:
|
||||
"""Return a set with all Paths()s of the previously failed nodeids."""
|
||||
"""Return a set with all Paths of the previously failed nodeids and
|
||||
their parents."""
|
||||
rootpath = self.config.rootpath
|
||||
result = {rootpath / nodeid.split("::")[0] for nodeid in self.lastfailed}
|
||||
result = set()
|
||||
for nodeid in self.lastfailed:
|
||||
path = rootpath / nodeid.split("::")[0]
|
||||
result.add(path)
|
||||
result.update(path.parents)
|
||||
return {x for x in result if x.exists()}
|
||||
|
||||
def pytest_report_collectionfinish(self) -> Optional[str]:
|
||||
@@ -440,7 +458,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"--last-failed",
|
||||
action="store_true",
|
||||
dest="lf",
|
||||
help="rerun only the tests that failed "
|
||||
help="Rerun only the tests that failed "
|
||||
"at the last run (or all if none failed)",
|
||||
)
|
||||
group.addoption(
|
||||
@@ -448,7 +466,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"--failed-first",
|
||||
action="store_true",
|
||||
dest="failedfirst",
|
||||
help="run all tests, but run the last failures first.\n"
|
||||
help="Run all tests, but run the last failures first. "
|
||||
"This may re-order tests and thus lead to "
|
||||
"repeated fixture setup/teardown.",
|
||||
)
|
||||
@@ -457,7 +475,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"--new-first",
|
||||
action="store_true",
|
||||
dest="newfirst",
|
||||
help="run tests from new files first, then the rest of the tests "
|
||||
help="Run tests from new files first, then the rest of the tests "
|
||||
"sorted by file mtime",
|
||||
)
|
||||
group.addoption(
|
||||
@@ -466,7 +484,7 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
nargs="?",
|
||||
dest="cacheshow",
|
||||
help=(
|
||||
"show cache contents, don't perform collection or tests. "
|
||||
"Show cache contents, don't perform collection or tests. "
|
||||
"Optional argument: glob (default: '*')."
|
||||
),
|
||||
)
|
||||
@@ -474,12 +492,12 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"--cache-clear",
|
||||
action="store_true",
|
||||
dest="cacheclear",
|
||||
help="remove all cache contents at start of test run.",
|
||||
help="Remove all cache contents at start of test run",
|
||||
)
|
||||
cache_dir_default = ".pytest_cache"
|
||||
if "TOX_ENV_DIR" in os.environ:
|
||||
cache_dir_default = os.path.join(os.environ["TOX_ENV_DIR"], cache_dir_default)
|
||||
parser.addini("cache_dir", default=cache_dir_default, help="cache directory path.")
|
||||
parser.addini("cache_dir", default=cache_dir_default, help="Cache directory path")
|
||||
group.addoption(
|
||||
"--lfnf",
|
||||
"--last-failed-no-failures",
|
||||
@@ -487,12 +505,16 @@ 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.",
|
||||
)
|
||||
|
||||
|
||||
def pytest_cmdline_main(config: Config) -> Optional[Union[int, ExitCode]]:
|
||||
if config.option.cacheshow:
|
||||
if config.option.cacheshow and not config.option.help:
|
||||
from _pytest.main import wrap_session
|
||||
|
||||
return wrap_session(config, cacheshow)
|
||||
|
||||
@@ -1,19 +1,26 @@
|
||||
"""Per-test stdout/stderr capturing mechanism."""
|
||||
import abc
|
||||
import collections
|
||||
import contextlib
|
||||
import functools
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
from io import UnsupportedOperation
|
||||
from tempfile import TemporaryFile
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import AnyStr
|
||||
from typing import BinaryIO
|
||||
from typing import Generator
|
||||
from typing import Generic
|
||||
from typing import Iterable
|
||||
from typing import Iterator
|
||||
from typing import List
|
||||
from typing import NamedTuple
|
||||
from typing import Optional
|
||||
from typing import TextIO
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
@@ -29,6 +36,7 @@ from _pytest.nodes import File
|
||||
from _pytest.nodes import Item
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Final
|
||||
from typing_extensions import Literal
|
||||
|
||||
_CaptureMethod = Literal["fd", "sys", "no", "tee-sys"]
|
||||
@@ -42,14 +50,14 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
default="fd",
|
||||
metavar="method",
|
||||
choices=["fd", "sys", "no", "tee-sys"],
|
||||
help="per-test capturing method: one of fd|sys|no|tee-sys.",
|
||||
help="Per-test capturing method: one of fd|sys|no|tee-sys",
|
||||
)
|
||||
group._addoption(
|
||||
"-s",
|
||||
action="store_const",
|
||||
const="no",
|
||||
dest="capture",
|
||||
help="shortcut for --capture=no.",
|
||||
help="Shortcut for --capture=no",
|
||||
)
|
||||
|
||||
|
||||
@@ -185,53 +193,151 @@ class TeeCaptureIO(CaptureIO):
|
||||
return self._other.write(s)
|
||||
|
||||
|
||||
class DontReadFromInput:
|
||||
encoding = None
|
||||
class DontReadFromInput(TextIO):
|
||||
@property
|
||||
def encoding(self) -> str:
|
||||
return sys.__stdin__.encoding
|
||||
|
||||
def read(self, *args):
|
||||
def read(self, size: int = -1) -> str:
|
||||
raise OSError(
|
||||
"pytest: reading from stdin while output is captured! Consider using `-s`."
|
||||
)
|
||||
|
||||
readline = read
|
||||
readlines = read
|
||||
__next__ = read
|
||||
|
||||
def __iter__(self):
|
||||
def __next__(self) -> str:
|
||||
return self.readline()
|
||||
|
||||
def readlines(self, hint: Optional[int] = -1) -> List[str]:
|
||||
raise OSError(
|
||||
"pytest: reading from stdin while output is captured! Consider using `-s`."
|
||||
)
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return self
|
||||
|
||||
def fileno(self) -> int:
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no fileno()")
|
||||
|
||||
def flush(self) -> None:
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no flush()")
|
||||
|
||||
def isatty(self) -> bool:
|
||||
return False
|
||||
|
||||
def close(self) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def buffer(self):
|
||||
def readable(self) -> bool:
|
||||
return False
|
||||
|
||||
def seek(self, offset: int, whence: int = 0) -> int:
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no seek(int)")
|
||||
|
||||
def seekable(self) -> bool:
|
||||
return False
|
||||
|
||||
def tell(self) -> int:
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, has no tell()")
|
||||
|
||||
def truncate(self, size: Optional[int] = None) -> int:
|
||||
raise UnsupportedOperation("cannot truncate stdin")
|
||||
|
||||
def write(self, data: str) -> int:
|
||||
raise UnsupportedOperation("cannot write to stdin")
|
||||
|
||||
def writelines(self, lines: Iterable[str]) -> None:
|
||||
raise UnsupportedOperation("Cannot write to stdin")
|
||||
|
||||
def writable(self) -> bool:
|
||||
return False
|
||||
|
||||
def __enter__(self) -> "DontReadFromInput":
|
||||
return self
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
type: Optional[Type[BaseException]],
|
||||
value: Optional[BaseException],
|
||||
traceback: Optional[TracebackType],
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
@property
|
||||
def buffer(self) -> BinaryIO:
|
||||
# The str/bytes doesn't actually matter in this type, so OK to fake.
|
||||
return self # type: ignore[return-value]
|
||||
|
||||
|
||||
# Capture classes.
|
||||
|
||||
|
||||
class CaptureBase(abc.ABC, Generic[AnyStr]):
|
||||
EMPTY_BUFFER: AnyStr
|
||||
|
||||
@abc.abstractmethod
|
||||
def __init__(self, fd: int) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def start(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def done(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def suspend(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def resume(self) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def writeorg(self, data: AnyStr) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def snap(self) -> AnyStr:
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
||||
|
||||
|
||||
class NoCapture:
|
||||
EMPTY_BUFFER = None
|
||||
__init__ = start = done = suspend = resume = lambda *args: None
|
||||
class NoCapture(CaptureBase[str]):
|
||||
EMPTY_BUFFER = ""
|
||||
|
||||
def __init__(self, fd: int) -> None:
|
||||
pass
|
||||
|
||||
def start(self) -> None:
|
||||
pass
|
||||
|
||||
def done(self) -> None:
|
||||
pass
|
||||
|
||||
def suspend(self) -> None:
|
||||
pass
|
||||
|
||||
def resume(self) -> None:
|
||||
pass
|
||||
|
||||
def snap(self) -> str:
|
||||
return ""
|
||||
|
||||
def writeorg(self, data: str) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class SysCaptureBinary:
|
||||
|
||||
EMPTY_BUFFER = b""
|
||||
|
||||
def __init__(self, fd: int, tmpfile=None, *, tee: bool = False) -> None:
|
||||
class SysCaptureBase(CaptureBase[AnyStr]):
|
||||
def __init__(
|
||||
self, fd: int, tmpfile: Optional[TextIO] = None, *, tee: bool = False
|
||||
) -> None:
|
||||
name = patchsysdict[fd]
|
||||
self._old = getattr(sys, name)
|
||||
self._old: TextIO = getattr(sys, name)
|
||||
self.name = name
|
||||
if tmpfile is None:
|
||||
if name == "stdin":
|
||||
@@ -271,14 +377,6 @@ class SysCaptureBinary:
|
||||
setattr(sys, self.name, self.tmpfile)
|
||||
self._state = "started"
|
||||
|
||||
def snap(self):
|
||||
self._assert_state("snap", ("started", "suspended"))
|
||||
self.tmpfile.seek(0)
|
||||
res = self.tmpfile.buffer.read()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def done(self) -> None:
|
||||
self._assert_state("done", ("initialized", "started", "suspended", "done"))
|
||||
if self._state == "done":
|
||||
@@ -300,36 +398,43 @@ class SysCaptureBinary:
|
||||
setattr(sys, self.name, self.tmpfile)
|
||||
self._state = "started"
|
||||
|
||||
def writeorg(self, data) -> None:
|
||||
|
||||
class SysCaptureBinary(SysCaptureBase[bytes]):
|
||||
EMPTY_BUFFER = b""
|
||||
|
||||
def snap(self) -> bytes:
|
||||
self._assert_state("snap", ("started", "suspended"))
|
||||
self.tmpfile.seek(0)
|
||||
res = self.tmpfile.buffer.read()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def writeorg(self, data: bytes) -> None:
|
||||
self._assert_state("writeorg", ("started", "suspended"))
|
||||
self._old.flush()
|
||||
self._old.buffer.write(data)
|
||||
self._old.buffer.flush()
|
||||
|
||||
|
||||
class SysCapture(SysCaptureBinary):
|
||||
EMPTY_BUFFER = "" # type: ignore[assignment]
|
||||
class SysCapture(SysCaptureBase[str]):
|
||||
EMPTY_BUFFER = ""
|
||||
|
||||
def snap(self):
|
||||
def snap(self) -> str:
|
||||
self._assert_state("snap", ("started", "suspended"))
|
||||
assert isinstance(self.tmpfile, CaptureIO)
|
||||
res = self.tmpfile.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def writeorg(self, data):
|
||||
def writeorg(self, data: str) -> None:
|
||||
self._assert_state("writeorg", ("started", "suspended"))
|
||||
self._old.write(data)
|
||||
self._old.flush()
|
||||
|
||||
|
||||
class FDCaptureBinary:
|
||||
"""Capture IO to/from a given OS-level file descriptor.
|
||||
|
||||
snap() produces `bytes`.
|
||||
"""
|
||||
|
||||
EMPTY_BUFFER = b""
|
||||
|
||||
class FDCaptureBase(CaptureBase[AnyStr]):
|
||||
def __init__(self, targetfd: int) -> None:
|
||||
self.targetfd = targetfd
|
||||
|
||||
@@ -354,8 +459,8 @@ class FDCaptureBinary:
|
||||
self.targetfd_save = os.dup(targetfd)
|
||||
|
||||
if targetfd == 0:
|
||||
self.tmpfile = open(os.devnull)
|
||||
self.syscapture = SysCapture(targetfd)
|
||||
self.tmpfile = open(os.devnull, encoding="utf-8")
|
||||
self.syscapture: CaptureBase[str] = SysCapture(targetfd)
|
||||
else:
|
||||
self.tmpfile = EncodedFile(
|
||||
TemporaryFile(buffering=0),
|
||||
@@ -367,7 +472,7 @@ class FDCaptureBinary:
|
||||
if targetfd in patchsysdict:
|
||||
self.syscapture = SysCapture(targetfd, self.tmpfile)
|
||||
else:
|
||||
self.syscapture = NoCapture()
|
||||
self.syscapture = NoCapture(targetfd)
|
||||
|
||||
self._state = "initialized"
|
||||
|
||||
@@ -394,14 +499,6 @@ class FDCaptureBinary:
|
||||
self.syscapture.start()
|
||||
self._state = "started"
|
||||
|
||||
def snap(self):
|
||||
self._assert_state("snap", ("started", "suspended"))
|
||||
self.tmpfile.seek(0)
|
||||
res = self.tmpfile.buffer.read()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def done(self) -> None:
|
||||
"""Stop capturing, restore streams, return original capture file,
|
||||
seeked to position zero."""
|
||||
@@ -434,22 +531,38 @@ class FDCaptureBinary:
|
||||
os.dup2(self.tmpfile.fileno(), self.targetfd)
|
||||
self._state = "started"
|
||||
|
||||
def writeorg(self, data):
|
||||
|
||||
class FDCaptureBinary(FDCaptureBase[bytes]):
|
||||
"""Capture IO to/from a given OS-level file descriptor.
|
||||
|
||||
snap() produces `bytes`.
|
||||
"""
|
||||
|
||||
EMPTY_BUFFER = b""
|
||||
|
||||
def snap(self) -> bytes:
|
||||
self._assert_state("snap", ("started", "suspended"))
|
||||
self.tmpfile.seek(0)
|
||||
res = self.tmpfile.buffer.read()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def writeorg(self, data: bytes) -> None:
|
||||
"""Write to original file descriptor."""
|
||||
self._assert_state("writeorg", ("started", "suspended"))
|
||||
os.write(self.targetfd_save, data)
|
||||
|
||||
|
||||
class FDCapture(FDCaptureBinary):
|
||||
class FDCapture(FDCaptureBase[str]):
|
||||
"""Capture IO to/from a given OS-level file descriptor.
|
||||
|
||||
snap() produces text.
|
||||
"""
|
||||
|
||||
# Ignore type because it doesn't match the type in the superclass (bytes).
|
||||
EMPTY_BUFFER = "" # type: ignore
|
||||
EMPTY_BUFFER = ""
|
||||
|
||||
def snap(self):
|
||||
def snap(self) -> str:
|
||||
self._assert_state("snap", ("started", "suspended"))
|
||||
self.tmpfile.seek(0)
|
||||
res = self.tmpfile.read()
|
||||
@@ -457,77 +570,49 @@ class FDCapture(FDCaptureBinary):
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def writeorg(self, data):
|
||||
def writeorg(self, data: str) -> None:
|
||||
"""Write to original file descriptor."""
|
||||
super().writeorg(data.encode("utf-8")) # XXX use encoding of original stream
|
||||
self._assert_state("writeorg", ("started", "suspended"))
|
||||
# XXX use encoding of original stream
|
||||
os.write(self.targetfd_save, data.encode("utf-8"))
|
||||
|
||||
|
||||
# MultiCapture
|
||||
|
||||
|
||||
# This class was a namedtuple, but due to mypy limitation[0] it could not be
|
||||
# made generic, so was replaced by a regular class which tries to emulate the
|
||||
# pertinent parts of a namedtuple. If the mypy limitation is ever lifted, can
|
||||
# make it a namedtuple again.
|
||||
# [0]: https://github.com/python/mypy/issues/685
|
||||
@final
|
||||
@functools.total_ordering
|
||||
class CaptureResult(Generic[AnyStr]):
|
||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
||||
# Generic NamedTuple only supported since Python 3.11.
|
||||
if sys.version_info >= (3, 11) or TYPE_CHECKING:
|
||||
|
||||
__slots__ = ("out", "err")
|
||||
@final
|
||||
class CaptureResult(NamedTuple, Generic[AnyStr]):
|
||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
||||
|
||||
def __init__(self, out: AnyStr, err: AnyStr) -> None:
|
||||
self.out: AnyStr = out
|
||||
self.err: AnyStr = err
|
||||
out: AnyStr
|
||||
err: AnyStr
|
||||
|
||||
def __len__(self) -> int:
|
||||
return 2
|
||||
else:
|
||||
|
||||
def __iter__(self) -> Iterator[AnyStr]:
|
||||
return iter((self.out, self.err))
|
||||
class CaptureResult(
|
||||
collections.namedtuple("CaptureResult", ["out", "err"]), Generic[AnyStr]
|
||||
):
|
||||
"""The result of :method:`CaptureFixture.readouterr`."""
|
||||
|
||||
def __getitem__(self, item: int) -> AnyStr:
|
||||
return tuple(self)[item]
|
||||
|
||||
def _replace(
|
||||
self, *, out: Optional[AnyStr] = None, err: Optional[AnyStr] = None
|
||||
) -> "CaptureResult[AnyStr]":
|
||||
return CaptureResult(
|
||||
out=self.out if out is None else out, err=self.err if err is None else err
|
||||
)
|
||||
|
||||
def count(self, value: AnyStr) -> int:
|
||||
return tuple(self).count(value)
|
||||
|
||||
def index(self, value) -> int:
|
||||
return tuple(self).index(value)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, (CaptureResult, tuple)):
|
||||
return NotImplemented
|
||||
return tuple(self) == tuple(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(tuple(self))
|
||||
|
||||
def __lt__(self, other: object) -> bool:
|
||||
if not isinstance(other, (CaptureResult, tuple)):
|
||||
return NotImplemented
|
||||
return tuple(self) < tuple(other)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"CaptureResult(out={self.out!r}, err={self.err!r})"
|
||||
__slots__ = ()
|
||||
|
||||
|
||||
class MultiCapture(Generic[AnyStr]):
|
||||
_state = None
|
||||
_in_suspended = False
|
||||
|
||||
def __init__(self, in_, out, err) -> None:
|
||||
self.in_ = in_
|
||||
self.out = out
|
||||
self.err = err
|
||||
def __init__(
|
||||
self,
|
||||
in_: Optional[CaptureBase[AnyStr]],
|
||||
out: Optional[CaptureBase[AnyStr]],
|
||||
err: Optional[CaptureBase[AnyStr]],
|
||||
) -> None:
|
||||
self.in_: Optional[CaptureBase[AnyStr]] = in_
|
||||
self.out: Optional[CaptureBase[AnyStr]] = out
|
||||
self.err: Optional[CaptureBase[AnyStr]] = err
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "<MultiCapture out={!r} err={!r} in_={!r} _state={!r} _in_suspended={!r}>".format(
|
||||
@@ -551,8 +636,10 @@ class MultiCapture(Generic[AnyStr]):
|
||||
"""Pop current snapshot out/err capture and flush to orig streams."""
|
||||
out, err = self.readouterr()
|
||||
if out:
|
||||
assert self.out is not None
|
||||
self.out.writeorg(out)
|
||||
if err:
|
||||
assert self.err is not None
|
||||
self.err.writeorg(err)
|
||||
return out, err
|
||||
|
||||
@@ -573,6 +660,7 @@ class MultiCapture(Generic[AnyStr]):
|
||||
if self.err:
|
||||
self.err.resume()
|
||||
if self._in_suspended:
|
||||
assert self.in_ is not None
|
||||
self.in_.resume()
|
||||
self._in_suspended = False
|
||||
|
||||
@@ -595,7 +683,8 @@ class MultiCapture(Generic[AnyStr]):
|
||||
def readouterr(self) -> CaptureResult[AnyStr]:
|
||||
out = self.out.snap() if self.out else ""
|
||||
err = self.err.snap() if self.err else ""
|
||||
return CaptureResult(out, err)
|
||||
# TODO: This type error is real, need to fix.
|
||||
return CaptureResult(out, err) # type: ignore[arg-type]
|
||||
|
||||
|
||||
def _get_multicapture(method: "_CaptureMethod") -> MultiCapture[str]:
|
||||
@@ -635,7 +724,7 @@ class CaptureManager:
|
||||
"""
|
||||
|
||||
def __init__(self, method: "_CaptureMethod") -> None:
|
||||
self._method = method
|
||||
self._method: Final = method
|
||||
self._global_capturing: Optional[MultiCapture[str]] = None
|
||||
self._capture_fixture: Optional[CaptureFixture[Any]] = None
|
||||
|
||||
@@ -804,14 +893,18 @@ class CaptureFixture(Generic[AnyStr]):
|
||||
:fixture:`capfd` and :fixture:`capfdbinary` fixtures."""
|
||||
|
||||
def __init__(
|
||||
self, captureclass, request: SubRequest, *, _ispytest: bool = False
|
||||
self,
|
||||
captureclass: Type[CaptureBase[AnyStr]],
|
||||
request: SubRequest,
|
||||
*,
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self.captureclass = captureclass
|
||||
self.captureclass: Type[CaptureBase[AnyStr]] = captureclass
|
||||
self.request = request
|
||||
self._capture: Optional[MultiCapture[AnyStr]] = None
|
||||
self._captured_out = self.captureclass.EMPTY_BUFFER
|
||||
self._captured_err = self.captureclass.EMPTY_BUFFER
|
||||
self._captured_out: AnyStr = self.captureclass.EMPTY_BUFFER
|
||||
self._captured_err: AnyStr = self.captureclass.EMPTY_BUFFER
|
||||
|
||||
def _start(self) -> None:
|
||||
if self._capture is None:
|
||||
@@ -866,7 +959,9 @@ class CaptureFixture(Generic[AnyStr]):
|
||||
@contextlib.contextmanager
|
||||
def disabled(self) -> Generator[None, None, None]:
|
||||
"""Temporarily disable capturing while inside the ``with`` block."""
|
||||
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
|
||||
capmanager: CaptureManager = self.request.config.pluginmanager.getplugin(
|
||||
"capturemanager"
|
||||
)
|
||||
with capmanager.global_and_fixture_disabled():
|
||||
yield
|
||||
|
||||
@@ -876,14 +971,25 @@ class CaptureFixture(Generic[AnyStr]):
|
||||
|
||||
@fixture
|
||||
def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
r"""Enable text capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsys.readouterr()`` method
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsys):
|
||||
print("hello")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[str](SysCapture, request, _ispytest=True)
|
||||
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture(SysCapture, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
@@ -893,14 +999,25 @@ def capsys(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
|
||||
@fixture
|
||||
def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
|
||||
"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
r"""Enable bytes capturing of writes to ``sys.stdout`` and ``sys.stderr``.
|
||||
|
||||
The captured output is made available via ``capsysbinary.readouterr()``
|
||||
method calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``bytes`` objects.
|
||||
|
||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_output(capsysbinary):
|
||||
print("hello")
|
||||
captured = capsysbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[bytes](SysCaptureBinary, request, _ispytest=True)
|
||||
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture(SysCaptureBinary, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
@@ -910,14 +1027,25 @@ def capsysbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None,
|
||||
|
||||
@fixture
|
||||
def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||
r"""Enable text capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``text`` objects.
|
||||
|
||||
Returns an instance of :class:`CaptureFixture[str] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfd):
|
||||
os.system('echo "hello"')
|
||||
captured = capfd.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[str](FDCapture, request, _ispytest=True)
|
||||
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture(FDCapture, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
@@ -927,14 +1055,26 @@ def capfd(request: SubRequest) -> Generator[CaptureFixture[str], None, None]:
|
||||
|
||||
@fixture
|
||||
def capfdbinary(request: SubRequest) -> Generator[CaptureFixture[bytes], None, None]:
|
||||
"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||
r"""Enable bytes capturing of writes to file descriptors ``1`` and ``2``.
|
||||
|
||||
The captured output is made available via ``capfd.readouterr()`` method
|
||||
calls, which return a ``(out, err)`` namedtuple.
|
||||
``out`` and ``err`` will be ``byte`` objects.
|
||||
|
||||
Returns an instance of :class:`CaptureFixture[bytes] <pytest.CaptureFixture>`.
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_system_echo(capfdbinary):
|
||||
os.system('echo "hello"')
|
||||
captured = capfdbinary.readouterr()
|
||||
assert captured.out == b"hello\n"
|
||||
|
||||
"""
|
||||
capman = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture[bytes](FDCaptureBinary, request, _ispytest=True)
|
||||
capman: CaptureManager = request.config.pluginmanager.getplugin("capturemanager")
|
||||
capture_fixture = CaptureFixture(FDCaptureBinary, request, _ispytest=True)
|
||||
capman.set_fixture(capture_fixture)
|
||||
capture_fixture._start()
|
||||
yield capture_fixture
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Python version compatibility code."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import enum
|
||||
import functools
|
||||
import inspect
|
||||
@@ -10,17 +13,23 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generic
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
from typing import NoReturn
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TypeVar
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
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 import NoReturn
|
||||
from typing_extensions import Final
|
||||
|
||||
|
||||
@@ -36,7 +45,7 @@ LEGACY_PATH = py.path. local
|
||||
# fmt: on
|
||||
|
||||
|
||||
def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
|
||||
def legacy_path(path: str | os.PathLike[str]) -> LEGACY_PATH:
|
||||
"""Internal wrapper to prepare lazy proxies for legacy_path instances"""
|
||||
return LEGACY_PATH(path)
|
||||
|
||||
@@ -46,13 +55,15 @@ def legacy_path(path: Union[str, "os.PathLike[str]"]) -> LEGACY_PATH:
|
||||
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
|
||||
class NotSetType(enum.Enum):
|
||||
token = 0
|
||||
NOTSET: "Final" = NotSetType.token # noqa: E305
|
||||
NOTSET: Final = NotSetType.token # noqa: E305
|
||||
# fmt: on
|
||||
|
||||
if sys.version_info >= (3, 8):
|
||||
from importlib import metadata as importlib_metadata
|
||||
import importlib.metadata
|
||||
|
||||
importlib_metadata = importlib.metadata
|
||||
else:
|
||||
import importlib_metadata # noqa: F401
|
||||
import importlib_metadata as importlib_metadata # noqa: F401
|
||||
|
||||
|
||||
def _format_args(func: Callable[..., Any]) -> str:
|
||||
@@ -82,7 +93,7 @@ def is_async_function(func: object) -> bool:
|
||||
return iscoroutinefunction(func) or inspect.isasyncgenfunction(func)
|
||||
|
||||
|
||||
def getlocation(function, curdir: Optional[str] = None) -> str:
|
||||
def getlocation(function, curdir: str | None = None) -> str:
|
||||
function = get_real_func(function)
|
||||
fn = Path(inspect.getfile(function))
|
||||
lineno = function.__code__.co_firstlineno
|
||||
@@ -120,8 +131,8 @@ def getfuncargnames(
|
||||
*,
|
||||
name: str = "",
|
||||
is_method: bool = False,
|
||||
cls: Optional[type] = None,
|
||||
) -> Tuple[str, ...]:
|
||||
cls: type | None = None,
|
||||
) -> tuple[str, ...]:
|
||||
"""Return the names of a function's mandatory arguments.
|
||||
|
||||
Should return the names of all function arguments that:
|
||||
@@ -185,7 +196,7 @@ def getfuncargnames(
|
||||
return arg_names
|
||||
|
||||
|
||||
def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]:
|
||||
def get_default_arg_names(function: Callable[..., Any]) -> tuple[str, ...]:
|
||||
# Note: this code intentionally mirrors the code at the beginning of
|
||||
# getfuncargnames, to get the arguments which were excluded from its result
|
||||
# because they had default values.
|
||||
@@ -216,7 +227,7 @@ def _bytes_to_ascii(val: bytes) -> str:
|
||||
return val.decode("ascii", "backslashreplace")
|
||||
|
||||
|
||||
def ascii_escaped(val: Union[bytes, str]) -> str:
|
||||
def ascii_escaped(val: bytes | str) -> str:
|
||||
r"""If val is pure ASCII, return it as an str, otherwise, escape
|
||||
bytes objects into a sequence of escaped bytes:
|
||||
|
||||
@@ -240,7 +251,7 @@ def ascii_escaped(val: Union[bytes, str]) -> str:
|
||||
return _translate_non_printable(ret)
|
||||
|
||||
|
||||
@attr.s
|
||||
@dataclasses.dataclass
|
||||
class _PytestWrapper:
|
||||
"""Dummy wrapper around a function object for internal use only.
|
||||
|
||||
@@ -249,7 +260,7 @@ class _PytestWrapper:
|
||||
decorator to issue warnings when the fixture function is called directly.
|
||||
"""
|
||||
|
||||
obj = attr.ib()
|
||||
obj: Any
|
||||
|
||||
|
||||
def get_real_func(obj):
|
||||
@@ -343,8 +354,6 @@ else:
|
||||
if sys.version_info >= (3, 8):
|
||||
from functools import cached_property as cached_property
|
||||
else:
|
||||
from typing import overload
|
||||
from typing import Type
|
||||
|
||||
class cached_property(Generic[_S, _T]):
|
||||
__slots__ = ("func", "__doc__")
|
||||
@@ -355,12 +364,12 @@ else:
|
||||
|
||||
@overload
|
||||
def __get__(
|
||||
self, instance: None, owner: Optional[Type[_S]] = ...
|
||||
) -> "cached_property[_S, _T]":
|
||||
self, instance: None, owner: type[_S] | None = ...
|
||||
) -> cached_property[_S, _T]:
|
||||
...
|
||||
|
||||
@overload
|
||||
def __get__(self, instance: _S, owner: Optional[Type[_S]] = ...) -> _T:
|
||||
def __get__(self, instance: _S, owner: type[_S] | None = ...) -> _T:
|
||||
...
|
||||
|
||||
def __get__(self, instance, owner=None):
|
||||
@@ -370,6 +379,27 @@ else:
|
||||
return value
|
||||
|
||||
|
||||
def get_user_id() -> int | None:
|
||||
"""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
|
||||
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.
|
||||
#
|
||||
# Consider this example:
|
||||
@@ -401,5 +431,5 @@ else:
|
||||
# previously.
|
||||
#
|
||||
# This also work for Enums (if you use `is` to compare) and Literals.
|
||||
def assert_never(value: "NoReturn") -> "NoReturn":
|
||||
def assert_never(value: NoReturn) -> NoReturn:
|
||||
assert False, f"Unhandled value: {value} ({type(value).__name__})"
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import argparse
|
||||
import collections.abc
|
||||
import copy
|
||||
import dataclasses
|
||||
import enum
|
||||
import glob
|
||||
import inspect
|
||||
import os
|
||||
import re
|
||||
@@ -13,6 +15,7 @@ import warnings
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from types import FunctionType
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
@@ -32,7 +35,6 @@ from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import attr
|
||||
from pluggy import HookimplMarker
|
||||
from pluggy import HookspecMarker
|
||||
from pluggy import PluginManager
|
||||
@@ -47,7 +49,7 @@ from _pytest._code import ExceptionInfo
|
||||
from _pytest._code import filter_traceback
|
||||
from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import final
|
||||
from _pytest.compat import importlib_metadata
|
||||
from _pytest.compat import importlib_metadata # type: ignore[attr-defined]
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.outcomes import Skipped
|
||||
from _pytest.pathlib import absolutepath
|
||||
@@ -55,11 +57,12 @@ 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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
from _pytest._code.code import _TracebackStyle
|
||||
from _pytest.terminal import TerminalReporter
|
||||
from .argparsing import Argument
|
||||
@@ -135,7 +138,9 @@ def main(
|
||||
) -> Union[int, ExitCode]:
|
||||
"""Perform an in-process test run.
|
||||
|
||||
:param args: List of command line arguments.
|
||||
:param args:
|
||||
List of command line arguments. If `None` or not given, defaults to reading
|
||||
arguments directly from the process command line (:data:`sys.argv`).
|
||||
:param plugins: List of plugin objects to be auto-registered during initialization.
|
||||
|
||||
:returns: An exit code.
|
||||
@@ -309,7 +314,9 @@ def _prepareconfig(
|
||||
elif isinstance(args, os.PathLike):
|
||||
args = [os.fspath(args)]
|
||||
elif not isinstance(args, list):
|
||||
msg = "`args` parameter expected to be a list of strings, got: {!r} (type: {})"
|
||||
msg = ( # type:ignore[unreachable]
|
||||
"`args` parameter expected to be a list of strings, got: {!r} (type: {})"
|
||||
)
|
||||
raise TypeError(msg.format(args, type(args)))
|
||||
|
||||
config = get_config(args, plugins)
|
||||
@@ -338,6 +345,38 @@ def _get_directory(path: Path) -> Path:
|
||||
return path
|
||||
|
||||
|
||||
def _get_legacy_hook_marks(
|
||||
method: Any,
|
||||
hook_type: str,
|
||||
opt_names: Tuple[str, ...],
|
||||
) -> Dict[str, bool]:
|
||||
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] = {}
|
||||
for opt_name in opt_names:
|
||||
opt_attr = getattr(method, opt_name, AttributeError)
|
||||
if opt_attr is not AttributeError:
|
||||
must_warn.append(f"{opt_name}={opt_attr}")
|
||||
opts[opt_name] = True
|
||||
elif opt_name in known_marks:
|
||||
must_warn.append(f"{opt_name}=True")
|
||||
opts[opt_name] = True
|
||||
else:
|
||||
opts[opt_name] = False
|
||||
if must_warn:
|
||||
hook_opts = ", ".join(must_warn)
|
||||
message = _pytest.deprecated.HOOK_LEGACY_MARKING.format(
|
||||
type=hook_type,
|
||||
fullname=method.__qualname__,
|
||||
hook_opts=hook_opts,
|
||||
)
|
||||
warn_explicit_for(cast(FunctionType, method), message)
|
||||
return opts
|
||||
|
||||
|
||||
@final
|
||||
class PytestPluginManager(PluginManager):
|
||||
"""A :py:class:`pluggy.PluginManager <pluggy.PluginManager>` with
|
||||
@@ -406,45 +445,34 @@ class PytestPluginManager(PluginManager):
|
||||
# so we avoid accessing possibly non-readable attributes
|
||||
# (see issue #1073).
|
||||
if not name.startswith("pytest_"):
|
||||
return
|
||||
return None
|
||||
# Ignore names which can not be hooks.
|
||||
if name == "pytest_plugins":
|
||||
return
|
||||
return None
|
||||
|
||||
opts = super().parse_hookimpl_opts(plugin, name)
|
||||
if opts is not None:
|
||||
return opts
|
||||
|
||||
method = getattr(plugin, name)
|
||||
opts = super().parse_hookimpl_opts(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.
|
||||
if opts is None and name.startswith("pytest_"):
|
||||
opts = {}
|
||||
if opts is not None:
|
||||
# TODO: DeprecationWarning, people should use hookimpl
|
||||
# https://github.com/pytest-dev/pytest/issues/4562
|
||||
known_marks = {m.name for m in getattr(method, "pytestmark", [])}
|
||||
|
||||
for name in ("tryfirst", "trylast", "optionalhook", "hookwrapper"):
|
||||
opts.setdefault(name, hasattr(method, name) or name in known_marks)
|
||||
return opts
|
||||
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):
|
||||
opts = super().parse_hookspec_opts(module_or_class, name)
|
||||
if opts is None:
|
||||
method = getattr(module_or_class, name)
|
||||
|
||||
if name.startswith("pytest_"):
|
||||
# todo: deprecate hookspec hacks
|
||||
# https://github.com/pytest-dev/pytest/issues/4562
|
||||
known_marks = {m.name for m in getattr(method, "pytestmark", [])}
|
||||
opts = {
|
||||
"firstresult": hasattr(method, "firstresult")
|
||||
or "firstresult" in known_marks,
|
||||
"historic": hasattr(method, "historic")
|
||||
or "historic" in known_marks,
|
||||
}
|
||||
opts = _get_legacy_hook_marks( # type: ignore[assignment]
|
||||
method,
|
||||
"spec",
|
||||
("firstresult", "historic"),
|
||||
)
|
||||
return opts
|
||||
|
||||
def register(
|
||||
@@ -486,12 +514,14 @@ class PytestPluginManager(PluginManager):
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"tryfirst: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it first/as early as possible.",
|
||||
"plugin machinery will try to call it first/as early as possible. "
|
||||
"DEPRECATED, use @pytest.hookimpl(tryfirst=True) instead.",
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"trylast: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it last/as late as possible.",
|
||||
"plugin machinery will try to call it last/as late as possible. "
|
||||
"DEPRECATED, use @pytest.hookimpl(trylast=True) instead.",
|
||||
)
|
||||
self._configured = True
|
||||
|
||||
@@ -499,7 +529,13 @@ class PytestPluginManager(PluginManager):
|
||||
# Internal API for local conftest plugin handling.
|
||||
#
|
||||
def _set_initial_conftests(
|
||||
self, namespace: argparse.Namespace, rootpath: Path
|
||||
self,
|
||||
args: Sequence[Union[str, Path]],
|
||||
pyargs: bool,
|
||||
noconftest: bool,
|
||||
rootpath: Path,
|
||||
confcutdir: Optional[Path],
|
||||
importmode: Union[ImportMode, str],
|
||||
) -> None:
|
||||
"""Load initial conftest files given a preparsed "namespace".
|
||||
|
||||
@@ -509,27 +545,25 @@ class PytestPluginManager(PluginManager):
|
||||
common options will not confuse our logic here.
|
||||
"""
|
||||
current = Path.cwd()
|
||||
self._confcutdir = (
|
||||
absolutepath(current / namespace.confcutdir)
|
||||
if namespace.confcutdir
|
||||
else None
|
||||
)
|
||||
self._noconftest = namespace.noconftest
|
||||
self._using_pyargs = namespace.pyargs
|
||||
testpaths = namespace.file_or_dir
|
||||
self._confcutdir = absolutepath(current / confcutdir) if confcutdir else None
|
||||
self._noconftest = noconftest
|
||||
self._using_pyargs = pyargs
|
||||
foundanchor = False
|
||||
for testpath in testpaths:
|
||||
path = str(testpath)
|
||||
for intitial_path in args:
|
||||
path = str(intitial_path)
|
||||
# remove node-id syntax
|
||||
i = path.find("::")
|
||||
if i != -1:
|
||||
path = path[:i]
|
||||
anchor = absolutepath(current / path)
|
||||
if anchor.exists(): # we found some file object
|
||||
self._try_load_conftest(anchor, namespace.importmode, rootpath)
|
||||
|
||||
# Ensure we do not break if what appears to be an anchor
|
||||
# is in fact a very long option (#10169, #11394).
|
||||
if safe_exists(anchor):
|
||||
self._try_load_conftest(anchor, importmode, rootpath)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
self._try_load_conftest(current, namespace.importmode, rootpath)
|
||||
self._try_load_conftest(current, importmode, rootpath)
|
||||
|
||||
def _is_in_confcutdir(self, path: Path) -> bool:
|
||||
"""Whether a path is within the confcutdir.
|
||||
@@ -538,11 +572,7 @@ class PytestPluginManager(PluginManager):
|
||||
"""
|
||||
if self._confcutdir is None:
|
||||
return True
|
||||
try:
|
||||
path.relative_to(self._confcutdir)
|
||||
except ValueError:
|
||||
return False
|
||||
return True
|
||||
return path not in self._confcutdir.parents
|
||||
|
||||
def _try_load_conftest(
|
||||
self, anchor: Path, importmode: Union[str, ImportMode], rootpath: Path
|
||||
@@ -673,6 +703,7 @@ class PytestPluginManager(PluginManager):
|
||||
parg = opt[2:]
|
||||
else:
|
||||
continue
|
||||
parg = parg.strip()
|
||||
if exclude_only and not parg.startswith("no:"):
|
||||
continue
|
||||
self.consider_pluginarg(parg)
|
||||
@@ -837,7 +868,8 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
|
||||
if is_simple_module:
|
||||
module_name, _ = os.path.splitext(fn)
|
||||
# we ignore "setup.py" at the root of the distribution
|
||||
if module_name != "setup":
|
||||
# as well as editable installation finder modules made by setuptools
|
||||
if module_name != "setup" and not module_name.startswith("__editable__"):
|
||||
seen_some = True
|
||||
yield module_name
|
||||
elif is_package:
|
||||
@@ -861,10 +893,6 @@ def _iter_rewritable_modules(package_files: Iterable[str]) -> Iterator[str]:
|
||||
yield from _iter_rewritable_modules(new_package_files)
|
||||
|
||||
|
||||
def _args_converter(args: Iterable[str]) -> Tuple[str, ...]:
|
||||
return tuple(args)
|
||||
|
||||
|
||||
@final
|
||||
class Config:
|
||||
"""Access to configuration values, pluginmanager and plugin hooks.
|
||||
@@ -878,7 +906,7 @@ class Config:
|
||||
"""
|
||||
|
||||
@final
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class InvocationParams:
|
||||
"""Holds parameters passed during :func:`pytest.main`.
|
||||
|
||||
@@ -894,13 +922,37 @@ class Config:
|
||||
Plugins accessing ``InvocationParams`` must be aware of that.
|
||||
"""
|
||||
|
||||
args: Tuple[str, ...] = attr.ib(converter=_args_converter)
|
||||
args: Tuple[str, ...]
|
||||
"""The command-line arguments as passed to :func:`pytest.main`."""
|
||||
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]]
|
||||
"""Extra plugins, might be `None`."""
|
||||
dir: Path
|
||||
"""The directory from which :func:`pytest.main` was invoked."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
args: Iterable[str],
|
||||
plugins: Optional[Sequence[Union[str, _PluggyPlugin]]],
|
||||
dir: Path,
|
||||
) -> None:
|
||||
object.__setattr__(self, "args", tuple(args))
|
||||
object.__setattr__(self, "plugins", plugins)
|
||||
object.__setattr__(self, "dir", dir)
|
||||
|
||||
class ArgsSource(enum.Enum):
|
||||
"""Indicates the source of the test arguments.
|
||||
|
||||
.. versionadded:: 7.2
|
||||
"""
|
||||
|
||||
#: Command line arguments.
|
||||
ARGS = enum.auto()
|
||||
#: Invocation directory.
|
||||
INCOVATION_DIR = enum.auto()
|
||||
#: 'testpaths' configuration value.
|
||||
TESTPATHS = enum.auto()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
pluginmanager: PytestPluginManager,
|
||||
@@ -960,6 +1012,8 @@ class Config:
|
||||
self.hook.pytest_addoption.call_historic(
|
||||
kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager)
|
||||
)
|
||||
self.args_source = Config.ArgsSource.ARGS
|
||||
self.args: List[str] = []
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _pytest.cacheprovider import Cache
|
||||
@@ -1008,9 +1062,10 @@ class Config:
|
||||
fin()
|
||||
|
||||
def get_terminal_writer(self) -> TerminalWriter:
|
||||
terminalreporter: TerminalReporter = self.pluginmanager.get_plugin(
|
||||
terminalreporter: Optional[TerminalReporter] = self.pluginmanager.get_plugin(
|
||||
"terminalreporter"
|
||||
)
|
||||
assert terminalreporter is not None
|
||||
return terminalreporter._tw
|
||||
|
||||
def pytest_cmdline_parse(
|
||||
@@ -1019,7 +1074,6 @@ class Config:
|
||||
try:
|
||||
self.parse(args)
|
||||
except UsageError:
|
||||
|
||||
# Handle --version and --help here in a minimal fashion.
|
||||
# This gets done via helpconfig normally, but its
|
||||
# pytest_cmdline_main is not called in case of errors.
|
||||
@@ -1084,8 +1138,25 @@ class Config:
|
||||
|
||||
@hookimpl(trylast=True)
|
||||
def pytest_load_initial_conftests(self, early_config: "Config") -> None:
|
||||
# We haven't fully parsed the command line arguments yet, so
|
||||
# early_config.args it not set yet. But we need it for
|
||||
# discovering the initial conftests. So "pre-run" the logic here.
|
||||
# It will be done for real in `parse()`.
|
||||
args, args_source = early_config._decide_args(
|
||||
args=early_config.known_args_namespace.file_or_dir,
|
||||
pyargs=early_config.known_args_namespace.pyargs,
|
||||
testpaths=early_config.getini("testpaths"),
|
||||
invocation_dir=early_config.invocation_params.dir,
|
||||
rootpath=early_config.rootpath,
|
||||
warn=False,
|
||||
)
|
||||
self.pluginmanager._set_initial_conftests(
|
||||
early_config.known_args_namespace, rootpath=early_config.rootpath
|
||||
args=args,
|
||||
pyargs=early_config.known_args_namespace.pyargs,
|
||||
noconftest=early_config.known_args_namespace.noconftest,
|
||||
rootpath=early_config.rootpath,
|
||||
confcutdir=early_config.known_args_namespace.confcutdir,
|
||||
importmode=early_config.known_args_namespace.importmode,
|
||||
)
|
||||
|
||||
def _initini(self, args: Sequence[str]) -> None:
|
||||
@@ -1103,11 +1174,11 @@ class Config:
|
||||
self.inicfg = inicfg
|
||||
self._parser.extra_info["rootdir"] = str(self.rootpath)
|
||||
self._parser.extra_info["inifile"] = str(self.inipath)
|
||||
self._parser.addini("addopts", "extra command line options", "args")
|
||||
self._parser.addini("minversion", "minimally required pytest version")
|
||||
self._parser.addini("addopts", "Extra command line options", "args")
|
||||
self._parser.addini("minversion", "Minimally required pytest version")
|
||||
self._parser.addini(
|
||||
"required_plugins",
|
||||
"plugins that must be present for pytest to run",
|
||||
"Plugins that must be present for pytest to run",
|
||||
type="args",
|
||||
default=[],
|
||||
)
|
||||
@@ -1165,6 +1236,49 @@ class Config:
|
||||
|
||||
return args
|
||||
|
||||
def _decide_args(
|
||||
self,
|
||||
*,
|
||||
args: List[str],
|
||||
pyargs: List[str],
|
||||
testpaths: List[str],
|
||||
invocation_dir: Path,
|
||||
rootpath: Path,
|
||||
warn: bool,
|
||||
) -> Tuple[List[str], ArgsSource]:
|
||||
"""Decide the args (initial paths/nodeids) to use given the relevant inputs.
|
||||
|
||||
:param warn: Whether can issue warnings.
|
||||
"""
|
||||
if args:
|
||||
source = Config.ArgsSource.ARGS
|
||||
result = args
|
||||
else:
|
||||
if invocation_dir == rootpath:
|
||||
source = Config.ArgsSource.TESTPATHS
|
||||
if pyargs:
|
||||
result = testpaths
|
||||
else:
|
||||
result = []
|
||||
for path in testpaths:
|
||||
result.extend(sorted(glob.iglob(path, recursive=True)))
|
||||
if testpaths and not result:
|
||||
if warn:
|
||||
warning_text = (
|
||||
"No files were found in testpaths; "
|
||||
"consider removing or adjusting your testpaths configuration. "
|
||||
"Searching recursively from the current directory instead."
|
||||
)
|
||||
self.issue_config_time_warning(
|
||||
PytestConfigWarning(warning_text), stacklevel=3
|
||||
)
|
||||
else:
|
||||
result = []
|
||||
if not result:
|
||||
source = Config.ArgsSource.INCOVATION_DIR
|
||||
result = [str(invocation_dir)]
|
||||
return result, source
|
||||
|
||||
def _preparse(self, args: List[str], addopts: bool = True) -> None:
|
||||
if addopts:
|
||||
env_addopts = os.environ.get("PYTEST_ADDOPTS", "")
|
||||
@@ -1203,8 +1317,11 @@ class Config:
|
||||
_pytest.deprecated.STRICT_OPTION, stacklevel=2
|
||||
)
|
||||
|
||||
if self.known_args_namespace.confcutdir is None and self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
if self.known_args_namespace.confcutdir is None:
|
||||
if self.inipath is not None:
|
||||
confcutdir = str(self.inipath.parent)
|
||||
else:
|
||||
confcutdir = str(self.rootpath)
|
||||
self.known_args_namespace.confcutdir = confcutdir
|
||||
try:
|
||||
self.hook.pytest_load_initial_conftests(
|
||||
@@ -1299,8 +1416,8 @@ class Config:
|
||||
|
||||
def parse(self, args: List[str], addopts: bool = True) -> None:
|
||||
# Parse given cmdline arguments into this config object.
|
||||
assert not hasattr(
|
||||
self, "args"
|
||||
assert (
|
||||
self.args == []
|
||||
), "can only parse cmdline args at most once per Config object"
|
||||
self.hook.pytest_addhooks.call_historic(
|
||||
kwargs=dict(pluginmanager=self.pluginmanager)
|
||||
@@ -1313,12 +1430,14 @@ class Config:
|
||||
args = self._parser.parse_setoption(
|
||||
args, self.option, namespace=self.option
|
||||
)
|
||||
if not args:
|
||||
if self.invocation_params.dir == self.rootpath:
|
||||
args = self.getini("testpaths")
|
||||
if not args:
|
||||
args = [str(self.invocation_params.dir)]
|
||||
self.args = args
|
||||
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
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import cast
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Mapping
|
||||
from typing import NoReturn
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
from typing import Tuple
|
||||
@@ -24,7 +25,6 @@ from _pytest.deprecated import ARGUMENT_TYPE_STR_CHOICE
|
||||
from _pytest.deprecated import check_ispytest
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import NoReturn
|
||||
from typing_extensions import Literal
|
||||
|
||||
FILE_OR_DIR = "file_or_dir"
|
||||
@@ -48,7 +48,7 @@ class Parser:
|
||||
_ispytest: bool = False,
|
||||
) -> None:
|
||||
check_ispytest(_ispytest)
|
||||
self._anonymous = OptionGroup("custom options", parser=self, _ispytest=True)
|
||||
self._anonymous = OptionGroup("Custom options", parser=self, _ispytest=True)
|
||||
self._groups: List[OptionGroup] = []
|
||||
self._processopt = processopt
|
||||
self._usage = usage
|
||||
@@ -66,14 +66,15 @@ class Parser:
|
||||
) -> "OptionGroup":
|
||||
"""Get (or create) a named option Group.
|
||||
|
||||
:name: Name of the option group.
|
||||
:description: Long description for --help output.
|
||||
:after: Name of another group, used for ordering --help output.
|
||||
:param name: Name of the option group.
|
||||
:param description: Long description for --help output.
|
||||
:param after: Name of another group, used for ordering --help output.
|
||||
:returns: The option group.
|
||||
|
||||
The returned group object has an ``addoption`` method with the same
|
||||
signature as :func:`parser.addoption <pytest.Parser.addoption>` but
|
||||
will be shown in the respective group in the output of
|
||||
``pytest. --help``.
|
||||
``pytest --help``.
|
||||
"""
|
||||
for group in self._groups:
|
||||
if group.name == name:
|
||||
@@ -89,10 +90,11 @@ class Parser:
|
||||
def addoption(self, *opts: str, **attrs: Any) -> None:
|
||||
"""Register a command line option.
|
||||
|
||||
:opts: Option names, can be short or long options.
|
||||
:attrs: Same attributes which the ``add_argument()`` function of the
|
||||
`argparse library <https://docs.python.org/library/argparse.html>`_
|
||||
accepts.
|
||||
:param opts:
|
||||
Option names, can be short or long options.
|
||||
:param attrs:
|
||||
Same attributes as the argparse library's :py:func:`add_argument()
|
||||
<argparse.ArgumentParser.add_argument>` function accepts.
|
||||
|
||||
After command line parsing, options are available on the pytest config
|
||||
object via ``config.option.NAME`` where ``NAME`` is usually set
|
||||
@@ -148,7 +150,10 @@ class Parser:
|
||||
args: Sequence[Union[str, "os.PathLike[str]"]],
|
||||
namespace: Optional[argparse.Namespace] = None,
|
||||
) -> argparse.Namespace:
|
||||
"""Parse and return a namespace object with known arguments at this point."""
|
||||
"""Parse the known arguments at this point.
|
||||
|
||||
:returns: An argparse namespace object.
|
||||
"""
|
||||
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
|
||||
|
||||
def parse_known_and_unknown_args(
|
||||
@@ -156,8 +161,13 @@ class Parser:
|
||||
args: Sequence[Union[str, "os.PathLike[str]"]],
|
||||
namespace: Optional[argparse.Namespace] = None,
|
||||
) -> Tuple[argparse.Namespace, List[str]]:
|
||||
"""Parse and return a namespace object with known arguments, and
|
||||
the remaining arguments unknown at this point."""
|
||||
"""Parse the known arguments at this point, and also return the
|
||||
remaining unknown arguments.
|
||||
|
||||
:returns:
|
||||
A tuple containing an argparse namespace object for the known
|
||||
arguments, and a list of the unknown arguments.
|
||||
"""
|
||||
optparser = self._getparser()
|
||||
strargs = [os.fspath(x) for x in args]
|
||||
return optparser.parse_known_args(strargs, namespace=namespace)
|
||||
@@ -169,13 +179,13 @@ class Parser:
|
||||
type: Optional[
|
||||
"Literal['string', 'paths', 'pathlist', 'args', 'linelist', 'bool']"
|
||||
] = None,
|
||||
default=None,
|
||||
default: Any = None,
|
||||
) -> None:
|
||||
"""Register an ini-file option.
|
||||
|
||||
:name:
|
||||
:param name:
|
||||
Name of the ini-variable.
|
||||
:type:
|
||||
:param type:
|
||||
Type of the variable. Can be:
|
||||
|
||||
* ``string``: a string
|
||||
@@ -189,7 +199,7 @@ class Parser:
|
||||
The ``paths`` variable type.
|
||||
|
||||
Defaults to ``string`` if ``None`` or not passed.
|
||||
:default:
|
||||
:param default:
|
||||
Default value if no ini-file option exists but is queried.
|
||||
|
||||
The value of ini-variables can be retrieved via a call to
|
||||
@@ -227,7 +237,7 @@ class Argument:
|
||||
_typ_map = {"int": int, "string": str, "float": float, "complex": complex}
|
||||
|
||||
def __init__(self, *names: str, **attrs: Any) -> None:
|
||||
"""Store parms in private vars for use in add_argument."""
|
||||
"""Store params in private vars for use in add_argument."""
|
||||
self._attrs = attrs
|
||||
self._short_opts: List[str] = []
|
||||
self._long_opts: List[str] = []
|
||||
@@ -354,24 +364,30 @@ class OptionGroup:
|
||||
self.options: List[Argument] = []
|
||||
self.parser = parser
|
||||
|
||||
def addoption(self, *optnames: str, **attrs: Any) -> None:
|
||||
def addoption(self, *opts: str, **attrs: Any) -> None:
|
||||
"""Add an option to this group.
|
||||
|
||||
If a shortened version of a long option is specified, it will
|
||||
be suppressed in the help. ``addoption('--twowords', '--two-words')``
|
||||
results in help showing ``--two-words`` only, but ``--twowords`` gets
|
||||
accepted **and** the automatic destination is in ``args.twowords``.
|
||||
|
||||
:param opts:
|
||||
Option names, can be short or long options.
|
||||
:param attrs:
|
||||
Same attributes as the argparse library's :py:func:`add_argument()
|
||||
<argparse.ArgumentParser.add_argument>` function accepts.
|
||||
"""
|
||||
conflict = set(optnames).intersection(
|
||||
conflict = set(opts).intersection(
|
||||
name for opt in self.options for name in opt.names()
|
||||
)
|
||||
if conflict:
|
||||
raise ValueError("option names %s already added" % conflict)
|
||||
option = Argument(*optnames, **attrs)
|
||||
option = Argument(*opts, **attrs)
|
||||
self._addoption_instance(option, shortupper=False)
|
||||
|
||||
def _addoption(self, *optnames: str, **attrs: Any) -> None:
|
||||
option = Argument(*optnames, **attrs)
|
||||
def _addoption(self, *opts: str, **attrs: Any) -> None:
|
||||
option = Argument(*opts, **attrs)
|
||||
self._addoption_instance(option, shortupper=True)
|
||||
|
||||
def _addoption_instance(self, option: "Argument", shortupper: bool = False) -> None:
|
||||
@@ -403,7 +419,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
# an usage error to provide more contextual information to the user.
|
||||
self.extra_info = extra_info if extra_info else {}
|
||||
|
||||
def error(self, message: str) -> "NoReturn":
|
||||
def error(self, message: str) -> NoReturn:
|
||||
"""Transform argparse error message into UsageError."""
|
||||
msg = f"{self.prog}: error: {message}"
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ class PathAwareHookProxy:
|
||||
|
||||
@_wraps(hook)
|
||||
def fixed_hook(**kw):
|
||||
|
||||
path_value: Optional[Path] = kw.pop(path_var, None)
|
||||
fspath_value: Optional[LEGACY_PATH] = kw.pop(fspath_var, None)
|
||||
if fspath_value is not None:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
from typing import Iterable
|
||||
@@ -15,6 +16,7 @@ from .exceptions import UsageError
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.pathlib import absolutepath
|
||||
from _pytest.pathlib import commonpath
|
||||
from _pytest.pathlib import safe_exists
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import Config
|
||||
@@ -64,12 +66,15 @@ def load_config_dict_from_file(
|
||||
|
||||
# '.toml' files are considered if they contain a [tool.pytest.ini_options] table.
|
||||
elif filepath.suffix == ".toml":
|
||||
import tomli
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
import tomli as tomllib
|
||||
|
||||
toml_text = filepath.read_text(encoding="utf-8")
|
||||
try:
|
||||
config = tomli.loads(toml_text)
|
||||
except tomli.TOMLDecodeError as exc:
|
||||
config = tomllib.loads(toml_text)
|
||||
except tomllib.TOMLDecodeError as exc:
|
||||
raise UsageError(f"{filepath}: {exc}") from exc
|
||||
|
||||
result = config.get("tool", {}).get("pytest", {}).get("ini_options", None)
|
||||
@@ -92,6 +97,7 @@ def locate_config(
|
||||
and return a tuple of (rootdir, inifile, cfg-dict)."""
|
||||
config_names = [
|
||||
"pytest.ini",
|
||||
".pytest.ini",
|
||||
"pyproject.toml",
|
||||
"tox.ini",
|
||||
"setup.cfg",
|
||||
@@ -146,14 +152,6 @@ def get_dirs_from_args(args: Iterable[str]) -> List[Path]:
|
||||
return path
|
||||
return path.parent
|
||||
|
||||
def safe_exists(path: Path) -> bool:
|
||||
# This can throw on paths that contain characters unrepresentable at the OS level,
|
||||
# or with invalid syntax on Windows (https://bugs.python.org/issue35306)
|
||||
try:
|
||||
return path.exists()
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
# These look like paths but may not exist
|
||||
possible_paths = (
|
||||
absolutepath(get_file_part_from_node_id(arg))
|
||||
@@ -198,8 +196,7 @@ def determine_setup(
|
||||
else:
|
||||
cwd = Path.cwd()
|
||||
rootdir = get_common_ancestor([cwd, ancestor])
|
||||
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
|
||||
if is_fs_root:
|
||||
if is_fs_root(rootdir):
|
||||
rootdir = ancestor
|
||||
if rootdir_cmd_arg:
|
||||
rootdir = absolutepath(os.path.expandvars(rootdir_cmd_arg))
|
||||
@@ -211,3 +208,11 @@ def determine_setup(
|
||||
)
|
||||
assert rootdir is not None
|
||||
return rootdir, inipath, inicfg or {}
|
||||
|
||||
|
||||
def is_fs_root(p: Path) -> bool:
|
||||
r"""
|
||||
Return True if the given path is pointing to the root of the
|
||||
file system ("/" on Unix and "C:\\" on Windows for example).
|
||||
"""
|
||||
return os.path.splitdrive(str(p))[1] == os.sep
|
||||
|
||||
@@ -3,6 +3,7 @@ import argparse
|
||||
import functools
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Generator
|
||||
@@ -46,21 +47,21 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
"--pdb",
|
||||
dest="usepdb",
|
||||
action="store_true",
|
||||
help="start the interactive Python debugger on errors or KeyboardInterrupt.",
|
||||
help="Start the interactive Python debugger on errors or KeyboardInterrupt",
|
||||
)
|
||||
group._addoption(
|
||||
"--pdbcls",
|
||||
dest="usepdb_cls",
|
||||
metavar="modulename:classname",
|
||||
type=_validate_usepdb_cls,
|
||||
help="specify a custom interactive Python debugger for use with --pdb."
|
||||
help="Specify a custom interactive Python debugger for use with --pdb."
|
||||
"For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
|
||||
)
|
||||
group._addoption(
|
||||
"--trace",
|
||||
dest="trace",
|
||||
action="store_true",
|
||||
help="Immediately break when running each test.",
|
||||
help="Immediately break when running each test",
|
||||
)
|
||||
|
||||
|
||||
@@ -293,7 +294,9 @@ class PdbInvoke:
|
||||
sys.stdout.write(out)
|
||||
sys.stdout.write(err)
|
||||
assert call.excinfo is not None
|
||||
_enter_pdb(node, call.excinfo, report)
|
||||
|
||||
if not isinstance(call.excinfo.value, unittest.SkipTest):
|
||||
_enter_pdb(node, call.excinfo, report)
|
||||
|
||||
def pytest_internalerror(self, excinfo: ExceptionInfo[BaseException]) -> None:
|
||||
tb = _postmortem_traceback(excinfo)
|
||||
|
||||
@@ -22,6 +22,21 @@ DEPRECATED_EXTERNAL_PLUGINS = {
|
||||
"pytest_faulthandler",
|
||||
}
|
||||
|
||||
NOSE_SUPPORT = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"Support for nose tests is deprecated and will be removed in a future release.\n"
|
||||
"{nodeid} is using nose method: `{method}` ({stage})\n"
|
||||
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
|
||||
)
|
||||
|
||||
NOSE_SUPPORT_METHOD = UnformattedWarning(
|
||||
PytestRemovedIn8Warning,
|
||||
"Support for nose tests is deprecated and will be removed in a future release.\n"
|
||||
"{nodeid} is using nose-specific method: `{method}(self)`\n"
|
||||
"To remove this warning, rename it to `{method}_method(self)`\n"
|
||||
"See docs: https://docs.pytest.org/en/stable/deprecations.html#support-for-tests-written-for-nose",
|
||||
)
|
||||
|
||||
|
||||
# This can be* removed pytest 8, but it's harmless and common, so no rush to remove.
|
||||
# * If you're in the future: "could have been".
|
||||
@@ -98,6 +113,14 @@ INSTANCE_COLLECTOR = PytestRemovedIn8Warning(
|
||||
"The pytest.Instance collector type is deprecated and is no longer used. "
|
||||
"See https://docs.pytest.org/en/latest/deprecations.html#the-pytest-instance-collector",
|
||||
)
|
||||
HOOK_LEGACY_MARKING = UnformattedWarning(
|
||||
PytestDeprecationWarning,
|
||||
"The hook{type} {fullname} uses old-style configuration options (marks or attributes).\n"
|
||||
"Please use the pytest.hook{type}({hook_opts}) decorator instead\n"
|
||||
" to configure the hooks.\n"
|
||||
" See https://docs.pytest.org/en/latest/deprecations.html"
|
||||
"#configuring-hook-specs-impls-using-markers",
|
||||
)
|
||||
|
||||
# You want to make some `__init__` or function "private".
|
||||
#
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Discover and run doctests in modules and test files."""
|
||||
import bdb
|
||||
import functools
|
||||
import inspect
|
||||
import os
|
||||
import platform
|
||||
@@ -23,7 +24,6 @@ from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
import pytest
|
||||
from _pytest import outcomes
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest._code.code import ReprFileLocation
|
||||
@@ -32,11 +32,15 @@ from _pytest._io import TerminalWriter
|
||||
from _pytest.compat import safe_getattr
|
||||
from _pytest.config import Config
|
||||
from _pytest.config.argparsing import Parser
|
||||
from _pytest.fixtures import fixture
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
from _pytest.nodes import Collector
|
||||
from _pytest.nodes import Item
|
||||
from _pytest.outcomes import OutcomeException
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.pathlib import fnmatch_ex
|
||||
from _pytest.pathlib import import_path
|
||||
from _pytest.python import Module
|
||||
from _pytest.python_api import approx
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
@@ -66,26 +70,26 @@ CHECKER_CLASS: Optional[Type["doctest.OutputChecker"]] = None
|
||||
def pytest_addoption(parser: Parser) -> None:
|
||||
parser.addini(
|
||||
"doctest_optionflags",
|
||||
"option flags for doctests",
|
||||
"Option flags for doctests",
|
||||
type="args",
|
||||
default=["ELLIPSIS"],
|
||||
)
|
||||
parser.addini(
|
||||
"doctest_encoding", "encoding used for doctest files", default="utf-8"
|
||||
"doctest_encoding", "Encoding used for doctest files", default="utf-8"
|
||||
)
|
||||
group = parser.getgroup("collect")
|
||||
group.addoption(
|
||||
"--doctest-modules",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="run doctests in all .py modules",
|
||||
help="Run doctests in all .py modules",
|
||||
dest="doctestmodules",
|
||||
)
|
||||
group.addoption(
|
||||
"--doctest-report",
|
||||
type=str.lower,
|
||||
default="udiff",
|
||||
help="choose another output format for diffs on doctest failure",
|
||||
help="Choose another output format for diffs on doctest failure",
|
||||
choices=DOCTEST_REPORT_CHOICES,
|
||||
dest="doctestreport",
|
||||
)
|
||||
@@ -94,21 +98,21 @@ def pytest_addoption(parser: Parser) -> None:
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="pat",
|
||||
help="doctests file matching pattern, default: test*.txt",
|
||||
help="Doctests file matching pattern, default: test*.txt",
|
||||
dest="doctestglob",
|
||||
)
|
||||
group.addoption(
|
||||
"--doctest-ignore-import-errors",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="ignore doctest ImportErrors",
|
||||
help="Ignore doctest ImportErrors",
|
||||
dest="doctest_ignore_import_errors",
|
||||
)
|
||||
group.addoption(
|
||||
"--doctest-continue-on-failure",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="for a given doctest, continue to run after the first failure",
|
||||
help="For a given doctest, continue to run after the first failure",
|
||||
dest="doctest_continue_on_failure",
|
||||
)
|
||||
|
||||
@@ -246,7 +250,7 @@ def _get_runner(
|
||||
)
|
||||
|
||||
|
||||
class DoctestItem(pytest.Item):
|
||||
class DoctestItem(Item):
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
@@ -411,7 +415,7 @@ def _get_continue_on_failure(config):
|
||||
return continue_on_failure
|
||||
|
||||
|
||||
class DoctestTextfile(pytest.Module):
|
||||
class DoctestTextfile(Module):
|
||||
obj = None
|
||||
|
||||
def collect(self) -> Iterable[DoctestItem]:
|
||||
@@ -449,7 +453,7 @@ def _check_all_skipped(test: "doctest.DocTest") -> None:
|
||||
|
||||
all_skipped = all(x.options.get(doctest.SKIP, False) for x in test.examples)
|
||||
if all_skipped:
|
||||
pytest.skip("all tests skipped by +SKIP option")
|
||||
skip("all tests skipped by +SKIP option")
|
||||
|
||||
|
||||
def _is_mocked(obj: object) -> bool:
|
||||
@@ -491,7 +495,7 @@ def _patch_unwrap_mock_aware() -> Generator[None, None, None]:
|
||||
inspect.unwrap = real_unwrap
|
||||
|
||||
|
||||
class DoctestModule(pytest.Module):
|
||||
class DoctestModule(Module):
|
||||
def collect(self) -> Iterable[DoctestItem]:
|
||||
import doctest
|
||||
|
||||
@@ -528,12 +532,30 @@ class DoctestModule(pytest.Module):
|
||||
if _is_mocked(obj):
|
||||
return
|
||||
with _patch_unwrap_mock_aware():
|
||||
|
||||
# Type ignored because this is a private function.
|
||||
super()._find( # type:ignore[misc]
|
||||
tests, obj, name, module, source_lines, globs, seen
|
||||
)
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
|
||||
def _from_module(self, module, object):
|
||||
"""`cached_property` objects are never considered a part
|
||||
of the 'current module'. As such they are skipped by doctest.
|
||||
Here we override `_from_module` to check the underlying
|
||||
function instead. https://github.com/python/cpython/issues/107995
|
||||
"""
|
||||
if hasattr(functools, "cached_property") and isinstance(
|
||||
object, functools.cached_property
|
||||
):
|
||||
object = object.func
|
||||
|
||||
# Type ignored because this is a private function.
|
||||
return super()._from_module(module, object) # type: ignore[misc]
|
||||
|
||||
else: # pragma: no cover
|
||||
pass
|
||||
|
||||
if self.path.name == "conftest.py":
|
||||
module = self.config.pluginmanager._importconftest(
|
||||
self.path,
|
||||
@@ -542,10 +564,14 @@ class DoctestModule(pytest.Module):
|
||||
)
|
||||
else:
|
||||
try:
|
||||
module = import_path(self.path, root=self.config.rootpath)
|
||||
module = import_path(
|
||||
self.path,
|
||||
root=self.config.rootpath,
|
||||
mode=self.config.getoption("importmode"),
|
||||
)
|
||||
except ImportError:
|
||||
if self.config.getvalue("doctest_ignore_import_errors"):
|
||||
pytest.skip("unable to import module %r" % self.path)
|
||||
skip("unable to import module %r" % self.path)
|
||||
else:
|
||||
raise
|
||||
# Uses internal doctest module parsing mechanism.
|
||||
@@ -727,8 +753,19 @@ def _get_report_choice(key: str) -> int:
|
||||
}[key]
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
@fixture(scope="session")
|
||||
def doctest_namespace() -> Dict[str, Any]:
|
||||
"""Fixture that returns a :py:class:`dict` that will be injected into the
|
||||
namespace of doctests."""
|
||||
namespace of doctests.
|
||||
|
||||
Usually this fixture is used in conjunction with another ``autouse`` fixture:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def add_np(doctest_namespace):
|
||||
doctest_namespace["np"] = numpy
|
||||
|
||||
For more details: :ref:`doctest_namespace`.
|
||||
"""
|
||||
return dict()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user