Compare commits
572 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3673c7429 | ||
|
|
25fe3706a4 | ||
|
|
1a323fbd3c | ||
|
|
9d971d33be | ||
|
|
bc009a8582 | ||
|
|
5e7d427df1 | ||
|
|
dd59ed3b18 | ||
|
|
20d0f0e56b | ||
|
|
d24a7e6c5a | ||
|
|
4dc73bda45 | ||
|
|
732cc2687d | ||
|
|
5d2d64c190 | ||
|
|
7a6d16c1eb | ||
|
|
c2179c3127 | ||
|
|
d8d7f73e1c | ||
|
|
3c23b5b010 | ||
|
|
783019a8e6 | ||
|
|
d2fc7ca6e0 | ||
|
|
2d06927a06 | ||
|
|
44d29d887e | ||
|
|
32c5a113e2 | ||
|
|
ba5630e0f8 | ||
|
|
808df48ee8 | ||
|
|
a089a9577e | ||
|
|
6be2136f20 | ||
|
|
f9ab81a493 | ||
|
|
1636522563 | ||
|
|
b1fbb2ab92 | ||
|
|
e85edf5212 | ||
|
|
b03bad5dbb | ||
|
|
19ec300b2a | ||
|
|
11442f2ad7 | ||
|
|
97748b6605 | ||
|
|
2b762337bd | ||
|
|
9899b8f1fb | ||
|
|
4474beeb82 | ||
|
|
eca3e781b6 | ||
|
|
c61ff31ffa | ||
|
|
ec57cbf82d | ||
|
|
3f6a46c2a4 | ||
|
|
4ba3cb25b0 | ||
|
|
650c458df9 | ||
|
|
58aa4f91f5 | ||
|
|
9b382ed16c | ||
|
|
f02dbaf97f | ||
|
|
41f6ea13ce | ||
|
|
f6eb39df33 | ||
|
|
7a5e11bbcf | ||
|
|
7122fa5613 | ||
|
|
7aff81739e | ||
|
|
27772f67c0 | ||
|
|
10b3b2dc68 | ||
|
|
c2841542af | ||
|
|
1f28096587 | ||
|
|
e86b01e831 | ||
|
|
d1fa8ae08e | ||
|
|
29dac03314 | ||
|
|
e7eb7e799b | ||
|
|
7f48f552c1 | ||
|
|
1e2e65f0fa | ||
|
|
28c9cc7321 | ||
|
|
ccb90b5c46 | ||
|
|
37d2469266 | ||
|
|
1df6d28080 | ||
|
|
03eaad376b | ||
|
|
739f9a4a4b | ||
|
|
93224f8cf9 | ||
|
|
bb57186dd4 | ||
|
|
2803eb9fbb | ||
|
|
f53eff93db | ||
|
|
86a14d007d | ||
|
|
a4dd6ee3ce | ||
|
|
130cf7e0db | ||
|
|
cbb41f1ae2 | ||
|
|
fa78da3c03 | ||
|
|
87ddb2dbd5 | ||
|
|
9aa6b0903b | ||
|
|
49800ea134 | ||
|
|
8fe55b1d18 | ||
|
|
a0ce9a4441 | ||
|
|
2cf2dc3d95 | ||
|
|
7537e94ddf | ||
|
|
2c90b3db9e | ||
|
|
826adafe2e | ||
|
|
3dd2933dbd | ||
|
|
b55351274e | ||
|
|
c00d934b21 | ||
|
|
6b526cbe6a | ||
|
|
e0539e6ede | ||
|
|
5eb85efa14 | ||
|
|
9ee8d72fd2 | ||
|
|
8c4ca383ca | ||
|
|
f2a427da25 | ||
|
|
e0466d0ad8 | ||
|
|
418a66a09f | ||
|
|
5e2bd17d18 | ||
|
|
ec6fca4aa7 | ||
|
|
1f20626618 | ||
|
|
69b34f7658 | ||
|
|
531b76a513 | ||
|
|
f63c683faa | ||
|
|
410d5762c0 | ||
|
|
ddb308455a | ||
|
|
f42b5019ec | ||
|
|
adc9ed85bc | ||
|
|
4592def14d | ||
|
|
2e0a7cf78d | ||
|
|
5a52acaa92 | ||
|
|
6d497f2c77 | ||
|
|
b7560a8808 | ||
|
|
d3ca739c00 | ||
|
|
3db76ccf3d | ||
|
|
438f7a1254 | ||
|
|
47bf58d69e | ||
|
|
5ef51262f7 | ||
|
|
a054aa4797 | ||
|
|
f1cfd10c94 | ||
|
|
d3f72ca202 | ||
|
|
022c58bf64 | ||
|
|
b42518acd5 | ||
|
|
284a2d110f | ||
|
|
9ae0a3cd85 | ||
|
|
615c671434 | ||
|
|
29bfa5efa4 | ||
|
|
016f8f1536 | ||
|
|
e9417be9df | ||
|
|
c304998ed7 | ||
|
|
415a62e373 | ||
|
|
8ce3aeadbf | ||
|
|
b818314045 | ||
|
|
56d414177a | ||
|
|
0fffa6ba2f | ||
|
|
60499d221e | ||
|
|
9965ed84da | ||
|
|
7e13593452 | ||
|
|
208dd3aad1 | ||
|
|
19a01c9849 | ||
|
|
78ac7d99f5 | ||
|
|
0c8dbdcd92 | ||
|
|
8e4501ee29 | ||
|
|
0100f61b62 | ||
|
|
1a9d913ee1 | ||
|
|
51e32cf7cc | ||
|
|
3fcc4cdbd5 | ||
|
|
ffd47ceefc | ||
|
|
10f21b423a | ||
|
|
eec7081b8d | ||
|
|
b01704cce1 | ||
|
|
15ede8aab8 | ||
|
|
f7dc9b9fef | ||
|
|
dc13f0b469 | ||
|
|
a13c6a84df | ||
|
|
dfa713163a | ||
|
|
90c00dfd54 | ||
|
|
f3b9b21996 | ||
|
|
885b8a3b4c | ||
|
|
4675912d89 | ||
|
|
495b44198f | ||
|
|
8d8e68cf90 | ||
|
|
f3b0caf299 | ||
|
|
75d29acc06 | ||
|
|
cbbb36fc9b | ||
|
|
d53e449296 | ||
|
|
01df368d93 | ||
|
|
2256f2f04d | ||
|
|
95881c870e | ||
|
|
019e33ee3f | ||
|
|
19fa01b91d | ||
|
|
96aad2983b | ||
|
|
c18a5b5179 | ||
|
|
ed4b94a180 | ||
|
|
29c5ac71bc | ||
|
|
84a9f7a263 | ||
|
|
11e591e442 | ||
|
|
64f00683f2 | ||
|
|
84a033fd97 | ||
|
|
0183d46275 | ||
|
|
9bd4b0a05e | ||
|
|
f0e852b4db | ||
|
|
ade01b1f5b | ||
|
|
3035b2724d | ||
|
|
8c96eea583 | ||
|
|
338953a25d | ||
|
|
f1bd46266b | ||
|
|
77cad3c436 | ||
|
|
3ca70692de | ||
|
|
417516c378 | ||
|
|
f730291e67 | ||
|
|
d76fb8345c | ||
|
|
aea962dc21 | ||
|
|
4345efaffc | ||
|
|
bf47033169 | ||
|
|
37a65684d6 | ||
|
|
eab5020e24 | ||
|
|
8ef21f56d3 | ||
|
|
103d980b2d | ||
|
|
28c3ef1c77 | ||
|
|
67c3c28877 | ||
|
|
e040fd20a3 | ||
|
|
00e0b43010 | ||
|
|
f19cfbb825 | ||
|
|
bde3d1a0cd | ||
|
|
2e090896d5 | ||
|
|
b0a32da0b5 | ||
|
|
10c1c7c41a | ||
|
|
16f452ef98 | ||
|
|
b77e533693 | ||
|
|
a605ad4d11 | ||
|
|
4b94760c8e | ||
|
|
82a7ca9615 | ||
|
|
23295e1e98 | ||
|
|
32575f92c9 | ||
|
|
a260e58020 | ||
|
|
b2f7e02a02 | ||
|
|
29e114b463 | ||
|
|
2a059b1c1b | ||
|
|
cdc72bf5a3 | ||
|
|
f786335dbb | ||
|
|
ab5af524a4 | ||
|
|
9620b167d9 | ||
|
|
8f4685e024 | ||
|
|
10544c4cb8 | ||
|
|
1e8e17c01e | ||
|
|
80eef29681 | ||
|
|
47bb53f5cb | ||
|
|
6991a16edb | ||
|
|
2f2d5861bb | ||
|
|
a31967431f | ||
|
|
e74ad4ff9b | ||
|
|
70bdacf01a | ||
|
|
b69f853acb | ||
|
|
c31018d9bc | ||
|
|
7ae23901d3 | ||
|
|
4d19b94347 | ||
|
|
c15b537e3d | ||
|
|
2577a6ce8a | ||
|
|
dd5f5ca4cb | ||
|
|
508774742e | ||
|
|
2a917a582e | ||
|
|
325319dc3b | ||
|
|
dda5e5ea32 | ||
|
|
5e260c4d34 | ||
|
|
d3f5324386 | ||
|
|
3da88d794f | ||
|
|
71b4995775 | ||
|
|
b0541e9d31 | ||
|
|
415fcb912b | ||
|
|
f872fcb5d0 | ||
|
|
de6f2c0336 | ||
|
|
be4b359c74 | ||
|
|
72a58bbafe | ||
|
|
c336449729 | ||
|
|
1e4ecda884 | ||
|
|
8cf0e46bbf | ||
|
|
f0226e9329 | ||
|
|
dce8df45d5 | ||
|
|
f6948597e4 | ||
|
|
e3df1031ca | ||
|
|
14ffadf004 | ||
|
|
459b040d21 | ||
|
|
3396225f74 | ||
|
|
c82906105c | ||
|
|
4c14740798 | ||
|
|
5fefc48f33 | ||
|
|
72e6482994 | ||
|
|
93f783228c | ||
|
|
5f8b50c094 | ||
|
|
99e31f6fb1 | ||
|
|
f2e35c8c4f | ||
|
|
40b4fe64af | ||
|
|
d10d59c013 | ||
|
|
d54aa8ce13 | ||
|
|
52fa8c98bb | ||
|
|
3f336869e2 | ||
|
|
85482d575e | ||
|
|
6f7365509d | ||
|
|
7099ea9bb0 | ||
|
|
dccac69d82 | ||
|
|
c2cd337886 | ||
|
|
0fc4a806e5 | ||
|
|
4d3c1ab4f0 | ||
|
|
e4f76f6350 | ||
|
|
0d65783dce | ||
|
|
8bb8b91357 | ||
|
|
8804c7333a | ||
|
|
17eec5b97e | ||
|
|
cd07c4d4ff | ||
|
|
917b99e438 | ||
|
|
b08e156b79 | ||
|
|
8e2c7b4979 | ||
|
|
5a7aa123ea | ||
|
|
a12eadd9ef | ||
|
|
2137e2b15b | ||
|
|
89446af51e | ||
|
|
3b521bedf8 | ||
|
|
5b2c8fa007 | ||
|
|
eb8d145195 | ||
|
|
80bea79512 | ||
|
|
07a560ff24 | ||
|
|
717775a1c6 | ||
|
|
672f4bb5aa | ||
|
|
f1079a8222 | ||
|
|
044d2b8e6e | ||
|
|
223eef6261 | ||
|
|
43657f252f | ||
|
|
70ebab3537 | ||
|
|
d3bdfc704b | ||
|
|
4de247cfa0 | ||
|
|
d611b03589 | ||
|
|
308d789d92 | ||
|
|
539a22c750 | ||
|
|
e4bea9068b | ||
|
|
e620798d33 | ||
|
|
7ea4992f16 | ||
|
|
0564b52c0e | ||
|
|
8b2c91836b | ||
|
|
9e382e8d29 | ||
|
|
2255892d65 | ||
|
|
7d9b198f73 | ||
|
|
a6cdd0d9da | ||
|
|
c64a8c9c7f | ||
|
|
f4c5994d27 | ||
|
|
c24c7e75e2 | ||
|
|
273670b2a2 | ||
|
|
28aff051ab | ||
|
|
29975e5b37 | ||
|
|
5cf7d1dba2 | ||
|
|
2fe824b8c4 | ||
|
|
f674217c43 | ||
|
|
9f7345d663 | ||
|
|
eb2d074530 | ||
|
|
9fa7745795 | ||
|
|
14db2f91ba | ||
|
|
c3e494f6cf | ||
|
|
090f67a980 | ||
|
|
3059bfb1b3 | ||
|
|
e391c47ed8 | ||
|
|
f66764e1c0 | ||
|
|
e0b088b52e | ||
|
|
e5a3c870b4 | ||
|
|
2b71cb9c38 | ||
|
|
da9d814da4 | ||
|
|
7d4c4c66d4 | ||
|
|
939a792c41 | ||
|
|
17644ff285 | ||
|
|
64faa41d06 | ||
|
|
ca1bb9a3a1 | ||
|
|
78ef531420 | ||
|
|
212ee450b7 | ||
|
|
c1c08852f9 | ||
|
|
e06a077ac2 | ||
|
|
cb77e65c97 | ||
|
|
6367f0f5f1 | ||
|
|
b88e09a697 | ||
|
|
68bbd42213 | ||
|
|
22ee2093b8 | ||
|
|
87a99275fb | ||
|
|
abbd7c30a4 | ||
|
|
abae60c8d0 | ||
|
|
e92893ed24 | ||
|
|
27b5435a40 | ||
|
|
50db718a6a | ||
|
|
bfd0addaeb | ||
|
|
be11d3e195 | ||
|
|
266f05c4c4 | ||
|
|
d0bd01beca | ||
|
|
220288ac77 | ||
|
|
4d8903fd0b | ||
|
|
67106f056b | ||
|
|
5d3c5123f8 | ||
|
|
5f97711377 | ||
|
|
74d9f56d0f | ||
|
|
051db6a33d | ||
|
|
aa358433b0 | ||
|
|
126896f69d | ||
|
|
e723069165 | ||
|
|
855fd17014 | ||
|
|
d11781920b | ||
|
|
2c0d2eef40 | ||
|
|
ef8ec01e39 | ||
|
|
0a1c2a7ca1 | ||
|
|
fe0a76e1a6 | ||
|
|
dcafb8c48c | ||
|
|
a76cc8f8c4 | ||
|
|
4d2fa581e1 | ||
|
|
f7a3f45a18 | ||
|
|
dff7b203f7 | ||
|
|
4705fd2bbe | ||
|
|
ca0476953e | ||
|
|
7e92930fa9 | ||
|
|
33769d0328 | ||
|
|
5db2e6c7a1 | ||
|
|
804fc4063a | ||
|
|
82a2174867 | ||
|
|
a80e031c62 | ||
|
|
452e5c1cf0 | ||
|
|
c6b11b9f62 | ||
|
|
b8255308d6 | ||
|
|
a5c0fb7f6b | ||
|
|
f25683354e | ||
|
|
7d13599ba1 | ||
|
|
43664d7841 | ||
|
|
2a2f888909 | ||
|
|
ad5ddaf55a | ||
|
|
4588130c1e | ||
|
|
5003bae0de | ||
|
|
6e32a1f73d | ||
|
|
611d254ed5 | ||
|
|
57a8f208bc | ||
|
|
fcdc1d867e | ||
|
|
098dca3a9f | ||
|
|
8e2ed76227 | ||
|
|
bf7c188cc0 | ||
|
|
8c9efd8608 | ||
|
|
e1ad1a14af | ||
|
|
327fe4cfcc | ||
|
|
d02491931a | ||
|
|
032db159c9 | ||
|
|
cd2085ee71 | ||
|
|
ad305e71d7 | ||
|
|
7d8688d54b | ||
|
|
253419316c | ||
|
|
997ef59306 | ||
|
|
60b1913ba2 | ||
|
|
2c09930b6d | ||
|
|
d461e931dd | ||
|
|
eada0b1fd7 | ||
|
|
150535b6c1 | ||
|
|
f1ec02cdcd | ||
|
|
9f5d73d44a | ||
|
|
8609f8d25a | ||
|
|
953a618102 | ||
|
|
cf6d8e7e53 | ||
|
|
8af78f417f | ||
|
|
535fd1f311 | ||
|
|
762eaf443a | ||
|
|
330640eb96 | ||
|
|
e3d412d1f4 | ||
|
|
6f9a12a8a3 | ||
|
|
0e47599572 | ||
|
|
c480223e88 | ||
|
|
0b522d40a7 | ||
|
|
d900a6c8bd | ||
|
|
fe46fbb719 | ||
|
|
8d401cdb9d | ||
|
|
3f3f6f1be4 | ||
|
|
b6da5cc54c | ||
|
|
eaeeedc9c3 | ||
|
|
bf127a63b2 | ||
|
|
fe16f81da1 | ||
|
|
d0ba242c46 | ||
|
|
57b0c60cb4 | ||
|
|
6e57d123bb | ||
|
|
011f88f7e7 | ||
|
|
2eb9301ad5 | ||
|
|
f0db64ac2e | ||
|
|
514ca6f4ad | ||
|
|
f8749eeb5c | ||
|
|
f5165064ee | ||
|
|
c9a0881309 | ||
|
|
5167933395 | ||
|
|
75db608479 | ||
|
|
7bff5866b1 | ||
|
|
0bb29d5649 | ||
|
|
db33f03c15 | ||
|
|
ac9ceaacd8 | ||
|
|
d2fe619120 | ||
|
|
f6ceedd15b | ||
|
|
5226c7fac3 | ||
|
|
803302e70c | ||
|
|
05f1d0d3ef | ||
|
|
1cd62f8c38 | ||
|
|
a522fc745a | ||
|
|
303133f013 | ||
|
|
1e94ac784f | ||
|
|
027d2336b8 | ||
|
|
3c19370cec | ||
|
|
3f5e06ecc4 | ||
|
|
7696d5371a | ||
|
|
067de257e1 | ||
|
|
4a925ef5e9 | ||
|
|
4afb8c428b | ||
|
|
2f1a2cf07f | ||
|
|
6cc4fe2412 | ||
|
|
10a8691eca | ||
|
|
b75320ba95 | ||
|
|
bc268a58d1 | ||
|
|
0b70477930 | ||
|
|
8801162275 | ||
|
|
a604a71185 | ||
|
|
66fa6bb42e | ||
|
|
713d32c4da | ||
|
|
57198d477b | ||
|
|
533f4cc10c | ||
|
|
a46b94950c | ||
|
|
952bbefaac | ||
|
|
54d3cd587d | ||
|
|
0fd86ec8a8 | ||
|
|
4ae7e9788c | ||
|
|
5582ad0445 | ||
|
|
982b614010 | ||
|
|
7845ab4bc3 | ||
|
|
8680dfc939 | ||
|
|
76ac670f7d | ||
|
|
7b47dfb744 | ||
|
|
3c73d6298a | ||
|
|
c220fb235a | ||
|
|
1dc5e97ac2 | ||
|
|
e9371a58a0 | ||
|
|
ea379ba10f | ||
|
|
17e01993d9 | ||
|
|
8a6345515b | ||
|
|
581d49635e | ||
|
|
e860ff7299 | ||
|
|
0672bc633f | ||
|
|
2dfb52f7e0 | ||
|
|
cc6eb9f83c | ||
|
|
6b239263da | ||
|
|
89e0a3ec27 | ||
|
|
5b186cd609 | ||
|
|
5a156b3645 | ||
|
|
95f00de0df | ||
|
|
c4c666cbc4 | ||
|
|
ee30bf45c9 | ||
|
|
603df1ea1c | ||
|
|
abbf73ad1a | ||
|
|
1226cdab47 | ||
|
|
ab80e0fba0 | ||
|
|
fb992a0c81 | ||
|
|
23581d44bd | ||
|
|
c7eb53317b | ||
|
|
de98939ebf | ||
|
|
0d3914b626 | ||
|
|
eb94bce3e2 | ||
|
|
b897008887 | ||
|
|
998d540b73 | ||
|
|
c672bfa32e | ||
|
|
8f1d8ac970 | ||
|
|
53d4710c62 | ||
|
|
31f089db6a | ||
|
|
9d60cf25c0 | ||
|
|
9e32b6ae48 | ||
|
|
99402cf1c0 | ||
|
|
3ac2ae3c8c | ||
|
|
ea906056fa | ||
|
|
c081c5ee23 | ||
|
|
3dcdaab103 | ||
|
|
3615977608 | ||
|
|
94c41bec64 | ||
|
|
8d072205e9 | ||
|
|
b5102d03a6 | ||
|
|
791bb3502c | ||
|
|
eb0c6a8287 | ||
|
|
9ddd573774 | ||
|
|
f99da9058f | ||
|
|
7d0dba18de | ||
|
|
6fc7f07a80 | ||
|
|
05b5b64379 | ||
|
|
229c8e551d | ||
|
|
d483b401ee | ||
|
|
acacf75f49 | ||
|
|
f8350c6304 | ||
|
|
fedc78522b | ||
|
|
b0474398ec | ||
|
|
dc90c9108f | ||
|
|
69031d0033 | ||
|
|
e44d4e6508 | ||
|
|
c416b1d935 | ||
|
|
7d923c389e | ||
|
|
c02e8d8b0d | ||
|
|
35df2cdbee | ||
|
|
2b1410895e |
11
.coveragerc
11
.coveragerc
@@ -1,4 +1,9 @@
|
||||
[run]
|
||||
omit =
|
||||
# standlonetemplate is read dynamically and tested by test_genscript
|
||||
*standalonetemplate.py
|
||||
source = pytest,_pytest,testing/
|
||||
parallel = 1
|
||||
branch = 1
|
||||
|
||||
[paths]
|
||||
source = src/
|
||||
.tox/*/lib/python*/site-packages/
|
||||
.tox\*\Lib\site-packages\
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1 +1 @@
|
||||
CHANGELOG merge=union
|
||||
*.bat text eol=crlf
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -24,6 +24,7 @@ src/_pytest/_version.py
|
||||
.eggs/
|
||||
|
||||
doc/*/_build
|
||||
doc/*/.doctrees
|
||||
build/
|
||||
dist/
|
||||
*.egg-info
|
||||
@@ -35,6 +36,11 @@ env/
|
||||
.cache
|
||||
.pytest_cache
|
||||
.coverage
|
||||
.coverage.*
|
||||
coverage.xml
|
||||
.ropeproject
|
||||
.idea
|
||||
.hypothesis
|
||||
.pydevproject
|
||||
.project
|
||||
.settings
|
||||
|
||||
@@ -5,13 +5,13 @@ repos:
|
||||
hooks:
|
||||
- id: black
|
||||
args: [--safe, --quiet]
|
||||
language_version: python3.6
|
||||
language_version: python3
|
||||
- repo: https://github.com/asottile/blacken-docs
|
||||
rev: v0.2.0
|
||||
hooks:
|
||||
- id: blacken-docs
|
||||
additional_dependencies: [black==18.6b4]
|
||||
language_version: python3.6
|
||||
language_version: python3
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v1.3.0
|
||||
hooks:
|
||||
@@ -25,6 +25,10 @@ repos:
|
||||
rev: v1.2.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
- repo: https://github.com/pre-commit/pygrep-hooks
|
||||
rev: v1.0.0
|
||||
hooks:
|
||||
- id: rst-backticks
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: rst
|
||||
@@ -33,9 +37,9 @@ repos:
|
||||
files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$
|
||||
language: python
|
||||
additional_dependencies: [pygments, restructuredtext_lint]
|
||||
python_version: python3.6
|
||||
- id: rst-backticks
|
||||
name: rst ``code`` is two backticks
|
||||
entry: ' `[^`]+[^_]`([^_]|$)'
|
||||
language: pygrep
|
||||
types: [rst]
|
||||
- id: changelogs-rst
|
||||
name: changelog filenames
|
||||
language: fail
|
||||
entry: 'changelog files must be named ####.(feature|bugfix|doc|removal|vendor|trivial).rst'
|
||||
exclude: changelog/(\d+\.(feature|bugfix|doc|removal|vendor|trivial).rst|README.rst|_template.rst)
|
||||
files: ^changelog/
|
||||
|
||||
82
.travis.yml
82
.travis.yml
@@ -1,8 +1,9 @@
|
||||
sudo: false
|
||||
language: python
|
||||
stages:
|
||||
- linting
|
||||
- test
|
||||
- baseline
|
||||
- name: test
|
||||
if: repo = pytest-dev/pytest AND tag IS NOT present
|
||||
- name: deploy
|
||||
if: repo = pytest-dev/pytest AND tag IS present
|
||||
python:
|
||||
@@ -11,41 +12,60 @@ install:
|
||||
- pip install --upgrade --pre tox
|
||||
env:
|
||||
matrix:
|
||||
# coveralls is not listed in tox's envlist, but should run in travis
|
||||
- TOXENV=coveralls
|
||||
# note: please use "tox --listenvs" to populate the build matrix below
|
||||
# please remove the linting env in all cases
|
||||
- TOXENV=py27
|
||||
- TOXENV=py34
|
||||
- TOXENV=py36
|
||||
- TOXENV=py27-pexpect
|
||||
- TOXENV=py27-xdist
|
||||
- TOXENV=py27-trial
|
||||
- TOXENV=py27-numpy
|
||||
- TOXENV=py27-pluggymaster
|
||||
- TOXENV=py27-pluggymaster PYTEST_NO_COVERAGE=1
|
||||
- TOXENV=py36-pexpect
|
||||
- TOXENV=py36-xdist
|
||||
- TOXENV=py36-trial
|
||||
- TOXENV=py36-numpy
|
||||
- TOXENV=py36-pluggymaster
|
||||
- TOXENV=py36-pluggymaster PYTEST_NO_COVERAGE=1
|
||||
- TOXENV=py27-nobyte
|
||||
- TOXENV=doctesting
|
||||
- TOXENV=docs
|
||||
- TOXENV=docs PYTEST_NO_COVERAGE=1
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- env: TOXENV=pypy
|
||||
# Coverage tracking is slow with pypy, skip it.
|
||||
- env: TOXENV=pypy PYTEST_NO_COVERAGE=1
|
||||
python: 'pypy-5.4'
|
||||
- env: TOXENV=py35
|
||||
python: '3.5'
|
||||
- env: TOXENV=py36-freeze
|
||||
- env: TOXENV=py36-freeze PYTEST_NO_COVERAGE=1
|
||||
python: '3.6'
|
||||
- env: TOXENV=py37
|
||||
python: 'nightly'
|
||||
python: '3.7'
|
||||
sudo: required
|
||||
dist: xenial
|
||||
- &test-macos
|
||||
language: generic
|
||||
os: osx
|
||||
osx_image: xcode9.4
|
||||
sudo: required
|
||||
install:
|
||||
- python -m pip install --pre tox
|
||||
env: TOXENV=py27
|
||||
- <<: *test-macos
|
||||
env: TOXENV=py37
|
||||
before_install:
|
||||
- brew update
|
||||
- brew upgrade python
|
||||
- brew unlink python
|
||||
- brew link python
|
||||
|
||||
- stage: baseline
|
||||
env: TOXENV=py27
|
||||
- env: TOXENV=py34
|
||||
- env: TOXENV=py36
|
||||
- env: TOXENV=linting PYTEST_NO_COVERAGE=1
|
||||
|
||||
- stage: deploy
|
||||
python: '3.6'
|
||||
env:
|
||||
env: PYTEST_NO_COVERAGE=1
|
||||
install: pip install -U setuptools setuptools_scm
|
||||
script: skip
|
||||
deploy:
|
||||
@@ -58,17 +78,35 @@ jobs:
|
||||
on:
|
||||
tags: true
|
||||
repo: pytest-dev/pytest
|
||||
- stage: linting
|
||||
python: '3.6'
|
||||
env:
|
||||
install:
|
||||
- pip install pre-commit
|
||||
- pre-commit install-hooks
|
||||
script:
|
||||
- pre-commit run --all-files
|
||||
|
||||
before_script:
|
||||
- |
|
||||
if [[ "$PYTEST_NO_COVERAGE" != 1 ]]; then
|
||||
export COVERAGE_FILE="$PWD/.coverage"
|
||||
export COVERAGE_PROCESS_START="$PWD/.coveragerc"
|
||||
export _PYTEST_TOX_COVERAGE_RUN="coverage run -m"
|
||||
export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess
|
||||
fi
|
||||
|
||||
script: tox --recreate
|
||||
|
||||
after_success:
|
||||
- |
|
||||
if [[ "$PYTEST_NO_COVERAGE" != 1 ]]; then
|
||||
set -e
|
||||
pip install coverage
|
||||
coverage combine
|
||||
coverage xml --ignore-errors
|
||||
coverage report -m --ignore-errors
|
||||
bash <(curl -s https://codecov.io/bash) -Z -X gcov -X coveragepy -X search -X xcode -X gcovout -X fix -f coverage.xml -F "${TOXENV//-/,},linux"
|
||||
|
||||
# Coveralls does not support merged reports.
|
||||
if [[ "$TOXENV" = py37 ]]; then
|
||||
pip install coveralls
|
||||
coveralls
|
||||
fi
|
||||
fi
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
|
||||
18
AUTHORS
18
AUTHORS
@@ -10,6 +10,7 @@ Ahn Ki-Wook
|
||||
Alan Velasco
|
||||
Alexander Johnson
|
||||
Alexei Kozlenok
|
||||
Allan Feldman
|
||||
Anatoly Bubenkoff
|
||||
Anders Hovmöller
|
||||
Andras Tim
|
||||
@@ -46,7 +47,9 @@ Christian Boelsen
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christopher Gilling
|
||||
CrazyMerlyn
|
||||
Cyrus Maden
|
||||
Dhiren Serai
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Nuri
|
||||
@@ -71,6 +74,7 @@ Endre Galaczi
|
||||
Eric Hunsberger
|
||||
Eric Siegerman
|
||||
Erik M. Bray
|
||||
Fabio Zadrozny
|
||||
Feng Ma
|
||||
Florian Bruhin
|
||||
Floris Bruynooghe
|
||||
@@ -89,6 +93,8 @@ Hugo van Kemenade
|
||||
Hui Wang (coldnight)
|
||||
Ian Bicking
|
||||
Ian Lesperance
|
||||
Ionuț Turturică
|
||||
Iwan Briquemont
|
||||
Jaap Broekhuizen
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
@@ -97,6 +103,7 @@ Javier Domingo Cansino
|
||||
Javier Romero
|
||||
Jeff Rackauckas
|
||||
Jeff Widman
|
||||
Jenni Rinker
|
||||
John Eddie Ayson
|
||||
John Towler
|
||||
Jon Sonesen
|
||||
@@ -173,6 +180,7 @@ Raphael Pierzina
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
Roberto Polli
|
||||
Roland Puntaier
|
||||
Romain Dorgueil
|
||||
Roman Bolshakov
|
||||
Ronny Pfannschmidt
|
||||
@@ -181,7 +189,9 @@ Russel Winder
|
||||
Ryan Wooden
|
||||
Samuel Dion-Girardeau
|
||||
Samuele Pedroni
|
||||
Sankt Petersbug
|
||||
Segev Finer
|
||||
Serhii Mozghovyi
|
||||
Simon Gomizelj
|
||||
Skylar Downes
|
||||
Srinivas Reddy Thatiparthy
|
||||
@@ -190,6 +200,7 @@ Stefan Zimmermann
|
||||
Stefano Taschini
|
||||
Steffen Allner
|
||||
Stephan Obermann
|
||||
Tadek Teleżyński
|
||||
Tarcisio Fischer
|
||||
Tareq Alayan
|
||||
Ted Xiao
|
||||
@@ -202,14 +213,17 @@ Trevor Bekolay
|
||||
Tyler Goodlet
|
||||
Tzu-ping Chung
|
||||
Vasily Kuznetsov
|
||||
Victor Maryama
|
||||
Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
Virgil Dupras
|
||||
Vitaly Lashmanov
|
||||
Vlad Dragos
|
||||
Wil Cooley
|
||||
William Lee
|
||||
Wim Glenn
|
||||
Wouter van Ackooy
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Zac Hatfield-Dodds
|
||||
Zoltán Máté
|
||||
Roland Puntaier
|
||||
Allan Feldman
|
||||
|
||||
435
CHANGELOG.rst
435
CHANGELOG.rst
@@ -1,3 +1,13 @@
|
||||
=================
|
||||
Changelog history
|
||||
=================
|
||||
|
||||
Versions follow `Semantic Versioning <https://semver.org/>`_ (``<major>.<minor>.<patch>``).
|
||||
|
||||
Backward incompatible (breaking) changes will only be introduced in major versions
|
||||
with advance notice in the **Deprecations** section of releases.
|
||||
|
||||
|
||||
..
|
||||
You should *NOT* be adding new change log entries to this file, this
|
||||
file is managed by towncrier. You *may* edit previous change logs to
|
||||
@@ -8,6 +18,381 @@
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
pytest 3.8.2 (2018-10-02)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- `#4036 <https://github.com/pytest-dev/pytest/issues/4036>`_: The ``item`` parameter of ``pytest_warning_captured`` hook is now documented as deprecated. We realized only after
|
||||
the ``3.8`` release that this parameter is incompatible with ``pytest-xdist``.
|
||||
|
||||
Our policy is to not deprecate features during bugfix releases, but in this case we believe it makes sense as we are
|
||||
only documenting it as deprecated, without issuing warnings which might potentially break test suites. This will get
|
||||
the word out that hook implementers should not use this parameter at all.
|
||||
|
||||
In a future release ``item`` will always be ``None`` and will emit a proper warning when a hook implementation
|
||||
makes use of it.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3539 <https://github.com/pytest-dev/pytest/issues/3539>`_: Fix reload on assertion rewritten modules.
|
||||
|
||||
|
||||
- `#4034 <https://github.com/pytest-dev/pytest/issues/4034>`_: The ``.user_properties`` attribute of ``TestReport`` objects is a list
|
||||
of (name, value) tuples, but could sometimes be instantiated as a tuple
|
||||
of tuples. It is now always a list.
|
||||
|
||||
|
||||
- `#4039 <https://github.com/pytest-dev/pytest/issues/4039>`_: No longer issue warnings about using ``pytest_plugins`` in non-top-level directories when using ``--pyargs``: the
|
||||
current ``--pyargs`` mechanism is not reliable and might give false negatives.
|
||||
|
||||
|
||||
- `#4040 <https://github.com/pytest-dev/pytest/issues/4040>`_: Exclude empty reports for passed tests when ``-rP`` option is used.
|
||||
|
||||
|
||||
- `#4051 <https://github.com/pytest-dev/pytest/issues/4051>`_: Improve error message when an invalid Python expression is passed to the ``-m`` option.
|
||||
|
||||
|
||||
- `#4056 <https://github.com/pytest-dev/pytest/issues/4056>`_: ``MonkeyPatch.setenv`` and ``MonkeyPatch.delenv`` issue a warning if the environment variable name is not ``str`` on Python 2.
|
||||
|
||||
In Python 2, adding ``unicode`` keys to ``os.environ`` causes problems with ``subprocess`` (and possible other modules),
|
||||
making this a subtle bug specially susceptible when used with ``from __future__ import unicode_literals``.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3928 <https://github.com/pytest-dev/pytest/issues/3928>`_: Add possible values for fixture scope to docs.
|
||||
|
||||
|
||||
pytest 3.8.1 (2018-09-22)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3286 <https://github.com/pytest-dev/pytest/issues/3286>`_: ``.pytest_cache`` directory is now automatically ignored by Git. Users who would like to contribute a solution for other SCMs please consult/comment on this issue.
|
||||
|
||||
|
||||
- `#3749 <https://github.com/pytest-dev/pytest/issues/3749>`_: Fix the following error during collection of tests inside packages::
|
||||
|
||||
TypeError: object of type 'Package' has no len()
|
||||
|
||||
|
||||
- `#3941 <https://github.com/pytest-dev/pytest/issues/3941>`_: Fix bug where indirect parametrization would consider the scope of all fixtures used by the test function to determine the parametrization scope, and not only the scope of the fixtures being parametrized.
|
||||
|
||||
|
||||
- `#3973 <https://github.com/pytest-dev/pytest/issues/3973>`_: Fix crash of the assertion rewriter if a test changed the current working directory without restoring it afterwards.
|
||||
|
||||
|
||||
- `#3998 <https://github.com/pytest-dev/pytest/issues/3998>`_: Fix issue that prevented some caplog properties (for example ``record_tuples``) from being available when entering the debugger with ``--pdb``.
|
||||
|
||||
|
||||
- `#3999 <https://github.com/pytest-dev/pytest/issues/3999>`_: Fix ``UnicodeDecodeError`` in python2.x when a class returns a non-ascii binary ``__repr__`` in an assertion which also contains non-ascii text.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3996 <https://github.com/pytest-dev/pytest/issues/3996>`_: New `Deprecations and Removals <https://docs.pytest.org/en/latest/deprecations.html>`_ page shows all currently
|
||||
deprecated features, the rationale to do so, and alternatives to update your code. It also list features removed
|
||||
from pytest in past major releases to help those with ancient pytest versions to upgrade.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#3955 <https://github.com/pytest-dev/pytest/issues/3955>`_: Improve pre-commit detection for changelog filenames
|
||||
|
||||
|
||||
- `#3975 <https://github.com/pytest-dev/pytest/issues/3975>`_: Remove legacy code around im_func as that was python2 only
|
||||
|
||||
|
||||
pytest 3.8.0 (2018-09-05)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- `#2452 <https://github.com/pytest-dev/pytest/issues/2452>`_: ``Config.warn`` and ``Node.warn`` have been
|
||||
deprecated, see `<https://docs.pytest.org/en/latest/deprecations.html#config-warn-and-node-warn>`_ for rationale and
|
||||
examples.
|
||||
|
||||
- `#3936 <https://github.com/pytest-dev/pytest/issues/3936>`_: ``@pytest.mark.filterwarnings`` second parameter is no longer regex-escaped,
|
||||
making it possible to actually use regular expressions to check the warning message.
|
||||
|
||||
**Note**: regex-escaping the match string was an implementation oversight that might break test suites which depend
|
||||
on the old behavior.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#2452 <https://github.com/pytest-dev/pytest/issues/2452>`_: Internal pytest warnings are now issued using the standard ``warnings`` module, making it possible to use
|
||||
the standard warnings filters to manage those warnings. This introduces ``PytestWarning``,
|
||||
``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API.
|
||||
|
||||
Consult `the documentation <https://docs.pytest.org/en/latest/warnings.html#internal-pytest-warnings>`_ for more info.
|
||||
|
||||
|
||||
- `#2908 <https://github.com/pytest-dev/pytest/issues/2908>`_: ``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is
|
||||
configured. This makes pytest more compliant with
|
||||
`PEP-0506 <https://www.python.org/dev/peps/pep-0565/#recommended-filter-settings-for-test-runners>`_. See
|
||||
`the docs <https://docs.pytest.org/en/latest/warnings.html#deprecationwarning-and-pendingdeprecationwarning>`_ for
|
||||
more info.
|
||||
|
||||
|
||||
- `#3251 <https://github.com/pytest-dev/pytest/issues/3251>`_: Warnings are now captured and displayed during test collection.
|
||||
|
||||
|
||||
- `#3784 <https://github.com/pytest-dev/pytest/issues/3784>`_: ``PYTEST_DISABLE_PLUGIN_AUTOLOAD`` environment variable disables plugin auto-loading when set.
|
||||
|
||||
|
||||
- `#3829 <https://github.com/pytest-dev/pytest/issues/3829>`_: Added the ``count`` option to ``console_output_style`` to enable displaying the progress as a count instead of a percentage.
|
||||
|
||||
|
||||
- `#3837 <https://github.com/pytest-dev/pytest/issues/3837>`_: Added support for 'xfailed' and 'xpassed' outcomes to the ``pytester.RunResult.assert_outcomes`` signature.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3911 <https://github.com/pytest-dev/pytest/issues/3911>`_: Terminal writer now takes into account unicode character width when writing out progress.
|
||||
|
||||
|
||||
- `#3913 <https://github.com/pytest-dev/pytest/issues/3913>`_: Pytest now returns with correct exit code (EXIT_USAGEERROR, 4) when called with unknown arguments.
|
||||
|
||||
|
||||
- `#3918 <https://github.com/pytest-dev/pytest/issues/3918>`_: Improve performance of assertion rewriting.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3566 <https://github.com/pytest-dev/pytest/issues/3566>`_: Added a blurb in usage.rst for the usage of -r flag which is used to show an extra test summary info.
|
||||
|
||||
|
||||
- `#3907 <https://github.com/pytest-dev/pytest/issues/3907>`_: Corrected type of the exceptions collection passed to ``xfail``: ``raises`` argument accepts a ``tuple`` instead of ``list``.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#3853 <https://github.com/pytest-dev/pytest/issues/3853>`_: Removed ``"run all (no recorded failures)"`` message printed with ``--failed-first`` and ``--last-failed`` when there are no failed tests.
|
||||
|
||||
|
||||
pytest 3.7.4 (2018-08-29)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3506 <https://github.com/pytest-dev/pytest/issues/3506>`_: Fix possible infinite recursion when writing ``.pyc`` files.
|
||||
|
||||
|
||||
- `#3853 <https://github.com/pytest-dev/pytest/issues/3853>`_: Cache plugin now obeys the ``-q`` flag when ``--last-failed`` and ``--failed-first`` flags are used.
|
||||
|
||||
|
||||
- `#3883 <https://github.com/pytest-dev/pytest/issues/3883>`_: Fix bad console output when using ``console_output_style=classic``.
|
||||
|
||||
|
||||
- `#3888 <https://github.com/pytest-dev/pytest/issues/3888>`_: Fix macOS specific code using ``capturemanager`` plugin in doctests.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3902 <https://github.com/pytest-dev/pytest/issues/3902>`_: Fix pytest.org links
|
||||
|
||||
|
||||
pytest 3.7.3 (2018-08-26)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3033 <https://github.com/pytest-dev/pytest/issues/3033>`_: Fixtures during teardown can again use ``capsys`` and ``capfd`` to inspect output captured during tests.
|
||||
|
||||
|
||||
- `#3773 <https://github.com/pytest-dev/pytest/issues/3773>`_: Fix collection of tests from ``__init__.py`` files if they match the ``python_files`` configuration option.
|
||||
|
||||
|
||||
- `#3796 <https://github.com/pytest-dev/pytest/issues/3796>`_: Fix issue where teardown of fixtures of consecutive sub-packages were executed once, at the end of the outer
|
||||
package.
|
||||
|
||||
|
||||
- `#3816 <https://github.com/pytest-dev/pytest/issues/3816>`_: Fix bug where ``--show-capture=no`` option would still show logs printed during fixture teardown.
|
||||
|
||||
|
||||
- `#3819 <https://github.com/pytest-dev/pytest/issues/3819>`_: Fix ``stdout/stderr`` not getting captured when real-time cli logging is active.
|
||||
|
||||
|
||||
- `#3843 <https://github.com/pytest-dev/pytest/issues/3843>`_: Fix collection error when specifying test functions directly in the command line using ``test.py::test`` syntax together with ``--doctest-modules``.
|
||||
|
||||
|
||||
- `#3848 <https://github.com/pytest-dev/pytest/issues/3848>`_: Fix bugs where unicode arguments could not be passed to ``testdir.runpytest`` on Python 2.
|
||||
|
||||
|
||||
- `#3854 <https://github.com/pytest-dev/pytest/issues/3854>`_: Fix double collection of tests within packages when the filename starts with a capital letter.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3824 <https://github.com/pytest-dev/pytest/issues/3824>`_: Added example for multiple glob pattern matches in ``python_files``.
|
||||
|
||||
|
||||
- `#3833 <https://github.com/pytest-dev/pytest/issues/3833>`_: Added missing docs for ``pytester.Testdir``.
|
||||
|
||||
|
||||
- `#3870 <https://github.com/pytest-dev/pytest/issues/3870>`_: Correct documentation for setuptools integration.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#3826 <https://github.com/pytest-dev/pytest/issues/3826>`_: Replace broken type annotations with type comments.
|
||||
|
||||
|
||||
- `#3845 <https://github.com/pytest-dev/pytest/issues/3845>`_: Remove a reference to issue `#568 <https://github.com/pytest-dev/pytest/issues/568>`_ from the documentation, which has since been
|
||||
fixed.
|
||||
|
||||
|
||||
pytest 3.7.2 (2018-08-16)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3671 <https://github.com/pytest-dev/pytest/issues/3671>`_: Fix ``filterwarnings`` not being registered as a builtin mark.
|
||||
|
||||
|
||||
- `#3768 <https://github.com/pytest-dev/pytest/issues/3768>`_, `#3789 <https://github.com/pytest-dev/pytest/issues/3789>`_: Fix test collection from packages mixed with normal directories.
|
||||
|
||||
|
||||
- `#3771 <https://github.com/pytest-dev/pytest/issues/3771>`_: Fix infinite recursion during collection if a ``pytest_ignore_collect`` hook returns ``False`` instead of ``None``.
|
||||
|
||||
|
||||
- `#3774 <https://github.com/pytest-dev/pytest/issues/3774>`_: Fix bug where decorated fixtures would lose functionality (for example ``@mock.patch``).
|
||||
|
||||
|
||||
- `#3775 <https://github.com/pytest-dev/pytest/issues/3775>`_: Fix bug where importing modules or other objects with prefix ``pytest_`` prefix would raise a ``PluginValidationError``.
|
||||
|
||||
|
||||
- `#3788 <https://github.com/pytest-dev/pytest/issues/3788>`_: Fix ``AttributeError`` during teardown of ``TestCase`` subclasses which raise an exception during ``__init__``.
|
||||
|
||||
|
||||
- `#3804 <https://github.com/pytest-dev/pytest/issues/3804>`_: Fix traceback reporting for exceptions with ``__cause__`` cycles.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3746 <https://github.com/pytest-dev/pytest/issues/3746>`_: Add documentation for ``metafunc.config`` that had been mistakenly hidden.
|
||||
|
||||
|
||||
pytest 3.7.1 (2018-08-02)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#3473 <https://github.com/pytest-dev/pytest/issues/3473>`_: Raise immediately if ``approx()`` is given an expected value of a type it doesn't understand (e.g. strings, nested dicts, etc.).
|
||||
|
||||
|
||||
- `#3712 <https://github.com/pytest-dev/pytest/issues/3712>`_: Correctly represent the dimensions of an numpy array when calling ``repr()`` on ``approx()``.
|
||||
|
||||
- `#3742 <https://github.com/pytest-dev/pytest/issues/3742>`_: Fix incompatibility with third party plugins during collection, which produced the error ``object has no attribute '_collectfile'``.
|
||||
|
||||
- `#3745 <https://github.com/pytest-dev/pytest/issues/3745>`_: Display the absolute path if ``cache_dir`` is not relative to the ``rootdir`` instead of failing.
|
||||
|
||||
|
||||
- `#3747 <https://github.com/pytest-dev/pytest/issues/3747>`_: Fix compatibility problem with plugins and the warning code issued by fixture functions when they are called directly.
|
||||
|
||||
|
||||
- `#3748 <https://github.com/pytest-dev/pytest/issues/3748>`_: Fix infinite recursion in ``pytest.approx`` with arrays in ``numpy<1.13``.
|
||||
|
||||
|
||||
- `#3757 <https://github.com/pytest-dev/pytest/issues/3757>`_: Pin pathlib2 to ``>=2.2.0`` as we require ``__fspath__`` support.
|
||||
|
||||
|
||||
- `#3763 <https://github.com/pytest-dev/pytest/issues/3763>`_: Fix ``TypeError`` when the assertion message is ``bytes`` in python 3.
|
||||
|
||||
|
||||
pytest 3.7.0 (2018-07-30)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- `#2639 <https://github.com/pytest-dev/pytest/issues/2639>`_: ``pytest_namespace`` has been `deprecated <https://docs.pytest.org/en/latest/deprecations.html#pytest-namespace>`_.
|
||||
|
||||
|
||||
- `#3661 <https://github.com/pytest-dev/pytest/issues/3661>`_: Calling a fixture function directly, as opposed to request them in a test function, now issues a ``RemovedInPytest4Warning``. See `the documentation for rationale and examples <https://docs.pytest.org/en/latest/deprecations.html#calling-fixtures-directly>`_.
|
||||
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `#2283 <https://github.com/pytest-dev/pytest/issues/2283>`_: New ``package`` fixture scope: fixtures are finalized when the last test of a *package* finishes. This feature is considered **experimental**, so use it sparingly.
|
||||
|
||||
|
||||
- `#3576 <https://github.com/pytest-dev/pytest/issues/3576>`_: ``Node.add_marker`` now supports an ``append=True/False`` parameter to determine whether the mark comes last (default) or first.
|
||||
|
||||
|
||||
- `#3579 <https://github.com/pytest-dev/pytest/issues/3579>`_: Fixture ``caplog`` now has a ``messages`` property, providing convenient access to the format-interpolated log messages without the extra data provided by the formatter/handler.
|
||||
|
||||
|
||||
- `#3610 <https://github.com/pytest-dev/pytest/issues/3610>`_: New ``--trace`` option to enter the debugger at the start of a test.
|
||||
|
||||
|
||||
- `#3623 <https://github.com/pytest-dev/pytest/issues/3623>`_: Introduce ``pytester.copy_example`` as helper to do acceptance tests against examples from the project.
|
||||
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- `#2220 <https://github.com/pytest-dev/pytest/issues/2220>`_: Fix a bug where fixtures overridden by direct parameters (for example parametrization) were being instantiated even if they were not being used by a test.
|
||||
|
||||
|
||||
- `#3695 <https://github.com/pytest-dev/pytest/issues/3695>`_: Fix ``ApproxNumpy`` initialisation argument mixup, ``abs`` and ``rel`` tolerances were flipped causing strange comparsion results.
|
||||
Add tests to check ``abs`` and ``rel`` tolerances for ``np.array`` and test for expecting ``nan`` with ``np.array()``
|
||||
|
||||
|
||||
- `#980 <https://github.com/pytest-dev/pytest/issues/980>`_: Fix truncated locals output in verbose mode.
|
||||
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- `#3295 <https://github.com/pytest-dev/pytest/issues/3295>`_: Correct the usage documentation of ``--last-failed-no-failures`` by adding the missing ``--last-failed`` argument in the presented examples, because they are misleading and lead to think that the missing argument is not needed.
|
||||
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- `#3519 <https://github.com/pytest-dev/pytest/issues/3519>`_: Now a ``README.md`` file is created in ``.pytest_cache`` to make it clear why the directory exists.
|
||||
|
||||
|
||||
pytest 3.6.4 (2018-07-28)
|
||||
=========================
|
||||
|
||||
@@ -35,7 +420,7 @@ Trivial/Internal Changes
|
||||
- Pin ``pluggy`` to ``<0.8``. (`#3727 <https://github.com/pytest-dev/pytest/issues/3727>`_)
|
||||
|
||||
|
||||
Pytest 3.6.3 (2018-07-04)
|
||||
pytest 3.6.3 (2018-07-04)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -81,7 +466,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3653>`_)
|
||||
|
||||
|
||||
Pytest 3.6.2 (2018-06-20)
|
||||
pytest 3.6.2 (2018-06-20)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -127,7 +512,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3567>`_)
|
||||
|
||||
|
||||
Pytest 3.6.1 (2018-06-05)
|
||||
pytest 3.6.1 (2018-06-05)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -171,7 +556,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3529>`_)
|
||||
|
||||
|
||||
Pytest 3.6.0 (2018-05-23)
|
||||
pytest 3.6.0 (2018-05-23)
|
||||
=========================
|
||||
|
||||
Features
|
||||
@@ -257,7 +642,7 @@ Trivial/Internal Changes
|
||||
3.7 or newer. (`#3497 <https://github.com/pytest-dev/pytest/issues/3497>`_)
|
||||
|
||||
|
||||
Pytest 3.5.1 (2018-04-23)
|
||||
pytest 3.5.1 (2018-04-23)
|
||||
=========================
|
||||
|
||||
|
||||
@@ -309,7 +694,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3398>`_)
|
||||
|
||||
|
||||
Pytest 3.5.0 (2018-03-21)
|
||||
pytest 3.5.0 (2018-03-21)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
@@ -320,7 +705,7 @@ Deprecations and Removals
|
||||
<https://github.com/pytest-dev/pytest/issues/2770>`_)
|
||||
|
||||
- Defining ``pytest_plugins`` is now deprecated in non-top-level conftest.py
|
||||
files, because they "leak" to the entire directory tree. (`#3084
|
||||
files, because they "leak" to the entire directory tree. `See the docs <https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files>`_ for the rationale behind this decision (`#3084
|
||||
<https://github.com/pytest-dev/pytest/issues/3084>`_)
|
||||
|
||||
|
||||
@@ -461,7 +846,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3308>`_)
|
||||
|
||||
|
||||
Pytest 3.4.2 (2018-03-04)
|
||||
pytest 3.4.2 (2018-03-04)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -498,7 +883,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3259>`_)
|
||||
|
||||
|
||||
Pytest 3.4.1 (2018-02-20)
|
||||
pytest 3.4.1 (2018-02-20)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -559,7 +944,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/985>`_)
|
||||
|
||||
|
||||
Pytest 3.4.0 (2018-01-30)
|
||||
pytest 3.4.0 (2018-01-30)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
@@ -691,7 +1076,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/3129>`_)
|
||||
|
||||
|
||||
Pytest 3.3.2 (2017-12-25)
|
||||
pytest 3.3.2 (2017-12-25)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -728,7 +1113,7 @@ Trivial/Internal Changes
|
||||
(`#3018 <https://github.com/pytest-dev/pytest/issues/3018>`_)
|
||||
|
||||
|
||||
Pytest 3.3.1 (2017-12-05)
|
||||
pytest 3.3.1 (2017-12-05)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -770,13 +1155,13 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/2949>`_)
|
||||
|
||||
|
||||
Pytest 3.3.0 (2017-11-23)
|
||||
pytest 3.3.0 (2017-11-23)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- Pytest no longer supports Python **2.6** and **3.3**. Those Python versions
|
||||
- pytest no longer supports Python **2.6** and **3.3**. Those Python versions
|
||||
are EOL for some time now and incur maintenance and compatibility costs on
|
||||
the pytest core team, and following up with the rest of the community we
|
||||
decided that they will no longer be supported starting on this version. Users
|
||||
@@ -830,7 +1215,7 @@ Features
|
||||
- Match ``warns`` signature to ``raises`` by adding ``match`` keyword. (`#2708
|
||||
<https://github.com/pytest-dev/pytest/issues/2708>`_)
|
||||
|
||||
- Pytest now captures and displays output from the standard ``logging`` module.
|
||||
- pytest now captures and displays output from the standard ``logging`` module.
|
||||
The user can control the logging level to be captured by specifying options
|
||||
in ``pytest.ini``, the command line and also during individual tests using
|
||||
markers. Also, a ``caplog`` fixture is available that enables users to test
|
||||
@@ -895,7 +1280,7 @@ Bug Fixes
|
||||
avoids a number of potential problems. (`#2751
|
||||
<https://github.com/pytest-dev/pytest/issues/2751>`_)
|
||||
|
||||
- Pytest no longer complains about warnings with unicode messages being
|
||||
- pytest no longer complains about warnings with unicode messages being
|
||||
non-ascii compatible even for ascii-compatible messages. As a result of this,
|
||||
warnings with unicode messages are converted first to an ascii representation
|
||||
for safety. (`#2809 <https://github.com/pytest-dev/pytest/issues/2809>`_)
|
||||
@@ -947,7 +1332,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/2922>`_)
|
||||
|
||||
|
||||
Pytest 3.2.5 (2017-11-15)
|
||||
pytest 3.2.5 (2017-11-15)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -958,7 +1343,7 @@ Bug Fixes
|
||||
<https://github.com/pytest-dev/pytest/issues/2926>`_)
|
||||
|
||||
|
||||
Pytest 3.2.4 (2017-11-13)
|
||||
pytest 3.2.4 (2017-11-13)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -1007,7 +1392,7 @@ Improved Documentation
|
||||
<https://github.com/pytest-dev/pytest/issues/911>`_)
|
||||
|
||||
|
||||
Pytest 3.2.3 (2017-10-03)
|
||||
pytest 3.2.3 (2017-10-03)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -1047,7 +1432,7 @@ Trivial/Internal Changes
|
||||
(`#2765 <https://github.com/pytest-dev/pytest/issues/2765>`_)
|
||||
|
||||
|
||||
Pytest 3.2.2 (2017-09-06)
|
||||
pytest 3.2.2 (2017-09-06)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -1094,7 +1479,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/2739>`_)
|
||||
|
||||
|
||||
Pytest 3.2.1 (2017-08-08)
|
||||
pytest 3.2.1 (2017-08-08)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -1124,7 +1509,7 @@ Improved Documentation
|
||||
<https://github.com/pytest-dev/pytest/issues/2626>`_)
|
||||
|
||||
|
||||
Pytest 3.2.0 (2017-07-30)
|
||||
pytest 3.2.0 (2017-07-30)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
@@ -1290,7 +1675,7 @@ Trivial/Internal Changes
|
||||
<https://github.com/pytest-dev/pytest/issues/2620>`_)
|
||||
|
||||
|
||||
Pytest 3.1.3 (2017-07-03)
|
||||
pytest 3.1.3 (2017-07-03)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -1336,7 +1721,7 @@ Trivial/Internal Changes
|
||||
(`#2499 <https://github.com/pytest-dev/pytest/issues/2499>`_)
|
||||
|
||||
|
||||
Pytest 3.1.2 (2017-06-08)
|
||||
pytest 3.1.2 (2017-06-08)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
@@ -1368,7 +1753,7 @@ Improved Documentation
|
||||
and improve overall flow of the ``skipping`` docs. (#810)
|
||||
|
||||
|
||||
Pytest 3.1.1 (2017-05-30)
|
||||
pytest 3.1.1 (2017-05-30)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
|
||||
@@ -28,10 +28,13 @@ taking a lot of time to make a new one.
|
||||
|
||||
#. After all tests pass and the PR has been approved, publish to PyPI by pushing the tag::
|
||||
|
||||
git tag <VERSION>
|
||||
git push git@github.com:pytest-dev/pytest.git <VERSION>
|
||||
|
||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
||||
|
||||
#. Merge the PR into ``master``.
|
||||
|
||||
#. Send an email announcement with the contents from::
|
||||
|
||||
doc/en/announce/release-<VERSION>.rst
|
||||
|
||||
27
README.rst
27
README.rst
@@ -1,5 +1,5 @@
|
||||
.. image:: http://docs.pytest.org/en/latest/_static/pytest1.png
|
||||
:target: http://docs.pytest.org
|
||||
.. image:: https://docs.pytest.org/en/latest/_static/pytest1.png
|
||||
:target: https://docs.pytest.org/en/latest/
|
||||
:align: center
|
||||
:alt: pytest
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
.. image:: https://img.shields.io/pypi/pyversions/pytest.svg
|
||||
:target: https://pypi.org/project/pytest/
|
||||
|
||||
.. image:: https://img.shields.io/coveralls/pytest-dev/pytest/master.svg
|
||||
:target: https://coveralls.io/r/pytest-dev/pytest
|
||||
.. image:: https://codecov.io/gh/pytest-dev/pytest/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/pytest-dev/pytest
|
||||
:alt: Code coverage Status
|
||||
|
||||
.. image:: https://travis-ci.org/pytest-dev/pytest.svg?branch=master
|
||||
:target: https://travis-ci.org/pytest-dev/pytest
|
||||
@@ -25,7 +26,7 @@
|
||||
:target: https://ci.appveyor.com/project/pytestbot/pytest
|
||||
|
||||
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||
:target: https://github.com/ambv/black
|
||||
:target: https://github.com/ambv/black
|
||||
|
||||
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
|
||||
:target: https://www.codetriage.com/pytest-dev/pytest
|
||||
@@ -66,23 +67,23 @@ To execute it::
|
||||
========================== 1 failed in 0.04 seconds ===========================
|
||||
|
||||
|
||||
Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See `getting-started <http://docs.pytest.org/en/latest/getting-started.html#our-first-test-run>`_ for more examples.
|
||||
Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See `getting-started <https://docs.pytest.org/en/latest/getting-started.html#our-first-test-run>`_ for more examples.
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Detailed info on failing `assert statements <http://docs.pytest.org/en/latest/assert.html>`_ (no need to remember ``self.assert*`` names);
|
||||
- Detailed info on failing `assert statements <https://docs.pytest.org/en/latest/assert.html>`_ (no need to remember ``self.assert*`` names);
|
||||
|
||||
- `Auto-discovery
|
||||
<http://docs.pytest.org/en/latest/goodpractices.html#python-test-discovery>`_
|
||||
<https://docs.pytest.org/en/latest/goodpractices.html#python-test-discovery>`_
|
||||
of test modules and functions;
|
||||
|
||||
- `Modular fixtures <http://docs.pytest.org/en/latest/fixture.html>`_ for
|
||||
- `Modular fixtures <https://docs.pytest.org/en/latest/fixture.html>`_ for
|
||||
managing small or parametrized long-lived test resources;
|
||||
|
||||
- Can run `unittest <http://docs.pytest.org/en/latest/unittest.html>`_ (or trial),
|
||||
`nose <http://docs.pytest.org/en/latest/nose.html>`_ test suites out of the box;
|
||||
- Can run `unittest <https://docs.pytest.org/en/latest/unittest.html>`_ (or trial),
|
||||
`nose <https://docs.pytest.org/en/latest/nose.html>`_ test suites out of the box;
|
||||
|
||||
- Python 2.7, Python 3.4+, PyPy 2.3, Jython 2.5 (untested);
|
||||
|
||||
@@ -92,7 +93,7 @@ Features
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
For full documentation, including installation, tutorials and PDF documents, please see http://docs.pytest.org.
|
||||
For full documentation, including installation, tutorials and PDF documents, please see https://docs.pytest.org/en/latest/.
|
||||
|
||||
|
||||
Bugs/Requests
|
||||
@@ -104,7 +105,7 @@ Please use the `GitHub issue tracker <https://github.com/pytest-dev/pytest/issue
|
||||
Changelog
|
||||
---------
|
||||
|
||||
Consult the `Changelog <http://docs.pytest.org/en/latest/changelog.html>`__ page for fixes and enhancements of each version.
|
||||
Consult the `Changelog <https://docs.pytest.org/en/latest/changelog.html>`__ page for fixes and enhancements of each version.
|
||||
|
||||
|
||||
License
|
||||
|
||||
27
appveyor.yml
27
appveyor.yml
@@ -1,34 +1,30 @@
|
||||
environment:
|
||||
COVERALLS_REPO_TOKEN:
|
||||
secure: 2NJ5Ct55cHJ9WEg3xbSqCuv0rdgzzb6pnzOIG5OkMbTndw3wOBrXntWFoQrXiMFi
|
||||
# this is pytest's token in coveralls.io, encrypted
|
||||
# using pytestbot account as detailed here:
|
||||
# https://www.appveyor.com/docs/build-configuration#secure-variables
|
||||
|
||||
matrix:
|
||||
# coveralls is not in the default env list
|
||||
- TOXENV: "coveralls"
|
||||
# note: please use "tox --listenvs" to populate the build matrix below
|
||||
- TOXENV: "linting"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "py27"
|
||||
- TOXENV: "py34"
|
||||
- TOXENV: "py35"
|
||||
- TOXENV: "py36"
|
||||
- TOXENV: "py37"
|
||||
- TOXENV: "pypy"
|
||||
- TOXENV: "py27-pexpect"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "py27-xdist"
|
||||
- TOXENV: "py27-trial"
|
||||
- TOXENV: "py27-numpy"
|
||||
- TOXENV: "py27-pluggymaster"
|
||||
- TOXENV: "py36-pexpect"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "py36-xdist"
|
||||
- TOXENV: "py36-trial"
|
||||
- TOXENV: "py36-numpy"
|
||||
- TOXENV: "py36-pluggymaster"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "py27-nobyte"
|
||||
- TOXENV: "doctesting"
|
||||
- TOXENV: "py36-freeze"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
- TOXENV: "docs"
|
||||
PYTEST_NO_COVERAGE: "1"
|
||||
|
||||
install:
|
||||
- echo Installed Pythons
|
||||
@@ -36,12 +32,19 @@ install:
|
||||
|
||||
- if "%TOXENV%" == "pypy" call scripts\install-pypy.bat
|
||||
|
||||
- C:\Python36\python -m pip install --upgrade pip
|
||||
- C:\Python36\python -m pip install --upgrade --pre tox
|
||||
|
||||
build: false # Not a C# project, build stuff at the test step instead.
|
||||
|
||||
before_test:
|
||||
- call scripts\prepare-coverage.bat
|
||||
|
||||
test_script:
|
||||
- call scripts\call-tox.bat
|
||||
- C:\Python36\python -m tox
|
||||
|
||||
on_success:
|
||||
- call scripts\upload-coverage.bat
|
||||
|
||||
cache:
|
||||
- '%LOCALAPPDATA%\pip\cache'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import py
|
||||
import six
|
||||
|
||||
for i in range(1000):
|
||||
py.builtin.exec_("def test_func_%d(): pass" % i)
|
||||
six.exec_("def test_func_%d(): pass" % i)
|
||||
|
||||
@@ -26,7 +26,7 @@ changelog using that instead.
|
||||
|
||||
If you are not sure what issue type to use, don't hesitate to ask in your PR.
|
||||
|
||||
Note that the ``towncrier`` tool will automatically
|
||||
reflow your text, so it will work best if you stick to a single paragraph, but multiple sentences and links are OK
|
||||
and encouraged. You can install ``towncrier`` and then run ``towncrier --draft``
|
||||
``towncrier`` preserves multiple paragraphs and formatting (code blocks, lists, and so on), but for entries
|
||||
other than ``features`` it is usually better to stick to a single paragraph to keep it concise. You can install
|
||||
``towncrier`` and then run ``towncrier --draft``
|
||||
if you want to get a preview of how your change will look in the final release notes.
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% if definitions[category]['showcontent'] %}
|
||||
{% for text, values in sections[section][category]|dictsort(by='value') %}
|
||||
{% set issue_joiner = joiner(', ') %}
|
||||
- {{ text }}{% if category != 'vendor' %} ({% for value in values|sort %}{{ issue_joiner() }}`{{ value }} <https://github.com/pytest-dev/pytest/issues/{{ value[1:] }}>`_{% endfor %}){% endif %}
|
||||
- {% for value in values|sort %}{{ issue_joiner() }}`{{ value }} <https://github.com/pytest-dev/pytest/issues/{{ value[1:] }}>`_{% endfor %}: {{ text }}
|
||||
|
||||
|
||||
{% endfor %}
|
||||
|
||||
@@ -6,6 +6,14 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-3.8.2
|
||||
release-3.8.1
|
||||
release-3.8.0
|
||||
release-3.7.4
|
||||
release-3.7.3
|
||||
release-3.7.2
|
||||
release-3.7.1
|
||||
release-3.7.0
|
||||
release-3.6.4
|
||||
release-3.6.3
|
||||
release-3.6.2
|
||||
|
||||
41
doc/en/announce/release-3.7.0.rst
Normal file
41
doc/en/announce/release-3.7.0.rst
Normal file
@@ -0,0 +1,41 @@
|
||||
pytest-3.7.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.7.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 2000 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
This release contains a number of bugs fixes and improvements, so users are encouraged
|
||||
to take a look at the CHANGELOG:
|
||||
|
||||
http://doc.pytest.org/en/latest/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
http://docs.pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Alan
|
||||
* Alan Brammer
|
||||
* Ammar Najjar
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Jeffrey Rackauckas
|
||||
* Kale Kundert
|
||||
* Ronny Pfannschmidt
|
||||
* Serhii Mozghovyi
|
||||
* Tadek Teleżyński
|
||||
* Wil Cooley
|
||||
* abrammer
|
||||
* avirlrma
|
||||
* turturica
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
21
doc/en/announce/release-3.7.1.rst
Normal file
21
doc/en/announce/release-3.7.1.rst
Normal file
@@ -0,0 +1,21 @@
|
||||
pytest-3.7.1
|
||||
=======================================
|
||||
|
||||
pytest 3.7.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 http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Kale Kundert
|
||||
* Ronny Pfannschmidt
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
25
doc/en/announce/release-3.7.2.rst
Normal file
25
doc/en/announce/release-3.7.2.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-3.7.2
|
||||
=======================================
|
||||
|
||||
pytest 3.7.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 http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Josh Holland
|
||||
* Ronny Pfannschmidt
|
||||
* Sankt Petersbug
|
||||
* Wes Thomas
|
||||
* turturica
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
33
doc/en/announce/release-3.7.3.rst
Normal file
33
doc/en/announce/release-3.7.3.rst
Normal file
@@ -0,0 +1,33 @@
|
||||
pytest-3.7.3
|
||||
=======================================
|
||||
|
||||
pytest 3.7.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 http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Andrew Champion
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Gandalf Saxe
|
||||
* Jennifer Rinker
|
||||
* Natan Lao
|
||||
* Ondřej Súkup
|
||||
* Ronny Pfannschmidt
|
||||
* Sankt Petersbug
|
||||
* Tyler Richard
|
||||
* Victor
|
||||
* Vlad Shcherbina
|
||||
* turturica
|
||||
* victor
|
||||
* wim glenn
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
22
doc/en/announce/release-3.7.4.rst
Normal file
22
doc/en/announce/release-3.7.4.rst
Normal file
@@ -0,0 +1,22 @@
|
||||
pytest-3.7.4
|
||||
=======================================
|
||||
|
||||
pytest 3.7.4 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/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Jiri Kuncar
|
||||
* Steve Piercy
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
38
doc/en/announce/release-3.8.0.rst
Normal file
38
doc/en/announce/release-3.8.0.rst
Normal file
@@ -0,0 +1,38 @@
|
||||
pytest-3.8.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.8.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 2000 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
This release contains a number of bugs fixes and improvements, so users are encouraged
|
||||
to take a look at the CHANGELOG:
|
||||
|
||||
https://docs.pytest.org/en/latest/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
https://docs.pytest.org/en/latest/
|
||||
|
||||
As usual, you can upgrade from pypi via:
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* CrazyMerlyn
|
||||
* Daniel Hahler
|
||||
* Fabio Zadrozny
|
||||
* Jeffrey Rackauckas
|
||||
* Ronny Pfannschmidt
|
||||
* Virgil Dupras
|
||||
* dhirensr
|
||||
* hoefling
|
||||
* wim glenn
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
25
doc/en/announce/release-3.8.1.rst
Normal file
25
doc/en/announce/release-3.8.1.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-3.8.1
|
||||
=======================================
|
||||
|
||||
pytest 3.8.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/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Ankit Goel
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Maximilian Albert
|
||||
* Ronny Pfannschmidt
|
||||
* William Jamir Silva
|
||||
* wim glenn
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
28
doc/en/announce/release-3.8.2.rst
Normal file
28
doc/en/announce/release-3.8.2.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pytest-3.8.2
|
||||
=======================================
|
||||
|
||||
pytest 3.8.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/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Ankit Goel
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Denis Otkidach
|
||||
* Harry Percival
|
||||
* Jeffrey Rackauckas
|
||||
* Jose Carlos Menezes
|
||||
* Ronny Pfannschmidt
|
||||
* Zac-HD
|
||||
* iwanb
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -264,8 +264,12 @@ Advanced assertion introspection
|
||||
Reporting details about a failing assertion is achieved by rewriting assert
|
||||
statements before they are run. Rewritten assert statements put introspection
|
||||
information into the assertion failure message. ``pytest`` only rewrites test
|
||||
modules directly discovered by its test collection process, so asserts in
|
||||
supporting modules which are not themselves test modules will not be rewritten.
|
||||
modules directly discovered by its test collection process, so **asserts in
|
||||
supporting modules which are not themselves test modules will not be rewritten**.
|
||||
|
||||
You can manually enable assertion rewriting for an imported module by calling
|
||||
`register_assert_rewrite <https://docs.pytest.org/en/latest/writing_plugins.html#assertion-rewriting>`_
|
||||
before you import it (a good place to do that is in ``conftest.py``).
|
||||
|
||||
.. note::
|
||||
|
||||
|
||||
@@ -7,14 +7,16 @@ Keeping backwards compatibility has a very high priority in the pytest project.
|
||||
|
||||
With the pytest 3.0 release we introduced a clear communication scheme for when we will actually remove the old busted joint and politely ask you to use the new hotness instead, while giving you enough time to adjust your tests or raise concerns if there are valid reasons to keep deprecated functionality around.
|
||||
|
||||
To communicate changes we are already issuing deprecation warnings, but they are not displayed by default. In pytest 3.0 we changed the default setting so that pytest deprecation warnings are displayed if not explicitly silenced (with ``--disable-pytest-warnings``).
|
||||
To communicate changes we issue deprecation warnings using a custom warning hierarchy (see :ref:`internal-warnings`). These warnings may be suppressed using the standard means: ``-W`` command-line flag or ``filterwarnings`` ini options (see :ref:`warnings`), but we suggest to use these sparingly and temporarily, and heed the warnings when possible.
|
||||
|
||||
We will only remove deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we will not remove it in 4.0 but in 5.0).
|
||||
We will only start the removal of deprecated functionality in major releases (e.g. if we deprecate something in 3.0 we will start to remove it in 4.0), and keep it around for at least two minor releases (e.g. if we deprecate something in 3.9 and 4.0 is the next release, we start to remove it in 5.0, not in 4.0).
|
||||
|
||||
When the deprecation expires (e.g. 4.0 is released), we won't remove the deprecated functionality immediately, but will use the standard warning filters to turn them into **errors** by default. This approach makes it explicit that removal is imminent, and still gives you time to turn the deprecated feature into a warning instead of an error so it can be dealt with in your own time. In the next minor release (e.g. 4.1), the feature will be effectively removed.
|
||||
|
||||
|
||||
Deprecation Roadmap
|
||||
-------------------
|
||||
|
||||
We track 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.
|
||||
Features currently deprecated and removed in previous releases can be found in :ref:`deprecations`.
|
||||
|
||||
Following our deprecation policy, after starting issuing deprecation warnings we keep features for *at least* two minor versions before considering removal.
|
||||
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.
|
||||
|
||||
@@ -162,8 +162,8 @@ 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::
|
||||
|
||||
pytest --last-failed-no-failures all # run all tests (default behavior)
|
||||
pytest --last-failed-no-failures none # run no tests and exit
|
||||
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
|
||||
|
||||
The new config.cache object
|
||||
--------------------------------
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
|
||||
.. _changelog:
|
||||
|
||||
Changelog history
|
||||
=================================
|
||||
|
||||
.. include:: ../../CHANGELOG.rst
|
||||
|
||||
@@ -329,7 +329,7 @@ texinfo_documents = [
|
||||
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {"python": ("http://docs.python.org/3", None)}
|
||||
intersphinx_mapping = {"python": ("https://docs.python.org/3", None)}
|
||||
|
||||
|
||||
def setup(app):
|
||||
|
||||
@@ -39,6 +39,7 @@ Full pytest documentation
|
||||
bash-completion
|
||||
|
||||
backwards-compatibility
|
||||
deprecations
|
||||
historical-notes
|
||||
license
|
||||
contributing
|
||||
|
||||
@@ -32,7 +32,7 @@ Here's a summary what ``pytest`` uses ``rootdir`` for:
|
||||
class name, function name and parametrization (if any).
|
||||
|
||||
* Is used by plugins as a stable location to store project/test run specific information;
|
||||
for example, the internal :ref:`cache <cache>` plugin creates a ``.cache`` subdirectory
|
||||
for example, the internal :ref:`cache <cache>` plugin creates a ``.pytest_cache`` subdirectory
|
||||
in ``rootdir`` to store its cross-test run state.
|
||||
|
||||
Important to emphasize that ``rootdir`` is **NOT** used to modify ``sys.path``/``PYTHONPATH`` or
|
||||
|
||||
325
doc/en/deprecations.rst
Normal file
325
doc/en/deprecations.rst
Normal file
@@ -0,0 +1,325 @@
|
||||
.. _deprecations:
|
||||
|
||||
Deprecations and Removals
|
||||
=========================
|
||||
|
||||
This page lists all pytest features that are currently deprecated or have been removed in past major releases.
|
||||
The objective is to give users a clear rationale why a certain feature has been removed, and what alternatives
|
||||
should be used instead.
|
||||
|
||||
Deprecated Features
|
||||
-------------------
|
||||
|
||||
Below is a complete list of all pytest features which are considered deprecated. Using those features will issue
|
||||
:class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using
|
||||
:ref:`standard warning filters <warnings>`.
|
||||
|
||||
``Config.warn`` and ``Node.warn``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.8
|
||||
|
||||
Those methods were part of the internal pytest warnings system, but since ``3.8`` pytest is using the builtin warning
|
||||
system for its own warnings, so those two functions are now deprecated.
|
||||
|
||||
``Config.warn`` should be replaced by calls to the standard ``warnings.warn``, example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
config.warn("C1", "some warning")
|
||||
|
||||
Becomes:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
warnings.warn(pytest.PytestWarning("some warning"))
|
||||
|
||||
``Node.warn`` now supports two signatures:
|
||||
|
||||
* ``node.warn(PytestWarning("some message"))``: is now the **recommended** way to call this function.
|
||||
The warning instance must be a PytestWarning or subclass.
|
||||
|
||||
* ``node.warn("CI", "some message")``: this code/message form is now **deprecated** and should be converted to the warning instance form above.
|
||||
|
||||
|
||||
``pytest_namespace``
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.7
|
||||
|
||||
This hook is deprecated because it greatly complicates the pytest internals regarding configuration and initialization, making some
|
||||
bug fixes and refactorings impossible.
|
||||
|
||||
Example of usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class MySymbol:
|
||||
...
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {"my_symbol": MySymbol()}
|
||||
|
||||
|
||||
Plugin authors relying on this hook should instead require that users now import the plugin modules directly (with an appropriate public API).
|
||||
|
||||
As a stopgap measure, plugin authors may still inject their names into pytest's namespace, usually during ``pytest_configure``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_configure():
|
||||
pytest.my_symbol = MySymbol()
|
||||
|
||||
|
||||
|
||||
Calling fixtures directly
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.7
|
||||
|
||||
Calling a fixture function directly, as opposed to request them in a test function, is deprecated.
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture
|
||||
def cell():
|
||||
return ...
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def full_cell():
|
||||
cell = cell()
|
||||
cell.make_full()
|
||||
return cell
|
||||
|
||||
This is a great source of confusion to new users, which will often call the fixture functions and request them from test functions interchangeably, which breaks the fixture resolution model.
|
||||
|
||||
In those cases just request the function directly in the dependent fixture:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture
|
||||
def cell():
|
||||
return ...
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def full_cell(cell):
|
||||
cell.make_full()
|
||||
return cell
|
||||
|
||||
``Node.get_marker``
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.6
|
||||
|
||||
As part of a large :ref:`marker-revamp`, :meth:`_pytest.nodes.Node.get_marker` is deprecated. See
|
||||
:ref:`the documentation <update marker code>` on tips on how to update your code.
|
||||
|
||||
|
||||
record_xml_property
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.5
|
||||
|
||||
The ``record_xml_property`` fixture is now deprecated in favor of the more generic ``record_property``, which
|
||||
can be used by other consumers (for example ``pytest-html``) to obtain custom information about the test run.
|
||||
|
||||
This is just a matter of renaming the fixture as the API is the same:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_foo(record_xml_property):
|
||||
...
|
||||
|
||||
Change to:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_foo(record_property):
|
||||
...
|
||||
|
||||
pytest_plugins in non-top-level conftest files
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.5
|
||||
|
||||
Defining ``pytest_plugins`` is now deprecated in non-top-level conftest.py
|
||||
files because they will activate referenced plugins *globally*, which is surprising because for all other pytest
|
||||
features ``conftest.py`` files are only *active* for tests at or below it.
|
||||
|
||||
Metafunc.addcall
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.3
|
||||
|
||||
:meth:`_pytest.python.Metafunc.addcall` was a precursor to the current parametrized mechanism. Users should use
|
||||
:meth:`_pytest.python.Metafunc.parametrize` instead.
|
||||
|
||||
marks in ``pytest.mark.parametrize``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.2
|
||||
|
||||
Applying marks to values of a ``pytest.mark.parametrize`` call is now deprecated. For example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"a, b", [(3, 9), pytest.mark.xfail(reason="flaky")(6, 36), (10, 100)]
|
||||
)
|
||||
def test_foo(a, b):
|
||||
...
|
||||
|
||||
This code applies the ``pytest.mark.xfail(reason="flaky")`` mark to the ``(6, 36)`` value of the above parametrization
|
||||
call.
|
||||
|
||||
This was considered hard to read and understand, and also its implementation presented problems to the code preventing
|
||||
further internal improvements in the marks architecture.
|
||||
|
||||
To update the code, use ``pytest.param``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"a, b",
|
||||
[(3, 9), pytest.param((6, 36), marks=pytest.mark.xfail(reason="flaky")), (10, 100)],
|
||||
)
|
||||
def test_foo(a, b):
|
||||
...
|
||||
|
||||
|
||||
|
||||
Passing command-line string to ``pytest.main()``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
||||
Passing a command-line string to ``pytest.main()`` is deprecated:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pytest.main("-v -s")
|
||||
|
||||
Pass a list instead:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
pytest.main(["-v", "-s"])
|
||||
|
||||
|
||||
By passing a string, users expect that pytest will interpret that command-line using the shell rules they are working
|
||||
on (for example ``bash`` or ``Powershell``), but this is very hard/impossible to do in a portable way.
|
||||
|
||||
|
||||
``yield`` tests
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
||||
pytest supports ``yield``-style tests, where a test function actually ``yield`` functions and values
|
||||
that are then turned into proper test methods. Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def check(x, y):
|
||||
assert x ** x == y
|
||||
|
||||
|
||||
def test_squared():
|
||||
yield check, 2, 4
|
||||
yield check, 3, 9
|
||||
|
||||
This would result into two actual test functions being generated.
|
||||
|
||||
This form of test function doesn't support fixtures properly, and users should switch to ``pytest.mark.parametrize``:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.mark.parametrize("x, y", [(2, 4), (3, 9)])
|
||||
def test_squared():
|
||||
assert x ** x == y
|
||||
|
||||
|
||||
``pytest_funcarg__`` prefix
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
||||
In very early pytest versions fixtures could be defined using the ``pytest_funcarg__`` prefix:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def pytest_funcarg__data():
|
||||
return SomeData()
|
||||
|
||||
Switch over to the ``@pytest.fixture`` decorator:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@pytest.fixture
|
||||
def data():
|
||||
return SomeData()
|
||||
|
||||
[pytest] section in setup.cfg files
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
||||
``[pytest]`` sections in ``setup.cfg`` files should now be named ``[tool:pytest]``
|
||||
to avoid conflicts with other distutils commands.
|
||||
|
||||
Result log (``--result-log``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. deprecated:: 3.0
|
||||
|
||||
The ``--resultlog`` command line option has been deprecated: it is little used
|
||||
and there are more modern and better alternatives, for example `pytest-tap <https://tappy.readthedocs.io/en/latest/>`_.
|
||||
|
||||
Removed Features
|
||||
----------------
|
||||
|
||||
As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after
|
||||
an appropriate period of deprecation has passed.
|
||||
|
||||
|
||||
Reinterpretation mode (``--assert=reinterp``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
*Removed in version 3.0.*
|
||||
|
||||
Reinterpretation mode has now been removed and only plain and rewrite
|
||||
mode are available, consequently the ``--assert=reinterp`` option is
|
||||
no longer available. This also means files imported from plugins or
|
||||
``conftest.py`` will not benefit from improved assertions by
|
||||
default, you should use ``pytest.register_assert_rewrite()`` to
|
||||
explicitly turn on assertion rewriting for those files.
|
||||
|
||||
Removed command-line options
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
*Removed in version 3.0.*
|
||||
|
||||
The following deprecated commandline options were removed:
|
||||
|
||||
* ``--genscript``: no longer supported;
|
||||
* ``--no-assert``: use ``--assert=plain`` instead;
|
||||
* ``--nomagic``: use ``--assert=plain`` instead;
|
||||
* ``--report``: use ``-r`` instead;
|
||||
|
||||
py.test-X* entry points
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
*Removed in version 3.0.*
|
||||
|
||||
Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points
|
||||
were never documented and a leftover from a pre-virtualenv era. These entry
|
||||
points also created broken entry points in wheels, so removing them also
|
||||
removes a source of confusion for users.
|
||||
@@ -1,6 +1,6 @@
|
||||
from pytest import raises
|
||||
import _pytest._code
|
||||
import py
|
||||
import six
|
||||
|
||||
|
||||
def otherfunc(a, b):
|
||||
@@ -177,7 +177,7 @@ def test_dynamic_compile_shows_nicely():
|
||||
name = "abc-123"
|
||||
module = imp.new_module(name)
|
||||
code = _pytest._code.compile(src, name, "exec")
|
||||
py.builtin.exec_(code, module.__dict__)
|
||||
six.exec_(code, module.__dict__)
|
||||
sys.modules[name] = module
|
||||
module.foo()
|
||||
|
||||
|
||||
@@ -10,4 +10,4 @@ def pytest_runtest_setup(item):
|
||||
return
|
||||
mod = item.getparent(pytest.Module).obj
|
||||
if hasattr(mod, "hello"):
|
||||
print("mod.hello %r" % (mod.hello,))
|
||||
print("mod.hello {!r}".format(mod.hello))
|
||||
|
||||
@@ -31,7 +31,7 @@ You can then restrict a test run to only run tests marked with ``webtest``::
|
||||
|
||||
$ pytest -v -m webtest
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items / 3 deselected
|
||||
@@ -44,7 +44,7 @@ Or the inverse, running all tests except the webtest ones::
|
||||
|
||||
$ pytest -v -m "not webtest"
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items / 1 deselected
|
||||
@@ -64,7 +64,7 @@ tests based on their module, class, method, or function name::
|
||||
|
||||
$ pytest -v test_server.py::TestClass::test_method
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 1 item
|
||||
@@ -77,7 +77,7 @@ You can also select on the class::
|
||||
|
||||
$ pytest -v test_server.py::TestClass
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 1 item
|
||||
@@ -90,7 +90,7 @@ Or select multiple nodes::
|
||||
|
||||
$ pytest -v test_server.py::TestClass test_server.py::test_send_http
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 2 items
|
||||
@@ -128,7 +128,7 @@ select tests based on their names::
|
||||
|
||||
$ pytest -v -k http # running with the above defined example module
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items / 3 deselected
|
||||
@@ -141,7 +141,7 @@ And you can also run all tests except the ones that match the keyword::
|
||||
|
||||
$ pytest -k "not send_http" -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items / 1 deselected
|
||||
@@ -156,7 +156,7 @@ Or to select "http" and "quick" tests::
|
||||
|
||||
$ pytest -k "http or quick" -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items / 2 deselected
|
||||
@@ -200,15 +200,17 @@ You can ask which markers exist for your test suite - the list includes our just
|
||||
$ pytest --markers
|
||||
@pytest.mark.webtest: mark a test as a webtest.
|
||||
|
||||
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/latest/warnings.html#pytest-mark-filterwarnings
|
||||
|
||||
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
|
||||
|
||||
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html
|
||||
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see https://docs.pytest.org/en/latest/skipping.html
|
||||
|
||||
@pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False): mark the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See http://pytest.org/latest/skipping.html
|
||||
@pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False): mark the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/latest/skipping.html
|
||||
|
||||
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see http://pytest.org/latest/parametrize.html for more info and examples.
|
||||
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/latest/parametrize.html for more info and examples.
|
||||
|
||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures
|
||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/latest/fixture.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.
|
||||
|
||||
@@ -374,15 +376,17 @@ The ``--markers`` option always gives you a list of available markers::
|
||||
$ pytest --markers
|
||||
@pytest.mark.env(name): mark test to run only on named environment
|
||||
|
||||
@pytest.mark.filterwarnings(warning): add a warning filter to the given test. see https://docs.pytest.org/en/latest/warnings.html#pytest-mark-filterwarnings
|
||||
|
||||
@pytest.mark.skip(reason=None): skip the given test function with an optional reason. Example: skip(reason="no way of currently testing this") skips the test.
|
||||
|
||||
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see http://pytest.org/latest/skipping.html
|
||||
@pytest.mark.skipif(condition): skip the given test function if eval(condition) results in a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform. see https://docs.pytest.org/en/latest/skipping.html
|
||||
|
||||
@pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False): mark the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See http://pytest.org/latest/skipping.html
|
||||
@pytest.mark.xfail(condition, reason=None, run=True, raises=None, strict=False): mark the test function as an expected failure if eval(condition) has a True value. Optionally specify a reason for better reporting and run=False if you don't even want to execute the test function. If only specific exception(s) are expected, you can list them in raises, and if the test fails in other ways, it will be reported as a true failure. See https://docs.pytest.org/en/latest/skipping.html
|
||||
|
||||
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see http://pytest.org/latest/parametrize.html for more info and examples.
|
||||
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in different arguments in turn. argvalues generally needs to be a list of values if argnames specifies only one name or a list of tuples of values if argnames specifies multiple names. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.see https://docs.pytest.org/en/latest/parametrize.html for more info and examples.
|
||||
|
||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures
|
||||
@pytest.mark.usefixtures(fixturename1, fixturename2, ...): mark tests as needing all of the specified fixtures. see https://docs.pytest.org/en/latest/fixture.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.
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
module containing a parametrized tests testing cross-python
|
||||
serialization via the pickle module.
|
||||
"""
|
||||
import textwrap
|
||||
|
||||
import py
|
||||
import pytest
|
||||
import _pytest._code
|
||||
|
||||
pythonlist = ["python2.7", "python3.4", "python3.5"]
|
||||
|
||||
@@ -24,42 +25,44 @@ class Python(object):
|
||||
def __init__(self, version, picklefile):
|
||||
self.pythonpath = py.path.local.sysfind(version)
|
||||
if not self.pythonpath:
|
||||
pytest.skip("%r not found" % (version,))
|
||||
pytest.skip("{!r} not found".format(version))
|
||||
self.picklefile = picklefile
|
||||
|
||||
def dumps(self, obj):
|
||||
dumpfile = self.picklefile.dirpath("dump.py")
|
||||
dumpfile.write(
|
||||
_pytest._code.Source(
|
||||
"""
|
||||
import pickle
|
||||
f = open(%r, 'wb')
|
||||
s = pickle.dump(%r, f, protocol=2)
|
||||
f.close()
|
||||
"""
|
||||
% (str(self.picklefile), obj)
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
import pickle
|
||||
f = open({!r}, 'wb')
|
||||
s = pickle.dump({!r}, f, protocol=2)
|
||||
f.close()
|
||||
""".format(
|
||||
str(self.picklefile), obj
|
||||
)
|
||||
)
|
||||
)
|
||||
py.process.cmdexec("%s %s" % (self.pythonpath, dumpfile))
|
||||
py.process.cmdexec("{} {}".format(self.pythonpath, dumpfile))
|
||||
|
||||
def load_and_is_true(self, expression):
|
||||
loadfile = self.picklefile.dirpath("load.py")
|
||||
loadfile.write(
|
||||
_pytest._code.Source(
|
||||
"""
|
||||
import pickle
|
||||
f = open(%r, 'rb')
|
||||
obj = pickle.load(f)
|
||||
f.close()
|
||||
res = eval(%r)
|
||||
if not res:
|
||||
raise SystemExit(1)
|
||||
"""
|
||||
% (str(self.picklefile), expression)
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
import pickle
|
||||
f = open({!r}, 'rb')
|
||||
obj = pickle.load(f)
|
||||
f.close()
|
||||
res = eval({!r})
|
||||
if not res:
|
||||
raise SystemExit(1)
|
||||
""".format(
|
||||
str(self.picklefile), expression
|
||||
)
|
||||
)
|
||||
)
|
||||
print(loadfile)
|
||||
py.process.cmdexec("%s %s" % (self.pythonpath, loadfile))
|
||||
py.process.cmdexec("{} {}".format(self.pythonpath, loadfile))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("obj", [42, {}, {1: 3}])
|
||||
|
||||
@@ -59,7 +59,7 @@ consulted when reporting in ``verbose`` mode::
|
||||
|
||||
nonpython $ pytest -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
|
||||
collecting ... collected 2 items
|
||||
@@ -84,8 +84,9 @@ interesting to just look at the collection tree::
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
|
||||
collected 2 items
|
||||
<YamlFile 'test_simple.yml'>
|
||||
<YamlItem 'hello'>
|
||||
<YamlItem 'ok'>
|
||||
<Package '$REGENDOC_TMPDIR/nonpython'>
|
||||
<YamlFile 'test_simple.yml'>
|
||||
<YamlItem 'hello'>
|
||||
<YamlItem 'ok'>
|
||||
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
@@ -413,7 +413,7 @@ Running it results in some skips if we don't have all the python interpreters in
|
||||
. $ pytest -rs -q multipython.py
|
||||
...sss...sssssssss...sss... [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIP [15] $REGENDOC_TMPDIR/CWD/multipython.py:28: 'python3.4' not found
|
||||
SKIP [15] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.4' not found
|
||||
12 passed, 15 skipped in 0.12 seconds
|
||||
|
||||
Indirect parametrization of optional implementations/imports
|
||||
|
||||
@@ -100,19 +100,21 @@ Changing naming conventions
|
||||
|
||||
You can configure different naming conventions by setting
|
||||
the :confval:`python_files`, :confval:`python_classes` and
|
||||
:confval:`python_functions` configuration options. Example::
|
||||
:confval:`python_functions` configuration options.
|
||||
Here is an example::
|
||||
|
||||
# content of pytest.ini
|
||||
# Example 1: have pytest look for "check" instead of "test"
|
||||
# can also be defined in tox.ini or setup.cfg file, although the section
|
||||
# name in setup.cfg files should be "tool:pytest"
|
||||
[pytest]
|
||||
python_files=check_*.py
|
||||
python_classes=Check
|
||||
python_functions=*_check
|
||||
python_files = check_*.py
|
||||
python_classes = Check
|
||||
python_functions = *_check
|
||||
|
||||
This would make ``pytest`` look for tests in files that match the ``check_*
|
||||
.py`` glob-pattern, ``Check`` prefixes in classes, and functions and methods
|
||||
that match ``*_check``. For example, if we have::
|
||||
that match ``*_check``. For example, if we have::
|
||||
|
||||
# content of check_myapp.py
|
||||
class CheckMyApp(object):
|
||||
@@ -121,7 +123,7 @@ that match ``*_check``. For example, if we have::
|
||||
def complex_check(self):
|
||||
pass
|
||||
|
||||
then the test collection looks like this::
|
||||
The test collection would look like this::
|
||||
|
||||
$ pytest --collect-only
|
||||
=========================== test session starts ============================
|
||||
@@ -136,11 +138,19 @@ then the test collection looks like this::
|
||||
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
You can check for multiple glob patterns by adding a space between the patterns::
|
||||
|
||||
# Example 2: have pytest look for files with "test" and "example"
|
||||
# content of pytest.ini, tox.ini, or setup.cfg file (replace "pytest"
|
||||
# with "tool:pytest" for setup.cfg)
|
||||
[pytest]
|
||||
python_files = test_*.py example_*.py
|
||||
|
||||
.. note::
|
||||
|
||||
the ``python_functions`` and ``python_classes`` options has no effect
|
||||
for ``unittest.TestCase`` test discovery because pytest delegates
|
||||
detection of test case methods to unittest code.
|
||||
discovery of test case methods to unittest code.
|
||||
|
||||
Interpreting cmdline arguments as Python packages
|
||||
-----------------------------------------------------
|
||||
|
||||
@@ -363,7 +363,7 @@ get on the terminal - we are working on that)::
|
||||
> int(s)
|
||||
E ValueError: invalid literal for int() with base 10: 'qwe'
|
||||
|
||||
<0-codegen $PYTHON_PREFIX/lib/python3.5/site-packages/_pytest/python_api.py:635>:1: ValueError
|
||||
<0-codegen $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/python_api.py:682>:1: ValueError
|
||||
______________________ TestRaises.test_raises_doesnt _______________________
|
||||
|
||||
self = <failure_demo.TestRaises object at 0xdeadbeef>
|
||||
@@ -423,7 +423,7 @@ get on the terminal - we are working on that)::
|
||||
name = "abc-123"
|
||||
module = imp.new_module(name)
|
||||
code = _pytest._code.compile(src, name, "exec")
|
||||
py.builtin.exec_(code, module.__dict__)
|
||||
six.exec_(code, module.__dict__)
|
||||
sys.modules[name] = module
|
||||
> module.foo()
|
||||
|
||||
@@ -613,9 +613,9 @@ get on the terminal - we are working on that)::
|
||||
|
||||
failure_demo.py:261: AssertionError
|
||||
============================= warnings summary =============================
|
||||
<undetermined location>
|
||||
Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.
|
||||
Please use Metafunc.parametrize instead.
|
||||
$REGENDOC_TMPDIR/assertion/failure_demo.py:24: RemovedInPytest4Warning: Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.
|
||||
Please use Metafunc.parametrize instead.
|
||||
metafunc.addcall(funcargs=dict(param1=3, param2=6))
|
||||
|
||||
-- Docs: http://doc.pytest.org/en/latest/warnings.html
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
================== 42 failed, 1 warnings in 0.12 seconds ===================
|
||||
|
||||
@@ -357,7 +357,7 @@ which will add info only when run with "--v"::
|
||||
|
||||
$ pytest -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
info1: did you know that ...
|
||||
did you?
|
||||
|
||||
@@ -171,6 +171,7 @@ to cause the decorated ``smtp_connection`` fixture function to only be invoked
|
||||
once per test *module* (the default is to invoke once per test *function*).
|
||||
Multiple test functions in a test module will thus
|
||||
each receive the same ``smtp_connection`` fixture instance, thus saving time.
|
||||
Possible values for ``scope`` are: ``function``, ``class``, ``module``, ``package`` or ``session``.
|
||||
|
||||
The next example puts the fixture function into a separate ``conftest.py`` file
|
||||
so that tests from multiple test modules in the directory can
|
||||
@@ -259,6 +260,22 @@ instance, you can simply declare it:
|
||||
Finally, the ``class`` scope will invoke the fixture once per test *class*.
|
||||
|
||||
|
||||
``package`` scope (experimental)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
.. versionadded:: 3.7
|
||||
|
||||
In pytest 3.7 the ``package`` scope has been introduced. Package-scoped fixtures
|
||||
are finalized when the last test of a *package* finishes.
|
||||
|
||||
.. warning::
|
||||
This functionality is considered **experimental** and may be removed in future
|
||||
versions if hidden corner-cases or serious problems with this functionality
|
||||
are discovered after it gets more usage in the wild.
|
||||
|
||||
Use this new feature sparingly and please make sure to report any issues you find.
|
||||
|
||||
|
||||
Higher-scoped fixtures are instantiated first
|
||||
---------------------------------------------
|
||||
|
||||
@@ -710,7 +727,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``::
|
||||
|
||||
$ pytest test_fixture_marks.py -v
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 3 items
|
||||
@@ -753,7 +770,7 @@ Here we declare an ``app`` fixture which receives the previously defined
|
||||
|
||||
$ pytest -v test_appsetup.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 2 items
|
||||
@@ -822,7 +839,7 @@ Let's run the tests in verbose mode and with looking at the print-output::
|
||||
|
||||
$ pytest -v -s test_module.py
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 8 items
|
||||
|
||||
@@ -7,7 +7,7 @@ pytest-2.3: reasoning for fixture/funcarg evolution
|
||||
|
||||
**Target audience**: Reading this document requires basic knowledge of
|
||||
python testing, xUnit setup methods and the (previous) basic pytest
|
||||
funcarg mechanism, see http://pytest.org/2.2.4/funcargs.html
|
||||
funcarg mechanism, see https://docs.pytest.org/en/latest/historical-notes.html#funcargs-and-pytest-funcarg.
|
||||
If you are new to pytest, then you can simply ignore this
|
||||
section and read the other sections.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Installation and Getting Started
|
||||
===================================
|
||||
|
||||
**Pythons**: Python 2.7, 3.4, 3.5, 3.6, Jython, PyPy-2.3
|
||||
**Pythons**: Python 2.7, 3.4, 3.5, 3.6, 3.7, Jython, PyPy-2.3
|
||||
|
||||
**Platforms**: Unix/Posix and Windows
|
||||
|
||||
@@ -27,7 +27,7 @@ Install ``pytest``
|
||||
2. Check that you installed the correct version::
|
||||
|
||||
$ pytest --version
|
||||
This is pytest version 3.x.y, imported from $PYTHON_PREFIX/lib/python3.5/site-packages/pytest.py
|
||||
This is pytest version 3.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py
|
||||
|
||||
.. _`simpletest`:
|
||||
|
||||
|
||||
@@ -4,6 +4,27 @@
|
||||
Good Integration Practices
|
||||
=================================================
|
||||
|
||||
Install package with pip
|
||||
-------------------------------------------------
|
||||
|
||||
For development, we recommend to use virtualenv_ environments and pip_
|
||||
for installing your application and any dependencies
|
||||
as well as the ``pytest`` package itself. This ensures your code and
|
||||
dependencies are isolated from the system Python installation.
|
||||
|
||||
First you need to place a ``setup.py`` file in the root of your package with the following minimum content::
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(name="PACKAGENAME", packages=find_packages())
|
||||
|
||||
Where ``PACKAGENAME`` is the name of your package. You can then install your package in "editable" mode by running from the same directory::
|
||||
|
||||
pip install -e .
|
||||
|
||||
which lets you change your source code (both tests and application) and rerun tests at will.
|
||||
This is similar to running ``python setup.py develop`` or ``conda develop`` in that it installs
|
||||
your package using a symlink to your development code.
|
||||
|
||||
.. _`test discovery`:
|
||||
.. _`Python test discovery`:
|
||||
@@ -177,19 +198,6 @@ Note that this layout also works in conjunction with the ``src`` layout mentione
|
||||
tox
|
||||
------
|
||||
|
||||
For development, we recommend to use virtualenv_ environments and pip_
|
||||
for installing your application and any dependencies
|
||||
as well as the ``pytest`` package itself. This ensures your code and
|
||||
dependencies are isolated from the system Python installation.
|
||||
|
||||
You can then install your package in "editable" mode::
|
||||
|
||||
pip install -e .
|
||||
|
||||
which lets you change your source code (both tests and application) and rerun tests at will.
|
||||
This is similar to running ``python setup.py develop`` or ``conda develop`` in that it installs
|
||||
your package using a symlink to your development code.
|
||||
|
||||
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 `tox`_, the
|
||||
virtualenv test automation tool and its `pytest support
|
||||
@@ -282,7 +290,7 @@ your own setuptools Test command for invoking pytest.
|
||||
setup(
|
||||
# ...,
|
||||
tests_require=["pytest"],
|
||||
cmdclass={"test": PyTest},
|
||||
cmdclass={"pytest": PyTest},
|
||||
)
|
||||
|
||||
Now if you run::
|
||||
|
||||
@@ -52,6 +52,8 @@ should add ``--strict`` to ``addopts``:
|
||||
serial
|
||||
|
||||
|
||||
.. _marker-revamp:
|
||||
|
||||
Marker revamp and iteration
|
||||
---------------------------
|
||||
|
||||
|
||||
@@ -460,7 +460,7 @@ To use it, include in your top-most ``conftest.py`` file::
|
||||
|
||||
|
||||
.. autoclass:: Testdir()
|
||||
:members: runpytest,runpytest_subprocess,runpytest_inprocess,makeconftest,makepyfile
|
||||
:members:
|
||||
|
||||
.. autoclass:: RunResult()
|
||||
:members:
|
||||
@@ -611,6 +611,8 @@ Session related reporting hooks:
|
||||
.. autofunction:: pytest_terminal_summary
|
||||
.. autofunction:: pytest_fixture_setup
|
||||
.. autofunction:: pytest_fixture_post_finalizer
|
||||
.. autofunction:: pytest_logwarning
|
||||
.. autofunction:: pytest_warning_captured
|
||||
|
||||
And here is the central hook for reporting about
|
||||
test execution:
|
||||
@@ -787,7 +789,7 @@ TestReport
|
||||
_Result
|
||||
~~~~~~~
|
||||
|
||||
.. autoclass:: pluggy._Result
|
||||
.. autoclass:: pluggy.callers._Result
|
||||
:members:
|
||||
|
||||
Special Variables
|
||||
@@ -866,6 +868,11 @@ Contains comma-separated list of modules that should be loaded as plugins:
|
||||
|
||||
export PYTEST_PLUGINS=mymodule.plugin,xdist
|
||||
|
||||
PYTEST_DISABLE_PLUGIN_AUTOLOAD
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When set, disables plugin auto-loading through setuptools entrypoints. Only explicitly specified plugins will be
|
||||
loaded.
|
||||
|
||||
PYTEST_CURRENT_TEST
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
@@ -935,6 +942,7 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
* ``classic``: classic pytest output.
|
||||
* ``progress``: like classic pytest output, but with a progress indicator.
|
||||
* ``count``: like progress, but shows progress as the number of tests completed instead of a percent.
|
||||
|
||||
The default is ``progress``, but you can fallback to ``classic`` if you prefer or
|
||||
the new mode is causing unexpected problems:
|
||||
@@ -1229,7 +1237,8 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
.. confval:: python_classes
|
||||
|
||||
One or more name prefixes or glob-style patterns determining which classes
|
||||
are considered for test collection. By default, pytest will consider any
|
||||
are considered for test collection. Search for multiple glob patterns by
|
||||
adding a space between patterns. By default, pytest will consider any
|
||||
class prefixed with ``Test`` as a test collection. Here is an example of how
|
||||
to collect tests from classes that end in ``Suite``:
|
||||
|
||||
@@ -1246,15 +1255,33 @@ passed multiple times. The expected format is ``name=value``. For example::
|
||||
.. confval:: python_files
|
||||
|
||||
One or more Glob-style file patterns determining which python files
|
||||
are considered as test modules. By default, pytest will consider
|
||||
any file matching with ``test_*.py`` and ``*_test.py`` globs as a test
|
||||
module.
|
||||
are considered as test modules. Search for multiple glob patterns by
|
||||
adding a space between patterns:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
python_files = test_*.py check_*.py example_*.py
|
||||
|
||||
Or one per line:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
python_files =
|
||||
test_*.py
|
||||
check_*.py
|
||||
example_*.py
|
||||
|
||||
By default, files matching ``test_*.py`` and ``*_test.py`` will be considered
|
||||
test modules.
|
||||
|
||||
|
||||
.. confval:: python_functions
|
||||
|
||||
One or more name prefixes or glob-patterns determining which test functions
|
||||
and methods are considered tests. By default, pytest will consider any
|
||||
and methods are considered tests. Search for multiple glob patterns by
|
||||
adding a space between patterns. By default, pytest will consider any
|
||||
function prefixed with ``test`` as a test. Here is an example of how
|
||||
to collect test functions and methods that end in ``_test``:
|
||||
|
||||
|
||||
@@ -136,12 +136,6 @@ You can use the ``skipif`` marker (as any other marker) on classes::
|
||||
If the condition is ``True``, this marker will produce a skip result for
|
||||
each of the test methods of that class.
|
||||
|
||||
.. warning::
|
||||
|
||||
The use of ``skipif`` on classes that use inheritance is strongly
|
||||
discouraged. `A Known bug <https://github.com/pytest-dev/pytest/issues/568>`_
|
||||
in pytest's markers may cause unexpected behavior in super classes.
|
||||
|
||||
If you want to skip all test functions of a module, you may use
|
||||
the ``pytestmark`` name on the global level:
|
||||
|
||||
@@ -283,7 +277,7 @@ on a particular platform::
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If you want to be more specific as to why the test is failing, you can specify
|
||||
a single exception, or a list of exceptions, in the ``raises`` argument.
|
||||
a single exception, or a tuple of exceptions, in the ``raises`` argument.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
||||
@@ -14,6 +14,9 @@ Talks and Tutorials
|
||||
Books
|
||||
---------------------------------------------
|
||||
|
||||
- `pytest Quick Start Guide, by Bruno Oliveira (2018)
|
||||
<https://www.packtpub.com/web-development/pytest-quick-start-guide>`_.
|
||||
|
||||
- `Python Testing with pytest, by Brian Okken (2017)
|
||||
<https://pragprog.com/book/bopytest/python-testing-with-pytest>`_.
|
||||
|
||||
|
||||
@@ -140,6 +140,49 @@ will be shown (because KeyboardInterrupt is caught by pytest). By using this
|
||||
option you make sure a trace is shown.
|
||||
|
||||
|
||||
.. _`pytest.detailed_failed_tests_usage`:
|
||||
|
||||
Detailed summary report
|
||||
-----------------------
|
||||
|
||||
.. versionadded:: 2.9
|
||||
|
||||
The ``-r`` flag can be used to display test results summary at the end of the test session,
|
||||
making it easy in large test suites to get a clear picture of all failures, skips, xfails, etc.
|
||||
|
||||
Example::
|
||||
|
||||
$ pytest -ra
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 0 items
|
||||
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes".
|
||||
|
||||
Here is the full list of available characters that can be used:
|
||||
|
||||
- ``f`` - failed
|
||||
- ``E`` - error
|
||||
- ``s`` - skipped
|
||||
- ``x`` - xfailed
|
||||
- ``X`` - xpassed
|
||||
- ``p`` - passed
|
||||
- ``P`` - passed with output
|
||||
- ``a`` - all except ``pP``
|
||||
|
||||
More than one character can be used, so for example to only see failed and skipped tests, you can execute::
|
||||
|
||||
$ pytest -rfs
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 0 items
|
||||
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
.. _pdb-option:
|
||||
|
||||
Dropping to PDB_ (Python Debugger) on failures
|
||||
@@ -171,6 +214,18 @@ for example::
|
||||
>>> sys.last_value
|
||||
AssertionError('assert result == "ok"',)
|
||||
|
||||
.. _trace-option:
|
||||
|
||||
Dropping to PDB_ (Python Debugger) at the start of a test
|
||||
----------------------------------------------------------
|
||||
|
||||
|
||||
``pytest`` allows one to drop into the PDB_ prompt immediately at the start of each test via a command line option::
|
||||
|
||||
pytest --trace
|
||||
|
||||
This will invoke the Python debugger at the start of every test.
|
||||
|
||||
.. _breakpoints:
|
||||
|
||||
Setting breakpoints
|
||||
@@ -200,7 +255,7 @@ Pytest supports the use of ``breakpoint()`` with the following behaviours:
|
||||
|
||||
- When ``breakpoint()`` is called and ``PYTHONBREAKPOINT`` is set to the default value, pytest will use the custom internal PDB trace UI instead of the system default ``Pdb``.
|
||||
- When tests are complete, the system will default back to the system ``Pdb`` trace UI.
|
||||
- If ``--pdb`` is called on execution of pytest, the custom internal Pdb trace UI is used on ``bothbreakpoint()`` and failed tests/unhandled exceptions.
|
||||
- If ``--pdb`` is called on execution of pytest, the custom internal Pdb trace UI is used on both ``breakpoint()`` and failed tests/unhandled exceptions.
|
||||
- If ``--pdbcls`` is used, the custom class debugger will be executed when a test fails (as expected within existing behaviour), but also when ``breakpoint()`` is called from within a test, the custom class debugger will be instantiated.
|
||||
|
||||
.. _durations:
|
||||
|
||||
@@ -29,15 +29,12 @@ Running pytest now produces this output::
|
||||
test_show_warnings.py . [100%]
|
||||
|
||||
============================= warnings summary =============================
|
||||
test_show_warnings.py::test_one
|
||||
$REGENDOC_TMPDIR/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2
|
||||
warnings.warn(UserWarning("api v1, should use functions from v2"))
|
||||
$REGENDOC_TMPDIR/test_show_warnings.py:4: UserWarning: api v1, should use functions from v2
|
||||
warnings.warn(UserWarning("api v1, should use functions from v2"))
|
||||
|
||||
-- Docs: http://doc.pytest.org/en/latest/warnings.html
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
=================== 1 passed, 1 warnings in 0.12 seconds ===================
|
||||
|
||||
Pytest by default catches all warnings except for ``DeprecationWarning`` and ``PendingDeprecationWarning``.
|
||||
|
||||
The ``-W`` flag can be passed to control which warnings will be displayed or even turn
|
||||
them into errors::
|
||||
|
||||
@@ -78,6 +75,53 @@ Both ``-W`` command-line option and ``filterwarnings`` ini option are based on P
|
||||
`-W option`_ and `warnings.simplefilter`_, so please refer to those sections in the Python
|
||||
documentation for other examples and advanced usage.
|
||||
|
||||
Disabling warning summary
|
||||
-------------------------
|
||||
|
||||
Although not recommended, you can use the ``--disable-warnings`` command-line option to suppress the
|
||||
warning summary entirely from the test run output.
|
||||
|
||||
Disabling warning capture entirely
|
||||
----------------------------------
|
||||
|
||||
This plugin is enabled by default but can be disabled entirely in your ``pytest.ini`` file with:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
addopts = -p no:warnings
|
||||
|
||||
Or passing ``-p no:warnings`` in the command-line. This might be useful if your test suites handles warnings
|
||||
using an external system.
|
||||
|
||||
|
||||
.. _`deprecation-warnings`:
|
||||
|
||||
DeprecationWarning and PendingDeprecationWarning
|
||||
------------------------------------------------
|
||||
|
||||
.. versionadded:: 3.8
|
||||
|
||||
By default pytest will display ``DeprecationWarning`` and ``PendingDeprecationWarning`` if no other warning filters
|
||||
are configured.
|
||||
|
||||
To disable showing ``DeprecationWarning`` and ``PendingDeprecationWarning`` warnings, you might define any warnings
|
||||
filter either in the command-line or in the ini file, or you can use:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
ignore::DeprecationWarning
|
||||
ignore::PendingDeprecationWarning
|
||||
|
||||
.. note::
|
||||
This makes pytest more compliant with `PEP-0506 <https://www.python.org/dev/peps/pep-0565/#recommended-filter-settings-for-test-runners>`_ which suggests that those warnings should
|
||||
be shown by default by test runners, but pytest doesn't follow ``PEP-0506`` completely because resetting all
|
||||
warning filters like suggested in the PEP will break existing test suites that configure warning filters themselves
|
||||
by calling ``warnings.simplefilter`` (see issue `#2430 <https://github.com/pytest-dev/pytest/issues/2430>`_
|
||||
for an example of that).
|
||||
|
||||
|
||||
.. _`filterwarnings`:
|
||||
|
||||
@@ -144,18 +188,6 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable:
|
||||
.. _`pytest-warnings`: https://github.com/fschulze/pytest-warnings
|
||||
|
||||
|
||||
Disabling warning capture
|
||||
-------------------------
|
||||
|
||||
This feature is enabled by default but can be disabled entirely in your ``pytest.ini`` file with:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
addopts = -p no:warnings
|
||||
|
||||
Or passing ``-p no:warnings`` in the command-line.
|
||||
|
||||
.. _`asserting warnings`:
|
||||
|
||||
.. _assertwarnings:
|
||||
@@ -296,3 +328,53 @@ You can also use it as a contextmanager::
|
||||
def test_global():
|
||||
with pytest.deprecated_call():
|
||||
myobject.deprecated_method()
|
||||
|
||||
|
||||
|
||||
.. _internal-warnings:
|
||||
|
||||
Internal pytest warnings
|
||||
------------------------
|
||||
|
||||
.. versionadded:: 3.8
|
||||
|
||||
pytest may generate its own warnings in some situations, such as improper usage or deprecated features.
|
||||
|
||||
For example, pytest will emit a warning if it encounters a class that matches :confval:`python_classes` but also
|
||||
defines an ``__init__`` constructor, as this prevents the class from being instantiated:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_pytest_warnings.py
|
||||
class Test:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def test_foo(self):
|
||||
assert 1 == 1
|
||||
|
||||
::
|
||||
|
||||
$ pytest test_pytest_warnings.py -q
|
||||
|
||||
============================= warnings summary =============================
|
||||
$REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestWarning: cannot collect test class 'Test' because it has a __init__ constructor
|
||||
class Test:
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
1 warnings in 0.12 seconds
|
||||
|
||||
These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings.
|
||||
|
||||
Please read our :ref:`backwards-compatibility` to learn how we proceed about deprecating and eventually removing
|
||||
features.
|
||||
|
||||
The following warning types ares used by pytest and are part of the public API:
|
||||
|
||||
.. autoclass:: pytest.PytestWarning
|
||||
|
||||
.. autoclass:: pytest.PytestDeprecationWarning
|
||||
|
||||
.. autoclass:: pytest.RemovedInPytest4Warning
|
||||
|
||||
.. autoclass:: pytest.PytestExperimentalApiWarning
|
||||
|
||||
@@ -386,11 +386,51 @@ return a result object, with which we can assert the tests' outcomes.
|
||||
result.assert_outcomes(passed=4)
|
||||
|
||||
|
||||
additionally it is possible to copy examples for a example folder before running pytest on it
|
||||
|
||||
.. code:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
pytester_example_dir = .
|
||||
|
||||
|
||||
.. code:: python
|
||||
|
||||
# content of test_example.py
|
||||
|
||||
|
||||
def test_plugin(testdir):
|
||||
testdir.copy_example("test_example.py")
|
||||
testdir.runpytest("-k", "test_example")
|
||||
|
||||
def test_example():
|
||||
pass
|
||||
|
||||
.. code::
|
||||
|
||||
$ pytest
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini
|
||||
collected 2 items
|
||||
|
||||
test_example.py .. [100%]
|
||||
|
||||
============================= warnings summary =============================
|
||||
$REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time
|
||||
testdir.copy_example("test_example.py")
|
||||
|
||||
-- Docs: https://docs.pytest.org/en/latest/warnings.html
|
||||
=================== 2 passed, 1 warnings in 0.12 seconds ===================
|
||||
|
||||
For more information about the result object that ``runpytest()`` returns, and
|
||||
the methods that it provides please check out the :py:class:`RunResult
|
||||
<_pytest.pytester.RunResult>` documentation.
|
||||
|
||||
|
||||
|
||||
|
||||
.. _`writinghooks`:
|
||||
|
||||
Writing hook functions
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
REM skip "coveralls" run in PRs or forks
|
||||
if "%TOXENV%" == "coveralls" (
|
||||
if not defined COVERALLS_REPO_TOKEN (
|
||||
echo skipping coveralls run because COVERALLS_REPO_TOKEN is not defined
|
||||
exit /b 0
|
||||
)
|
||||
)
|
||||
C:\Python36\python -m tox
|
||||
10
scripts/prepare-coverage.bat
Normal file
10
scripts/prepare-coverage.bat
Normal file
@@ -0,0 +1,10 @@
|
||||
REM scripts called by AppVeyor to setup the environment variables to enable coverage
|
||||
if not defined PYTEST_NO_COVERAGE (
|
||||
set "COVERAGE_FILE=%CD%\.coverage"
|
||||
set "COVERAGE_PROCESS_START=%CD%\.coveragerc"
|
||||
set "_PYTEST_TOX_COVERAGE_RUN=coverage run -m"
|
||||
set "_PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess"
|
||||
echo Coverage setup completed
|
||||
) else (
|
||||
echo Skipping coverage setup, PYTEST_NO_COVERAGE is set
|
||||
)
|
||||
@@ -9,11 +9,11 @@ against itself, passing on many different interpreters and platforms.
|
||||
This release contains a number of bugs fixes and improvements, so users are encouraged
|
||||
to take a look at the CHANGELOG:
|
||||
|
||||
http://doc.pytest.org/en/latest/changelog.html
|
||||
https://docs.pytest.org/en/latest/changelog.html
|
||||
|
||||
For complete documentation, please visit:
|
||||
|
||||
http://docs.pytest.org
|
||||
https://docs.pytest.org/en/latest/
|
||||
|
||||
As usual, you can upgrade from pypi via:
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
The full changelog is available at https://docs.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
|
||||
11
scripts/upload-coverage.bat
Normal file
11
scripts/upload-coverage.bat
Normal file
@@ -0,0 +1,11 @@
|
||||
REM script called by AppVeyor to combine and upload coverage information to codecov
|
||||
if not defined PYTEST_NO_COVERAGE (
|
||||
echo Prepare to upload coverage information
|
||||
C:\Python36\Scripts\pip install codecov
|
||||
C:\Python36\Scripts\coverage combine
|
||||
C:\Python36\Scripts\coverage xml --ignore-errors
|
||||
C:\Python36\Scripts\coverage report -m --ignore-errors
|
||||
C:\Python36\Scripts\codecov --required -X gcov pycov search -f coverage.xml --flags %TOXENV:-= % windows
|
||||
) else (
|
||||
echo Skipping coverage upload, PYTEST_NO_COVERAGE is set
|
||||
)
|
||||
10
setup.py
10
setup.py
@@ -59,7 +59,7 @@ def get_environment_marker_support_level():
|
||||
def main():
|
||||
extras_require = {}
|
||||
install_requires = [
|
||||
"py>=1.5.0",
|
||||
"py>=1.5.0", # if py gets upgrade to >=1.6, remove _width_of_current_line in terminal.py
|
||||
"six>=1.10.0",
|
||||
"setuptools",
|
||||
"attrs>=17.4.0",
|
||||
@@ -69,26 +69,30 @@ def main():
|
||||
# if _PYTEST_SETUP_SKIP_PLUGGY_DEP is set, skip installing pluggy;
|
||||
# used by tox.ini to test with pluggy master
|
||||
if "_PYTEST_SETUP_SKIP_PLUGGY_DEP" not in os.environ:
|
||||
install_requires.append("pluggy>=0.5,<0.8")
|
||||
install_requires.append("pluggy>=0.7")
|
||||
environment_marker_support_level = get_environment_marker_support_level()
|
||||
if environment_marker_support_level >= 2:
|
||||
install_requires.append('funcsigs;python_version<"3.0"')
|
||||
install_requires.append('pathlib2>=2.2.0;python_version<"3.6"')
|
||||
install_requires.append('colorama;sys_platform=="win32"')
|
||||
elif environment_marker_support_level == 1:
|
||||
extras_require[':python_version<"3.0"'] = ["funcsigs"]
|
||||
extras_require[':python_version<"3.6"'] = ["pathlib2>=2.2.0"]
|
||||
extras_require[':sys_platform=="win32"'] = ["colorama"]
|
||||
else:
|
||||
if sys.platform == "win32":
|
||||
install_requires.append("colorama")
|
||||
if sys.version_info < (3, 0):
|
||||
install_requires.append("funcsigs")
|
||||
if sys.version_info < (3, 6):
|
||||
install_requires.append("pathlib2>=2.2.0")
|
||||
|
||||
setup(
|
||||
name="pytest",
|
||||
description="pytest: simple powerful testing with Python",
|
||||
long_description=long_description,
|
||||
use_scm_version={"write_to": "src/_pytest/_version.py"},
|
||||
url="http://pytest.org",
|
||||
url="https://docs.pytest.org/en/latest/",
|
||||
project_urls={
|
||||
"Source": "https://github.com/pytest-dev/pytest",
|
||||
"Tracker": "https://github.com/pytest-dev/pytest/issues",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# CHANGES:
|
||||
# - some_str is replaced, trying to create unicode strings
|
||||
#
|
||||
from __future__ import absolute_import, division, print_function
|
||||
from __future__ import absolute_import, division, print_function, unicode_literals
|
||||
import types
|
||||
from six import text_type
|
||||
|
||||
@@ -51,17 +51,17 @@ def format_exception_only(etype, value):
|
||||
pass
|
||||
else:
|
||||
filename = filename or "<string>"
|
||||
lines.append(' File "%s", line %d\n' % (filename, lineno))
|
||||
lines.append(' File "{}", line {}\n'.format(filename, lineno))
|
||||
if badline is not None:
|
||||
if isinstance(badline, bytes): # python 2 only
|
||||
badline = badline.decode("utf-8", "replace")
|
||||
lines.append(u" %s\n" % badline.strip())
|
||||
lines.append(" {}\n".format(badline.strip()))
|
||||
if offset is not None:
|
||||
caretspace = badline.rstrip("\n")[:offset].lstrip()
|
||||
# non-space whitespace (likes tabs) must be kept for alignment
|
||||
caretspace = ((c.isspace() and c or " ") for c in caretspace)
|
||||
# only three spaces to account for offset1 == pos 0
|
||||
lines.append(" %s^\n" % "".join(caretspace))
|
||||
lines.append(" {}^\n".format("".join(caretspace)))
|
||||
value = msg
|
||||
|
||||
lines.append(_format_final_exc_line(stype, value))
|
||||
@@ -72,9 +72,9 @@ def _format_final_exc_line(etype, value):
|
||||
"""Return a list of a single line -- normal case for format_exception_only"""
|
||||
valuestr = _some_str(value)
|
||||
if value is None or not valuestr:
|
||||
line = "%s\n" % etype
|
||||
line = "{}\n".format(etype)
|
||||
else:
|
||||
line = "%s: %s\n" % (etype, valuestr)
|
||||
line = "{}: {}\n".format(etype, valuestr)
|
||||
return line
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ def _some_str(value):
|
||||
return text_type(value)
|
||||
except Exception:
|
||||
try:
|
||||
return str(value)
|
||||
return bytes(value).decode("UTF-8", "replace")
|
||||
except Exception:
|
||||
pass
|
||||
return "<unprintable %s object>" % type(value).__name__
|
||||
return "<unprintable {} object>".format(type(value).__name__)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import inspect
|
||||
import pprint
|
||||
import sys
|
||||
import traceback
|
||||
from inspect import CO_VARARGS, CO_VARKEYWORDS
|
||||
@@ -10,6 +11,7 @@ from weakref import ref
|
||||
from _pytest.compat import _PY2, _PY3, PY35, safe_str
|
||||
from six import text_type
|
||||
import py
|
||||
import six
|
||||
|
||||
builtin_repr = repr
|
||||
|
||||
@@ -127,7 +129,7 @@ class Frame(object):
|
||||
"""
|
||||
f_locals = self.f_locals.copy()
|
||||
f_locals.update(vars)
|
||||
py.builtin.exec_(code, self.f_globals, f_locals)
|
||||
six.exec_(code, self.f_globals, f_locals)
|
||||
|
||||
def repr(self, object):
|
||||
""" return a 'safe' (non-recursive, one-line) string repr for 'object'
|
||||
@@ -448,6 +450,7 @@ class ExceptionInfo(object):
|
||||
abspath=False,
|
||||
tbfilter=True,
|
||||
funcargs=False,
|
||||
truncate_locals=True,
|
||||
):
|
||||
""" return str()able representation of this exception info.
|
||||
showlocals: show locals per traceback entry
|
||||
@@ -472,6 +475,7 @@ class ExceptionInfo(object):
|
||||
abspath=abspath,
|
||||
tbfilter=tbfilter,
|
||||
funcargs=funcargs,
|
||||
truncate_locals=truncate_locals,
|
||||
)
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
@@ -511,6 +515,7 @@ class FormattedExcinfo(object):
|
||||
abspath = attr.ib(default=True)
|
||||
tbfilter = attr.ib(default=True)
|
||||
funcargs = attr.ib(default=False)
|
||||
truncate_locals = attr.ib(default=True)
|
||||
astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False)
|
||||
|
||||
def _getindent(self, source):
|
||||
@@ -593,7 +598,10 @@ class FormattedExcinfo(object):
|
||||
# This formatting could all be handled by the
|
||||
# _repr() function, which is only reprlib.Repr in
|
||||
# disguise, so is very configurable.
|
||||
str_repr = self._saferepr(value)
|
||||
if self.truncate_locals:
|
||||
str_repr = self._saferepr(value)
|
||||
else:
|
||||
str_repr = pprint.pformat(value)
|
||||
# if len(str_repr) < 70 or not isinstance(value,
|
||||
# (list, tuple, dict)):
|
||||
lines.append("%-10s = %s" % (name, str_repr))
|
||||
@@ -712,7 +720,9 @@ class FormattedExcinfo(object):
|
||||
repr_chain = []
|
||||
e = excinfo.value
|
||||
descr = None
|
||||
while e is not None:
|
||||
seen = set()
|
||||
while e is not None and id(e) not in seen:
|
||||
seen.add(id(e))
|
||||
if excinfo:
|
||||
reprtraceback = self.repr_traceback(excinfo)
|
||||
reprcrash = excinfo._getreprcrash()
|
||||
|
||||
@@ -8,6 +8,7 @@ import marshal
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import string
|
||||
import struct
|
||||
import sys
|
||||
import types
|
||||
@@ -16,7 +17,8 @@ import atomicwrites
|
||||
import py
|
||||
|
||||
from _pytest.assertion import util
|
||||
|
||||
from _pytest.compat import PurePath, spec_from_file_location
|
||||
from _pytest.paths import fnmatch_ex
|
||||
|
||||
# pytest caches rewritten pycs in __pycache__.
|
||||
if hasattr(imp, "get_tag"):
|
||||
@@ -45,14 +47,6 @@ else:
|
||||
return ast.Call(a, b, c, None, None)
|
||||
|
||||
|
||||
if sys.version_info >= (3, 4):
|
||||
from importlib.util import spec_from_file_location
|
||||
else:
|
||||
|
||||
def spec_from_file_location(*_, **__):
|
||||
return None
|
||||
|
||||
|
||||
class AssertionRewritingHook(object):
|
||||
"""PEP302 Import hook which rewrites asserts."""
|
||||
|
||||
@@ -64,12 +58,27 @@ class AssertionRewritingHook(object):
|
||||
self._rewritten_names = set()
|
||||
self._register_with_pkg_resources()
|
||||
self._must_rewrite = set()
|
||||
# flag to guard against trying to rewrite a pyc file while we are already writing another pyc file,
|
||||
# which might result in infinite recursion (#3506)
|
||||
self._writing_pyc = False
|
||||
self._basenames_to_check_rewrite = {"conftest"}
|
||||
self._marked_for_rewrite_cache = {}
|
||||
self._session_paths_checked = False
|
||||
|
||||
def set_session(self, session):
|
||||
self.session = session
|
||||
self._session_paths_checked = False
|
||||
|
||||
def _imp_find_module(self, name, path=None):
|
||||
"""Indirection so we can mock calls to find_module originated from the hook during testing"""
|
||||
return imp.find_module(name, path)
|
||||
|
||||
def find_module(self, name, path=None):
|
||||
if self._writing_pyc:
|
||||
return None
|
||||
state = self.config._assertstate
|
||||
if self._early_rewrite_bailout(name, state):
|
||||
return None
|
||||
state.trace("find_module called for: %s" % name)
|
||||
names = name.rsplit(".", 1)
|
||||
lastname = names[-1]
|
||||
@@ -82,7 +91,7 @@ class AssertionRewritingHook(object):
|
||||
pth = path[0]
|
||||
if pth is None:
|
||||
try:
|
||||
fd, fn, desc = imp.find_module(lastname, path)
|
||||
fd, fn, desc = self._imp_find_module(lastname, path)
|
||||
except ImportError:
|
||||
return None
|
||||
if fd is not None:
|
||||
@@ -151,12 +160,54 @@ class AssertionRewritingHook(object):
|
||||
# Probably a SyntaxError in the test.
|
||||
return None
|
||||
if write:
|
||||
_write_pyc(state, co, source_stat, pyc)
|
||||
self._writing_pyc = True
|
||||
try:
|
||||
_write_pyc(state, co, source_stat, pyc)
|
||||
finally:
|
||||
self._writing_pyc = False
|
||||
else:
|
||||
state.trace("found cached rewritten pyc for %r" % (fn,))
|
||||
self.modules[name] = co, pyc
|
||||
return self
|
||||
|
||||
def _early_rewrite_bailout(self, name, state):
|
||||
"""
|
||||
This is a fast way to get out of rewriting modules. Profiling has
|
||||
shown that the call to imp.find_module (inside of the find_module
|
||||
from this class) is a major slowdown, so, this method tries to
|
||||
filter what we're sure won't be rewritten before getting to it.
|
||||
"""
|
||||
if self.session is not None and not self._session_paths_checked:
|
||||
self._session_paths_checked = True
|
||||
for path in self.session._initialpaths:
|
||||
# Make something as c:/projects/my_project/path.py ->
|
||||
# ['c:', 'projects', 'my_project', 'path.py']
|
||||
parts = str(path).split(os.path.sep)
|
||||
# add 'path' to basenames to be checked.
|
||||
self._basenames_to_check_rewrite.add(os.path.splitext(parts[-1])[0])
|
||||
|
||||
# Note: conftest already by default in _basenames_to_check_rewrite.
|
||||
parts = name.split(".")
|
||||
if parts[-1] in self._basenames_to_check_rewrite:
|
||||
return False
|
||||
|
||||
# For matching the name it must be as if it was a filename.
|
||||
path = PurePath(os.path.sep.join(parts) + ".py")
|
||||
|
||||
for pat in self.fnpats:
|
||||
# if the pattern contains subdirectories ("tests/**.py" for example) we can't bail out based
|
||||
# on the name alone because we need to match against the full path
|
||||
if os.path.dirname(pat):
|
||||
return False
|
||||
if fnmatch_ex(pat, path):
|
||||
return False
|
||||
|
||||
if self._is_marked_for_rewrite(name, state):
|
||||
return False
|
||||
|
||||
state.trace("early skip of rewriting module: %s" % (name,))
|
||||
return True
|
||||
|
||||
def _should_rewrite(self, name, fn_pypath, state):
|
||||
# always rewrite conftest files
|
||||
fn = str(fn_pypath)
|
||||
@@ -176,12 +227,20 @@ class AssertionRewritingHook(object):
|
||||
state.trace("matched test file %r" % (fn,))
|
||||
return True
|
||||
|
||||
for marked in self._must_rewrite:
|
||||
if name == marked or name.startswith(marked + "."):
|
||||
state.trace("matched marked file %r (from %r)" % (name, marked))
|
||||
return True
|
||||
return self._is_marked_for_rewrite(name, state)
|
||||
|
||||
return False
|
||||
def _is_marked_for_rewrite(self, name, state):
|
||||
try:
|
||||
return self._marked_for_rewrite_cache[name]
|
||||
except KeyError:
|
||||
for marked in self._must_rewrite:
|
||||
if name == marked or name.startswith(marked + "."):
|
||||
state.trace("matched marked file %r (from %r)" % (name, marked))
|
||||
self._marked_for_rewrite_cache[name] = True
|
||||
return True
|
||||
|
||||
self._marked_for_rewrite_cache[name] = False
|
||||
return False
|
||||
|
||||
def mark_rewrite(self, *names):
|
||||
"""Mark import names as needing to be rewritten.
|
||||
@@ -198,24 +257,29 @@ class AssertionRewritingHook(object):
|
||||
):
|
||||
self._warn_already_imported(name)
|
||||
self._must_rewrite.update(names)
|
||||
self._marked_for_rewrite_cache.clear()
|
||||
|
||||
def _warn_already_imported(self, name):
|
||||
self.config.warn(
|
||||
"P1", "Module already imported so cannot be rewritten: %s" % name
|
||||
from _pytest.warning_types import PytestWarning
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
||||
_issue_config_warning(
|
||||
PytestWarning("Module already imported so cannot be rewritten: %s" % name),
|
||||
self.config,
|
||||
)
|
||||
|
||||
def load_module(self, name):
|
||||
# If there is an existing module object named 'fullname' in
|
||||
# sys.modules, the loader must use that existing module. (Otherwise,
|
||||
# the reload() builtin will not work correctly.)
|
||||
if name in sys.modules:
|
||||
return sys.modules[name]
|
||||
|
||||
co, pyc = self.modules.pop(name)
|
||||
# I wish I could just call imp.load_compiled here, but __file__ has to
|
||||
# be set properly. In Python 3.2+, this all would be handled correctly
|
||||
# by load_compiled.
|
||||
mod = sys.modules[name] = imp.new_module(name)
|
||||
if name in sys.modules:
|
||||
# If there is an existing module object named 'fullname' in
|
||||
# sys.modules, the loader must use that existing module. (Otherwise,
|
||||
# the reload() builtin will not work correctly.)
|
||||
mod = sys.modules[name]
|
||||
else:
|
||||
# I wish I could just call imp.load_compiled here, but __file__ has to
|
||||
# be set properly. In Python 3.2+, this all would be handled correctly
|
||||
# by load_compiled.
|
||||
mod = sys.modules[name] = imp.new_module(name)
|
||||
try:
|
||||
mod.__file__ = co.co_filename
|
||||
# Normally, this attribute is 3.2+.
|
||||
@@ -223,7 +287,7 @@ class AssertionRewritingHook(object):
|
||||
mod.__loader__ = self
|
||||
# Normally, this attribute is 3.4+
|
||||
mod.__spec__ = spec_from_file_location(name, co.co_filename, loader=self)
|
||||
py.builtin.exec_(co, mod.__dict__)
|
||||
six.exec_(co, mod.__dict__)
|
||||
except: # noqa
|
||||
if name in sys.modules:
|
||||
del sys.modules[name]
|
||||
@@ -232,7 +296,7 @@ class AssertionRewritingHook(object):
|
||||
|
||||
def is_package(self, name):
|
||||
try:
|
||||
fd, fn, desc = imp.find_module(name)
|
||||
fd, fn, desc = self._imp_find_module(name)
|
||||
except ImportError:
|
||||
return False
|
||||
if fd is not None:
|
||||
@@ -402,12 +466,15 @@ def _saferepr(obj):
|
||||
JSON reprs.
|
||||
|
||||
"""
|
||||
repr = py.io.saferepr(obj)
|
||||
if isinstance(repr, six.text_type):
|
||||
t = six.text_type
|
||||
else:
|
||||
t = six.binary_type
|
||||
return repr.replace(t("\n"), t("\\n"))
|
||||
r = py.io.saferepr(obj)
|
||||
# only occurs in python2.x, repr must return text in python3+
|
||||
if isinstance(r, bytes):
|
||||
# Represent unprintable bytes as `\x##`
|
||||
r = u"".join(
|
||||
u"\\x{:x}".format(ord(c)) if c not in string.printable else c.decode()
|
||||
for c in r
|
||||
)
|
||||
return r.replace(u"\n", u"\\n")
|
||||
|
||||
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation # noqa
|
||||
@@ -425,20 +492,18 @@ def _format_assertmsg(obj):
|
||||
# contains a newline it gets escaped, however if an object has a
|
||||
# .__repr__() which contains newlines it does not get escaped.
|
||||
# However in either case we want to preserve the newline.
|
||||
if isinstance(obj, six.text_type) or isinstance(obj, six.binary_type):
|
||||
s = obj
|
||||
is_repr = False
|
||||
else:
|
||||
s = py.io.saferepr(obj)
|
||||
is_repr = True
|
||||
if isinstance(s, six.text_type):
|
||||
t = six.text_type
|
||||
else:
|
||||
t = six.binary_type
|
||||
s = s.replace(t("\n"), t("\n~")).replace(t("%"), t("%%"))
|
||||
if is_repr:
|
||||
s = s.replace(t("\\n"), t("\n~"))
|
||||
return s
|
||||
replaces = [(u"\n", u"\n~"), (u"%", u"%%")]
|
||||
if not isinstance(obj, six.string_types):
|
||||
obj = py.io.saferepr(obj)
|
||||
replaces.append((u"\\n", u"\n~"))
|
||||
|
||||
if isinstance(obj, bytes):
|
||||
replaces = [(r1.encode(), r2.encode()) for r1, r2 in replaces]
|
||||
|
||||
for r1, r2 in replaces:
|
||||
obj = obj.replace(r1, r2)
|
||||
|
||||
return obj
|
||||
|
||||
|
||||
def _should_repr_global_name(obj):
|
||||
@@ -448,10 +513,9 @@ def _should_repr_global_name(obj):
|
||||
def _format_boolop(explanations, is_or):
|
||||
explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
|
||||
if isinstance(explanation, six.text_type):
|
||||
t = six.text_type
|
||||
return explanation.replace(u"%", u"%%")
|
||||
else:
|
||||
t = six.binary_type
|
||||
return explanation.replace(t("%"), t("%%"))
|
||||
return explanation.replace(b"%", b"%%")
|
||||
|
||||
|
||||
def _call_reprcompare(ops, results, expls, each_obj):
|
||||
@@ -741,13 +805,17 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
the expression is false.
|
||||
|
||||
"""
|
||||
if isinstance(assert_.test, ast.Tuple) and self.config is not None:
|
||||
fslocation = (self.module_path, assert_.lineno)
|
||||
self.config.warn(
|
||||
"R1",
|
||||
"assertion is always true, perhaps " "remove parentheses?",
|
||||
fslocation=fslocation,
|
||||
if isinstance(assert_.test, ast.Tuple) and len(assert_.test.elts) >= 1:
|
||||
from _pytest.warning_types import PytestWarning
|
||||
import warnings
|
||||
|
||||
warnings.warn_explicit(
|
||||
PytestWarning("assertion is always true, perhaps remove parentheses?"),
|
||||
category=None,
|
||||
filename=str(self.module_path),
|
||||
lineno=assert_.lineno,
|
||||
)
|
||||
|
||||
self.statements = []
|
||||
self.variables = []
|
||||
self.variable_counter = itertools.count()
|
||||
|
||||
@@ -187,9 +187,9 @@ def _diff_text(left, right, verbose=False):
|
||||
r = r.replace(r"\r", "\r")
|
||||
return r
|
||||
|
||||
if isinstance(left, six.binary_type):
|
||||
if isinstance(left, bytes):
|
||||
left = escape_for_readable_diff(left)
|
||||
if isinstance(right, six.binary_type):
|
||||
if isinstance(right, bytes):
|
||||
right = escape_for_readable_diff(right)
|
||||
if not verbose:
|
||||
i = 0 # just in case left or right has zero length
|
||||
|
||||
@@ -5,38 +5,55 @@ the name cache was not chosen to ensure pluggy automatically
|
||||
ignores the external pytest-cache
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import py
|
||||
import six
|
||||
import attr
|
||||
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
from os.path import sep as _sep, altsep as _altsep
|
||||
import shutil
|
||||
|
||||
from . import paths
|
||||
from .compat import _PY2 as PY2, Path
|
||||
|
||||
README_CONTENT = u"""\
|
||||
# pytest cache directory #
|
||||
|
||||
This directory contains data from the pytest's cache plugin,
|
||||
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
|
||||
|
||||
**Do not** commit this to version control.
|
||||
|
||||
See [the docs](https://docs.pytest.org/en/latest/cache.html) for more information.
|
||||
"""
|
||||
|
||||
|
||||
@attr.s
|
||||
class Cache(object):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self._cachedir = Cache.cache_dir_from_config(config)
|
||||
self.trace = config.trace.root.get("cache")
|
||||
if config.getoption("cacheclear"):
|
||||
self.trace("clearing cachedir")
|
||||
if self._cachedir.check():
|
||||
self._cachedir.remove()
|
||||
self._cachedir.mkdir()
|
||||
_cachedir = attr.ib(repr=False)
|
||||
_config = attr.ib(repr=False)
|
||||
|
||||
@classmethod
|
||||
def for_config(cls, config):
|
||||
cachedir = cls.cache_dir_from_config(config)
|
||||
if config.getoption("cacheclear") and cachedir.exists():
|
||||
shutil.rmtree(str(cachedir))
|
||||
cachedir.mkdir()
|
||||
return cls(cachedir, config)
|
||||
|
||||
@staticmethod
|
||||
def cache_dir_from_config(config):
|
||||
cache_dir = config.getini("cache_dir")
|
||||
cache_dir = os.path.expanduser(cache_dir)
|
||||
cache_dir = os.path.expandvars(cache_dir)
|
||||
if os.path.isabs(cache_dir):
|
||||
return py.path.local(cache_dir)
|
||||
else:
|
||||
return config.rootdir.join(cache_dir)
|
||||
return paths.resolve_from_str(config.getini("cache_dir"), config.rootdir)
|
||||
|
||||
def warn(self, fmt, **args):
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
_issue_config_warning(
|
||||
PytestWarning(fmt.format(**args) if args else fmt), self._config
|
||||
)
|
||||
|
||||
def makedir(self, name):
|
||||
""" return a directory path object with the given name. If the
|
||||
@@ -48,12 +65,15 @@ class Cache(object):
|
||||
Make sure the name contains your plugin or application
|
||||
identifiers to prevent clashes with other cache users.
|
||||
"""
|
||||
if _sep in name or _altsep is not None and _altsep in name:
|
||||
name = Path(name)
|
||||
if len(name.parts) > 1:
|
||||
raise ValueError("name is not allowed to contain path separators")
|
||||
return self._cachedir.ensure_dir("d", name)
|
||||
res = self._cachedir.joinpath("d", name)
|
||||
res.mkdir(exist_ok=True, parents=True)
|
||||
return py.path.local(res)
|
||||
|
||||
def _getvaluepath(self, key):
|
||||
return self._cachedir.join("v", *key.split("/"))
|
||||
return self._cachedir.joinpath("v", Path(key))
|
||||
|
||||
def get(self, key, default):
|
||||
""" return cached value for the given key. If no value
|
||||
@@ -67,13 +87,11 @@ class Cache(object):
|
||||
|
||||
"""
|
||||
path = self._getvaluepath(key)
|
||||
if path.check():
|
||||
try:
|
||||
with path.open("r") as f:
|
||||
return json.load(f)
|
||||
except ValueError:
|
||||
self.trace("cache-invalid at %s" % (path,))
|
||||
return default
|
||||
try:
|
||||
with path.open("r") as f:
|
||||
return json.load(f)
|
||||
except (ValueError, IOError, OSError):
|
||||
return default
|
||||
|
||||
def set(self, key, value):
|
||||
""" save value for the given key.
|
||||
@@ -86,22 +104,28 @@ class Cache(object):
|
||||
"""
|
||||
path = self._getvaluepath(key)
|
||||
try:
|
||||
path.dirpath().ensure_dir()
|
||||
except (py.error.EEXIST, py.error.EACCES):
|
||||
self.config.warn(
|
||||
code="I9", message="could not create cache path %s" % (path,)
|
||||
)
|
||||
path.parent.mkdir(exist_ok=True, parents=True)
|
||||
except (IOError, OSError):
|
||||
self.warn("could not create cache path {path}", path=path)
|
||||
return
|
||||
try:
|
||||
f = path.open("w")
|
||||
except py.error.ENOTDIR:
|
||||
self.config.warn(
|
||||
code="I9", message="cache could not write path %s" % (path,)
|
||||
)
|
||||
f = path.open("wb" if PY2 else "w")
|
||||
except (IOError, OSError):
|
||||
self.warn("cache could not write path {path}", path=path)
|
||||
else:
|
||||
with f:
|
||||
self.trace("cache-write %s: %r" % (key, value))
|
||||
json.dump(value, f, indent=2, sort_keys=True)
|
||||
self._ensure_supporting_files()
|
||||
|
||||
def _ensure_supporting_files(self):
|
||||
"""Create supporting files in the cache dir that are not really part of the cache."""
|
||||
if self._cachedir.is_dir():
|
||||
readme_path = self._cachedir / "README.md"
|
||||
if not readme_path.is_file():
|
||||
readme_path.write_text(README_CONTENT)
|
||||
|
||||
msg = u"# created by pytest automatically, do not change\n*"
|
||||
self._cachedir.joinpath(".gitignore").write_text(msg, encoding="UTF-8")
|
||||
|
||||
|
||||
class LFPlugin(object):
|
||||
@@ -116,17 +140,14 @@ class LFPlugin(object):
|
||||
self._no_failures_behavior = self.config.getoption("last_failed_no_failures")
|
||||
|
||||
def pytest_report_collectionfinish(self):
|
||||
if self.active:
|
||||
if self.active and self.config.getoption("verbose") >= 0:
|
||||
if not self._previously_failed_count:
|
||||
mode = "run {} (no recorded failures)".format(
|
||||
self._no_failures_behavior
|
||||
)
|
||||
else:
|
||||
noun = "failure" if self._previously_failed_count == 1 else "failures"
|
||||
suffix = " first" if self.config.getoption("failedfirst") else ""
|
||||
mode = "rerun previous {count} {noun}{suffix}".format(
|
||||
count=self._previously_failed_count, suffix=suffix, noun=noun
|
||||
)
|
||||
return None
|
||||
noun = "failure" if self._previously_failed_count == 1 else "failures"
|
||||
suffix = " first" if self.config.getoption("failedfirst") else ""
|
||||
mode = "rerun previous {count} {noun}{suffix}".format(
|
||||
count=self._previously_failed_count, suffix=suffix, noun=noun
|
||||
)
|
||||
return "run-last-failure: %s" % mode
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
@@ -273,7 +294,7 @@ def pytest_cmdline_main(config):
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_configure(config):
|
||||
config.cache = Cache(config)
|
||||
config.cache = Cache.for_config(config)
|
||||
config.pluginmanager.register(LFPlugin(config), "lfplugin")
|
||||
config.pluginmanager.register(NFPlugin(config), "nfplugin")
|
||||
|
||||
@@ -296,41 +317,47 @@ def cache(request):
|
||||
|
||||
def pytest_report_header(config):
|
||||
if config.option.verbose:
|
||||
relpath = py.path.local().bestrelpath(config.cache._cachedir)
|
||||
return "cachedir: %s" % relpath
|
||||
cachedir = config.cache._cachedir
|
||||
# TODO: evaluate generating upward relative paths
|
||||
# starting with .., ../.. if sensible
|
||||
|
||||
try:
|
||||
displaypath = cachedir.relative_to(config.rootdir)
|
||||
except ValueError:
|
||||
displaypath = cachedir
|
||||
return "cachedir: {}".format(displaypath)
|
||||
|
||||
|
||||
def cacheshow(config, session):
|
||||
from pprint import pprint
|
||||
from pprint import pformat
|
||||
|
||||
tw = py.io.TerminalWriter()
|
||||
tw.line("cachedir: " + str(config.cache._cachedir))
|
||||
if not config.cache._cachedir.check():
|
||||
if not config.cache._cachedir.is_dir():
|
||||
tw.line("cache is empty")
|
||||
return 0
|
||||
dummy = object()
|
||||
basedir = config.cache._cachedir
|
||||
vdir = basedir.join("v")
|
||||
vdir = basedir / "v"
|
||||
tw.sep("-", "cache values")
|
||||
for valpath in sorted(vdir.visit(lambda x: x.isfile())):
|
||||
key = valpath.relto(vdir).replace(valpath.sep, "/")
|
||||
for valpath in sorted(x for x in vdir.rglob("*") if x.is_file()):
|
||||
key = valpath.relative_to(vdir)
|
||||
val = config.cache.get(key, dummy)
|
||||
if val is dummy:
|
||||
tw.line("%s contains unreadable content, " "will be ignored" % key)
|
||||
else:
|
||||
tw.line("%s contains:" % key)
|
||||
stream = py.io.TextIO()
|
||||
pprint(val, stream=stream)
|
||||
for line in stream.getvalue().splitlines():
|
||||
for line in pformat(val).splitlines():
|
||||
tw.line(" " + line)
|
||||
|
||||
ddir = basedir.join("d")
|
||||
if ddir.isdir() and ddir.listdir():
|
||||
ddir = basedir / "d"
|
||||
if ddir.is_dir():
|
||||
contents = sorted(ddir.rglob("*"))
|
||||
tw.sep("-", "cache directories")
|
||||
for p in sorted(basedir.join("d").visit()):
|
||||
for p in contents:
|
||||
# if p.check(dir=1):
|
||||
# print("%s/" % p.relto(basedir))
|
||||
if p.isfile():
|
||||
key = p.relto(basedir)
|
||||
tw.line("%s is a file of length %d" % (key, p.size()))
|
||||
if p.is_file():
|
||||
key = p.relative_to(basedir)
|
||||
tw.line("{} is a file of length {:d}".format(key, p.stat().st_size))
|
||||
return 0
|
||||
|
||||
@@ -16,7 +16,6 @@ import six
|
||||
import pytest
|
||||
from _pytest.compat import CaptureIO
|
||||
|
||||
|
||||
patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"}
|
||||
|
||||
|
||||
@@ -63,8 +62,9 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||
# finally trigger conftest loading but while capturing (issue93)
|
||||
capman.start_global_capturing()
|
||||
outcome = yield
|
||||
out, err = capman.suspend_global_capture()
|
||||
capman.suspend_global_capture()
|
||||
if outcome.excinfo is not None:
|
||||
out, err = capman.read_global_capture()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
@@ -85,6 +85,7 @@ class CaptureManager(object):
|
||||
def __init__(self, method):
|
||||
self._method = method
|
||||
self._global_capturing = None
|
||||
self._current_item = None
|
||||
|
||||
def _getcapture(self, method):
|
||||
if method == "fd":
|
||||
@@ -96,6 +97,8 @@ class CaptureManager(object):
|
||||
else:
|
||||
raise ValueError("unknown capturing method: %r" % method)
|
||||
|
||||
# Global capturing control
|
||||
|
||||
def start_global_capturing(self):
|
||||
assert self._global_capturing is None
|
||||
self._global_capturing = self._getcapture(self._method)
|
||||
@@ -110,16 +113,15 @@ class CaptureManager(object):
|
||||
def resume_global_capture(self):
|
||||
self._global_capturing.resume_capturing()
|
||||
|
||||
def suspend_global_capture(self, item=None, in_=False):
|
||||
if item is not None:
|
||||
self.deactivate_fixture(item)
|
||||
def suspend_global_capture(self, in_=False):
|
||||
cap = getattr(self, "_global_capturing", None)
|
||||
if cap is not None:
|
||||
try:
|
||||
outerr = cap.readouterr()
|
||||
finally:
|
||||
cap.suspend_capturing(in_=in_)
|
||||
return outerr
|
||||
cap.suspend_capturing(in_=in_)
|
||||
|
||||
def read_global_capture(self):
|
||||
return self._global_capturing.readouterr()
|
||||
|
||||
# Fixture Control (its just forwarding, think about removing this later)
|
||||
|
||||
def activate_fixture(self, item):
|
||||
"""If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
|
||||
@@ -135,12 +137,53 @@ class CaptureManager(object):
|
||||
if fixture is not None:
|
||||
fixture.close()
|
||||
|
||||
def suspend_fixture(self, item):
|
||||
fixture = getattr(item, "_capture_fixture", None)
|
||||
if fixture is not None:
|
||||
fixture._suspend()
|
||||
|
||||
def resume_fixture(self, item):
|
||||
fixture = getattr(item, "_capture_fixture", None)
|
||||
if fixture is not None:
|
||||
fixture._resume()
|
||||
|
||||
# Helper context managers
|
||||
|
||||
@contextlib.contextmanager
|
||||
def global_and_fixture_disabled(self):
|
||||
"""Context manager to temporarily disables global and current fixture capturing."""
|
||||
# Need to undo local capsys-et-al if exists before disabling global capture
|
||||
self.suspend_fixture(self._current_item)
|
||||
self.suspend_global_capture(in_=False)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.resume_global_capture()
|
||||
self.resume_fixture(self._current_item)
|
||||
|
||||
@contextlib.contextmanager
|
||||
def item_capture(self, when, item):
|
||||
self.resume_global_capture()
|
||||
self.activate_fixture(item)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.deactivate_fixture(item)
|
||||
self.suspend_global_capture(in_=False)
|
||||
|
||||
out, err = self.read_global_capture()
|
||||
item.add_report_section(when, "stdout", out)
|
||||
item.add_report_section(when, "stderr", err)
|
||||
|
||||
# Hooks
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector):
|
||||
if isinstance(collector, pytest.File):
|
||||
self.resume_global_capture()
|
||||
outcome = yield
|
||||
out, err = self.suspend_global_capture()
|
||||
self.suspend_global_capture()
|
||||
out, err = self.read_global_capture()
|
||||
rep = outcome.get_result()
|
||||
if out:
|
||||
rep.sections.append(("Captured stdout", out))
|
||||
@@ -150,29 +193,25 @@ class CaptureManager(object):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
self.resume_global_capture()
|
||||
# no need to activate a capture fixture because they activate themselves during creation; this
|
||||
# only makes sense when a fixture uses a capture fixture, otherwise the capture fixture will
|
||||
# be activated during pytest_runtest_call
|
||||
def pytest_runtest_protocol(self, item):
|
||||
self._current_item = item
|
||||
yield
|
||||
self.suspend_capture_item(item, "setup")
|
||||
self._current_item = None
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
with self.item_capture("setup", item):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(self, item):
|
||||
self.resume_global_capture()
|
||||
# it is important to activate this fixture during the call phase so it overwrites the "global"
|
||||
# capture
|
||||
self.activate_fixture(item)
|
||||
yield
|
||||
self.suspend_capture_item(item, "call")
|
||||
with self.item_capture("call", item):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.resume_global_capture()
|
||||
self.activate_fixture(item)
|
||||
yield
|
||||
self.suspend_capture_item(item, "teardown")
|
||||
with self.item_capture("teardown", item):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
@@ -182,11 +221,6 @@ class CaptureManager(object):
|
||||
def pytest_internalerror(self, excinfo):
|
||||
self.stop_global_capturing()
|
||||
|
||||
def suspend_capture_item(self, item, when, in_=False):
|
||||
out, err = self.suspend_global_capture(item, in_=in_)
|
||||
item.add_report_section(when, "stdout", out)
|
||||
item.add_report_section(when, "stderr", err)
|
||||
|
||||
|
||||
capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"}
|
||||
|
||||
@@ -290,40 +324,54 @@ class CaptureFixture(object):
|
||||
def __init__(self, captureclass, request):
|
||||
self.captureclass = captureclass
|
||||
self.request = request
|
||||
self._capture = None
|
||||
self._captured_out = self.captureclass.EMPTY_BUFFER
|
||||
self._captured_err = self.captureclass.EMPTY_BUFFER
|
||||
|
||||
def _start(self):
|
||||
self._capture = MultiCapture(
|
||||
out=True, err=True, in_=False, Capture=self.captureclass
|
||||
)
|
||||
self._capture.start_capturing()
|
||||
# Start if not started yet
|
||||
if getattr(self, "_capture", None) is None:
|
||||
self._capture = MultiCapture(
|
||||
out=True, err=True, in_=False, Capture=self.captureclass
|
||||
)
|
||||
self._capture.start_capturing()
|
||||
|
||||
def close(self):
|
||||
cap = self.__dict__.pop("_capture", None)
|
||||
if cap is not None:
|
||||
self._outerr = cap.pop_outerr_to_orig()
|
||||
cap.stop_capturing()
|
||||
if self._capture is not None:
|
||||
out, err = self._capture.pop_outerr_to_orig()
|
||||
self._captured_out += out
|
||||
self._captured_err += err
|
||||
self._capture.stop_capturing()
|
||||
self._capture = None
|
||||
|
||||
def readouterr(self):
|
||||
"""Read and return the captured output so far, resetting the internal buffer.
|
||||
|
||||
:return: captured content as a namedtuple with ``out`` and ``err`` string attributes
|
||||
"""
|
||||
try:
|
||||
return self._capture.readouterr()
|
||||
except AttributeError:
|
||||
return self._outerr
|
||||
captured_out, captured_err = self._captured_out, self._captured_err
|
||||
if self._capture is not None:
|
||||
out, err = self._capture.readouterr()
|
||||
captured_out += out
|
||||
captured_err += err
|
||||
self._captured_out = self.captureclass.EMPTY_BUFFER
|
||||
self._captured_err = self.captureclass.EMPTY_BUFFER
|
||||
return CaptureResult(captured_out, captured_err)
|
||||
|
||||
def _suspend(self):
|
||||
"""Suspends this fixture's own capturing temporarily."""
|
||||
self._capture.suspend_capturing()
|
||||
|
||||
def _resume(self):
|
||||
"""Resumes this fixture's own capturing temporarily."""
|
||||
self._capture.resume_capturing()
|
||||
|
||||
@contextlib.contextmanager
|
||||
def disabled(self):
|
||||
"""Temporarily disables capture while inside the 'with' block."""
|
||||
self._capture.suspend_capturing()
|
||||
capmanager = self.request.config.pluginmanager.getplugin("capturemanager")
|
||||
capmanager.suspend_global_capture(item=None, in_=False)
|
||||
try:
|
||||
with capmanager.global_and_fixture_disabled():
|
||||
yield
|
||||
finally:
|
||||
capmanager.resume_global_capture()
|
||||
self._capture.resume_capturing()
|
||||
|
||||
|
||||
def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
||||
@@ -440,6 +488,7 @@ class MultiCapture(object):
|
||||
|
||||
|
||||
class NoCapture(object):
|
||||
EMPTY_BUFFER = None
|
||||
__init__ = start = done = suspend = resume = lambda *args: None
|
||||
|
||||
|
||||
@@ -449,6 +498,8 @@ class FDCaptureBinary(object):
|
||||
snap() produces `bytes`
|
||||
"""
|
||||
|
||||
EMPTY_BUFFER = bytes()
|
||||
|
||||
def __init__(self, targetfd, tmpfile=None):
|
||||
self.targetfd = targetfd
|
||||
try:
|
||||
@@ -522,6 +573,8 @@ class FDCapture(FDCaptureBinary):
|
||||
snap() produces text
|
||||
"""
|
||||
|
||||
EMPTY_BUFFER = str()
|
||||
|
||||
def snap(self):
|
||||
res = FDCaptureBinary.snap(self)
|
||||
enc = getattr(self.tmpfile, "encoding", None)
|
||||
@@ -531,6 +584,9 @@ class FDCapture(FDCaptureBinary):
|
||||
|
||||
|
||||
class SysCapture(object):
|
||||
|
||||
EMPTY_BUFFER = str()
|
||||
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
name = patchsysdict[fd]
|
||||
self._old = getattr(sys, name)
|
||||
@@ -568,6 +624,8 @@ class SysCapture(object):
|
||||
|
||||
|
||||
class SysCaptureBinary(SysCapture):
|
||||
EMPTY_BUFFER = bytes()
|
||||
|
||||
def snap(self):
|
||||
res = self.tmpfile.buffer.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
|
||||
@@ -8,6 +8,7 @@ import functools
|
||||
import inspect
|
||||
import re
|
||||
import sys
|
||||
from contextlib import contextmanager
|
||||
|
||||
import py
|
||||
|
||||
@@ -22,6 +23,7 @@ except ImportError: # pragma: no cover
|
||||
# Only available in Python 3.4+ or as a backport
|
||||
enum = None
|
||||
|
||||
__all__ = ["Path", "PurePath"]
|
||||
|
||||
_PY3 = sys.version_info > (3, 0)
|
||||
_PY2 = not _PY3
|
||||
@@ -32,7 +34,6 @@ if _PY3:
|
||||
else:
|
||||
from funcsigs import signature, Parameter as Parameter
|
||||
|
||||
|
||||
NoneType = type(None)
|
||||
NOTSET = object()
|
||||
|
||||
@@ -40,6 +41,12 @@ PY35 = sys.version_info[:2] >= (3, 5)
|
||||
PY36 = sys.version_info[:2] >= (3, 6)
|
||||
MODULE_NOT_FOUND_ERROR = "ModuleNotFoundError" if PY36 else "ImportError"
|
||||
|
||||
if PY36:
|
||||
from pathlib import Path, PurePath
|
||||
else:
|
||||
from pathlib2 import Path, PurePath
|
||||
|
||||
|
||||
if _PY3:
|
||||
from collections.abc import MutableMapping as MappingMixin
|
||||
from collections.abc import Mapping, Sequence
|
||||
@@ -49,6 +56,14 @@ else:
|
||||
from collections import Mapping, Sequence # noqa
|
||||
|
||||
|
||||
if sys.version_info >= (3, 4):
|
||||
from importlib.util import spec_from_file_location
|
||||
else:
|
||||
|
||||
def spec_from_file_location(*_, **__):
|
||||
return None
|
||||
|
||||
|
||||
def _format_args(func):
|
||||
return str(signature(func))
|
||||
|
||||
@@ -78,6 +93,7 @@ def iscoroutinefunction(func):
|
||||
|
||||
|
||||
def getlocation(function, curdir):
|
||||
function = get_real_func(function)
|
||||
fn = py.path.local(inspect.getfile(function))
|
||||
lineno = function.__code__.co_firstlineno
|
||||
if fn.relto(curdir):
|
||||
@@ -144,6 +160,13 @@ def getfuncargnames(function, is_method=False, cls=None):
|
||||
return arg_names
|
||||
|
||||
|
||||
@contextmanager
|
||||
def dummy_context_manager():
|
||||
"""Context manager that does nothing, useful in situations where you might need an actual context manager or not
|
||||
depending on some condition. Using this allow to keep the same code"""
|
||||
yield
|
||||
|
||||
|
||||
def get_default_arg_names(function):
|
||||
# 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
|
||||
@@ -221,12 +244,31 @@ else:
|
||||
return val.encode("unicode-escape")
|
||||
|
||||
|
||||
class _PytestWrapper(object):
|
||||
"""Dummy wrapper around a function object for internal use only.
|
||||
|
||||
Used to correctly unwrap the underlying function object
|
||||
when we are creating fixtures, because we wrap the function object ourselves with a decorator
|
||||
to issue warnings when the fixture function is called directly.
|
||||
"""
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
|
||||
|
||||
def get_real_func(obj):
|
||||
""" gets the real function object of the (possibly) wrapped object by
|
||||
functools.wraps or functools.partial.
|
||||
"""
|
||||
start_obj = obj
|
||||
for i in range(100):
|
||||
# __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function
|
||||
# to trigger a warning if it gets called directly instead of by pytest: we don't
|
||||
# want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774)
|
||||
new_obj = getattr(obj, "__pytest_wrapped__", None)
|
||||
if isinstance(new_obj, _PytestWrapper):
|
||||
obj = new_obj.obj
|
||||
break
|
||||
new_obj = getattr(obj, "__wrapped__", None)
|
||||
if new_obj is None:
|
||||
break
|
||||
@@ -242,6 +284,21 @@ def get_real_func(obj):
|
||||
return obj
|
||||
|
||||
|
||||
def get_real_method(obj, holder):
|
||||
"""
|
||||
Attempts to obtain the real function object that might be wrapping ``obj``, while at the same time
|
||||
returning a bound method to ``holder`` if the original object was a bound method.
|
||||
"""
|
||||
try:
|
||||
is_method = hasattr(obj, "__func__")
|
||||
obj = get_real_func(obj)
|
||||
except Exception:
|
||||
return obj
|
||||
if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
|
||||
obj = obj.__get__(holder)
|
||||
return obj
|
||||
|
||||
|
||||
def getfslineno(obj):
|
||||
# xxx let decorators etc specify a sane ordering
|
||||
obj = get_real_func(obj)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
""" command line options, ini-file and conftest.py processing. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import argparse
|
||||
import inspect
|
||||
import shlex
|
||||
import traceback
|
||||
import types
|
||||
@@ -50,6 +51,8 @@ def main(args=None, plugins=None):
|
||||
:arg plugins: list of plugin objects to be auto-registered during
|
||||
initialization.
|
||||
"""
|
||||
from _pytest.main import EXIT_USAGEERROR
|
||||
|
||||
try:
|
||||
try:
|
||||
config = _prepareconfig(args, plugins)
|
||||
@@ -68,7 +71,7 @@ def main(args=None, plugins=None):
|
||||
tw = py.io.TerminalWriter(sys.stderr)
|
||||
for msg in e.args:
|
||||
tw.line("ERROR: {}\n".format(msg), red=True)
|
||||
return 4
|
||||
return EXIT_USAGEERROR
|
||||
|
||||
|
||||
class cmdline(object): # compatibility namespace
|
||||
@@ -175,7 +178,9 @@ def _prepareconfig(args=None, plugins=None):
|
||||
else:
|
||||
pluginmanager.register(plugin)
|
||||
if warning:
|
||||
config.warn("C1", warning)
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
||||
_issue_config_warning(warning, config=config)
|
||||
return pluginmanager.hook.pytest_cmdline_parse(
|
||||
pluginmanager=pluginmanager, args=args
|
||||
)
|
||||
@@ -252,6 +257,10 @@ class PytestPluginManager(PluginManager):
|
||||
method = getattr(plugin, name)
|
||||
opts = super(PytestPluginManager, self).parse_hookimpl_opts(plugin, name)
|
||||
|
||||
# consider only actual functions for hooks (#3775)
|
||||
if not inspect.isroutine(method):
|
||||
return
|
||||
|
||||
# collect unmarked hooks as long as they have the `pytest_' prefix
|
||||
if opts is None and name.startswith("pytest_"):
|
||||
opts = {}
|
||||
@@ -342,6 +351,7 @@ class PytestPluginManager(PluginManager):
|
||||
else None
|
||||
)
|
||||
self._noconftest = namespace.noconftest
|
||||
self._using_pyargs = namespace.pyargs
|
||||
testpaths = namespace.file_or_dir
|
||||
foundanchor = False
|
||||
for path in testpaths:
|
||||
@@ -407,12 +417,21 @@ class PytestPluginManager(PluginManager):
|
||||
_ensure_removed_sysmodule(conftestpath.purebasename)
|
||||
try:
|
||||
mod = conftestpath.pyimport()
|
||||
if hasattr(mod, "pytest_plugins") and self._configured:
|
||||
if (
|
||||
hasattr(mod, "pytest_plugins")
|
||||
and self._configured
|
||||
and not self._using_pyargs
|
||||
):
|
||||
from _pytest.deprecated import (
|
||||
PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
|
||||
)
|
||||
|
||||
warnings.warn(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST)
|
||||
warnings.warn_explicit(
|
||||
PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST,
|
||||
category=None,
|
||||
filename=str(conftestpath),
|
||||
lineno=0,
|
||||
)
|
||||
except Exception:
|
||||
raise ConftestImportFailure(conftestpath, sys.exc_info())
|
||||
|
||||
@@ -597,7 +616,29 @@ class Config(object):
|
||||
fin()
|
||||
|
||||
def warn(self, code, message, fslocation=None, nodeid=None):
|
||||
""" generate a warning for this test session. """
|
||||
"""
|
||||
.. deprecated:: 3.8
|
||||
|
||||
Use :py:func:`warnings.warn` or :py:func:`warnings.warn_explicit` directly instead.
|
||||
|
||||
Generate a warning for this test session.
|
||||
"""
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
|
||||
if isinstance(fslocation, (tuple, list)) and len(fslocation) > 2:
|
||||
filename, lineno = fslocation[:2]
|
||||
else:
|
||||
filename = "unknown file"
|
||||
lineno = 0
|
||||
msg = "config.warn has been deprecated, use warnings.warn instead"
|
||||
if nodeid:
|
||||
msg = "{}: {}".format(nodeid, msg)
|
||||
warnings.warn_explicit(
|
||||
RemovedInPytest4Warning(msg),
|
||||
category=None,
|
||||
filename=filename,
|
||||
lineno=lineno,
|
||||
)
|
||||
self.hook.pytest_logwarning.call_historic(
|
||||
kwargs=dict(
|
||||
code=code, message=message, fslocation=fslocation, nodeid=nodeid
|
||||
@@ -662,8 +703,8 @@ class Config(object):
|
||||
r = determine_setup(
|
||||
ns.inifilename,
|
||||
ns.file_or_dir + unknown_args,
|
||||
warnfunc=self.warn,
|
||||
rootdir_cmd_arg=ns.rootdir or None,
|
||||
config=self,
|
||||
)
|
||||
self.rootdir, self.inifile, self.inicfg = r
|
||||
self._parser.extra_info["rootdir"] = self.rootdir
|
||||
@@ -701,6 +742,10 @@ class Config(object):
|
||||
|
||||
self.pluginmanager.rewrite_hook = hook
|
||||
|
||||
if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
||||
# We don't autoload from setuptools entry points, no need to continue.
|
||||
return
|
||||
|
||||
# 'RECORD' available for plugins installed normally (pip install)
|
||||
# 'SOURCES.txt' available for plugins installed in dev mode (pip install -e)
|
||||
# for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa
|
||||
@@ -726,7 +771,10 @@ class Config(object):
|
||||
self._checkversion()
|
||||
self._consider_importhook(args)
|
||||
self.pluginmanager.consider_preparse(args)
|
||||
self.pluginmanager.load_setuptools_entrypoints("pytest11")
|
||||
if not os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"):
|
||||
# Don't autoload from setuptools entry point. Only explicitly specified
|
||||
# plugins are going to be loaded.
|
||||
self.pluginmanager.load_setuptools_entrypoints("pytest11")
|
||||
self.pluginmanager.consider_env()
|
||||
self.known_args_namespace = ns = self._parser.parse_known_args(
|
||||
args, namespace=copy.copy(self.option)
|
||||
|
||||
@@ -2,6 +2,13 @@ import six
|
||||
import warnings
|
||||
import argparse
|
||||
|
||||
from gettext import gettext as _
|
||||
import sys as _sys
|
||||
|
||||
import py
|
||||
|
||||
from ..main import EXIT_USAGEERROR
|
||||
|
||||
FILE_OR_DIR = "file_or_dir"
|
||||
|
||||
|
||||
@@ -70,7 +77,8 @@ class Parser(object):
|
||||
|
||||
self.optparser = self._getparser()
|
||||
try_argcomplete(self.optparser)
|
||||
return self.optparser.parse_args([str(x) for x in args], namespace=namespace)
|
||||
args = [str(x) if isinstance(x, py.path.local) else x for x in args]
|
||||
return self.optparser.parse_args(args, namespace=namespace)
|
||||
|
||||
def _getparser(self):
|
||||
from _pytest._argcomplete import filescompleter
|
||||
@@ -106,7 +114,7 @@ class Parser(object):
|
||||
the remaining arguments unknown at this point.
|
||||
"""
|
||||
optparser = self._getparser()
|
||||
args = [str(x) for x in args]
|
||||
args = [str(x) if isinstance(x, py.path.local) else x for x in args]
|
||||
return optparser.parse_known_args(args, namespace=namespace)
|
||||
|
||||
def addini(self, name, help, type=None, default=None):
|
||||
@@ -174,23 +182,23 @@ class Argument(object):
|
||||
if isinstance(typ, six.string_types):
|
||||
if typ == "choice":
|
||||
warnings.warn(
|
||||
"type argument to addoption() is a string %r."
|
||||
" For parsearg this is optional and when supplied"
|
||||
" should be a type."
|
||||
"`type` argument to addoption() is the string %r."
|
||||
" For choices this is optional and can be omitted, "
|
||||
" but when supplied should be a type (for example `str` or `int`)."
|
||||
" (options: %s)" % (typ, names),
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
stacklevel=4,
|
||||
)
|
||||
# argparse expects a type here take it from
|
||||
# the type of the first element
|
||||
attrs["type"] = type(attrs["choices"][0])
|
||||
else:
|
||||
warnings.warn(
|
||||
"type argument to addoption() is a string %r."
|
||||
" For parsearg this should be a type."
|
||||
"`type` argument to addoption() is the string %r, "
|
||||
" but when supplied should be a type (for example `str` or `int`)."
|
||||
" (options: %s)" % (typ, names),
|
||||
DeprecationWarning,
|
||||
stacklevel=3,
|
||||
stacklevel=4,
|
||||
)
|
||||
attrs["type"] = Argument._typ_map[typ]
|
||||
# used in test_parseopt -> test_parse_defaultgetter
|
||||
@@ -326,6 +334,16 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
# an usage error to provide more contextual information to the user
|
||||
self.extra_info = extra_info
|
||||
|
||||
def error(self, message):
|
||||
"""error(message: string)
|
||||
|
||||
Prints a usage message incorporating the message to stderr and
|
||||
exits.
|
||||
Overrides the method in parent class to change exit code"""
|
||||
self.print_usage(_sys.stderr)
|
||||
args = {"prog": self.prog, "message": message}
|
||||
self.exit(EXIT_USAGEERROR, _("%(prog)s: error: %(message)s\n") % args)
|
||||
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
"""allow splitting of positional arguments"""
|
||||
args, argv = self.parse_known_args(args, namespace)
|
||||
|
||||
@@ -10,15 +10,12 @@ def exists(path, ignore=EnvironmentError):
|
||||
return False
|
||||
|
||||
|
||||
def getcfg(args, warnfunc=None):
|
||||
def getcfg(args, config=None):
|
||||
"""
|
||||
Search the list of arguments for a valid ini-file for pytest,
|
||||
and return a tuple of (rootdir, inifile, cfg-dict).
|
||||
|
||||
note: warnfunc is an optional function used to warn
|
||||
about ini-files that use deprecated features.
|
||||
This parameter should be removed when pytest
|
||||
adopts standard deprecation warnings (#1804).
|
||||
note: config is optional and used only to issue warnings explicitly (#2891).
|
||||
"""
|
||||
from _pytest.deprecated import CFG_PYTEST_SECTION
|
||||
|
||||
@@ -34,9 +31,15 @@ def getcfg(args, warnfunc=None):
|
||||
if exists(p):
|
||||
iniconfig = py.iniconfig.IniConfig(p)
|
||||
if "pytest" in iniconfig.sections:
|
||||
if inibasename == "setup.cfg" and warnfunc:
|
||||
warnfunc(
|
||||
"C1", CFG_PYTEST_SECTION.format(filename=inibasename)
|
||||
if inibasename == "setup.cfg" and config is not None:
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
|
||||
_issue_config_warning(
|
||||
RemovedInPytest4Warning(
|
||||
CFG_PYTEST_SECTION.format(filename=inibasename)
|
||||
),
|
||||
config=config,
|
||||
)
|
||||
return base, p, iniconfig["pytest"]
|
||||
if (
|
||||
@@ -95,7 +98,7 @@ def get_dirs_from_args(args):
|
||||
return [get_dir_from_path(path) for path in possible_paths if path.exists()]
|
||||
|
||||
|
||||
def determine_setup(inifile, args, warnfunc=None, rootdir_cmd_arg=None):
|
||||
def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None):
|
||||
dirs = get_dirs_from_args(args)
|
||||
if inifile:
|
||||
iniconfig = py.iniconfig.IniConfig(inifile)
|
||||
@@ -105,23 +108,30 @@ def determine_setup(inifile, args, warnfunc=None, rootdir_cmd_arg=None):
|
||||
for section in sections:
|
||||
try:
|
||||
inicfg = iniconfig[section]
|
||||
if is_cfg_file and section == "pytest" and warnfunc:
|
||||
if is_cfg_file and section == "pytest" and config is not None:
|
||||
from _pytest.deprecated import CFG_PYTEST_SECTION
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
||||
warnfunc("C1", CFG_PYTEST_SECTION.format(filename=str(inifile)))
|
||||
_issue_config_warning(
|
||||
RemovedInPytest4Warning(
|
||||
CFG_PYTEST_SECTION.format(filename=str(inifile))
|
||||
),
|
||||
config,
|
||||
)
|
||||
break
|
||||
except KeyError:
|
||||
inicfg = None
|
||||
rootdir = get_common_ancestor(dirs)
|
||||
else:
|
||||
ancestor = get_common_ancestor(dirs)
|
||||
rootdir, inifile, inicfg = getcfg([ancestor], warnfunc=warnfunc)
|
||||
rootdir, inifile, inicfg = getcfg([ancestor], config=config)
|
||||
if rootdir is None:
|
||||
for rootdir in ancestor.parts(reverse=True):
|
||||
if rootdir.join("setup.py").exists():
|
||||
break
|
||||
else:
|
||||
rootdir, inifile, inicfg = getcfg(dirs, warnfunc=warnfunc)
|
||||
rootdir, inifile, inicfg = getcfg(dirs, config=config)
|
||||
if rootdir is None:
|
||||
rootdir = get_common_ancestor([py.path.local(), ancestor])
|
||||
is_fs_root = os.path.splitdrive(str(rootdir))[1] == "/"
|
||||
|
||||
@@ -5,6 +5,8 @@ import sys
|
||||
import os
|
||||
from doctest import UnexpectedException
|
||||
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
try:
|
||||
from builtins import breakpoint # noqa
|
||||
|
||||
@@ -28,6 +30,12 @@ def pytest_addoption(parser):
|
||||
help="start a custom interactive Python debugger on errors. "
|
||||
"For example: --pdbcls=IPython.terminal.debugger:TerminalPdb",
|
||||
)
|
||||
group._addoption(
|
||||
"--trace",
|
||||
dest="trace",
|
||||
action="store_true",
|
||||
help="Immediately break when running each test.",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
@@ -38,6 +46,8 @@ def pytest_configure(config):
|
||||
else:
|
||||
pdb_cls = pdb.Pdb
|
||||
|
||||
if config.getvalue("trace"):
|
||||
config.pluginmanager.register(PdbTrace(), "pdbtrace")
|
||||
if config.getvalue("usepdb"):
|
||||
config.pluginmanager.register(PdbInvoke(), "pdbinvoke")
|
||||
|
||||
@@ -71,7 +81,7 @@ class pytestPDB(object):
|
||||
_pdb_cls = pdb.Pdb
|
||||
|
||||
@classmethod
|
||||
def set_trace(cls):
|
||||
def set_trace(cls, set_break=True):
|
||||
""" invoke PDB set_trace debugging, dropping any IO capturing. """
|
||||
import _pytest.config
|
||||
|
||||
@@ -84,14 +94,16 @@ class pytestPDB(object):
|
||||
tw.line()
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config)
|
||||
cls._pdb_cls().set_trace(frame)
|
||||
if set_break:
|
||||
cls._pdb_cls().set_trace(frame)
|
||||
|
||||
|
||||
class PdbInvoke(object):
|
||||
def pytest_exception_interact(self, node, call, report):
|
||||
capman = node.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
out, err = capman.suspend_global_capture(in_=True)
|
||||
capman.suspend_global_capture(in_=True)
|
||||
out, err = capman.read_global_capture()
|
||||
sys.stdout.write(out)
|
||||
sys.stdout.write(err)
|
||||
_enter_pdb(node, call.excinfo, report)
|
||||
@@ -104,6 +116,30 @@ class PdbInvoke(object):
|
||||
post_mortem(tb)
|
||||
|
||||
|
||||
class PdbTrace(object):
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(self, pyfuncitem):
|
||||
_test_pytest_function(pyfuncitem)
|
||||
yield
|
||||
|
||||
|
||||
def _test_pytest_function(pyfuncitem):
|
||||
pytestPDB.set_trace(set_break=False)
|
||||
testfunction = pyfuncitem.obj
|
||||
pyfuncitem.obj = pdb.runcall
|
||||
if pyfuncitem._isyieldedfunction():
|
||||
arg_list = list(pyfuncitem._args)
|
||||
arg_list.insert(0, testfunction)
|
||||
pyfuncitem._args = tuple(arg_list)
|
||||
else:
|
||||
if "func" in pyfuncitem._fixtureinfo.argnames:
|
||||
raise ValueError("--trace can't be used with a fixture named func!")
|
||||
pyfuncitem.funcargs["func"] = testfunction
|
||||
new_list = list(pyfuncitem._fixtureinfo.argnames)
|
||||
new_list.append("func")
|
||||
pyfuncitem._fixtureinfo.argnames = tuple(new_list)
|
||||
|
||||
|
||||
def _enter_pdb(node, excinfo, rep):
|
||||
# XXX we re-use the TerminalReporter's terminalwriter
|
||||
# because this seems to avoid some encoding related troubles
|
||||
|
||||
@@ -7,14 +7,16 @@ be removed when the time comes.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
|
||||
class RemovedInPytest4Warning(DeprecationWarning):
|
||||
"""warning class for features removed in pytest 4.0"""
|
||||
MAIN_STR_ARGS = RemovedInPytest4Warning(
|
||||
"passing a string to pytest.main() is deprecated, "
|
||||
"pass a list of arguments instead."
|
||||
)
|
||||
|
||||
|
||||
MAIN_STR_ARGS = "passing a string to pytest.main() is deprecated, " "pass a list of arguments instead."
|
||||
|
||||
YIELD_TESTS = "yield tests are deprecated, and scheduled to be removed in pytest 4.0"
|
||||
YIELD_TESTS = RemovedInPytest4Warning(
|
||||
"yield tests are deprecated, and scheduled to be removed in pytest 4.0"
|
||||
)
|
||||
|
||||
FUNCARG_PREFIX = (
|
||||
'{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated '
|
||||
@@ -22,11 +24,17 @@ FUNCARG_PREFIX = (
|
||||
"Please remove the prefix and use the @pytest.fixture decorator instead."
|
||||
)
|
||||
|
||||
FIXTURE_FUNCTION_CALL = (
|
||||
'Fixture "{name}" called directly. Fixtures are not meant to be called directly, '
|
||||
"are created automatically when test functions request them as parameters. "
|
||||
"See https://docs.pytest.org/en/latest/fixture.html for more information."
|
||||
)
|
||||
|
||||
CFG_PYTEST_SECTION = (
|
||||
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead."
|
||||
)
|
||||
|
||||
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue"
|
||||
GETFUNCARGVALUE = "getfuncargvalue is deprecated, use getfixturevalue"
|
||||
|
||||
RESULT_LOG = (
|
||||
"--result-log is deprecated and scheduled for removal in pytest 4.0.\n"
|
||||
@@ -45,7 +53,11 @@ MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
|
||||
"For more details, see: https://docs.pytest.org/en/latest/parametrize.html"
|
||||
)
|
||||
|
||||
RECORD_XML_PROPERTY = (
|
||||
NODE_WARN = RemovedInPytest4Warning(
|
||||
"Node.warn(code, message) form has been deprecated, use Node.warn(warning_instance) instead."
|
||||
)
|
||||
|
||||
RECORD_XML_PROPERTY = RemovedInPytest4Warning(
|
||||
'Fixture renamed from "record_xml_property" to "record_property" as user '
|
||||
"properties are now available to all reporters.\n"
|
||||
'"record_xml_property" is now deprecated.'
|
||||
@@ -55,7 +67,7 @@ COLLECTOR_MAKEITEM = RemovedInPytest4Warning(
|
||||
"pycollector makeitem was removed " "as it is an accidentially leaked internal api"
|
||||
)
|
||||
|
||||
METAFUNC_ADD_CALL = (
|
||||
METAFUNC_ADD_CALL = RemovedInPytest4Warning(
|
||||
"Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.\n"
|
||||
"Please use Metafunc.parametrize instead."
|
||||
)
|
||||
@@ -65,3 +77,7 @@ PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = RemovedInPytest4Warning(
|
||||
"because it affects the entire directory tree in a non-explicit way.\n"
|
||||
"Please move it to the top level conftest file instead."
|
||||
)
|
||||
|
||||
PYTEST_NAMESPACE = RemovedInPytest4Warning(
|
||||
"pytest_namespace is deprecated and will be removed soon"
|
||||
)
|
||||
|
||||
@@ -203,7 +203,8 @@ class DoctestItem(pytest.Item):
|
||||
return
|
||||
capman = self.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
out, err = capman.suspend_global_capture(in_=True)
|
||||
capman.suspend_global_capture(in_=True)
|
||||
out, err = capman.read_global_capture()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import inspect
|
||||
import sys
|
||||
import warnings
|
||||
from collections import OrderedDict, deque, defaultdict
|
||||
|
||||
import six
|
||||
from more_itertools import flatten
|
||||
|
||||
import attr
|
||||
@@ -27,7 +29,10 @@ from _pytest.compat import (
|
||||
getfuncargnames,
|
||||
safe_getattr,
|
||||
FuncargnamesCompatAttr,
|
||||
get_real_method,
|
||||
_PytestWrapper,
|
||||
)
|
||||
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
|
||||
from _pytest.outcomes import fail, TEST_OUTCOME
|
||||
|
||||
FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}'
|
||||
@@ -45,6 +50,7 @@ def pytest_sessionstart(session):
|
||||
|
||||
scopename2class.update(
|
||||
{
|
||||
"package": _pytest.python.Package,
|
||||
"class": _pytest.python.Class,
|
||||
"module": _pytest.python.Module,
|
||||
"function": _pytest.nodes.Item,
|
||||
@@ -58,6 +64,7 @@ scopename2class = {}
|
||||
|
||||
|
||||
scope2props = dict(session=())
|
||||
scope2props["package"] = ("fspath",)
|
||||
scope2props["module"] = ("fspath", "module")
|
||||
scope2props["class"] = scope2props["module"] + ("cls",)
|
||||
scope2props["instance"] = scope2props["class"] + ("instance",)
|
||||
@@ -80,6 +87,21 @@ def scopeproperty(name=None, doc=None):
|
||||
return decoratescope
|
||||
|
||||
|
||||
def get_scope_package(node, fixturedef):
|
||||
import pytest
|
||||
|
||||
cls = pytest.Package
|
||||
current = node
|
||||
fixture_package_name = "%s/%s" % (fixturedef.baseid, "__init__.py")
|
||||
while current and (
|
||||
type(current) is not cls or fixture_package_name != current.nodeid
|
||||
):
|
||||
current = current.parent
|
||||
if current is None:
|
||||
return node.session
|
||||
return current
|
||||
|
||||
|
||||
def get_scope_node(node, scope):
|
||||
cls = scopename2class.get(scope)
|
||||
if cls is None:
|
||||
@@ -173,9 +195,11 @@ def get_parametrized_fixture_keys(item, scopenum):
|
||||
continue
|
||||
if scopenum == 0: # session
|
||||
key = (argname, param_index)
|
||||
elif scopenum == 1: # module
|
||||
elif scopenum == 1: # package
|
||||
key = (argname, param_index, item.fspath.dirpath())
|
||||
elif scopenum == 2: # module
|
||||
key = (argname, param_index, item.fspath)
|
||||
elif scopenum == 2: # class
|
||||
elif scopenum == 3: # class
|
||||
key = (argname, param_index, item.fspath, item.cls)
|
||||
yield key
|
||||
|
||||
@@ -274,11 +298,43 @@ def get_direct_param_fixture_func(request):
|
||||
return request.param
|
||||
|
||||
|
||||
@attr.s(slots=True)
|
||||
class FuncFixtureInfo(object):
|
||||
def __init__(self, argnames, names_closure, name2fixturedefs):
|
||||
self.argnames = argnames
|
||||
self.names_closure = names_closure
|
||||
self.name2fixturedefs = name2fixturedefs
|
||||
# original function argument names
|
||||
argnames = attr.ib(type=tuple)
|
||||
# argnames that function immediately requires. These include argnames +
|
||||
# fixture names specified via usefixtures and via autouse=True in fixture
|
||||
# definitions.
|
||||
initialnames = attr.ib(type=tuple)
|
||||
names_closure = attr.ib() # type: List[str]
|
||||
name2fixturedefs = attr.ib() # type: List[str, List[FixtureDef]]
|
||||
|
||||
def prune_dependency_tree(self):
|
||||
"""Recompute names_closure from initialnames and name2fixturedefs
|
||||
|
||||
Can only reduce names_closure, which means that the new closure will
|
||||
always be a subset of the old one. The order is preserved.
|
||||
|
||||
This method is needed because direct parametrization may shadow some
|
||||
of the fixtures that were included in the originally built dependency
|
||||
tree. In this way the dependency tree can get pruned, and the closure
|
||||
of argnames may get reduced.
|
||||
"""
|
||||
closure = set()
|
||||
working_set = set(self.initialnames)
|
||||
while working_set:
|
||||
argname = working_set.pop()
|
||||
# argname may be smth not included in the original names_closure,
|
||||
# in which case we ignore it. This currently happens with pseudo
|
||||
# FixtureDefs which wrap 'get_direct_param_fixture_func(request)'.
|
||||
# So they introduce the new dependency 'request' which might have
|
||||
# been missing in the original tree (closure).
|
||||
if argname not in closure and argname in self.names_closure:
|
||||
closure.add(argname)
|
||||
if argname in self.name2fixturedefs:
|
||||
working_set.update(self.name2fixturedefs[argname][-1].argnames)
|
||||
|
||||
self.names_closure[:] = sorted(closure, key=self.names_closure.index)
|
||||
|
||||
|
||||
class FixtureRequest(FuncargnamesCompatAttr):
|
||||
@@ -580,7 +636,10 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
if scope == "function":
|
||||
# this might also be a non-function Item despite its attribute name
|
||||
return self._pyfuncitem
|
||||
node = get_scope_node(self._pyfuncitem, scope)
|
||||
if scope == "package":
|
||||
node = get_scope_package(self._pyfuncitem, self._fixturedef)
|
||||
else:
|
||||
node = get_scope_node(self._pyfuncitem, scope)
|
||||
if node is None and scope == "class":
|
||||
# fallback to function item itself
|
||||
node = self._pyfuncitem
|
||||
@@ -624,7 +683,7 @@ class ScopeMismatchError(Exception):
|
||||
"""
|
||||
|
||||
|
||||
scopes = "session module class function".split()
|
||||
scopes = "session package module class function".split()
|
||||
scopenum_function = scopes.index("function")
|
||||
|
||||
|
||||
@@ -734,23 +793,26 @@ def call_fixture_func(fixturefunc, request, kwargs):
|
||||
if yieldctx:
|
||||
it = fixturefunc(**kwargs)
|
||||
res = next(it)
|
||||
|
||||
def teardown():
|
||||
try:
|
||||
next(it)
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
fail_fixturefunc(
|
||||
fixturefunc, "yield_fixture function has more than one 'yield'"
|
||||
)
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
finalizer = functools.partial(_teardown_yield_fixture, fixturefunc, it)
|
||||
request.addfinalizer(finalizer)
|
||||
else:
|
||||
res = fixturefunc(**kwargs)
|
||||
return res
|
||||
|
||||
|
||||
def _teardown_yield_fixture(fixturefunc, it):
|
||||
"""Executes the teardown of a fixture function by advancing the iterator after the
|
||||
yield and ensure the iteration ends (if not it means there is more than one yield in the function)"""
|
||||
try:
|
||||
next(it)
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
fail_fixturefunc(
|
||||
fixturefunc, "yield_fixture function has more than one 'yield'"
|
||||
)
|
||||
|
||||
|
||||
class FixtureDef(object):
|
||||
""" A container for a factory definition. """
|
||||
|
||||
@@ -795,7 +857,7 @@ class FixtureDef(object):
|
||||
if exceptions:
|
||||
e = exceptions[0]
|
||||
del exceptions # ensure we don't keep all frames alive because of the traceback
|
||||
py.builtin._reraise(*e)
|
||||
six.reraise(*e)
|
||||
|
||||
finally:
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
||||
@@ -822,7 +884,7 @@ class FixtureDef(object):
|
||||
result, cache_key, err = cached_result
|
||||
if my_cache_key == cache_key:
|
||||
if err is not None:
|
||||
py.builtin._reraise(*err)
|
||||
six.reraise(*err)
|
||||
else:
|
||||
return result
|
||||
# we have a previous but differently parametrized fixture instance
|
||||
@@ -841,15 +903,10 @@ class FixtureDef(object):
|
||||
)
|
||||
|
||||
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
""" Execution of fixture setup. """
|
||||
kwargs = {}
|
||||
for argname in fixturedef.argnames:
|
||||
fixdef = request._get_active_fixturedef(argname)
|
||||
result, arg_cache_key, exc = fixdef.cached_result
|
||||
request._check_scope(argname, request.scope, fixdef.scope)
|
||||
kwargs[argname] = result
|
||||
|
||||
def resolve_fixture_function(fixturedef, request):
|
||||
"""Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific
|
||||
instances and bound methods.
|
||||
"""
|
||||
fixturefunc = fixturedef.func
|
||||
if fixturedef.unittest:
|
||||
if request.instance is not None:
|
||||
@@ -863,6 +920,19 @@ def pytest_fixture_setup(fixturedef, request):
|
||||
fixturefunc = getimfunc(fixturedef.func)
|
||||
if fixturefunc != fixturedef.func:
|
||||
fixturefunc = fixturefunc.__get__(request.instance)
|
||||
return fixturefunc
|
||||
|
||||
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
""" Execution of fixture setup. """
|
||||
kwargs = {}
|
||||
for argname in fixturedef.argnames:
|
||||
fixdef = request._get_active_fixturedef(argname)
|
||||
result, arg_cache_key, exc = fixdef.cached_result
|
||||
request._check_scope(argname, request.scope, fixdef.scope)
|
||||
kwargs[argname] = result
|
||||
|
||||
fixturefunc = resolve_fixture_function(fixturedef, request)
|
||||
my_cache_key = request.param_index
|
||||
try:
|
||||
result = call_fixture_func(fixturefunc, request, kwargs)
|
||||
@@ -881,6 +951,41 @@ def _ensure_immutable_ids(ids):
|
||||
return tuple(ids)
|
||||
|
||||
|
||||
def wrap_function_to_warning_if_called_directly(function, fixture_marker):
|
||||
"""Wrap the given fixture function so we can issue warnings about it being called directly, instead of
|
||||
used as an argument in a test function.
|
||||
"""
|
||||
is_yield_function = is_generator(function)
|
||||
msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__)
|
||||
warning = RemovedInPytest4Warning(msg)
|
||||
|
||||
if is_yield_function:
|
||||
|
||||
@functools.wraps(function)
|
||||
def result(*args, **kwargs):
|
||||
__tracebackhide__ = True
|
||||
warnings.warn(warning, stacklevel=3)
|
||||
for x in function(*args, **kwargs):
|
||||
yield x
|
||||
|
||||
else:
|
||||
|
||||
@functools.wraps(function)
|
||||
def result(*args, **kwargs):
|
||||
__tracebackhide__ = True
|
||||
warnings.warn(warning, stacklevel=3)
|
||||
return function(*args, **kwargs)
|
||||
|
||||
if six.PY2:
|
||||
result.__wrapped__ = function
|
||||
|
||||
# keep reference to the original function in our own custom attribute so we don't unwrap
|
||||
# further than this point and lose useful wrappings like @mock.patch (#3774)
|
||||
result.__pytest_wrapped__ = _PytestWrapper(function)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class FixtureFunctionMarker(object):
|
||||
scope = attr.ib()
|
||||
@@ -898,6 +1003,8 @@ class FixtureFunctionMarker(object):
|
||||
"fixture is being applied more than once to the same function"
|
||||
)
|
||||
|
||||
function = wrap_function_to_warning_if_called_directly(function, self)
|
||||
|
||||
function._pytestfixturefunction = self
|
||||
return function
|
||||
|
||||
@@ -905,16 +1012,27 @@ class FixtureFunctionMarker(object):
|
||||
def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
|
||||
"""Decorator to mark a fixture factory function.
|
||||
|
||||
This decorator can be used (with or without parameters) to define a
|
||||
fixture function. The name of the fixture function can later be
|
||||
referenced to cause its invocation ahead of running tests: test
|
||||
modules or classes can use the pytest.mark.usefixtures(fixturename)
|
||||
marker. Test functions can directly use fixture names as input
|
||||
This decorator can be used, with or without parameters, to define a
|
||||
fixture function.
|
||||
|
||||
The name of the fixture function can later be referenced to cause its
|
||||
invocation ahead of running tests: test
|
||||
modules or classes can use the ``pytest.mark.usefixtures(fixturename)``
|
||||
marker.
|
||||
|
||||
Test functions can directly use fixture names as input
|
||||
arguments in which case the fixture instance returned from the fixture
|
||||
function will be injected.
|
||||
|
||||
Fixtures can provide their values to test functions using ``return`` or ``yield``
|
||||
statements. When using ``yield`` the code block after the ``yield`` statement is executed
|
||||
as teardown code regardless of the test outcome, and must yield exactly once.
|
||||
|
||||
:arg scope: the scope for which this fixture is shared, one of
|
||||
"function" (default), "class", "module" or "session".
|
||||
``"function"`` (default), ``"class"``, ``"module"``,
|
||||
``"package"`` or ``"session"``.
|
||||
|
||||
``"package"`` is considered **experimental** at this time.
|
||||
|
||||
:arg params: an optional list of parameters which will cause multiple
|
||||
invocations of the fixture function and all of the tests
|
||||
@@ -935,10 +1053,6 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
|
||||
to resolve this is to name the decorated function
|
||||
``fixture_<fixturename>`` and then use
|
||||
``@pytest.fixture(name='<fixturename>')``.
|
||||
|
||||
Fixtures can optionally provide their values to test functions using a ``yield`` statement,
|
||||
instead of ``return``. In this case, the code block after the ``yield`` statement is executed
|
||||
as teardown code regardless of the test outcome. A fixture function must yield exactly once.
|
||||
"""
|
||||
if callable(scope) and params is None and autouse is False:
|
||||
# direct decoration
|
||||
@@ -954,13 +1068,7 @@ def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=N
|
||||
.. deprecated:: 3.0
|
||||
Use :py:func:`pytest.fixture` directly instead.
|
||||
"""
|
||||
if callable(scope) and params is None and not autouse:
|
||||
# direct decoration
|
||||
return FixtureFunctionMarker("function", params, autouse, ids=ids, name=name)(
|
||||
scope
|
||||
)
|
||||
else:
|
||||
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
|
||||
return fixture(scope=scope, params=params, autouse=autouse, ids=ids, name=name)
|
||||
|
||||
|
||||
defaultfuncargprefixmarker = fixture()
|
||||
@@ -1033,11 +1141,12 @@ class FixtureManager(object):
|
||||
usefixtures = flatten(
|
||||
mark.args for mark in node.iter_markers(name="usefixtures")
|
||||
)
|
||||
initialnames = argnames
|
||||
initialnames = tuple(usefixtures) + initialnames
|
||||
initialnames = tuple(usefixtures) + argnames
|
||||
fm = node.session._fixturemanager
|
||||
names_closure, arg2fixturedefs = fm.getfixtureclosure(initialnames, node)
|
||||
return FuncFixtureInfo(argnames, names_closure, arg2fixturedefs)
|
||||
initialnames, names_closure, arg2fixturedefs = fm.getfixtureclosure(
|
||||
initialnames, node
|
||||
)
|
||||
return FuncFixtureInfo(argnames, initialnames, names_closure, arg2fixturedefs)
|
||||
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
nodeid = None
|
||||
@@ -1085,6 +1194,12 @@ class FixtureManager(object):
|
||||
fixturenames_closure.append(arg)
|
||||
|
||||
merge(fixturenames)
|
||||
|
||||
# at this point, fixturenames_closure contains what we call "initialnames",
|
||||
# which is a set of fixturenames the function immediately requests. We
|
||||
# need to return it as well, so save this.
|
||||
initialnames = tuple(fixturenames_closure)
|
||||
|
||||
arg2fixturedefs = {}
|
||||
lastlen = -1
|
||||
while lastlen != len(fixturenames_closure):
|
||||
@@ -1106,7 +1221,7 @@ class FixtureManager(object):
|
||||
return fixturedefs[-1].scopenum
|
||||
|
||||
fixturenames_closure.sort(key=sort_by_scope)
|
||||
return fixturenames_closure, arg2fixturedefs
|
||||
return initialnames, fixturenames_closure, arg2fixturedefs
|
||||
|
||||
def pytest_generate_tests(self, metafunc):
|
||||
for argname in metafunc.fixturenames:
|
||||
@@ -1142,6 +1257,8 @@ class FixtureManager(object):
|
||||
items[:] = reorder_items(items)
|
||||
|
||||
def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
|
||||
from _pytest import deprecated
|
||||
|
||||
if nodeid is not NOTSET:
|
||||
holderobj = node_or_obj
|
||||
else:
|
||||
@@ -1155,19 +1272,24 @@ class FixtureManager(object):
|
||||
# The attribute can be an arbitrary descriptor, so the attribute
|
||||
# access below can raise. safe_getatt() ignores such exceptions.
|
||||
obj = safe_getattr(holderobj, name, None)
|
||||
marker = getfixturemarker(obj)
|
||||
# fixture functions have a pytest_funcarg__ prefix (pre-2.3 style)
|
||||
# or are "@pytest.fixture" marked
|
||||
marker = getfixturemarker(obj)
|
||||
if marker is None:
|
||||
if not name.startswith(self._argprefix):
|
||||
continue
|
||||
if not callable(obj):
|
||||
continue
|
||||
marker = defaultfuncargprefixmarker
|
||||
from _pytest import deprecated
|
||||
|
||||
self.config.warn(
|
||||
"C1", deprecated.FUNCARG_PREFIX.format(name=name), nodeid=nodeid
|
||||
filename, lineno = getfslineno(obj)
|
||||
warnings.warn_explicit(
|
||||
RemovedInPytest4Warning(
|
||||
deprecated.FUNCARG_PREFIX.format(name=name)
|
||||
),
|
||||
category=None,
|
||||
filename=str(filename),
|
||||
lineno=lineno + 1,
|
||||
)
|
||||
name = name[len(self._argprefix) :]
|
||||
elif not isinstance(marker, FixtureFunctionMarker):
|
||||
@@ -1179,6 +1301,15 @@ class FixtureManager(object):
|
||||
name = marker.name
|
||||
assert not name.startswith(self._argprefix), FIXTURE_MSG.format(name)
|
||||
|
||||
# during fixture definition we wrap the original fixture function
|
||||
# to issue a warning if called directly, so here we unwrap it in order to not emit the warning
|
||||
# when pytest itself calls the fixture function
|
||||
if six.PY2 and unittest:
|
||||
# hack on Python 2 because of the unbound methods
|
||||
obj = get_real_func(obj)
|
||||
else:
|
||||
obj = get_real_method(obj, holderobj)
|
||||
|
||||
fixture_def = FixtureDef(
|
||||
self,
|
||||
nodeid,
|
||||
|
||||
@@ -156,6 +156,7 @@ def showhelp(config):
|
||||
vars = [
|
||||
("PYTEST_ADDOPTS", "extra command line options"),
|
||||
("PYTEST_PLUGINS", "comma-separated plugins to load during startup"),
|
||||
("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "set to disable plugin auto-loading"),
|
||||
("PYTEST_DEBUG", "set to enable debug tracing of pytest's internals"),
|
||||
]
|
||||
for name, help in vars:
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
||||
|
||||
from pluggy import HookspecMarker
|
||||
from .deprecated import PYTEST_NAMESPACE
|
||||
|
||||
|
||||
hookspec = HookspecMarker("pytest")
|
||||
|
||||
@@ -22,10 +24,9 @@ def pytest_addhooks(pluginmanager):
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
@hookspec(historic=True, warn_on_impl=PYTEST_NAMESPACE)
|
||||
def pytest_namespace():
|
||||
"""
|
||||
(**Deprecated**) this hook causes direct monkeypatching on pytest, its use is strongly discouraged
|
||||
return dict of name->object to be made globally available in
|
||||
the pytest namespace.
|
||||
|
||||
@@ -33,6 +34,19 @@ def pytest_namespace():
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
|
||||
.. warning::
|
||||
This hook has been **deprecated** and will be removed in pytest 4.0.
|
||||
|
||||
Plugins whose users depend on the current namespace functionality should prepare to migrate to a
|
||||
namespace they actually own.
|
||||
|
||||
To support the migration its suggested to trigger ``DeprecationWarnings`` for objects they put into the
|
||||
pytest namespace.
|
||||
|
||||
An stopgap measure to avoid the warning is to monkeypatch the ``pytest`` module, but just as the
|
||||
``pytest_namespace`` hook this should be seen as a temporary measure to be removed in future versions after
|
||||
an appropriate transition period.
|
||||
"""
|
||||
|
||||
|
||||
@@ -512,7 +526,17 @@ def pytest_terminal_summary(terminalreporter, exitstatus):
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_logwarning(message, code, nodeid, fslocation):
|
||||
""" process a warning specified by a message, a code string,
|
||||
"""
|
||||
.. deprecated:: 3.8
|
||||
|
||||
This hook is will stop working in a future release.
|
||||
|
||||
pytest no longer triggers this hook, but the
|
||||
terminal writer still implements it to display warnings issued by
|
||||
:meth:`_pytest.config.Config.warn` and :meth:`_pytest.nodes.Node.warn`. Calling those functions will be
|
||||
an error in future releases.
|
||||
|
||||
process a warning specified by a message, a code string,
|
||||
a nodeid and fslocation (both of which may be None
|
||||
if the warning is not tied to a particular node/location).
|
||||
|
||||
@@ -521,6 +545,30 @@ def pytest_logwarning(message, code, nodeid, fslocation):
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_warning_captured(warning_message, when, item):
|
||||
"""
|
||||
Process a warning captured by the internal pytest warnings plugin.
|
||||
|
||||
:param warnings.WarningMessage warning_message:
|
||||
The captured warning. This is the same object produced by :py:func:`warnings.catch_warnings`, and contains
|
||||
the same attributes as the parameters of :py:func:`warnings.showwarning`.
|
||||
|
||||
:param str when:
|
||||
Indicates when the warning was captured. Possible values:
|
||||
|
||||
* ``"config"``: during pytest configuration/initialization stage.
|
||||
* ``"collect"``: during test collection.
|
||||
* ``"runtest"``: during test execution.
|
||||
|
||||
:param pytest.Item|None item:
|
||||
**DEPRECATED**: This parameter is incompatible with ``pytest-xdist``, and will always receive ``None``
|
||||
in a future release.
|
||||
|
||||
The item being executed if ``when`` is ``"runtest"``, otherwise ``None``.
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# doctest hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -258,12 +258,11 @@ def record_property(request):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def record_xml_property(record_property):
|
||||
def record_xml_property(record_property, request):
|
||||
"""(Deprecated) use record_property."""
|
||||
import warnings
|
||||
from _pytest import deprecated
|
||||
|
||||
warnings.warn(deprecated.RECORD_XML_PROPERTY, DeprecationWarning, stacklevel=2)
|
||||
request.node.warn(deprecated.RECORD_XML_PROPERTY)
|
||||
|
||||
return record_property
|
||||
|
||||
@@ -274,9 +273,9 @@ def record_xml_attribute(request):
|
||||
The fixture is callable with ``(name, value)``, with value being
|
||||
automatically xml-encoded
|
||||
"""
|
||||
request.node.warn(
|
||||
code="C3", message="record_xml_attribute is an experimental feature"
|
||||
)
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
request.node.warn(PytestWarning("record_xml_attribute is an experimental feature"))
|
||||
xml = getattr(request.config, "_xml", None)
|
||||
if xml is not None:
|
||||
node_reporter = xml.node_reporter(request.node.nodeid)
|
||||
|
||||
@@ -6,6 +6,7 @@ from contextlib import closing, contextmanager
|
||||
import re
|
||||
import six
|
||||
|
||||
from _pytest.compat import dummy_context_manager
|
||||
from _pytest.config import create_terminal_writer
|
||||
import pytest
|
||||
import py
|
||||
@@ -270,6 +271,22 @@ class LogCaptureFixture(object):
|
||||
"""
|
||||
return [(r.name, r.levelno, r.getMessage()) for r in self.records]
|
||||
|
||||
@property
|
||||
def messages(self):
|
||||
"""Returns a list of format-interpolated log messages.
|
||||
|
||||
Unlike 'records', which contains the format string and parameters for interpolation, log messages in this list
|
||||
are all interpolated.
|
||||
Unlike 'text', which contains the output from the handler, log messages in this list are unadorned with
|
||||
levels, timestamps, etc, making exact comparisions more reliable.
|
||||
|
||||
Note that traceback or stack info (from :func:`logging.exception` or the `exc_info` or `stack_info` arguments
|
||||
to the logging functions) is not included, as this is added by the formatter in the handler.
|
||||
|
||||
.. versionadded:: 3.7
|
||||
"""
|
||||
return [r.getMessage() for r in self.records]
|
||||
|
||||
def clear(self):
|
||||
"""Reset the list of log records and the captured log text."""
|
||||
self.handler.reset()
|
||||
@@ -353,11 +370,6 @@ def pytest_configure(config):
|
||||
config.pluginmanager.register(LoggingPlugin(config), "logging-plugin")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _dummy_context_manager():
|
||||
yield
|
||||
|
||||
|
||||
class LoggingPlugin(object):
|
||||
"""Attaches to the logging module and captures log messages for each test.
|
||||
"""
|
||||
@@ -433,8 +445,8 @@ class LoggingPlugin(object):
|
||||
try:
|
||||
yield # run test
|
||||
finally:
|
||||
del item.catch_log_handler
|
||||
if when == "teardown":
|
||||
del item.catch_log_handler
|
||||
del item.catch_log_handlers
|
||||
|
||||
if self.print_logs:
|
||||
@@ -521,7 +533,7 @@ class LoggingPlugin(object):
|
||||
log_cli_handler, formatter=log_cli_formatter, level=log_cli_level
|
||||
)
|
||||
else:
|
||||
self.live_logs_context = _dummy_context_manager()
|
||||
self.live_logs_context = dummy_context_manager()
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
@@ -556,9 +568,12 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
self._test_outcome_written = False
|
||||
|
||||
def emit(self, record):
|
||||
if self.capture_manager is not None:
|
||||
self.capture_manager.suspend_global_capture()
|
||||
try:
|
||||
ctx_manager = (
|
||||
self.capture_manager.global_and_fixture_disabled()
|
||||
if self.capture_manager
|
||||
else dummy_context_manager()
|
||||
)
|
||||
with ctx_manager:
|
||||
if not self._first_record_emitted:
|
||||
self.stream.write("\n")
|
||||
self._first_record_emitted = True
|
||||
@@ -570,6 +585,3 @@ class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
self.stream.section("live log " + self._when, sep="-", bold=True)
|
||||
self._section_name_shown = True
|
||||
logging.StreamHandler.emit(self, record)
|
||||
finally:
|
||||
if self.capture_manager is not None:
|
||||
self.capture_manager.resume_global_capture()
|
||||
|
||||
@@ -383,6 +383,9 @@ class Session(nodes.FSCollector):
|
||||
self.trace = config.trace.root.get("collection")
|
||||
self._norecursepatterns = config.getini("norecursedirs")
|
||||
self.startdir = py.path.local()
|
||||
self._initialpaths = frozenset()
|
||||
# Keep track of any collected nodes in here, so we don't duplicate fixtures
|
||||
self._node_cache = {}
|
||||
|
||||
self.config.pluginmanager.register(self, name="session")
|
||||
|
||||
@@ -439,13 +442,14 @@ class Session(nodes.FSCollector):
|
||||
self.trace("perform_collect", self, args)
|
||||
self.trace.root.indent += 1
|
||||
self._notfound = []
|
||||
self._initialpaths = set()
|
||||
initialpaths = []
|
||||
self._initialparts = []
|
||||
self.items = items = []
|
||||
for arg in args:
|
||||
parts = self._parsearg(arg)
|
||||
self._initialparts.append(parts)
|
||||
self._initialpaths.add(parts[0])
|
||||
initialpaths.append(parts[0])
|
||||
self._initialpaths = frozenset(initialpaths)
|
||||
rep = collect_one_node(self)
|
||||
self.ihook.pytest_collectreport(report=rep)
|
||||
self.trace.root.indent -= 1
|
||||
@@ -480,19 +484,66 @@ class Session(nodes.FSCollector):
|
||||
self.trace.root.indent -= 1
|
||||
|
||||
def _collect(self, arg):
|
||||
from _pytest.python import Package
|
||||
|
||||
names = self._parsearg(arg)
|
||||
path = names.pop(0)
|
||||
if path.check(dir=1):
|
||||
argpath = names.pop(0)
|
||||
paths = []
|
||||
|
||||
root = self
|
||||
# Start with a Session root, and delve to argpath item (dir or file)
|
||||
# and stack all Packages found on the way.
|
||||
# No point in finding packages when collecting doctests
|
||||
if not self.config.option.doctestmodules:
|
||||
for parent in argpath.parts():
|
||||
pm = self.config.pluginmanager
|
||||
if pm._confcutdir and pm._confcutdir.relto(parent):
|
||||
continue
|
||||
|
||||
if parent.isdir():
|
||||
pkginit = parent.join("__init__.py")
|
||||
if pkginit.isfile():
|
||||
if pkginit in self._node_cache:
|
||||
root = self._node_cache[pkginit][0]
|
||||
else:
|
||||
col = root._collectfile(pkginit)
|
||||
if col:
|
||||
if isinstance(col[0], Package):
|
||||
root = col[0]
|
||||
# always store a list in the cache, matchnodes expects it
|
||||
self._node_cache[root.fspath] = [root]
|
||||
|
||||
# If it's a directory argument, recurse and look for any Subpackages.
|
||||
# Let the Package collector deal with subnodes, don't collect here.
|
||||
if argpath.check(dir=1):
|
||||
assert not names, "invalid arg %r" % (arg,)
|
||||
for path in path.visit(
|
||||
for path in argpath.visit(
|
||||
fil=lambda x: x.check(file=1), rec=self._recurse, bf=True, sort=True
|
||||
):
|
||||
for x in self._collectfile(path):
|
||||
yield x
|
||||
pkginit = path.dirpath().join("__init__.py")
|
||||
if pkginit.exists() and not any(x in pkginit.parts() for x in paths):
|
||||
for x in root._collectfile(pkginit):
|
||||
yield x
|
||||
paths.append(x.fspath.dirpath())
|
||||
|
||||
if not any(x in path.parts() for x in paths):
|
||||
for x in root._collectfile(path):
|
||||
if (type(x), x.fspath) in self._node_cache:
|
||||
yield self._node_cache[(type(x), x.fspath)]
|
||||
else:
|
||||
self._node_cache[(type(x), x.fspath)] = x
|
||||
yield x
|
||||
else:
|
||||
assert path.check(file=1)
|
||||
for x in self.matchnodes(self._collectfile(path), names):
|
||||
yield x
|
||||
assert argpath.check(file=1)
|
||||
|
||||
if argpath in self._node_cache:
|
||||
col = self._node_cache[argpath]
|
||||
else:
|
||||
col = root._collectfile(argpath)
|
||||
if col:
|
||||
self._node_cache[argpath] = col
|
||||
for y in self.matchnodes(col, names):
|
||||
yield y
|
||||
|
||||
def _collectfile(self, path):
|
||||
ihook = self.gethookproxy(path)
|
||||
@@ -516,7 +567,6 @@ class Session(nodes.FSCollector):
|
||||
"""Convert a dotted module name to path.
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
with _patched_find_module():
|
||||
loader = pkgutil.find_loader(x)
|
||||
@@ -577,7 +627,12 @@ class Session(nodes.FSCollector):
|
||||
resultnodes.append(node)
|
||||
continue
|
||||
assert isinstance(node, nodes.Collector)
|
||||
rep = collect_one_node(node)
|
||||
key = (type(node), node.nodeid)
|
||||
if key in self._node_cache:
|
||||
rep = self._node_cache[key]
|
||||
else:
|
||||
rep = collect_one_node(node)
|
||||
self._node_cache[key] = rep
|
||||
if rep.passed:
|
||||
has_matched = False
|
||||
for x in rep.result:
|
||||
|
||||
@@ -66,7 +66,10 @@ python_keywords_allowed_list = ["or", "and", "not"]
|
||||
|
||||
def matchmark(colitem, markexpr):
|
||||
"""Tries to match on any marker names, attached to the given colitem."""
|
||||
return eval(markexpr, {}, MarkMapping.from_item(colitem))
|
||||
try:
|
||||
return eval(markexpr, {}, MarkMapping.from_item(colitem))
|
||||
except SyntaxError as e:
|
||||
raise SyntaxError(str(e) + "\nMarker expression must be valid Python!")
|
||||
|
||||
|
||||
def matchkeyword(colitem, keywordexpr):
|
||||
|
||||
@@ -65,7 +65,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||
return cls(values, marks, id_)
|
||||
|
||||
@classmethod
|
||||
def extract_from(cls, parameterset, legacy_force_tuple=False):
|
||||
def extract_from(cls, parameterset, belonging_definition, legacy_force_tuple=False):
|
||||
"""
|
||||
:param parameterset:
|
||||
a legacy style parameterset that may or may not be a tuple,
|
||||
@@ -75,6 +75,7 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||
enforce tuple wrapping so single argument tuple values
|
||||
don't get decomposed and break tests
|
||||
|
||||
:param belonging_definition: the item that we will be extracting the parameters from.
|
||||
"""
|
||||
|
||||
if isinstance(parameterset, cls):
|
||||
@@ -93,20 +94,24 @@ class ParameterSet(namedtuple("ParameterSet", "values, marks, id")):
|
||||
if legacy_force_tuple:
|
||||
argval = (argval,)
|
||||
|
||||
if newmarks:
|
||||
warnings.warn(MARK_PARAMETERSET_UNPACKING)
|
||||
if newmarks and belonging_definition is not None:
|
||||
belonging_definition.warn(MARK_PARAMETERSET_UNPACKING)
|
||||
|
||||
return cls(argval, marks=newmarks, id=None)
|
||||
|
||||
@classmethod
|
||||
def _for_parametrize(cls, argnames, argvalues, func, config):
|
||||
def _for_parametrize(cls, argnames, argvalues, func, config, function_definition):
|
||||
if not isinstance(argnames, (tuple, list)):
|
||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||
force_tuple = len(argnames) == 1
|
||||
else:
|
||||
force_tuple = False
|
||||
parameters = [
|
||||
ParameterSet.extract_from(x, legacy_force_tuple=force_tuple)
|
||||
ParameterSet.extract_from(
|
||||
x,
|
||||
legacy_force_tuple=force_tuple,
|
||||
belonging_definition=function_definition,
|
||||
)
|
||||
for x in argvalues
|
||||
]
|
||||
del argvalues
|
||||
|
||||
@@ -4,9 +4,12 @@ from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
|
||||
import six
|
||||
|
||||
import pytest
|
||||
from _pytest.fixtures import fixture
|
||||
|
||||
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
|
||||
@@ -209,22 +212,41 @@ class MonkeyPatch(object):
|
||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||
del dic[name]
|
||||
|
||||
def _warn_if_env_name_is_not_str(self, name):
|
||||
"""On Python 2, warn if the given environment variable name is not a native str (#4056)"""
|
||||
if six.PY2 and not isinstance(name, str):
|
||||
warnings.warn(
|
||||
pytest.PytestWarning(
|
||||
"Environment variable name {!r} should be str".format(name)
|
||||
)
|
||||
)
|
||||
|
||||
def setenv(self, name, value, prepend=None):
|
||||
""" Set environment variable ``name`` to ``value``. If ``prepend``
|
||||
is a character, read the current environment variable value
|
||||
and prepend the ``value`` adjoined with the ``prepend`` character."""
|
||||
value = str(value)
|
||||
if not isinstance(value, str):
|
||||
warnings.warn(
|
||||
pytest.PytestWarning(
|
||||
"Environment variable value {!r} should be str, converted to str implicitly".format(
|
||||
value
|
||||
)
|
||||
)
|
||||
)
|
||||
value = str(value)
|
||||
if prepend and name in os.environ:
|
||||
value = value + prepend + os.environ[name]
|
||||
self._warn_if_env_name_is_not_str(name)
|
||||
self.setitem(os.environ, name, value)
|
||||
|
||||
def delenv(self, name, raising=True):
|
||||
""" Delete ``name`` from the environment. Raise KeyError it does not
|
||||
exist.
|
||||
""" Delete ``name`` from the environment. Raise KeyError if it does
|
||||
not exist.
|
||||
|
||||
If ``raising`` is set to False, no exception will be raised if the
|
||||
environment variable is missing.
|
||||
"""
|
||||
self._warn_if_env_name_is_not_str(name)
|
||||
self.delitem(os.environ, name, raising=raising)
|
||||
|
||||
def syspath_prepend(self, path):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
import warnings
|
||||
|
||||
import six
|
||||
import py
|
||||
@@ -7,6 +8,7 @@ import attr
|
||||
|
||||
import _pytest
|
||||
import _pytest._code
|
||||
from _pytest.compat import getfslineno
|
||||
|
||||
from _pytest.mark.structures import NodeKeywords, MarkInfo
|
||||
|
||||
@@ -134,19 +136,98 @@ class Node(object):
|
||||
def __repr__(self):
|
||||
return "<%s %r>" % (self.__class__.__name__, getattr(self, "name", None))
|
||||
|
||||
def warn(self, code, message):
|
||||
""" generate a warning with the given code and message for this
|
||||
item. """
|
||||
def warn(self, _code_or_warning=None, message=None, code=None):
|
||||
"""Issue a warning for this item.
|
||||
|
||||
Warnings will be displayed after the test session, unless explicitly suppressed.
|
||||
|
||||
This can be called in two forms:
|
||||
|
||||
**Warning instance**
|
||||
|
||||
This was introduced in pytest 3.8 and uses the standard warning mechanism to issue warnings.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
node.warn(PytestWarning("some message"))
|
||||
|
||||
The warning instance must be a subclass of :class:`pytest.PytestWarning`.
|
||||
|
||||
**code/message (deprecated)**
|
||||
|
||||
This form was used in pytest prior to 3.8 and is considered deprecated. Using this form will emit another
|
||||
warning about the deprecation:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
node.warn("CI", "some message")
|
||||
|
||||
:param Union[Warning,str] _code_or_warning:
|
||||
warning instance or warning code (legacy). This parameter receives an underscore for backward
|
||||
compatibility with the legacy code/message form, and will be replaced for something
|
||||
more usual when the legacy form is removed.
|
||||
|
||||
:param Union[str,None] message: message to display when called in the legacy form.
|
||||
:param str code: code for the warning, in legacy form when using keyword arguments.
|
||||
:return:
|
||||
"""
|
||||
if message is None:
|
||||
if _code_or_warning is None:
|
||||
raise ValueError("code_or_warning must be given")
|
||||
self._std_warn(_code_or_warning)
|
||||
else:
|
||||
if _code_or_warning and code:
|
||||
raise ValueError(
|
||||
"code_or_warning and code cannot both be passed to this function"
|
||||
)
|
||||
code = _code_or_warning or code
|
||||
self._legacy_warn(code, message)
|
||||
|
||||
def _legacy_warn(self, code, message):
|
||||
"""
|
||||
.. deprecated:: 3.8
|
||||
|
||||
Use :meth:`Node.std_warn <_pytest.nodes.Node.std_warn>` instead.
|
||||
|
||||
Generate a warning with the given code and message for this item.
|
||||
"""
|
||||
from _pytest.deprecated import NODE_WARN
|
||||
|
||||
self._std_warn(NODE_WARN)
|
||||
|
||||
assert isinstance(code, str)
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
fslocation = get_fslocation_from_item(self)
|
||||
self.ihook.pytest_logwarning.call_historic(
|
||||
kwargs=dict(
|
||||
code=code, message=message, nodeid=self.nodeid, fslocation=fslocation
|
||||
)
|
||||
)
|
||||
|
||||
def _std_warn(self, warning):
|
||||
"""Issue a warning for this item.
|
||||
|
||||
Warnings will be displayed after the test session, unless explicitly suppressed
|
||||
|
||||
:param Warning warning: the warning instance to issue. Must be a subclass of PytestWarning.
|
||||
|
||||
:raise ValueError: if ``warning`` instance is not a subclass of PytestWarning.
|
||||
"""
|
||||
from _pytest.warning_types import PytestWarning
|
||||
|
||||
if not isinstance(warning, PytestWarning):
|
||||
raise ValueError(
|
||||
"warning must be an instance of PytestWarning or subclass, got {!r}".format(
|
||||
warning
|
||||
)
|
||||
)
|
||||
path, lineno = get_fslocation_from_item(self)
|
||||
warnings.warn_explicit(
|
||||
warning,
|
||||
category=None,
|
||||
filename=str(path),
|
||||
lineno=lineno + 1 if lineno is not None else None,
|
||||
)
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
@@ -173,10 +254,13 @@ class Node(object):
|
||||
chain.reverse()
|
||||
return chain
|
||||
|
||||
def add_marker(self, marker):
|
||||
def add_marker(self, marker, append=True):
|
||||
"""dynamically add a marker object to the node.
|
||||
|
||||
:type marker: str or pytest.mark.*
|
||||
:type marker: ``str`` or ``pytest.mark.*`` object
|
||||
:param marker:
|
||||
``append=True`` whether to append the marker,
|
||||
if ``False`` insert at position ``0``.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator, MARK_GEN
|
||||
|
||||
@@ -185,7 +269,10 @@ class Node(object):
|
||||
elif not isinstance(marker, MarkDecorator):
|
||||
raise ValueError("is not a string or pytest.mark.* Marker")
|
||||
self.keywords[marker.name] = marker
|
||||
self.own_markers.append(marker.mark)
|
||||
if append:
|
||||
self.own_markers.append(marker.mark)
|
||||
else:
|
||||
self.own_markers.insert(0, marker.mark)
|
||||
|
||||
def iter_markers(self, name=None):
|
||||
"""
|
||||
@@ -281,6 +368,11 @@ class Node(object):
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
if self.config.option.verbose > 1:
|
||||
truncate_locals = False
|
||||
else:
|
||||
truncate_locals = True
|
||||
|
||||
try:
|
||||
os.getcwd()
|
||||
abspath = False
|
||||
@@ -293,11 +385,30 @@ class Node(object):
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style,
|
||||
tbfilter=tbfilter,
|
||||
truncate_locals=truncate_locals,
|
||||
)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
|
||||
def get_fslocation_from_item(item):
|
||||
"""Tries to extract the actual location from an item, depending on available attributes:
|
||||
|
||||
* "fslocation": a pair (path, lineno)
|
||||
* "obj": a Python object that the item wraps.
|
||||
* "fspath": just a path
|
||||
|
||||
:rtype: a tuple of (str|LocalPath, int) with filename and line number.
|
||||
"""
|
||||
result = getattr(item, "location", None)
|
||||
if result is not None:
|
||||
return result[:2]
|
||||
obj = getattr(item, "obj", None)
|
||||
if obj is not None:
|
||||
return getfslineno(obj)
|
||||
return getattr(item, "fspath", "unknown location"), -1
|
||||
|
||||
|
||||
class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
@@ -352,7 +463,7 @@ class FSCollector(Collector):
|
||||
|
||||
if not nodeid:
|
||||
nodeid = _check_initialpaths_for_relpath(session, fspath)
|
||||
if os.sep != SEP:
|
||||
if nodeid and os.sep != SEP:
|
||||
nodeid = nodeid.replace(os.sep, SEP)
|
||||
|
||||
super(FSCollector, self).__init__(
|
||||
|
||||
@@ -3,7 +3,6 @@ exception classes and constants handling test outcomes
|
||||
as well as functions creating them
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import py
|
||||
import sys
|
||||
|
||||
|
||||
@@ -21,7 +20,7 @@ class OutcomeException(BaseException):
|
||||
if self.msg:
|
||||
val = self.msg
|
||||
if isinstance(val, bytes):
|
||||
val = py._builtin._totext(val, errors="replace")
|
||||
val = val.decode("UTF-8", errors="replace")
|
||||
return val
|
||||
return "<%s instance>" % (self.__class__.__name__,)
|
||||
|
||||
@@ -68,13 +67,19 @@ exit.Exception = Exit
|
||||
|
||||
|
||||
def skip(msg="", **kwargs):
|
||||
""" skip an executing test with the given message. Note: it's usually
|
||||
better to use the pytest.mark.skipif marker to declare a test to be
|
||||
skipped under certain conditions like mismatching platforms or
|
||||
dependencies. See the pytest_skipping plugin for details.
|
||||
"""
|
||||
Skip an executing test with the given message.
|
||||
|
||||
This function should be called only during testing (setup, call or teardown) or
|
||||
during collection by using the ``allow_module_level`` flag.
|
||||
|
||||
:kwarg bool allow_module_level: allows this function to be called at
|
||||
module level, skipping the rest of the module. Default to False.
|
||||
|
||||
.. note::
|
||||
It is better to use the :ref:`pytest.mark.skipif ref` marker when possible to declare a test to be
|
||||
skipped under certain conditions like mismatching platforms or
|
||||
dependencies.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
allow_module_level = kwargs.pop("allow_module_level", False)
|
||||
@@ -88,10 +93,12 @@ skip.Exception = Skipped
|
||||
|
||||
|
||||
def fail(msg="", pytrace=True):
|
||||
""" explicitly fail a currently-executing test with the given Message.
|
||||
"""
|
||||
Explicitly fail an executing test with the given message.
|
||||
|
||||
:arg pytrace: if false the msg represents the full failure information
|
||||
and no python traceback will be reported.
|
||||
:param str msg: the message to show the user as reason for the failure.
|
||||
:param bool pytrace: if false the msg represents the full failure information and no
|
||||
python traceback will be reported.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise Failed(msg=msg, pytrace=pytrace)
|
||||
@@ -105,7 +112,15 @@ class XFailed(fail.Exception):
|
||||
|
||||
|
||||
def xfail(reason=""):
|
||||
""" xfail an executing test or setup functions with the given reason."""
|
||||
"""
|
||||
Imperatively xfail an executing test or setup functions with the given reason.
|
||||
|
||||
This function should be called only during testing (setup, call or teardown).
|
||||
|
||||
.. note::
|
||||
It is better to use the :ref:`pytest.mark.xfail ref` marker when possible to declare a test to be
|
||||
xfailed under certain conditions like known bugs or missing features.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise XFailed(reason)
|
||||
|
||||
|
||||
52
src/_pytest/paths.py
Normal file
52
src/_pytest/paths.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from os.path import expanduser, expandvars, isabs, sep
|
||||
from posixpath import sep as posix_sep
|
||||
import fnmatch
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
from .compat import Path, PurePath
|
||||
|
||||
|
||||
def resolve_from_str(input, root):
|
||||
assert not isinstance(input, Path), "would break on py2"
|
||||
root = Path(root)
|
||||
input = expanduser(input)
|
||||
input = expandvars(input)
|
||||
if isabs(input):
|
||||
return Path(input)
|
||||
else:
|
||||
return root.joinpath(input)
|
||||
|
||||
|
||||
def fnmatch_ex(pattern, path):
|
||||
"""FNMatcher port from py.path.common which works with PurePath() instances.
|
||||
|
||||
The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions
|
||||
for each part of the path, while this algorithm uses the whole path instead.
|
||||
|
||||
For example:
|
||||
"tests/foo/bar/doc/test_foo.py" matches pattern "tests/**/doc/test*.py" with this algorithm, but not with
|
||||
PurePath.match().
|
||||
|
||||
This algorithm was ported to keep backward-compatibility with existing settings which assume paths match according
|
||||
this logic.
|
||||
|
||||
References:
|
||||
* https://bugs.python.org/issue29249
|
||||
* https://bugs.python.org/issue34731
|
||||
"""
|
||||
path = PurePath(path)
|
||||
iswin32 = sys.platform.startswith("win")
|
||||
|
||||
if iswin32 and sep not in pattern and posix_sep in pattern:
|
||||
# Running on Windows, the pattern has no Windows path separators,
|
||||
# and the pattern has one or more Posix path separators. Replace
|
||||
# the Posix path separators with the Windows path separator.
|
||||
pattern = pattern.replace(posix_sep, sep)
|
||||
|
||||
if sep not in pattern:
|
||||
name = path.name
|
||||
else:
|
||||
name = six.text_type(path)
|
||||
return fnmatch.fnmatch(name, pattern)
|
||||
@@ -21,7 +21,8 @@ import py
|
||||
import pytest
|
||||
from _pytest.main import Session, EXIT_OK
|
||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||
|
||||
from _pytest.compat import Path
|
||||
from _pytest.compat import safe_str
|
||||
|
||||
IGNORE_PAM = [ # filenames added when obtaining details about the current user
|
||||
u"/var/lib/sss/mc/passwd"
|
||||
@@ -34,7 +35,7 @@ def pytest_addoption(parser):
|
||||
action="store_true",
|
||||
dest="lsof",
|
||||
default=False,
|
||||
help=("run FD checks if lsof is available"),
|
||||
help="run FD checks if lsof is available",
|
||||
)
|
||||
|
||||
parser.addoption(
|
||||
@@ -48,6 +49,10 @@ def pytest_addoption(parser):
|
||||
),
|
||||
)
|
||||
|
||||
parser.addini(
|
||||
"pytester_example_dir", help="directory to take the pytester example files from"
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.getvalue("lsof"):
|
||||
@@ -121,7 +126,7 @@ class LsofFdLeakChecker(object):
|
||||
error.append(error[0])
|
||||
error.append("*** function %s:%s: %s " % item.location)
|
||||
error.append("See issue #2366")
|
||||
item.warn("", "\n".join(error))
|
||||
item.warn(pytest.PytestWarning("\n".join(error)))
|
||||
|
||||
|
||||
# XXX copied from execnet's conftest.py - needs to be merged
|
||||
@@ -269,7 +274,7 @@ class HookRecorder(object):
|
||||
del self.calls[i]
|
||||
return call
|
||||
lines = ["could not find call %r, in:" % (name,)]
|
||||
lines.extend([" %s" % str(x) for x in self.calls])
|
||||
lines.extend([" %s" % x for x in self.calls])
|
||||
pytest.fail("\n".join(lines))
|
||||
|
||||
def getcall(self, name):
|
||||
@@ -402,7 +407,9 @@ class RunResult(object):
|
||||
return d
|
||||
raise ValueError("Pytest terminal report not found")
|
||||
|
||||
def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0):
|
||||
def assert_outcomes(
|
||||
self, passed=0, skipped=0, failed=0, error=0, xpassed=0, xfailed=0
|
||||
):
|
||||
"""Assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run.
|
||||
|
||||
@@ -413,10 +420,18 @@ class RunResult(object):
|
||||
"skipped": d.get("skipped", 0),
|
||||
"failed": d.get("failed", 0),
|
||||
"error": d.get("error", 0),
|
||||
"xpassed": d.get("xpassed", 0),
|
||||
"xfailed": d.get("xfailed", 0),
|
||||
}
|
||||
assert obtained == dict(
|
||||
passed=passed, skipped=skipped, failed=failed, error=error
|
||||
)
|
||||
expected = {
|
||||
"passed": passed,
|
||||
"skipped": skipped,
|
||||
"failed": failed,
|
||||
"error": error,
|
||||
"xpassed": xpassed,
|
||||
"xfailed": xfailed,
|
||||
}
|
||||
assert obtained == expected
|
||||
|
||||
|
||||
class CwdSnapshot(object):
|
||||
@@ -510,7 +525,6 @@ class Testdir(object):
|
||||
|
||||
def make_hook_recorder(self, pluginmanager):
|
||||
"""Create a new :py:class:`HookRecorder` for a PluginManager."""
|
||||
assert not hasattr(pluginmanager, "reprec")
|
||||
pluginmanager.reprec = reprec = HookRecorder(pluginmanager)
|
||||
self.request.addfinalizer(reprec.finish_recording)
|
||||
return reprec
|
||||
@@ -546,18 +560,22 @@ class Testdir(object):
|
||||
return ret
|
||||
|
||||
def makefile(self, ext, *args, **kwargs):
|
||||
"""Create a new file in the testdir.
|
||||
r"""Create new file(s) in the testdir.
|
||||
|
||||
ext: The extension the file should use, including the dot, e.g. `.py`.
|
||||
|
||||
args: All args will be treated as strings and joined using newlines.
|
||||
:param str ext: The extension the file(s) should use, including the dot, e.g. `.py`.
|
||||
:param list[str] args: All args will be treated as strings and joined using newlines.
|
||||
The result will be written as contents to the file. The name of the
|
||||
file will be based on the test function requesting this fixture.
|
||||
E.g. "testdir.makefile('.txt', 'line1', 'line2')"
|
||||
|
||||
kwargs: Each keyword is the name of a file, while the value of it will
|
||||
:param kwargs: Each keyword is the name of a file, while the value of it will
|
||||
be written as contents of the file.
|
||||
E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')"
|
||||
|
||||
Examples:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
testdir.makefile(".txt", "line1", "line2")
|
||||
|
||||
testdir.makefile(".ini", pytest="[pytest]\naddopts=-rs\n")
|
||||
|
||||
"""
|
||||
return self._makefile(ext, args, kwargs)
|
||||
@@ -623,6 +641,50 @@ class Testdir(object):
|
||||
p.ensure("__init__.py")
|
||||
return p
|
||||
|
||||
def copy_example(self, name=None):
|
||||
import warnings
|
||||
from _pytest.warning_types import PYTESTER_COPY_EXAMPLE
|
||||
|
||||
warnings.warn(PYTESTER_COPY_EXAMPLE, stacklevel=2)
|
||||
example_dir = self.request.config.getini("pytester_example_dir")
|
||||
if example_dir is None:
|
||||
raise ValueError("pytester_example_dir is unset, can't copy examples")
|
||||
example_dir = self.request.config.rootdir.join(example_dir)
|
||||
|
||||
for extra_element in self.request.node.iter_markers("pytester_example_path"):
|
||||
assert extra_element.args
|
||||
example_dir = example_dir.join(*extra_element.args)
|
||||
|
||||
if name is None:
|
||||
func_name = self.request.function.__name__
|
||||
maybe_dir = example_dir / func_name
|
||||
maybe_file = example_dir / (func_name + ".py")
|
||||
|
||||
if maybe_dir.isdir():
|
||||
example_path = maybe_dir
|
||||
elif maybe_file.isfile():
|
||||
example_path = maybe_file
|
||||
else:
|
||||
raise LookupError(
|
||||
"{} cant be found as module or package in {}".format(
|
||||
func_name, example_dir.bestrelpath(self.request.confg.rootdir)
|
||||
)
|
||||
)
|
||||
else:
|
||||
example_path = example_dir.join(name)
|
||||
|
||||
if example_path.isdir() and not example_path.join("__init__.py").isfile():
|
||||
example_path.copy(self.tmpdir)
|
||||
return self.tmpdir
|
||||
elif example_path.isfile():
|
||||
result = self.tmpdir.join(example_path.basename)
|
||||
example_path.copy(result)
|
||||
return result
|
||||
else:
|
||||
raise LookupError(
|
||||
'example "{}" is not found as a file or directory'.format(example_path)
|
||||
)
|
||||
|
||||
Session = Session
|
||||
|
||||
def getnode(self, config, arg):
|
||||
@@ -835,14 +897,12 @@ class Testdir(object):
|
||||
return self._runpytest_method(*args, **kwargs)
|
||||
|
||||
def _ensure_basetemp(self, args):
|
||||
args = [str(x) for x in args]
|
||||
args = list(args)
|
||||
for x in args:
|
||||
if str(x).startswith("--basetemp"):
|
||||
# print("basedtemp exists: %s" %(args,))
|
||||
if safe_str(x).startswith("--basetemp"):
|
||||
break
|
||||
else:
|
||||
args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp"))
|
||||
# print("added basetemp: %s" %(args,))
|
||||
return args
|
||||
|
||||
def parseconfig(self, *args):
|
||||
@@ -929,14 +989,16 @@ class Testdir(object):
|
||||
same directory to ensure it is a package
|
||||
|
||||
"""
|
||||
kw = {self.request.function.__name__: Source(source).strip()}
|
||||
path = self.makepyfile(**kw)
|
||||
if isinstance(source, Path):
|
||||
path = self.tmpdir.join(str(source))
|
||||
assert not withinit, "not supported for paths"
|
||||
else:
|
||||
kw = {self.request.function.__name__: Source(source).strip()}
|
||||
path = self.makepyfile(**kw)
|
||||
if withinit:
|
||||
self.makepyfile(__init__="#")
|
||||
self.config = config = self.parseconfigure(path, *configargs)
|
||||
node = self.getnode(config, path)
|
||||
|
||||
return node
|
||||
return self.getnode(config, path)
|
||||
|
||||
def collect_by_name(self, modcol, name):
|
||||
"""Return the collection node for name from the module collection.
|
||||
@@ -966,7 +1028,7 @@ class Testdir(object):
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = os.pathsep.join(
|
||||
filter(None, [str(os.getcwd()), env.get("PYTHONPATH", "")])
|
||||
filter(None, [os.getcwd(), env.get("PYTHONPATH", "")])
|
||||
)
|
||||
kw["env"] = env
|
||||
|
||||
@@ -985,14 +1047,13 @@ class Testdir(object):
|
||||
Returns a :py:class:`RunResult`.
|
||||
|
||||
"""
|
||||
return self._run(*cmdargs)
|
||||
|
||||
def _run(self, *cmdargs):
|
||||
cmdargs = [str(x) for x in cmdargs]
|
||||
cmdargs = [
|
||||
str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs
|
||||
]
|
||||
p1 = self.tmpdir.join("stdout")
|
||||
p2 = self.tmpdir.join("stderr")
|
||||
print("running:", " ".join(cmdargs))
|
||||
print(" in:", str(py.path.local()))
|
||||
print("running:", *cmdargs)
|
||||
print(" in:", py.path.local())
|
||||
f1 = codecs.open(str(p1), "w", encoding="utf8")
|
||||
f2 = codecs.open(str(p2), "w", encoding="utf8")
|
||||
try:
|
||||
@@ -1024,7 +1085,7 @@ class Testdir(object):
|
||||
print("couldn't print to %s because of encoding" % (fp,))
|
||||
|
||||
def _getpytestargs(self):
|
||||
return (sys.executable, "-mpytest")
|
||||
return sys.executable, "-mpytest"
|
||||
|
||||
def runpython(self, script):
|
||||
"""Run a python script using sys.executable as interpreter.
|
||||
|
||||
@@ -12,6 +12,7 @@ from textwrap import dedent
|
||||
|
||||
import py
|
||||
import six
|
||||
from _pytest.main import FSHookProxy
|
||||
from _pytest.mark import MarkerError
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
@@ -36,6 +37,7 @@ from _pytest.compat import (
|
||||
getlocation,
|
||||
enum,
|
||||
get_default_arg_names,
|
||||
getimfunc,
|
||||
)
|
||||
from _pytest.outcomes import fail
|
||||
from _pytest.mark.structures import (
|
||||
@@ -43,7 +45,7 @@ from _pytest.mark.structures import (
|
||||
get_unpacked_marks,
|
||||
normalize_mark_list,
|
||||
)
|
||||
|
||||
from _pytest.warning_types import RemovedInPytest4Warning, PytestWarning
|
||||
|
||||
# relative paths that we use to filter traceback entries from appearing to the user;
|
||||
# see filter_traceback
|
||||
@@ -172,13 +174,14 @@ def pytest_configure(config):
|
||||
"or a list of tuples of values if argnames specifies multiple names. "
|
||||
"Example: @parametrize('arg1', [1,2]) would lead to two calls of the "
|
||||
"decorated test function, one with arg1=1 and another with arg1=2."
|
||||
"see http://pytest.org/latest/parametrize.html for more info and "
|
||||
"examples.",
|
||||
"see https://docs.pytest.org/en/latest/parametrize.html for more info "
|
||||
"and examples.",
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"usefixtures(fixturename1, fixturename2, ...): mark tests as needing "
|
||||
"all of the specified fixtures. see http://pytest.org/latest/fixture.html#usefixtures ",
|
||||
"all of the specified fixtures. see "
|
||||
"https://docs.pytest.org/en/latest/fixture.html#usefixtures ",
|
||||
)
|
||||
|
||||
|
||||
@@ -200,16 +203,22 @@ def pytest_collect_file(path, parent):
|
||||
ext = path.ext
|
||||
if ext == ".py":
|
||||
if not parent.session.isinitpath(path):
|
||||
for pat in parent.config.getini("python_files"):
|
||||
if path.fnmatch(pat):
|
||||
break
|
||||
else:
|
||||
if not path_matches_patterns(
|
||||
path, parent.config.getini("python_files") + ["__init__.py"]
|
||||
):
|
||||
return
|
||||
ihook = parent.session.gethookproxy(path)
|
||||
return ihook.pytest_pycollect_makemodule(path=path, parent=parent)
|
||||
|
||||
|
||||
def path_matches_patterns(path, patterns):
|
||||
"""Returns True if the given py.path.local matches one of the patterns in the list of globs given"""
|
||||
return any(path.fnmatch(pattern) for pattern in patterns)
|
||||
|
||||
|
||||
def pytest_pycollect_makemodule(path, parent):
|
||||
if path.basename == "__init__.py":
|
||||
return Package(path, parent)
|
||||
return Module(path, parent)
|
||||
|
||||
|
||||
@@ -231,9 +240,14 @@ def pytest_pycollect_makeitem(collector, name, obj):
|
||||
# or a funtools.wrapped.
|
||||
# We musn't if it's been wrapped with mock.patch (python 2 only)
|
||||
if not (isfunction(obj) or isfunction(get_real_func(obj))):
|
||||
collector.warn(
|
||||
code="C2",
|
||||
message="cannot collect %r because it is not a function." % name,
|
||||
filename, lineno = getfslineno(obj)
|
||||
warnings.warn_explicit(
|
||||
message=PytestWarning(
|
||||
"cannot collect %r because it is not a function." % name
|
||||
),
|
||||
category=None,
|
||||
filename=str(filename),
|
||||
lineno=lineno + 1,
|
||||
)
|
||||
elif getattr(obj, "__test__", True):
|
||||
if is_generator(obj):
|
||||
@@ -341,11 +355,6 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
if isinstance(obj, staticmethod):
|
||||
# static methods need to be unwrapped
|
||||
obj = safe_getattr(obj, "__func__", False)
|
||||
if obj is False:
|
||||
# Python 2.6 wraps in a different way that we won't try to handle
|
||||
msg = "cannot collect static method %r because it is not a function"
|
||||
self.warn(code="C2", message=msg % name)
|
||||
return False
|
||||
return (
|
||||
safe_getattr(obj, "__call__", False)
|
||||
and fixtures.getfixturemarker(obj) is None
|
||||
@@ -438,6 +447,11 @@ class PyCollector(PyobjMixin, nodes.Collector):
|
||||
# add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs
|
||||
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
|
||||
|
||||
# add_funcarg_pseudo_fixture_def may have shadowed some fixtures
|
||||
# with direct parametrization, so make sure we update what the
|
||||
# function really needs.
|
||||
fixtureinfo.prune_dependency_tree()
|
||||
|
||||
for callspec in metafunc._calls:
|
||||
subname = "%s[%s]" % (name, callspec.id)
|
||||
yield Function(
|
||||
@@ -525,6 +539,92 @@ class Module(nodes.File, PyCollector):
|
||||
self.addfinalizer(teardown_module)
|
||||
|
||||
|
||||
class Package(Module):
|
||||
def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None):
|
||||
session = parent.session
|
||||
nodes.FSCollector.__init__(
|
||||
self, fspath, parent=parent, config=config, session=session, nodeid=nodeid
|
||||
)
|
||||
self.name = fspath.dirname
|
||||
self.trace = session.trace
|
||||
self._norecursepatterns = session._norecursepatterns
|
||||
self.fspath = fspath
|
||||
|
||||
def _recurse(self, path):
|
||||
ihook = self.gethookproxy(path.dirpath())
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
return False
|
||||
for pat in self._norecursepatterns:
|
||||
if path.check(fnmatch=pat):
|
||||
return False
|
||||
ihook = self.gethookproxy(path)
|
||||
ihook.pytest_collect_directory(path=path, parent=self)
|
||||
return True
|
||||
|
||||
def gethookproxy(self, fspath):
|
||||
# check if we have the common case of running
|
||||
# hooks with all conftest.py filesall conftest.py
|
||||
pm = self.config.pluginmanager
|
||||
my_conftestmodules = pm._getconftestmodules(fspath)
|
||||
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
||||
if remove_mods:
|
||||
# one or more conftests are not in use at this fspath
|
||||
proxy = FSHookProxy(fspath, pm, remove_mods)
|
||||
else:
|
||||
# all plugis are active for this fspath
|
||||
proxy = self.config.hook
|
||||
return proxy
|
||||
|
||||
def _collectfile(self, path):
|
||||
ihook = self.gethookproxy(path)
|
||||
if not self.isinitpath(path):
|
||||
if ihook.pytest_ignore_collect(path=path, config=self.config):
|
||||
return ()
|
||||
return ihook.pytest_collect_file(path=path, parent=self)
|
||||
|
||||
def isinitpath(self, path):
|
||||
return path in self.session._initialpaths
|
||||
|
||||
def collect(self):
|
||||
# XXX: HACK!
|
||||
# Before starting to collect any files from this package we need
|
||||
# to cleanup the duplicate paths added by the session's collect().
|
||||
# Proper fix is to not track these as duplicates in the first place.
|
||||
for path in list(self.session.config.pluginmanager._duplicatepaths):
|
||||
# if path.parts()[:len(self.fspath.dirpath().parts())] == self.fspath.dirpath().parts():
|
||||
if path.dirname.startswith(self.name):
|
||||
self.session.config.pluginmanager._duplicatepaths.remove(path)
|
||||
|
||||
this_path = self.fspath.dirpath()
|
||||
init_module = this_path.join("__init__.py")
|
||||
if init_module.check(file=1) and path_matches_patterns(
|
||||
init_module, self.config.getini("python_files")
|
||||
):
|
||||
yield Module(init_module, self)
|
||||
pkg_prefixes = set()
|
||||
for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
|
||||
# we will visit our own __init__.py file, in which case we skip it
|
||||
skip = False
|
||||
if path.basename == "__init__.py" and path.dirpath() == this_path:
|
||||
continue
|
||||
|
||||
for pkg_prefix in pkg_prefixes:
|
||||
if (
|
||||
pkg_prefix in path.parts()
|
||||
and pkg_prefix.join("__init__.py") != path
|
||||
):
|
||||
skip = True
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
if path.isdir() and path.join("__init__.py").check(file=1):
|
||||
pkg_prefixes.add(path)
|
||||
|
||||
for x in self._collectfile(path):
|
||||
yield x
|
||||
|
||||
|
||||
def _get_xunit_setup_teardown(holder, attr_name, param_obj=None):
|
||||
"""
|
||||
Return a callable to perform xunit-style setup or teardown if
|
||||
@@ -563,16 +663,18 @@ class Class(PyCollector):
|
||||
return []
|
||||
if hasinit(self.obj):
|
||||
self.warn(
|
||||
"C1",
|
||||
"cannot collect test class %r because it has a "
|
||||
"__init__ constructor" % self.obj.__name__,
|
||||
PytestWarning(
|
||||
"cannot collect test class %r because it has a "
|
||||
"__init__ constructor" % self.obj.__name__
|
||||
)
|
||||
)
|
||||
return []
|
||||
elif hasnew(self.obj):
|
||||
self.warn(
|
||||
"C1",
|
||||
"cannot collect test class %r because it has a "
|
||||
"__new__ constructor" % self.obj.__name__,
|
||||
PytestWarning(
|
||||
"cannot collect test class %r because it has a "
|
||||
"__new__ constructor" % self.obj.__name__
|
||||
)
|
||||
)
|
||||
return []
|
||||
return [self._getcustomclass("Instance")(name="()", parent=self)]
|
||||
@@ -580,14 +682,12 @@ class Class(PyCollector):
|
||||
def setup(self):
|
||||
setup_class = _get_xunit_func(self.obj, "setup_class")
|
||||
if setup_class is not None:
|
||||
setup_class = getattr(setup_class, "im_func", setup_class)
|
||||
setup_class = getattr(setup_class, "__func__", setup_class)
|
||||
setup_class = getimfunc(setup_class)
|
||||
setup_class(self.obj)
|
||||
|
||||
fin_class = getattr(self.obj, "teardown_class", None)
|
||||
if fin_class is not None:
|
||||
fin_class = getattr(fin_class, "im_func", fin_class)
|
||||
fin_class = getattr(fin_class, "__func__", fin_class)
|
||||
fin_class = getimfunc(fin_class)
|
||||
self.addfinalizer(lambda: fin_class(self.obj))
|
||||
|
||||
|
||||
@@ -663,7 +763,7 @@ class FunctionMixin(PyobjMixin):
|
||||
def _repr_failure_py(self, excinfo, style="long"):
|
||||
if excinfo.errisinstance(fail.Exception):
|
||||
if not excinfo.value.pytrace:
|
||||
return py._builtin._totext(excinfo.value)
|
||||
return six.text_type(excinfo.value)
|
||||
return super(FunctionMixin, self)._repr_failure_py(excinfo, style=style)
|
||||
|
||||
def repr_failure(self, excinfo, outerr=None):
|
||||
@@ -700,7 +800,7 @@ class Generator(FunctionMixin, PyCollector):
|
||||
)
|
||||
seen[name] = True
|
||||
values.append(self.Function(name, self, args=args, callobj=call))
|
||||
self.warn("C1", deprecated.YIELD_TESTS)
|
||||
self.warn(deprecated.YIELD_TESTS)
|
||||
return values
|
||||
|
||||
def getcallargs(self, obj):
|
||||
@@ -800,12 +900,13 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
"""
|
||||
|
||||
def __init__(self, definition, fixtureinfo, config, cls=None, module=None):
|
||||
#: access to the :class:`_pytest.config.Config` object for the test session
|
||||
assert (
|
||||
isinstance(definition, FunctionDefinition)
|
||||
or type(definition).__name__ == "DefinitionMock"
|
||||
)
|
||||
self.definition = definition
|
||||
|
||||
#: access to the :class:`_pytest.config.Config` object for the test session
|
||||
self.config = config
|
||||
|
||||
#: the module object where the test function is defined in.
|
||||
@@ -866,7 +967,11 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
from _pytest.mark import ParameterSet
|
||||
|
||||
argnames, parameters = ParameterSet._for_parametrize(
|
||||
argnames, argvalues, self.function, self.config
|
||||
argnames,
|
||||
argvalues,
|
||||
self.function,
|
||||
self.config,
|
||||
function_definition=self.definition,
|
||||
)
|
||||
del argvalues
|
||||
|
||||
@@ -877,7 +982,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
|
||||
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
|
||||
|
||||
ids = self._resolve_arg_ids(argnames, ids, parameters)
|
||||
ids = self._resolve_arg_ids(argnames, ids, parameters, item=self.definition)
|
||||
|
||||
scopenum = scope2index(scope, descr="call to {}".format(self.parametrize))
|
||||
|
||||
@@ -900,13 +1005,14 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
newcalls.append(newcallspec)
|
||||
self._calls = newcalls
|
||||
|
||||
def _resolve_arg_ids(self, argnames, ids, parameters):
|
||||
def _resolve_arg_ids(self, argnames, ids, parameters, item):
|
||||
"""Resolves the actual ids for the given argnames, based on the ``ids`` parameter given
|
||||
to ``parametrize``.
|
||||
|
||||
:param List[str] argnames: list of argument names passed to ``parametrize()``.
|
||||
:param ids: the ids parameter of the parametrized call (see docs).
|
||||
:param List[ParameterSet] parameters: the list of parameter values, same size as ``argnames``.
|
||||
:param Item item: the item that generated this parametrized call.
|
||||
:rtype: List[str]
|
||||
:return: the list of ids for each argname given
|
||||
"""
|
||||
@@ -927,7 +1033,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
raise ValueError(
|
||||
msg % (saferepr(id_value), type(id_value).__name__)
|
||||
)
|
||||
ids = idmaker(argnames, parameters, idfn, ids, self.config)
|
||||
ids = idmaker(argnames, parameters, idfn, ids, self.config, item=item)
|
||||
return ids
|
||||
|
||||
def _resolve_arg_value_types(self, argnames, indirect):
|
||||
@@ -1000,10 +1106,8 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
|
||||
:arg param: a parameter which will be exposed to a later fixture function
|
||||
invocation through the ``request.param`` attribute.
|
||||
"""
|
||||
if self.config:
|
||||
self.config.warn(
|
||||
"C1", message=deprecated.METAFUNC_ADD_CALL, fslocation=None
|
||||
)
|
||||
warnings.warn(deprecated.METAFUNC_ADD_CALL, stacklevel=2)
|
||||
|
||||
assert funcargs is None or isinstance(funcargs, dict)
|
||||
if funcargs is not None:
|
||||
for name in funcargs:
|
||||
@@ -1037,13 +1141,18 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
|
||||
"""
|
||||
from _pytest.fixtures import scopes
|
||||
|
||||
indirect_as_list = isinstance(indirect, (list, tuple))
|
||||
all_arguments_are_fixtures = (
|
||||
indirect is True or indirect_as_list and len(indirect) == argnames
|
||||
)
|
||||
if isinstance(indirect, (list, tuple)):
|
||||
all_arguments_are_fixtures = len(indirect) == len(argnames)
|
||||
else:
|
||||
all_arguments_are_fixtures = bool(indirect)
|
||||
|
||||
if all_arguments_are_fixtures:
|
||||
fixturedefs = arg2fixturedefs or {}
|
||||
used_scopes = [fixturedef[0].scope for name, fixturedef in fixturedefs.items()]
|
||||
used_scopes = [
|
||||
fixturedef[0].scope
|
||||
for name, fixturedef in fixturedefs.items()
|
||||
if name in argnames
|
||||
]
|
||||
if used_scopes:
|
||||
# Takes the most narrow scope from used fixtures
|
||||
for scope in reversed(scopes):
|
||||
@@ -1053,21 +1162,20 @@ def _find_parametrized_scope(argnames, arg2fixturedefs, indirect):
|
||||
return "function"
|
||||
|
||||
|
||||
def _idval(val, argname, idx, idfn, config=None):
|
||||
def _idval(val, argname, idx, idfn, item, config):
|
||||
if idfn:
|
||||
s = None
|
||||
try:
|
||||
s = idfn(val)
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
# See issue https://github.com/pytest-dev/pytest/issues/2169
|
||||
import warnings
|
||||
|
||||
msg = (
|
||||
"Raised while trying to determine id of parameter %s at position %d."
|
||||
% (argname, idx)
|
||||
"While trying to determine id of parameter {} at position "
|
||||
"{} the following exception was raised:\n".format(argname, idx)
|
||||
)
|
||||
msg += "\nUpdate your code as this will raise an error in pytest-4.0."
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
msg += " {}: {}\n".format(type(e).__name__, e)
|
||||
msg += "This warning will be an error error in pytest-4.0."
|
||||
item.warn(RemovedInPytest4Warning(msg))
|
||||
if s:
|
||||
return ascii_escaped(s)
|
||||
|
||||
@@ -1091,12 +1199,12 @@ def _idval(val, argname, idx, idfn, config=None):
|
||||
return str(argname) + str(idx)
|
||||
|
||||
|
||||
def _idvalset(idx, parameterset, argnames, idfn, ids, config=None):
|
||||
def _idvalset(idx, parameterset, argnames, idfn, ids, item, config):
|
||||
if parameterset.id is not None:
|
||||
return parameterset.id
|
||||
if ids is None or (idx >= len(ids) or ids[idx] is None):
|
||||
this_id = [
|
||||
_idval(val, argname, idx, idfn, config)
|
||||
_idval(val, argname, idx, idfn, item=item, config=config)
|
||||
for val, argname in zip(parameterset.values, argnames)
|
||||
]
|
||||
return "-".join(this_id)
|
||||
@@ -1104,9 +1212,9 @@ def _idvalset(idx, parameterset, argnames, idfn, ids, config=None):
|
||||
return ascii_escaped(ids[idx])
|
||||
|
||||
|
||||
def idmaker(argnames, parametersets, idfn=None, ids=None, config=None):
|
||||
def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None):
|
||||
ids = [
|
||||
_idvalset(valindex, parameterset, argnames, idfn, ids, config)
|
||||
_idvalset(valindex, parameterset, argnames, idfn, ids, config=config, item=item)
|
||||
for valindex, parameterset in enumerate(parametersets)
|
||||
]
|
||||
if len(set(ids)) != len(ids):
|
||||
@@ -1329,7 +1437,7 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
|
||||
@property
|
||||
def function(self):
|
||||
"underlying python 'function' object"
|
||||
return getattr(self.obj, "im_func", self.obj)
|
||||
return getimfunc(self.obj)
|
||||
|
||||
def _getobj(self):
|
||||
name = self.name
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import math
|
||||
import pprint
|
||||
import sys
|
||||
from numbers import Number
|
||||
from decimal import Decimal
|
||||
|
||||
import py
|
||||
import six
|
||||
from six.moves import zip, filterfalse
|
||||
from more_itertools.more import always_iterable
|
||||
|
||||
@@ -30,6 +33,15 @@ def _cmp_raises_type_error(self, other):
|
||||
)
|
||||
|
||||
|
||||
def _non_numeric_type_error(value, at):
|
||||
at_str = " at {}".format(at) if at else ""
|
||||
return TypeError(
|
||||
"cannot make approximate comparisons to non-numeric values: {!r} {}".format(
|
||||
value, at_str
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# builtin pytest.approx helper
|
||||
|
||||
|
||||
@@ -39,15 +51,17 @@ class ApproxBase(object):
|
||||
or sequences of numbers.
|
||||
"""
|
||||
|
||||
# Tell numpy to use our `__eq__` operator instead of its
|
||||
# Tell numpy to use our `__eq__` operator instead of its.
|
||||
__array_ufunc__ = None
|
||||
__array_priority__ = 100
|
||||
|
||||
def __init__(self, expected, rel=None, abs=None, nan_ok=False):
|
||||
__tracebackhide__ = True
|
||||
self.expected = expected
|
||||
self.abs = abs
|
||||
self.rel = rel
|
||||
self.nan_ok = nan_ok
|
||||
self._check_type()
|
||||
|
||||
def __repr__(self):
|
||||
raise NotImplementedError
|
||||
@@ -75,21 +89,32 @@ class ApproxBase(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _check_type(self):
|
||||
"""
|
||||
Raise a TypeError if the expected value is not a valid type.
|
||||
"""
|
||||
# This is only a concern if the expected value is a sequence. In every
|
||||
# other case, the approx() function ensures that the expected value has
|
||||
# a numeric type. For this reason, the default is to do nothing. The
|
||||
# classes that deal with sequences should reimplement this method to
|
||||
# raise if there are any non-numeric elements in the sequence.
|
||||
pass
|
||||
|
||||
|
||||
def _recursive_list_map(f, x):
|
||||
if isinstance(x, list):
|
||||
return list(_recursive_list_map(f, xi) for xi in x)
|
||||
else:
|
||||
return f(x)
|
||||
|
||||
|
||||
class ApproxNumpy(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for numpy arrays.
|
||||
Perform approximate comparisons where the expected value is numpy array.
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
# It might be nice to rewrite this function to account for the
|
||||
# shape of the array...
|
||||
import numpy as np
|
||||
|
||||
list_scalars = []
|
||||
for x in np.ndindex(self.expected.shape):
|
||||
list_scalars.append(self._approx_scalar(np.asscalar(self.expected[x])))
|
||||
|
||||
list_scalars = _recursive_list_map(self._approx_scalar, self.expected.tolist())
|
||||
return "approx({!r})".format(list_scalars)
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
@@ -128,8 +153,8 @@ class ApproxNumpy(ApproxBase):
|
||||
|
||||
class ApproxMapping(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for mappings where the values are numbers
|
||||
(the keys can be anything).
|
||||
Perform approximate comparisons where the expected value is a mapping with
|
||||
numeric values (the keys can be anything).
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
@@ -147,10 +172,20 @@ class ApproxMapping(ApproxBase):
|
||||
for k in self.expected.keys():
|
||||
yield actual[k], self.expected[k]
|
||||
|
||||
def _check_type(self):
|
||||
__tracebackhide__ = True
|
||||
for key, value in self.expected.items():
|
||||
if isinstance(value, type(self.expected)):
|
||||
msg = "pytest.approx() does not support nested dictionaries: key={!r} value={!r}\n full mapping={}"
|
||||
raise TypeError(msg.format(key, value, pprint.pformat(self.expected)))
|
||||
elif not isinstance(value, Number):
|
||||
raise _non_numeric_type_error(self.expected, at="key={!r}".format(key))
|
||||
|
||||
|
||||
class ApproxSequence(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for sequences of numbers.
|
||||
Perform approximate comparisons where the expected value is a sequence of
|
||||
numbers.
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
@@ -169,10 +204,21 @@ class ApproxSequence(ApproxBase):
|
||||
def _yield_comparisons(self, actual):
|
||||
return zip(actual, self.expected)
|
||||
|
||||
def _check_type(self):
|
||||
__tracebackhide__ = True
|
||||
for index, x in enumerate(self.expected):
|
||||
if isinstance(x, type(self.expected)):
|
||||
msg = "pytest.approx() does not support nested data structures: {!r} at index {}\n full sequence: {}"
|
||||
raise TypeError(msg.format(x, index, pprint.pformat(self.expected)))
|
||||
elif not isinstance(x, Number):
|
||||
raise _non_numeric_type_error(
|
||||
self.expected, at="index {}".format(index)
|
||||
)
|
||||
|
||||
|
||||
class ApproxScalar(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for single numbers only.
|
||||
Perform approximate comparisons where the expected value is a single number.
|
||||
"""
|
||||
|
||||
DEFAULT_ABSOLUTE_TOLERANCE = 1e-12
|
||||
@@ -211,7 +257,9 @@ class ApproxScalar(ApproxBase):
|
||||
the pre-specified tolerance.
|
||||
"""
|
||||
if _is_numpy_array(actual):
|
||||
return ApproxNumpy(actual, self.abs, self.rel, self.nan_ok) == self.expected
|
||||
# Call ``__eq__()`` manually to prevent infinite-recursion with
|
||||
# numpy<1.13. See #3748.
|
||||
return all(self.__eq__(a) for a in actual.flat)
|
||||
|
||||
# Short-circuit exact equality.
|
||||
if actual == self.expected:
|
||||
@@ -286,7 +334,9 @@ class ApproxScalar(ApproxBase):
|
||||
|
||||
|
||||
class ApproxDecimal(ApproxScalar):
|
||||
from decimal import Decimal
|
||||
"""
|
||||
Perform approximate comparisons where the expected value is a decimal.
|
||||
"""
|
||||
|
||||
DEFAULT_ABSOLUTE_TOLERANCE = Decimal("1e-12")
|
||||
DEFAULT_RELATIVE_TOLERANCE = Decimal("1e-6")
|
||||
@@ -445,32 +495,35 @@ def approx(expected, rel=None, abs=None, nan_ok=False):
|
||||
__ https://docs.python.org/3/reference/datamodel.html#object.__ge__
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
# Delegate the comparison to a class that knows how to deal with the type
|
||||
# of the expected value (e.g. int, float, list, dict, numpy.array, etc).
|
||||
#
|
||||
# This architecture is really driven by the need to support numpy arrays.
|
||||
# The only way to override `==` for arrays without requiring that approx be
|
||||
# the left operand is to inherit the approx object from `numpy.ndarray`.
|
||||
# But that can't be a general solution, because it requires (1) numpy to be
|
||||
# installed and (2) the expected value to be a numpy array. So the general
|
||||
# solution is to delegate each type of expected value to a different class.
|
||||
# The primary responsibility of these classes is to implement ``__eq__()``
|
||||
# and ``__repr__()``. The former is used to actually check if some
|
||||
# "actual" value is equivalent to the given expected value within the
|
||||
# allowed tolerance. The latter is used to show the user the expected
|
||||
# value and tolerance, in the case that a test failed.
|
||||
#
|
||||
# This has the advantage that it made it easy to support mapping types
|
||||
# (i.e. dict). The old code accepted mapping types, but would only compare
|
||||
# their keys, which is probably not what most people would expect.
|
||||
# The actual logic for making approximate comparisons can be found in
|
||||
# ApproxScalar, which is used to compare individual numbers. All of the
|
||||
# other Approx classes eventually delegate to this class. The ApproxBase
|
||||
# class provides some convenient methods and overloads, but isn't really
|
||||
# essential.
|
||||
|
||||
if _is_numpy_array(expected):
|
||||
cls = ApproxNumpy
|
||||
__tracebackhide__ = True
|
||||
|
||||
if isinstance(expected, Decimal):
|
||||
cls = ApproxDecimal
|
||||
elif isinstance(expected, Number):
|
||||
cls = ApproxScalar
|
||||
elif isinstance(expected, Mapping):
|
||||
cls = ApproxMapping
|
||||
elif isinstance(expected, Sequence) and not isinstance(expected, STRING_TYPES):
|
||||
cls = ApproxSequence
|
||||
elif isinstance(expected, Decimal):
|
||||
cls = ApproxDecimal
|
||||
elif _is_numpy_array(expected):
|
||||
cls = ApproxNumpy
|
||||
else:
|
||||
cls = ApproxScalar
|
||||
raise _non_numeric_type_error(expected, at=None)
|
||||
|
||||
return cls(expected, rel, abs, nan_ok)
|
||||
|
||||
@@ -480,17 +533,11 @@ def _is_numpy_array(obj):
|
||||
Return true if the given object is a numpy array. Make a special effort to
|
||||
avoid importing numpy unless it's really necessary.
|
||||
"""
|
||||
import inspect
|
||||
|
||||
for cls in inspect.getmro(type(obj)):
|
||||
if cls.__module__ == "numpy":
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
return isinstance(obj, np.ndarray)
|
||||
except ImportError:
|
||||
pass
|
||||
import sys
|
||||
|
||||
np = sys.modules.get("numpy")
|
||||
if np is not None:
|
||||
return isinstance(obj, np.ndarray)
|
||||
return False
|
||||
|
||||
|
||||
@@ -633,8 +680,8 @@ def raises(expected_exception, *args, **kwargs):
|
||||
# print "raises frame scope: %r" % frame.f_locals
|
||||
try:
|
||||
code = _pytest._code.Source(code).compile()
|
||||
py.builtin.exec_(code, frame.f_globals, loc)
|
||||
# XXX didn'T mean f_globals == f_locals something special?
|
||||
six.exec_(code, frame.f_globals, loc)
|
||||
# XXX didn't mean f_globals == f_locals something special?
|
||||
# this is destroyed here ...
|
||||
except expected_exception:
|
||||
return _pytest._code.ExceptionInfo()
|
||||
|
||||
@@ -4,11 +4,11 @@ from __future__ import absolute_import, division, print_function
|
||||
import inspect
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import re
|
||||
import six
|
||||
|
||||
from _pytest.fixtures import yield_fixture
|
||||
from _pytest.outcomes import fail
|
||||
@@ -130,7 +130,7 @@ def warns(expected_warning, *args, **kwargs):
|
||||
|
||||
with WarningsChecker(expected_warning, match_expr=match_expr):
|
||||
code = _pytest._code.Source(code).compile()
|
||||
py.builtin.exec_(code, frame.f_globals, loc)
|
||||
six.exec_(code, frame.f_globals, loc)
|
||||
else:
|
||||
func = args[0]
|
||||
with WarningsChecker(expected_warning, match_expr=match_expr):
|
||||
@@ -212,6 +212,8 @@ class WarningsChecker(WarningsRecorder):
|
||||
def __exit__(self, *exc_info):
|
||||
super(WarningsChecker, self).__exit__(*exc_info)
|
||||
|
||||
__tracebackhide__ = True
|
||||
|
||||
# only check if we're not currently handling an exception
|
||||
if all(a is None for a in exc_info):
|
||||
if self.expected_warning is not None:
|
||||
|
||||
196
src/_pytest/reports.py
Normal file
196
src/_pytest/reports.py
Normal file
@@ -0,0 +1,196 @@
|
||||
import py
|
||||
from _pytest._code.code import TerminalRepr
|
||||
|
||||
|
||||
def getslaveinfoline(node):
|
||||
try:
|
||||
return node._slaveinfocache
|
||||
except AttributeError:
|
||||
d = node.slaveinfo
|
||||
ver = "%s.%s.%s" % d["version_info"][:3]
|
||||
node._slaveinfocache = s = "[%s] %s -- Python %s %s" % (
|
||||
d["id"],
|
||||
d["sysplatform"],
|
||||
ver,
|
||||
d["executable"],
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
class BaseReport(object):
|
||||
def __init__(self, **kw):
|
||||
self.__dict__.update(kw)
|
||||
|
||||
def toterminal(self, out):
|
||||
if hasattr(self, "node"):
|
||||
out.line(getslaveinfoline(self.node))
|
||||
|
||||
longrepr = self.longrepr
|
||||
if longrepr is None:
|
||||
return
|
||||
|
||||
if hasattr(longrepr, "toterminal"):
|
||||
longrepr.toterminal(out)
|
||||
else:
|
||||
try:
|
||||
out.line(longrepr)
|
||||
except UnicodeEncodeError:
|
||||
out.line("<unprintable longrepr>")
|
||||
|
||||
def get_sections(self, prefix):
|
||||
for name, content in self.sections:
|
||||
if name.startswith(prefix):
|
||||
yield prefix, content
|
||||
|
||||
@property
|
||||
def longreprtext(self):
|
||||
"""
|
||||
Read-only property that returns the full string representation
|
||||
of ``longrepr``.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
tw = py.io.TerminalWriter(stringio=True)
|
||||
tw.hasmarkup = False
|
||||
self.toterminal(tw)
|
||||
exc = tw.stringio.getvalue()
|
||||
return exc.strip()
|
||||
|
||||
@property
|
||||
def caplog(self):
|
||||
"""Return captured log lines, if log capturing is enabled
|
||||
|
||||
.. versionadded:: 3.5
|
||||
"""
|
||||
return "\n".join(
|
||||
content for (prefix, content) in self.get_sections("Captured log")
|
||||
)
|
||||
|
||||
@property
|
||||
def capstdout(self):
|
||||
"""Return captured text from stdout, if capturing is enabled
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return "".join(
|
||||
content for (prefix, content) in self.get_sections("Captured stdout")
|
||||
)
|
||||
|
||||
@property
|
||||
def capstderr(self):
|
||||
"""Return captured text from stderr, if capturing is enabled
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return "".join(
|
||||
content for (prefix, content) in self.get_sections("Captured stderr")
|
||||
)
|
||||
|
||||
passed = property(lambda x: x.outcome == "passed")
|
||||
failed = property(lambda x: x.outcome == "failed")
|
||||
skipped = property(lambda x: x.outcome == "skipped")
|
||||
|
||||
@property
|
||||
def fspath(self):
|
||||
return self.nodeid.split("::")[0]
|
||||
|
||||
|
||||
class TestReport(BaseReport):
|
||||
""" Basic test report object (also used for setup and teardown calls if
|
||||
they fail).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nodeid,
|
||||
location,
|
||||
keywords,
|
||||
outcome,
|
||||
longrepr,
|
||||
when,
|
||||
sections=(),
|
||||
duration=0,
|
||||
user_properties=None,
|
||||
**extra
|
||||
):
|
||||
#: normalized collection node id
|
||||
self.nodeid = nodeid
|
||||
|
||||
#: a (filesystempath, lineno, domaininfo) tuple indicating the
|
||||
#: actual location of a test item - it might be different from the
|
||||
#: collected one e.g. if a method is inherited from a different module.
|
||||
self.location = location
|
||||
|
||||
#: a name -> value dictionary containing all keywords and
|
||||
#: markers associated with a test invocation.
|
||||
self.keywords = keywords
|
||||
|
||||
#: test outcome, always one of "passed", "failed", "skipped".
|
||||
self.outcome = outcome
|
||||
|
||||
#: None or a failure representation.
|
||||
self.longrepr = longrepr
|
||||
|
||||
#: one of 'setup', 'call', 'teardown' to indicate runtest phase.
|
||||
self.when = when
|
||||
|
||||
#: user properties is a list of tuples (name, value) that holds user
|
||||
#: defined properties of the test
|
||||
self.user_properties = list(user_properties or [])
|
||||
|
||||
#: list of pairs ``(str, str)`` of extra information which needs to
|
||||
#: marshallable. Used by pytest to add captured text
|
||||
#: from ``stdout`` and ``stderr``, but may be used by other plugins
|
||||
#: to add arbitrary information to reports.
|
||||
self.sections = list(sections)
|
||||
|
||||
#: time it took to run just the test
|
||||
self.duration = duration
|
||||
|
||||
self.__dict__.update(extra)
|
||||
|
||||
def __repr__(self):
|
||||
return "<TestReport %r when=%r outcome=%r>" % (
|
||||
self.nodeid,
|
||||
self.when,
|
||||
self.outcome,
|
||||
)
|
||||
|
||||
|
||||
class TeardownErrorReport(BaseReport):
|
||||
outcome = "failed"
|
||||
when = "teardown"
|
||||
|
||||
def __init__(self, longrepr, **extra):
|
||||
self.longrepr = longrepr
|
||||
self.sections = []
|
||||
self.__dict__.update(extra)
|
||||
|
||||
|
||||
class CollectReport(BaseReport):
|
||||
def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
|
||||
self.nodeid = nodeid
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
self.result = result or []
|
||||
self.sections = list(sections)
|
||||
self.__dict__.update(extra)
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return (self.fspath, None, self.fspath)
|
||||
|
||||
def __repr__(self):
|
||||
return "<CollectReport %r lenresult=%s outcome=%r>" % (
|
||||
self.nodeid,
|
||||
len(self.result),
|
||||
self.outcome,
|
||||
)
|
||||
|
||||
|
||||
class CollectErrorRepr(TerminalRepr):
|
||||
def __init__(self, msg):
|
||||
self.longrepr = msg
|
||||
|
||||
def toterminal(self, out):
|
||||
out.line(self.longrepr, red=True)
|
||||
@@ -31,8 +31,10 @@ def pytest_configure(config):
|
||||
config.pluginmanager.register(config._resultlog)
|
||||
|
||||
from _pytest.deprecated import RESULT_LOG
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
from _pytest.warnings import _issue_config_warning
|
||||
|
||||
config.warn("C1", RESULT_LOG)
|
||||
_issue_config_warning(RemovedInPytest4Warning(RESULT_LOG), config)
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
|
||||
@@ -6,10 +6,12 @@ import os
|
||||
import sys
|
||||
from time import time
|
||||
|
||||
import py
|
||||
from _pytest._code.code import TerminalRepr, ExceptionInfo
|
||||
import six
|
||||
from _pytest._code.code import ExceptionInfo
|
||||
from _pytest.outcomes import skip, Skipped, TEST_OUTCOME
|
||||
|
||||
from .reports import TestReport, CollectReport, CollectErrorRepr
|
||||
|
||||
#
|
||||
# pytest plugin hooks
|
||||
|
||||
@@ -215,99 +217,6 @@ class CallInfo(object):
|
||||
return "<CallInfo when=%r %s>" % (self.when, status)
|
||||
|
||||
|
||||
def getslaveinfoline(node):
|
||||
try:
|
||||
return node._slaveinfocache
|
||||
except AttributeError:
|
||||
d = node.slaveinfo
|
||||
ver = "%s.%s.%s" % d["version_info"][:3]
|
||||
node._slaveinfocache = s = "[%s] %s -- Python %s %s" % (
|
||||
d["id"],
|
||||
d["sysplatform"],
|
||||
ver,
|
||||
d["executable"],
|
||||
)
|
||||
return s
|
||||
|
||||
|
||||
class BaseReport(object):
|
||||
def __init__(self, **kw):
|
||||
self.__dict__.update(kw)
|
||||
|
||||
def toterminal(self, out):
|
||||
if hasattr(self, "node"):
|
||||
out.line(getslaveinfoline(self.node))
|
||||
|
||||
longrepr = self.longrepr
|
||||
if longrepr is None:
|
||||
return
|
||||
|
||||
if hasattr(longrepr, "toterminal"):
|
||||
longrepr.toterminal(out)
|
||||
else:
|
||||
try:
|
||||
out.line(longrepr)
|
||||
except UnicodeEncodeError:
|
||||
out.line("<unprintable longrepr>")
|
||||
|
||||
def get_sections(self, prefix):
|
||||
for name, content in self.sections:
|
||||
if name.startswith(prefix):
|
||||
yield prefix, content
|
||||
|
||||
@property
|
||||
def longreprtext(self):
|
||||
"""
|
||||
Read-only property that returns the full string representation
|
||||
of ``longrepr``.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
tw = py.io.TerminalWriter(stringio=True)
|
||||
tw.hasmarkup = False
|
||||
self.toterminal(tw)
|
||||
exc = tw.stringio.getvalue()
|
||||
return exc.strip()
|
||||
|
||||
@property
|
||||
def caplog(self):
|
||||
"""Return captured log lines, if log capturing is enabled
|
||||
|
||||
.. versionadded:: 3.5
|
||||
"""
|
||||
return "\n".join(
|
||||
content for (prefix, content) in self.get_sections("Captured log")
|
||||
)
|
||||
|
||||
@property
|
||||
def capstdout(self):
|
||||
"""Return captured text from stdout, if capturing is enabled
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return "".join(
|
||||
content for (prefix, content) in self.get_sections("Captured stdout")
|
||||
)
|
||||
|
||||
@property
|
||||
def capstderr(self):
|
||||
"""Return captured text from stderr, if capturing is enabled
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return "".join(
|
||||
content for (prefix, content) in self.get_sections("Captured stderr")
|
||||
)
|
||||
|
||||
passed = property(lambda x: x.outcome == "passed")
|
||||
failed = property(lambda x: x.outcome == "failed")
|
||||
skipped = property(lambda x: x.outcome == "skipped")
|
||||
|
||||
@property
|
||||
def fspath(self):
|
||||
return self.nodeid.split("::")[0]
|
||||
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
when = call.when
|
||||
duration = call.stop - call.start
|
||||
@@ -348,78 +257,6 @@ def pytest_runtest_makereport(item, call):
|
||||
)
|
||||
|
||||
|
||||
class TestReport(BaseReport):
|
||||
""" Basic test report object (also used for setup and teardown calls if
|
||||
they fail).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
nodeid,
|
||||
location,
|
||||
keywords,
|
||||
outcome,
|
||||
longrepr,
|
||||
when,
|
||||
sections=(),
|
||||
duration=0,
|
||||
user_properties=(),
|
||||
**extra
|
||||
):
|
||||
#: normalized collection node id
|
||||
self.nodeid = nodeid
|
||||
|
||||
#: a (filesystempath, lineno, domaininfo) tuple indicating the
|
||||
#: actual location of a test item - it might be different from the
|
||||
#: collected one e.g. if a method is inherited from a different module.
|
||||
self.location = location
|
||||
|
||||
#: a name -> value dictionary containing all keywords and
|
||||
#: markers associated with a test invocation.
|
||||
self.keywords = keywords
|
||||
|
||||
#: test outcome, always one of "passed", "failed", "skipped".
|
||||
self.outcome = outcome
|
||||
|
||||
#: None or a failure representation.
|
||||
self.longrepr = longrepr
|
||||
|
||||
#: one of 'setup', 'call', 'teardown' to indicate runtest phase.
|
||||
self.when = when
|
||||
|
||||
#: user properties is a list of tuples (name, value) that holds user
|
||||
#: defined properties of the test
|
||||
self.user_properties = user_properties
|
||||
|
||||
#: list of pairs ``(str, str)`` of extra information which needs to
|
||||
#: marshallable. Used by pytest to add captured text
|
||||
#: from ``stdout`` and ``stderr``, but may be used by other plugins
|
||||
#: to add arbitrary information to reports.
|
||||
self.sections = list(sections)
|
||||
|
||||
#: time it took to run just the test
|
||||
self.duration = duration
|
||||
|
||||
self.__dict__.update(extra)
|
||||
|
||||
def __repr__(self):
|
||||
return "<TestReport %r when=%r outcome=%r>" % (
|
||||
self.nodeid,
|
||||
self.when,
|
||||
self.outcome,
|
||||
)
|
||||
|
||||
|
||||
class TeardownErrorReport(BaseReport):
|
||||
outcome = "failed"
|
||||
when = "teardown"
|
||||
|
||||
def __init__(self, longrepr, **extra):
|
||||
self.longrepr = longrepr
|
||||
self.sections = []
|
||||
self.__dict__.update(extra)
|
||||
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
call = CallInfo(lambda: list(collector.collect()), "collect")
|
||||
longrepr = None
|
||||
@@ -446,35 +283,6 @@ def pytest_make_collect_report(collector):
|
||||
return rep
|
||||
|
||||
|
||||
class CollectReport(BaseReport):
|
||||
def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra):
|
||||
self.nodeid = nodeid
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
self.result = result or []
|
||||
self.sections = list(sections)
|
||||
self.__dict__.update(extra)
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return (self.fspath, None, self.fspath)
|
||||
|
||||
def __repr__(self):
|
||||
return "<CollectReport %r lenresult=%s outcome=%r>" % (
|
||||
self.nodeid,
|
||||
len(self.result),
|
||||
self.outcome,
|
||||
)
|
||||
|
||||
|
||||
class CollectErrorRepr(TerminalRepr):
|
||||
def __init__(self, msg):
|
||||
self.longrepr = msg
|
||||
|
||||
def toterminal(self, out):
|
||||
out.line(self.longrepr, red=True)
|
||||
|
||||
|
||||
class SetupState(object):
|
||||
""" shared state for setting up/tearing down test items or collectors. """
|
||||
|
||||
@@ -509,7 +317,7 @@ class SetupState(object):
|
||||
if exc is None:
|
||||
exc = sys.exc_info()
|
||||
if exc:
|
||||
py.builtin._reraise(*exc)
|
||||
six.reraise(*exc)
|
||||
|
||||
def _teardown_with_finalization(self, colitem):
|
||||
self._callfinalizers(colitem)
|
||||
@@ -544,7 +352,7 @@ class SetupState(object):
|
||||
if exc is None:
|
||||
exc = sys.exc_info()
|
||||
if exc:
|
||||
py.builtin._reraise(*exc)
|
||||
six.reraise(*exc)
|
||||
|
||||
def prepare(self, colitem):
|
||||
""" setup objects along the collector chain to the test-method
|
||||
@@ -555,7 +363,7 @@ class SetupState(object):
|
||||
# check if the last collection node has raised an error
|
||||
for col in self.stack:
|
||||
if hasattr(col, "_prepare_exc"):
|
||||
py.builtin._reraise(*col._prepare_exc)
|
||||
six.reraise(*col._prepare_exc)
|
||||
for col in needed_collectors[len(self.stack) :]:
|
||||
self.stack.append(col)
|
||||
try:
|
||||
|
||||
@@ -51,7 +51,8 @@ def _show_fixture_action(fixturedef, msg):
|
||||
config = fixturedef._fixturemanager.config
|
||||
capman = config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
out, err = capman.suspend_global_capture()
|
||||
capman.suspend_global_capture()
|
||||
out, err = capman.read_global_capture()
|
||||
|
||||
tw = config.get_terminal_writer()
|
||||
tw.line()
|
||||
|
||||
@@ -51,7 +51,7 @@ def pytest_configure(config):
|
||||
"results in a True value. Evaluation happens within the "
|
||||
"module global context. Example: skipif('sys.platform == \"win32\"') "
|
||||
"skips the test if we are on the win32 platform. see "
|
||||
"http://pytest.org/latest/skipping.html",
|
||||
"https://docs.pytest.org/en/latest/skipping.html",
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
@@ -61,7 +61,7 @@ def pytest_configure(config):
|
||||
"and run=False if you don't even want to execute the test function. "
|
||||
"If only specific exception(s) are expected, you can list them in "
|
||||
"raises, and if the test fails in other ways, it will be reported as "
|
||||
"a true failure. See http://pytest.org/latest/skipping.html",
|
||||
"a true failure. See https://docs.pytest.org/en/latest/skipping.html",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import platform
|
||||
import sys
|
||||
import time
|
||||
|
||||
import attr
|
||||
import pluggy
|
||||
import py
|
||||
import six
|
||||
@@ -184,23 +185,23 @@ def pytest_report_teststatus(report):
|
||||
return report.outcome, letter, report.outcome.upper()
|
||||
|
||||
|
||||
@attr.s
|
||||
class WarningReport(object):
|
||||
"""
|
||||
Simple structure to hold warnings information captured by ``pytest_logwarning``.
|
||||
Simple structure to hold warnings information captured by ``pytest_logwarning`` and ``pytest_warning_captured``.
|
||||
|
||||
:ivar str message: user friendly message about the warning
|
||||
:ivar str|None nodeid: node id that generated the warning (see ``get_location``).
|
||||
:ivar tuple|py.path.local fslocation:
|
||||
file system location of the source of the warning (see ``get_location``).
|
||||
|
||||
:ivar bool legacy: if this warning report was generated from the deprecated ``pytest_logwarning`` hook.
|
||||
"""
|
||||
|
||||
def __init__(self, code, message, nodeid=None, fslocation=None):
|
||||
"""
|
||||
:param code: unused
|
||||
:param str message: user friendly message about the warning
|
||||
:param str|None nodeid: node id that generated the warning (see ``get_location``).
|
||||
:param tuple|py.path.local fslocation:
|
||||
file system location of the source of the warning (see ``get_location``).
|
||||
"""
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.nodeid = nodeid
|
||||
self.fslocation = fslocation
|
||||
message = attr.ib()
|
||||
nodeid = attr.ib(default=None)
|
||||
fslocation = attr.ib(default=None)
|
||||
legacy = attr.ib(default=False)
|
||||
|
||||
def get_location(self, config):
|
||||
"""
|
||||
@@ -213,6 +214,8 @@ class WarningReport(object):
|
||||
if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
|
||||
filename, linenum = self.fslocation[:2]
|
||||
relpath = py.path.local(filename).relto(config.invocation_dir)
|
||||
if not relpath:
|
||||
relpath = str(filename)
|
||||
return "%s:%s" % (relpath, linenum)
|
||||
else:
|
||||
return str(self.fslocation)
|
||||
@@ -254,7 +257,7 @@ class TerminalReporter(object):
|
||||
# do not show progress if we are showing fixture setup/teardown
|
||||
if self.config.getoption("setupshow"):
|
||||
return False
|
||||
return self.config.getini("console_output_style") == "progress"
|
||||
return self.config.getini("console_output_style") in ("progress", "count")
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {"xfailed": "x", "skipped": "s"}.get(char, char)
|
||||
@@ -263,7 +266,7 @@ class TerminalReporter(object):
|
||||
def write_fspath_result(self, nodeid, res):
|
||||
fspath = self.config.rootdir.join(nodeid.split("::")[0])
|
||||
if fspath != self.currentfspath:
|
||||
if self.currentfspath is not None:
|
||||
if self.currentfspath is not None and self._show_progress_info:
|
||||
self._write_progress_information_filling_space()
|
||||
self.currentfspath = fspath
|
||||
fspath = self.startdir.bestrelpath(fspath)
|
||||
@@ -327,13 +330,27 @@ class TerminalReporter(object):
|
||||
self.write_line("INTERNALERROR> " + line)
|
||||
return 1
|
||||
|
||||
def pytest_logwarning(self, code, fslocation, message, nodeid):
|
||||
def pytest_logwarning(self, fslocation, message, nodeid):
|
||||
warnings = self.stats.setdefault("warnings", [])
|
||||
warning = WarningReport(
|
||||
code=code, fslocation=fslocation, message=message, nodeid=nodeid
|
||||
fslocation=fslocation, message=message, nodeid=nodeid, legacy=True
|
||||
)
|
||||
warnings.append(warning)
|
||||
|
||||
def pytest_warning_captured(self, warning_message, item):
|
||||
# from _pytest.nodes import get_fslocation_from_item
|
||||
from _pytest.warnings import warning_record_to_str
|
||||
|
||||
warnings = self.stats.setdefault("warnings", [])
|
||||
fslocation = warning_message.filename, warning_message.lineno
|
||||
message = warning_record_to_str(warning_message)
|
||||
|
||||
nodeid = item.nodeid if item is not None else ""
|
||||
warning_report = WarningReport(
|
||||
fslocation=fslocation, message=message, nodeid=nodeid
|
||||
)
|
||||
warnings.append(warning_report)
|
||||
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
if self.config.option.traceconfig:
|
||||
msg = "PLUGIN registered: %s" % (plugin,)
|
||||
@@ -358,12 +375,12 @@ class TerminalReporter(object):
|
||||
def pytest_runtest_logreport(self, report):
|
||||
rep = report
|
||||
res = self.config.hook.pytest_report_teststatus(report=rep)
|
||||
cat, letter, word = res
|
||||
category, letter, word = res
|
||||
if isinstance(word, tuple):
|
||||
word, markup = word
|
||||
else:
|
||||
markup = None
|
||||
self.stats.setdefault(cat, []).append(rep)
|
||||
self.stats.setdefault(category, []).append(rep)
|
||||
self._tests_ran = True
|
||||
if not letter and not word:
|
||||
# probably passed setup/teardown
|
||||
@@ -404,6 +421,12 @@ class TerminalReporter(object):
|
||||
self.currentfspath = -2
|
||||
|
||||
def pytest_runtest_logfinish(self, nodeid):
|
||||
if self.config.getini("console_output_style") == "count":
|
||||
num_tests = self._session.testscollected
|
||||
progress_length = len(" [{}/{}]".format(str(num_tests), str(num_tests)))
|
||||
else:
|
||||
progress_length = len(" [100%]")
|
||||
|
||||
if self.verbosity <= 0 and self._show_progress_info:
|
||||
self._progress_nodeids_reported.add(nodeid)
|
||||
last_item = (
|
||||
@@ -412,31 +435,43 @@ class TerminalReporter(object):
|
||||
if last_item:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
past_edge = (
|
||||
self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1
|
||||
>= self._screen_width
|
||||
)
|
||||
w = self._width_of_current_line
|
||||
past_edge = w + progress_length + 1 >= self._screen_width
|
||||
if past_edge:
|
||||
msg = self._get_progress_information_message()
|
||||
self._tw.write(msg + "\n", cyan=True)
|
||||
|
||||
_PROGRESS_LENGTH = len(" [100%]")
|
||||
|
||||
def _get_progress_information_message(self):
|
||||
if self.config.getoption("capture") == "no":
|
||||
return ""
|
||||
collected = self._session.testscollected
|
||||
if collected:
|
||||
progress = len(self._progress_nodeids_reported) * 100 // collected
|
||||
return " [{:3d}%]".format(progress)
|
||||
return " [100%]"
|
||||
if self.config.getini("console_output_style") == "count":
|
||||
if collected:
|
||||
progress = self._progress_nodeids_reported
|
||||
counter_format = "{{:{}d}}".format(len(str(collected)))
|
||||
format_string = " [{}/{{}}]".format(counter_format)
|
||||
return format_string.format(len(progress), collected)
|
||||
return " [ {} / {} ]".format(collected, collected)
|
||||
else:
|
||||
if collected:
|
||||
progress = len(self._progress_nodeids_reported) * 100 // collected
|
||||
return " [{:3d}%]".format(progress)
|
||||
return " [100%]"
|
||||
|
||||
def _write_progress_information_filling_space(self):
|
||||
msg = self._get_progress_information_message()
|
||||
fill = " " * (
|
||||
self._tw.fullwidth - self._tw.chars_on_current_line - len(msg) - 1
|
||||
)
|
||||
self.write(fill + msg, cyan=True)
|
||||
w = self._width_of_current_line
|
||||
fill = self._tw.fullwidth - w - 1
|
||||
self.write(msg.rjust(fill), cyan=True)
|
||||
|
||||
@property
|
||||
def _width_of_current_line(self):
|
||||
"""Return the width of current line, using the superior implementation of py-1.6 when available"""
|
||||
try:
|
||||
return self._tw.width_of_current_line
|
||||
except AttributeError:
|
||||
# py < 1.6.0
|
||||
return self._tw.chars_on_current_line
|
||||
|
||||
def pytest_collection(self):
|
||||
if not self.isatty and self.config.option.verbose >= 1:
|
||||
@@ -685,13 +720,22 @@ class TerminalReporter(object):
|
||||
|
||||
self.write_sep("=", "warnings summary", yellow=True, bold=False)
|
||||
for location, warning_records in grouped:
|
||||
self._tw.line(str(location) if location else "<undetermined location>")
|
||||
# legacy warnings show their location explicitly, while standard warnings look better without
|
||||
# it because the location is already formatted into the message
|
||||
warning_records = list(warning_records)
|
||||
is_legacy = warning_records[0].legacy
|
||||
if location and is_legacy:
|
||||
self._tw.line(str(location))
|
||||
for w in warning_records:
|
||||
lines = w.message.splitlines()
|
||||
indented = "\n".join(" " + x for x in lines)
|
||||
self._tw.line(indented)
|
||||
if is_legacy:
|
||||
lines = w.message.splitlines()
|
||||
indented = "\n".join(" " + x for x in lines)
|
||||
message = indented.rstrip()
|
||||
else:
|
||||
message = w.message.rstrip()
|
||||
self._tw.line(message)
|
||||
self._tw.line()
|
||||
self._tw.line("-- Docs: http://doc.pytest.org/en/latest/warnings.html")
|
||||
self._tw.line("-- Docs: https://docs.pytest.org/en/latest/warnings.html")
|
||||
|
||||
def summary_passes(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
@@ -701,12 +745,18 @@ class TerminalReporter(object):
|
||||
return
|
||||
self.write_sep("=", "PASSES")
|
||||
for rep in reports:
|
||||
msg = self._getfailureheadline(rep)
|
||||
self.write_sep("_", msg)
|
||||
self._outrep_summary(rep)
|
||||
if rep.sections:
|
||||
msg = self._getfailureheadline(rep)
|
||||
self.write_sep("_", msg)
|
||||
self._outrep_summary(rep)
|
||||
|
||||
def print_teardown_sections(self, rep):
|
||||
showcapture = self.config.option.showcapture
|
||||
if showcapture == "no":
|
||||
return
|
||||
for secname, content in rep.sections:
|
||||
if showcapture != "all" and showcapture not in secname:
|
||||
continue
|
||||
if "teardown" in secname:
|
||||
self._tw.sep("-", secname)
|
||||
if content[-1:] == "\n":
|
||||
|
||||
@@ -9,6 +9,7 @@ import _pytest._code
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.outcomes import fail, skip, xfail
|
||||
from _pytest.python import transfer_markers, Class, Module, Function
|
||||
from _pytest.compat import getimfunc
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
@@ -53,7 +54,7 @@ class UnitTestCase(Class):
|
||||
x = getattr(self.obj, name)
|
||||
if not getattr(x, "__test__", True):
|
||||
continue
|
||||
funcobj = getattr(x, "im_func", x)
|
||||
funcobj = getimfunc(x)
|
||||
transfer_markers(funcobj, cls, module)
|
||||
yield TestCaseFunction(name, parent=self, callobj=funcobj)
|
||||
foundsomething = True
|
||||
@@ -69,6 +70,7 @@ class UnitTestCase(Class):
|
||||
class TestCaseFunction(Function):
|
||||
nofuncargs = True
|
||||
_excinfo = None
|
||||
_testcase = None
|
||||
|
||||
def setup(self):
|
||||
self._testcase = self.parent.obj(self.name)
|
||||
|
||||
42
src/_pytest/warning_types.py
Normal file
42
src/_pytest/warning_types.py
Normal file
@@ -0,0 +1,42 @@
|
||||
class PytestWarning(UserWarning):
|
||||
"""
|
||||
Bases: :class:`UserWarning`.
|
||||
|
||||
Base class for all warnings emitted by pytest.
|
||||
"""
|
||||
|
||||
|
||||
class PytestDeprecationWarning(PytestWarning, DeprecationWarning):
|
||||
"""
|
||||
Bases: :class:`pytest.PytestWarning`, :class:`DeprecationWarning`.
|
||||
|
||||
Warning class for features that will be removed in a future version.
|
||||
"""
|
||||
|
||||
|
||||
class RemovedInPytest4Warning(PytestDeprecationWarning):
|
||||
"""
|
||||
Bases: :class:`pytest.PytestDeprecationWarning`.
|
||||
|
||||
Warning class for features scheduled to be removed in pytest 4.0.
|
||||
"""
|
||||
|
||||
|
||||
class PytestExperimentalApiWarning(PytestWarning, FutureWarning):
|
||||
"""
|
||||
Bases: :class:`pytest.PytestWarning`, :class:`FutureWarning`.
|
||||
|
||||
Warning category used to denote experiments in pytest. Use sparingly as the API might change or even be
|
||||
removed completely in future version
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def simple(cls, apiname):
|
||||
return cls(
|
||||
"{apiname} is an experimental api that may change over time".format(
|
||||
apiname=apiname
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example")
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
|
||||
@@ -49,63 +50,123 @@ def pytest_addoption(parser):
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line(
|
||||
"markers",
|
||||
"filterwarnings(warning): add a warning filter to the given test. "
|
||||
"see https://docs.pytest.org/en/latest/warnings.html#pytest-mark-filterwarnings ",
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def catch_warnings_for_item(item):
|
||||
def catch_warnings_for_item(config, ihook, when, item):
|
||||
"""
|
||||
catches the warnings generated during setup/call/teardown execution
|
||||
of the given item and after it is done posts them as warnings to this
|
||||
item.
|
||||
Context manager that catches warnings generated in the contained execution block.
|
||||
|
||||
``item`` can be None if we are not in the context of an item execution.
|
||||
|
||||
Each warning captured triggers the ``pytest_warning_captured`` hook.
|
||||
"""
|
||||
args = item.config.getoption("pythonwarnings") or []
|
||||
inifilters = item.config.getini("filterwarnings")
|
||||
args = config.getoption("pythonwarnings") or []
|
||||
inifilters = config.getini("filterwarnings")
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
filters_configured = args or inifilters or sys.warnoptions
|
||||
|
||||
for arg in args:
|
||||
warnings._setoption(arg)
|
||||
|
||||
for arg in inifilters:
|
||||
_setoption(warnings, arg)
|
||||
|
||||
for mark in item.iter_markers(name="filterwarnings"):
|
||||
for arg in mark.args:
|
||||
warnings._setoption(arg)
|
||||
if item is not None:
|
||||
for mark in item.iter_markers(name="filterwarnings"):
|
||||
for arg in mark.args:
|
||||
_setoption(warnings, arg)
|
||||
filters_configured = True
|
||||
|
||||
if not filters_configured:
|
||||
# if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908)
|
||||
warnings.filterwarnings("always", category=DeprecationWarning)
|
||||
warnings.filterwarnings("always", category=PendingDeprecationWarning)
|
||||
|
||||
yield
|
||||
|
||||
for warning in log:
|
||||
warn_msg = warning.message
|
||||
unicode_warning = False
|
||||
|
||||
if compat._PY2 and any(
|
||||
isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args
|
||||
):
|
||||
new_args = []
|
||||
for m in warn_msg.args:
|
||||
new_args.append(
|
||||
compat.ascii_escaped(m)
|
||||
if isinstance(m, compat.UNICODE_TYPES)
|
||||
else m
|
||||
)
|
||||
unicode_warning = list(warn_msg.args) != new_args
|
||||
warn_msg.args = new_args
|
||||
|
||||
msg = warnings.formatwarning(
|
||||
warn_msg,
|
||||
warning.category,
|
||||
warning.filename,
|
||||
warning.lineno,
|
||||
warning.line,
|
||||
for warning_message in log:
|
||||
ihook.pytest_warning_captured.call_historic(
|
||||
kwargs=dict(warning_message=warning_message, when=when, item=item)
|
||||
)
|
||||
item.warn("unused", msg)
|
||||
|
||||
if unicode_warning:
|
||||
warnings.warn(
|
||||
"Warning is using unicode non convertible to ascii, "
|
||||
"converting to a safe representation:\n %s" % msg,
|
||||
UnicodeWarning,
|
||||
)
|
||||
|
||||
def warning_record_to_str(warning_message):
|
||||
"""Convert a warnings.WarningMessage to a string, taking in account a lot of unicode shenaningans in Python 2.
|
||||
|
||||
When Python 2 support is dropped this function can be greatly simplified.
|
||||
"""
|
||||
warn_msg = warning_message.message
|
||||
unicode_warning = False
|
||||
if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args):
|
||||
new_args = []
|
||||
for m in warn_msg.args:
|
||||
new_args.append(
|
||||
compat.ascii_escaped(m) if isinstance(m, compat.UNICODE_TYPES) else m
|
||||
)
|
||||
unicode_warning = list(warn_msg.args) != new_args
|
||||
warn_msg.args = new_args
|
||||
|
||||
msg = warnings.formatwarning(
|
||||
warn_msg,
|
||||
warning_message.category,
|
||||
warning_message.filename,
|
||||
warning_message.lineno,
|
||||
warning_message.line,
|
||||
)
|
||||
if unicode_warning:
|
||||
warnings.warn(
|
||||
"Warning is using unicode non convertible to ascii, "
|
||||
"converting to a safe representation:\n %s" % msg,
|
||||
UnicodeWarning,
|
||||
)
|
||||
return msg
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_runtest_protocol(item):
|
||||
with catch_warnings_for_item(
|
||||
config=item.config, ihook=item.ihook, when="runtest", item=item
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_collection(session):
|
||||
config = session.config
|
||||
with catch_warnings_for_item(
|
||||
config=config, ihook=config.hook, when="collect", item=None
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_protocol(item):
|
||||
with catch_warnings_for_item(item):
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
config = terminalreporter.config
|
||||
with catch_warnings_for_item(
|
||||
config=config, ihook=config.hook, when="config", item=None
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
def _issue_config_warning(warning, config):
|
||||
"""
|
||||
This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage:
|
||||
at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured
|
||||
hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891.
|
||||
|
||||
:param warning: the warning instance.
|
||||
:param config:
|
||||
"""
|
||||
with warnings.catch_warnings(record=True) as records:
|
||||
warnings.simplefilter("always", type(warning))
|
||||
warnings.warn(warning, stacklevel=2)
|
||||
config.hook.pytest_warning_captured.call_historic(
|
||||
kwargs=dict(warning_message=records[0], when="config", item=None)
|
||||
)
|
||||
|
||||
@@ -18,45 +18,55 @@ from _pytest.mark import MARK_GEN as mark, param
|
||||
from _pytest.main import Session
|
||||
from _pytest.nodes import Item, Collector, File
|
||||
from _pytest.fixtures import fillfixtures as _fillfuncargs
|
||||
from _pytest.python import Module, Class, Instance, Function, Generator
|
||||
|
||||
from _pytest.python import Package, Module, Class, Instance, Function, Generator
|
||||
from _pytest.python_api import approx, raises
|
||||
from _pytest.warning_types import (
|
||||
PytestWarning,
|
||||
PytestDeprecationWarning,
|
||||
RemovedInPytest4Warning,
|
||||
PytestExperimentalApiWarning,
|
||||
)
|
||||
|
||||
set_trace = __pytestPDB.set_trace
|
||||
|
||||
__all__ = [
|
||||
"main",
|
||||
"UsageError",
|
||||
"cmdline",
|
||||
"hookspec",
|
||||
"hookimpl",
|
||||
"__version__",
|
||||
"register_assert_rewrite",
|
||||
"freeze_includes",
|
||||
"set_trace",
|
||||
"warns",
|
||||
"deprecated_call",
|
||||
"fixture",
|
||||
"yield_fixture",
|
||||
"fail",
|
||||
"skip",
|
||||
"xfail",
|
||||
"importorskip",
|
||||
"exit",
|
||||
"mark",
|
||||
"param",
|
||||
"approx",
|
||||
"_fillfuncargs",
|
||||
"Item",
|
||||
"File",
|
||||
"Collector",
|
||||
"Session",
|
||||
"Module",
|
||||
"approx",
|
||||
"Class",
|
||||
"Instance",
|
||||
"cmdline",
|
||||
"Collector",
|
||||
"deprecated_call",
|
||||
"exit",
|
||||
"fail",
|
||||
"File",
|
||||
"fixture",
|
||||
"freeze_includes",
|
||||
"Function",
|
||||
"Generator",
|
||||
"hookimpl",
|
||||
"hookspec",
|
||||
"importorskip",
|
||||
"Instance",
|
||||
"Item",
|
||||
"main",
|
||||
"mark",
|
||||
"Module",
|
||||
"Package",
|
||||
"param",
|
||||
"PytestDeprecationWarning",
|
||||
"PytestExperimentalApiWarning",
|
||||
"PytestWarning",
|
||||
"raises",
|
||||
"register_assert_rewrite",
|
||||
"RemovedInPytest4Warning",
|
||||
"Session",
|
||||
"set_trace",
|
||||
"skip",
|
||||
"UsageError",
|
||||
"warns",
|
||||
"xfail",
|
||||
"yield_fixture",
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
import types
|
||||
|
||||
import six
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest
|
||||
from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR
|
||||
@@ -14,13 +14,7 @@ from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR
|
||||
|
||||
class TestGeneralUsage(object):
|
||||
def test_config_error(self, testdir):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
def pytest_configure(config):
|
||||
import pytest
|
||||
raise pytest.UsageError("hello")
|
||||
"""
|
||||
)
|
||||
testdir.copy_example("conftest_usageerror/conftest.py")
|
||||
result = testdir.runpytest(testdir.tmpdir)
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines(["*ERROR: hello"])
|
||||
@@ -137,7 +131,7 @@ class TestGeneralUsage(object):
|
||||
p2 = testdir.makefile(".pyc", "123")
|
||||
result = testdir.runpytest(p1, p2)
|
||||
assert result.ret
|
||||
result.stderr.fnmatch_lines(["*ERROR: not found:*%s" % (p2.basename,)])
|
||||
result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)])
|
||||
|
||||
def test_issue486_better_reporting_on_conftest_load_failure(self, testdir):
|
||||
testdir.makepyfile("")
|
||||
@@ -170,18 +164,7 @@ class TestGeneralUsage(object):
|
||||
result.stdout.fnmatch_lines(["*1 skip*"])
|
||||
|
||||
def test_issue88_initial_file_multinodes(self, testdir):
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
import pytest
|
||||
class MyFile(pytest.File):
|
||||
def collect(self):
|
||||
return [MyItem("hello", parent=self)]
|
||||
def pytest_collect_file(path, parent):
|
||||
return MyFile(path, parent)
|
||||
class MyItem(pytest.Item):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
testdir.copy_example("issue88_initial_file_multinodes")
|
||||
p = testdir.makepyfile("def test_hello(): pass")
|
||||
result = testdir.runpytest(p, "--collect-only")
|
||||
result.stdout.fnmatch_lines(["*MyFile*test_issue88*", "*Module*test_issue88*"])
|
||||
@@ -218,16 +201,16 @@ class TestGeneralUsage(object):
|
||||
testdir.tmpdir.join("py").mksymlinkto(py._pydir)
|
||||
p = testdir.tmpdir.join("main.py")
|
||||
p.write(
|
||||
_pytest._code.Source(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
import sys, os
|
||||
sys.path.insert(0, '')
|
||||
import py
|
||||
print(py.__file__)
|
||||
print(py.__path__)
|
||||
os.chdir(os.path.dirname(os.getcwd()))
|
||||
print(py.log)
|
||||
"""
|
||||
import sys, os
|
||||
sys.path.insert(0, '')
|
||||
import py
|
||||
print (py.__file__)
|
||||
print (py.__path__)
|
||||
os.chdir(os.path.dirname(os.getcwd()))
|
||||
print (py.log)
|
||||
"""
|
||||
)
|
||||
)
|
||||
result = testdir.runpython(p)
|
||||
@@ -470,7 +453,7 @@ class TestInvocationVariants(object):
|
||||
@pytest.mark.xfail("sys.platform.startswith('java')")
|
||||
def test_pydoc(self, testdir):
|
||||
for name in ("py.test", "pytest"):
|
||||
result = testdir.runpython_c("import %s;help(%s)" % (name, name))
|
||||
result = testdir.runpython_c("import {};help({})".format(name, name))
|
||||
assert result.ret == 0
|
||||
s = result.stdout.str()
|
||||
assert "MarkGenerator" in s
|
||||
@@ -594,7 +577,7 @@ class TestInvocationVariants(object):
|
||||
return what
|
||||
|
||||
empty_package = testdir.mkpydir("empty_package")
|
||||
monkeypatch.setenv("PYTHONPATH", join_pythonpath(empty_package))
|
||||
monkeypatch.setenv("PYTHONPATH", str(join_pythonpath(empty_package)))
|
||||
# the path which is not a package raises a warning on pypy;
|
||||
# no idea why only pypy and not normal python warn about it here
|
||||
with warnings.catch_warnings():
|
||||
@@ -603,7 +586,7 @@ class TestInvocationVariants(object):
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines(["*2 passed*"])
|
||||
|
||||
monkeypatch.setenv("PYTHONPATH", join_pythonpath(testdir))
|
||||
monkeypatch.setenv("PYTHONPATH", str(join_pythonpath(testdir)))
|
||||
result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True)
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines(["*not*found*test_missing*"])
|
||||
@@ -677,6 +660,16 @@ class TestInvocationVariants(object):
|
||||
["*test_world.py::test_other*PASSED*", "*1 passed*"]
|
||||
)
|
||||
|
||||
def test_invoke_test_and_doctestmodules(self, testdir):
|
||||
p = testdir.makepyfile(
|
||||
"""
|
||||
def test():
|
||||
pass
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest(str(p) + "::test", "--doctest-modules")
|
||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||
|
||||
@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks")
|
||||
def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
|
||||
"""
|
||||
@@ -843,7 +836,7 @@ class TestDurations(object):
|
||||
if ("test_%s" % x) in line and y in line:
|
||||
break
|
||||
else:
|
||||
raise AssertionError("not found %s %s" % (x, y))
|
||||
raise AssertionError("not found {} {}".format(x, y))
|
||||
|
||||
def test_with_deselected(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
@@ -1061,3 +1054,15 @@ def test_frame_leak_on_failing_test(testdir):
|
||||
)
|
||||
result = testdir.runpytest_subprocess()
|
||||
result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"])
|
||||
|
||||
|
||||
def test_fixture_mock_integration(testdir):
|
||||
"""Test that decorators applied to fixture are left working (#3774)"""
|
||||
p = testdir.copy_example("acceptance/fixture_mock_integration.py")
|
||||
result = testdir.runpytest(p)
|
||||
result.stdout.fnmatch_lines("*1 passed*")
|
||||
|
||||
|
||||
def test_usage_error_code(testdir):
|
||||
result = testdir.runpytest("-unknown-option-")
|
||||
assert result.ret == EXIT_USAGEERROR
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
# coding: utf-8
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import sys
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest
|
||||
from test_excinfo import TWMock
|
||||
from six import text_type
|
||||
|
||||
from test_excinfo import TWMock
|
||||
|
||||
try:
|
||||
import mock
|
||||
except ImportError:
|
||||
import unittest.mock as mock
|
||||
|
||||
|
||||
def test_ne():
|
||||
code1 = _pytest._code.Code(compile('foo = "bar"', "", "exec"))
|
||||
@@ -32,10 +38,8 @@ def test_code_with_class():
|
||||
pytest.raises(TypeError, "_pytest._code.Code(A)")
|
||||
|
||||
|
||||
if True:
|
||||
|
||||
def x():
|
||||
pass
|
||||
def x():
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def test_code_fullsource():
|
||||
@@ -48,7 +52,7 @@ def test_code_source():
|
||||
code = _pytest._code.Code(x)
|
||||
src = code.source()
|
||||
expected = """def x():
|
||||
pass"""
|
||||
raise NotImplementedError()"""
|
||||
assert str(src) == expected
|
||||
|
||||
|
||||
@@ -68,12 +72,8 @@ def test_getstatement_empty_fullsource():
|
||||
|
||||
f = func()
|
||||
f = _pytest._code.Frame(f)
|
||||
prop = f.code.__class__.fullsource
|
||||
try:
|
||||
f.code.__class__.fullsource = None
|
||||
assert f.statement == _pytest._code.Source("")
|
||||
finally:
|
||||
f.code.__class__.fullsource = prop
|
||||
with mock.patch.object(f.code.__class__, "fullsource", None):
|
||||
assert f.statement == ""
|
||||
|
||||
|
||||
def test_code_from_func():
|
||||
@@ -83,20 +83,20 @@ def test_code_from_func():
|
||||
|
||||
|
||||
def test_unicode_handling():
|
||||
value = py.builtin._totext("\xc4\x85\xc4\x87\n", "utf-8").encode("utf8")
|
||||
value = u"ąć".encode("UTF-8")
|
||||
|
||||
def f():
|
||||
raise Exception(value)
|
||||
|
||||
excinfo = pytest.raises(Exception, f)
|
||||
str(excinfo)
|
||||
if sys.version_info[0] < 3:
|
||||
text_type(excinfo)
|
||||
text_type(excinfo)
|
||||
if sys.version_info < (3,):
|
||||
bytes(excinfo)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.version_info[0] >= 3, reason="python 2 only issue")
|
||||
def test_unicode_handling_syntax_error():
|
||||
value = py.builtin._totext("\xc4\x85\xc4\x87\n", "utf-8").encode("utf8")
|
||||
value = u"ąć".encode("UTF-8")
|
||||
|
||||
def f():
|
||||
raise SyntaxError("invalid syntax", (None, 1, 3, value))
|
||||
@@ -109,25 +109,25 @@ def test_unicode_handling_syntax_error():
|
||||
|
||||
def test_code_getargs():
|
||||
def f1(x):
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
c1 = _pytest._code.Code(f1)
|
||||
assert c1.getargs(var=True) == ("x",)
|
||||
|
||||
def f2(x, *y):
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
c2 = _pytest._code.Code(f2)
|
||||
assert c2.getargs(var=True) == ("x", "y")
|
||||
|
||||
def f3(x, **z):
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
c3 = _pytest._code.Code(f3)
|
||||
assert c3.getargs(var=True) == ("x", "z")
|
||||
|
||||
def f4(x, *y, **z):
|
||||
pass
|
||||
raise NotImplementedError()
|
||||
|
||||
c4 = _pytest._code.Code(f4)
|
||||
assert c4.getargs(var=True) == ("x", "y", "z")
|
||||
@@ -192,11 +192,14 @@ class TestReprFuncArgs(object):
|
||||
|
||||
tw = TWMock()
|
||||
|
||||
args = [("unicode_string", u"São Paulo"), ("utf8_string", "S\xc3\xa3o Paulo")]
|
||||
args = [("unicode_string", u"São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")]
|
||||
|
||||
r = ReprFuncArgs(args)
|
||||
r.toterminal(tw)
|
||||
if sys.version_info[0] >= 3:
|
||||
assert tw.lines[0] == "unicode_string = São Paulo, utf8_string = São Paulo"
|
||||
assert (
|
||||
tw.lines[0]
|
||||
== r"unicode_string = São Paulo, utf8_string = b'S\xc3\xa3o Paulo'"
|
||||
)
|
||||
else:
|
||||
assert tw.lines[0] == "unicode_string = São Paulo, utf8_string = São Paulo"
|
||||
|
||||
@@ -4,9 +4,11 @@ from __future__ import absolute_import, division, print_function
|
||||
import operator
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
import _pytest
|
||||
import py
|
||||
import pytest
|
||||
import six
|
||||
from _pytest._code.code import (
|
||||
ExceptionInfo,
|
||||
FormattedExcinfo,
|
||||
@@ -147,7 +149,7 @@ class TestTraceback_f_g_h(object):
|
||||
except somenoname:
|
||||
pass
|
||||
xyz()
|
||||
"""
|
||||
"""
|
||||
)
|
||||
try:
|
||||
exec(source.compile())
|
||||
@@ -250,7 +252,7 @@ class TestTraceback_f_g_h(object):
|
||||
import sys
|
||||
|
||||
exc, val, tb = sys.exc_info()
|
||||
py.builtin._reraise(exc, val, tb)
|
||||
six.reraise(exc, val, tb)
|
||||
|
||||
def f(n):
|
||||
try:
|
||||
@@ -268,7 +270,7 @@ class TestTraceback_f_g_h(object):
|
||||
decorator = pytest.importorskip("decorator").decorator
|
||||
|
||||
def log(f, *k, **kw):
|
||||
print("%s %s" % (k, kw))
|
||||
print("{} {}".format(k, kw))
|
||||
f(*k, **kw)
|
||||
|
||||
log = decorator(log)
|
||||
@@ -424,7 +426,7 @@ class TestFormattedExcinfo(object):
|
||||
@pytest.fixture
|
||||
def importasmod(self, request):
|
||||
def importasmod(source):
|
||||
source = _pytest._code.Source(source)
|
||||
source = textwrap.dedent(source)
|
||||
tmpdir = request.getfixturevalue("tmpdir")
|
||||
modpath = tmpdir.join("mod.py")
|
||||
tmpdir.ensure("__init__.py")
|
||||
@@ -448,10 +450,10 @@ class TestFormattedExcinfo(object):
|
||||
def test_repr_source(self):
|
||||
pr = FormattedExcinfo()
|
||||
source = _pytest._code.Source(
|
||||
"""
|
||||
"""\
|
||||
def f(x):
|
||||
pass
|
||||
"""
|
||||
"""
|
||||
).strip()
|
||||
pr.flow_marker = "|"
|
||||
lines = pr.get_source(source, 0)
|
||||
@@ -579,6 +581,18 @@ raise ValueError()
|
||||
assert reprlocals.lines[2] == "y = 5"
|
||||
assert reprlocals.lines[3] == "z = 7"
|
||||
|
||||
def test_repr_local_truncated(self):
|
||||
loc = {"l": [i for i in range(10)]}
|
||||
p = FormattedExcinfo(showlocals=True)
|
||||
truncated_reprlocals = p.repr_locals(loc)
|
||||
assert truncated_reprlocals.lines
|
||||
assert truncated_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, ...]"
|
||||
|
||||
q = FormattedExcinfo(showlocals=True, truncate_locals=False)
|
||||
full_reprlocals = q.repr_locals(loc)
|
||||
assert full_reprlocals.lines
|
||||
assert full_reprlocals.lines[0] == "l = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]"
|
||||
|
||||
def test_repr_tracebackentry_lines(self, importasmod):
|
||||
mod = importasmod(
|
||||
"""
|
||||
@@ -871,10 +885,10 @@ raise ValueError()
|
||||
|
||||
class MyRepr(TerminalRepr):
|
||||
def toterminal(self, tw):
|
||||
tw.line(py.builtin._totext("я", "utf-8"))
|
||||
tw.line(u"я")
|
||||
|
||||
x = py.builtin._totext(MyRepr())
|
||||
assert x == py.builtin._totext("я", "utf-8")
|
||||
x = six.text_type(MyRepr())
|
||||
assert x == u"я"
|
||||
|
||||
def test_toterminal_long(self, importasmod):
|
||||
mod = importasmod(
|
||||
@@ -1253,6 +1267,50 @@ raise ValueError()
|
||||
]
|
||||
)
|
||||
|
||||
@pytest.mark.skipif("sys.version_info[0] < 3")
|
||||
def test_exc_chain_repr_cycle(self, importasmod):
|
||||
mod = importasmod(
|
||||
"""
|
||||
class Err(Exception):
|
||||
pass
|
||||
def fail():
|
||||
return 0 / 0
|
||||
def reraise():
|
||||
try:
|
||||
fail()
|
||||
except ZeroDivisionError as e:
|
||||
raise Err() from e
|
||||
def unreraise():
|
||||
try:
|
||||
reraise()
|
||||
except Err as e:
|
||||
raise e.__cause__
|
||||
"""
|
||||
)
|
||||
excinfo = pytest.raises(ZeroDivisionError, mod.unreraise)
|
||||
r = excinfo.getrepr(style="short")
|
||||
tw = TWMock()
|
||||
r.toterminal(tw)
|
||||
out = "\n".join(line for line in tw.lines if isinstance(line, str))
|
||||
expected_out = textwrap.dedent(
|
||||
"""\
|
||||
:13: in unreraise
|
||||
reraise()
|
||||
:10: in reraise
|
||||
raise Err() from e
|
||||
E test_exc_chain_repr_cycle0.mod.Err
|
||||
|
||||
During handling of the above exception, another exception occurred:
|
||||
:15: in unreraise
|
||||
raise e.__cause__
|
||||
:8: in reraise
|
||||
fail()
|
||||
:5: in fail
|
||||
return 0 / 0
|
||||
E ZeroDivisionError: division by zero"""
|
||||
)
|
||||
assert out == expected_out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("style", ["short", "long"])
|
||||
@pytest.mark.parametrize("encoding", [None, "utf8", "utf16"])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# flake8: noqa
|
||||
# disable flake check on this file because some constructs are strange
|
||||
# or redundant on purpose and can't be disable on a line-by-line basis
|
||||
@@ -6,8 +7,8 @@ import inspect
|
||||
import sys
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest
|
||||
import six
|
||||
from _pytest._code import Source
|
||||
from _pytest._code.source import ast
|
||||
|
||||
@@ -41,15 +42,11 @@ def test_source_str_function():
|
||||
|
||||
|
||||
def test_unicode():
|
||||
try:
|
||||
unicode
|
||||
except NameError:
|
||||
return
|
||||
x = Source(unicode("4"))
|
||||
x = Source(u"4")
|
||||
assert str(x) == "4"
|
||||
co = _pytest._code.compile(unicode('u"\xc3\xa5"', "utf8"), mode="eval")
|
||||
co = _pytest._code.compile(u'u"å"', mode="eval")
|
||||
val = eval(co)
|
||||
assert isinstance(val, unicode)
|
||||
assert isinstance(val, six.text_type)
|
||||
|
||||
|
||||
def test_source_from_function():
|
||||
@@ -132,7 +129,7 @@ def test_source_strip_multiline():
|
||||
def test_syntaxerror_rerepresentation():
|
||||
ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz")
|
||||
assert ex.value.lineno == 1
|
||||
assert ex.value.offset in (4, 7) # XXX pypy/jython versus cpython?
|
||||
assert ex.value.offset in (4, 5, 7) # XXX pypy/jython versus cpython?
|
||||
assert ex.value.text.strip(), "x x"
|
||||
|
||||
|
||||
@@ -323,7 +320,7 @@ class TestSourceParsingAndCompiling(object):
|
||||
|
||||
def test_compile_and_getsource(self):
|
||||
co = self.source.compile()
|
||||
py.builtin.exec_(co, globals())
|
||||
six.exec_(co, globals())
|
||||
f(7)
|
||||
excinfo = pytest.raises(AssertionError, "f(6)")
|
||||
frame = excinfo.traceback[-1].frame
|
||||
@@ -392,7 +389,7 @@ def test_getfuncsource_dynamic():
|
||||
def g(): pass
|
||||
"""
|
||||
co = _pytest._code.compile(source)
|
||||
py.builtin.exec_(co, globals())
|
||||
six.exec_(co, globals())
|
||||
assert str(_pytest._code.Source(f)).strip() == "def f():\n raise ValueError"
|
||||
assert str(_pytest._code.Source(g)).strip() == "def g(): pass"
|
||||
|
||||
@@ -632,7 +629,7 @@ def test_issue55():
|
||||
assert str(s) == ' round_trip("""\n""")'
|
||||
|
||||
|
||||
def XXXtest_multiline():
|
||||
def test_multiline():
|
||||
source = getstatement(
|
||||
0,
|
||||
"""\
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_yield_tests_deprecation(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
@@ -15,16 +19,18 @@ def test_yield_tests_deprecation(testdir):
|
||||
yield func1, 1, 1
|
||||
"""
|
||||
)
|
||||
result = testdir.runpytest("-ra")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*yield tests are deprecated, and scheduled to be removed in pytest 4.0*",
|
||||
"*test_yield_tests_deprecation.py:3:*yield tests are deprecated*",
|
||||
"*test_yield_tests_deprecation.py:6:*yield tests are deprecated*",
|
||||
"*2 passed*",
|
||||
]
|
||||
)
|
||||
assert result.stdout.str().count("yield tests are deprecated") == 2
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_funcarg_prefix_deprecation(testdir):
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
@@ -39,16 +45,15 @@ def test_funcarg_prefix_deprecation(testdir):
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
(
|
||||
"*pytest_funcarg__value: "
|
||||
'declaring fixtures using "pytest_funcarg__" prefix is deprecated '
|
||||
"and scheduled to be removed in pytest 4.0. "
|
||||
"Please remove the prefix and use the @pytest.fixture decorator instead."
|
||||
"*test_funcarg_prefix_deprecation.py:1: *pytest_funcarg__value: "
|
||||
'declaring fixtures using "pytest_funcarg__" prefix is deprecated*'
|
||||
),
|
||||
"*1 passed*",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_pytest_setup_cfg_deprecated(testdir):
|
||||
testdir.makefile(
|
||||
".cfg",
|
||||
@@ -63,6 +68,7 @@ def test_pytest_setup_cfg_deprecated(testdir):
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_pytest_custom_cfg_deprecated(testdir):
|
||||
testdir.makefile(
|
||||
".cfg",
|
||||
@@ -77,15 +83,15 @@ def test_pytest_custom_cfg_deprecated(testdir):
|
||||
)
|
||||
|
||||
|
||||
def test_str_args_deprecated(tmpdir, testdir):
|
||||
def test_str_args_deprecated(tmpdir):
|
||||
"""Deprecate passing strings to pytest.main(). Scheduled for removal in pytest-4.0."""
|
||||
from _pytest.main import EXIT_NOTESTSCOLLECTED
|
||||
|
||||
warnings = []
|
||||
|
||||
class Collect(object):
|
||||
def pytest_logwarning(self, message):
|
||||
warnings.append(message)
|
||||
def pytest_warning_captured(self, warning_message):
|
||||
warnings.append(str(warning_message.message))
|
||||
|
||||
ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()])
|
||||
msg = (
|
||||
@@ -100,6 +106,7 @@ def test_getfuncargvalue_is_deprecated(request):
|
||||
pytest.deprecated_call(request.getfuncargvalue, "tmpdir")
|
||||
|
||||
|
||||
@pytest.mark.filterwarnings("default")
|
||||
def test_resultlog_is_deprecated(testdir):
|
||||
result = testdir.runpytest("--help")
|
||||
result.stdout.fnmatch_lines(["*DEPRECATED path for machine-readable result log*"])
|
||||
@@ -114,7 +121,7 @@ def test_resultlog_is_deprecated(testdir):
|
||||
result.stdout.fnmatch_lines(
|
||||
[
|
||||
"*--result-log is deprecated and scheduled for removal in pytest 4.0*",
|
||||
"*See https://docs.pytest.org/*/usage.html#creating-resultlog-format-files for more information*",
|
||||
"*See https://docs.pytest.org/en/latest/usage.html#creating-resultlog-format-files for more information*",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -171,21 +178,12 @@ def test_pytest_catchlog_deprecated(testdir, plugin):
|
||||
def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir):
|
||||
from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
|
||||
|
||||
subdirectory = testdir.tmpdir.join("subdirectory")
|
||||
subdirectory.mkdir()
|
||||
# create the inner conftest with makeconftest and then move it to the subdirectory
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
testdir.makepyfile(
|
||||
**{
|
||||
"subdirectory/conftest.py": """
|
||||
pytest_plugins=['capture']
|
||||
"""
|
||||
)
|
||||
testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py"))
|
||||
# make the top level conftest
|
||||
testdir.makeconftest(
|
||||
"""
|
||||
import warnings
|
||||
warnings.filterwarnings('always', category=DeprecationWarning)
|
||||
"""
|
||||
}
|
||||
)
|
||||
testdir.makepyfile(
|
||||
"""
|
||||
@@ -193,13 +191,44 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir):
|
||||
pass
|
||||
"""
|
||||
)
|
||||
res = testdir.runpytest_subprocess()
|
||||
res = testdir.runpytest()
|
||||
assert res.ret == 0
|
||||
res.stderr.fnmatch_lines(
|
||||
"*" + str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
|
||||
msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
|
||||
res.stdout.fnmatch_lines(
|
||||
"*subdirectory{sep}conftest.py:0: RemovedInPytest4Warning: {msg}*".format(
|
||||
sep=os.sep, msg=msg
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("use_pyargs", [True, False])
|
||||
def test_pytest_plugins_in_non_top_level_conftest_deprecated_pyargs(
|
||||
testdir, use_pyargs
|
||||
):
|
||||
"""When using --pyargs, do not emit the warning about non-top-level conftest warnings (#4039, #4044)"""
|
||||
from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST
|
||||
|
||||
files = {
|
||||
"src/pkg/__init__.py": "",
|
||||
"src/pkg/conftest.py": "",
|
||||
"src/pkg/test_root.py": "def test(): pass",
|
||||
"src/pkg/sub/__init__.py": "",
|
||||
"src/pkg/sub/conftest.py": "pytest_plugins=['capture']",
|
||||
"src/pkg/sub/test_bar.py": "def test(): pass",
|
||||
}
|
||||
testdir.makepyfile(**files)
|
||||
testdir.syspathinsert(testdir.tmpdir.join("src"))
|
||||
|
||||
args = ("--pyargs", "pkg") if use_pyargs else ()
|
||||
res = testdir.runpytest(*args)
|
||||
assert res.ret == 0
|
||||
msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
|
||||
if use_pyargs:
|
||||
assert msg not in res.stdout.str()
|
||||
else:
|
||||
res.stdout.fnmatch_lines("*{msg}*".format(msg=msg))
|
||||
|
||||
|
||||
def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_top_level_conftest(
|
||||
testdir
|
||||
):
|
||||
@@ -225,8 +254,11 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_top_level_confte
|
||||
|
||||
res = testdir.runpytest_subprocess()
|
||||
assert res.ret == 0
|
||||
res.stderr.fnmatch_lines(
|
||||
"*" + str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
|
||||
msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
|
||||
res.stdout.fnmatch_lines(
|
||||
"*subdirectory{sep}conftest.py:0: RemovedInPytest4Warning: {msg}*".format(
|
||||
sep=os.sep, msg=msg
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -259,7 +291,36 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_false_positives(
|
||||
)
|
||||
res = testdir.runpytest_subprocess()
|
||||
assert res.ret == 0
|
||||
assert (
|
||||
str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
|
||||
not in res.stderr.str()
|
||||
)
|
||||
msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
|
||||
assert msg not in res.stdout.str()
|
||||
|
||||
|
||||
def test_call_fixture_function_deprecated():
|
||||
"""Check if a warning is raised if a fixture function is called directly (#3661)"""
|
||||
|
||||
@pytest.fixture
|
||||
def fix():
|
||||
return 1
|
||||
|
||||
with pytest.deprecated_call():
|
||||
assert fix() == 1
|
||||
|
||||
|
||||
def test_pycollector_makeitem_is_deprecated():
|
||||
from _pytest.python import PyCollector
|
||||
from _pytest.warning_types import RemovedInPytest4Warning
|
||||
|
||||
class PyCollectorMock(PyCollector):
|
||||
"""evil hack"""
|
||||
|
||||
def __init__(self):
|
||||
self.called = False
|
||||
|
||||
def _makeitem(self, *k):
|
||||
"""hack to disable the actual behaviour"""
|
||||
self.called = True
|
||||
|
||||
collector = PyCollectorMock()
|
||||
with pytest.warns(RemovedInPytest4Warning):
|
||||
collector.makeitem("foo", "bar")
|
||||
assert collector.called
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user