Compare commits
1044 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8368e6c2e | ||
|
|
fac8208e8f | ||
|
|
51abdb80db | ||
|
|
65534682aa | ||
|
|
e980fbbe39 | ||
|
|
07e768ab68 | ||
|
|
9a2e0c061d | ||
|
|
056d9e8dc2 | ||
|
|
46f5d7a1bb | ||
|
|
30453057e8 | ||
|
|
3b757b1b8c | ||
|
|
14a9b1ec83 | ||
|
|
9fcbf57163 | ||
|
|
1fb2457018 | ||
|
|
92219e576b | ||
|
|
143ac5af99 | ||
|
|
409b919fc0 | ||
|
|
eadd15fe45 | ||
|
|
3f7223af44 | ||
|
|
20085542e2 | ||
|
|
cfaf3600c1 | ||
|
|
188df8100c | ||
|
|
44fa5a77d4 | ||
|
|
31476c69ab | ||
|
|
6200920dc3 | ||
|
|
46c5d5355e | ||
|
|
39024a7536 | ||
|
|
ae62ced080 | ||
|
|
6166151ee4 | ||
|
|
bedceaacc4 | ||
|
|
1127d519db | ||
|
|
b5ac61657a | ||
|
|
48548767fc | ||
|
|
7536e949b1 | ||
|
|
287c003cfd | ||
|
|
aa53e37fa2 | ||
|
|
dd97c94035 | ||
|
|
264e455410 | ||
|
|
75f11f0b65 | ||
|
|
45d0a21294 | ||
|
|
147b43f832 | ||
|
|
6e14585ca2 | ||
|
|
dae74b674e | ||
|
|
2a99e5dd2a | ||
|
|
9361d48b61 | ||
|
|
ebddac6a5c | ||
|
|
b319375592 | ||
|
|
f8fdf0ae91 | ||
|
|
053fc118b7 | ||
|
|
b486e1294b | ||
|
|
9d879bee36 | ||
|
|
4131d3f300 | ||
|
|
254e357076 | ||
|
|
0f6879bf5e | ||
|
|
bfe2cbe875 | ||
|
|
00d3001138 | ||
|
|
6bc45d158d | ||
|
|
371eb8c6af | ||
|
|
435b8ddc7c | ||
|
|
3cbf0c8ec0 | ||
|
|
9849022eb2 | ||
|
|
18c84a1904 | ||
|
|
247cdb835a | ||
|
|
ce1872e7e8 | ||
|
|
d845af7b24 | ||
|
|
391553887b | ||
|
|
7656fc8320 | ||
|
|
b6166dccb4 | ||
|
|
e7bcc854d9 | ||
|
|
ffee213c85 | ||
|
|
e5b527d0e3 | ||
|
|
c04e248de5 | ||
|
|
3685c1bc01 | ||
|
|
eea169e515 | ||
|
|
16c52f05f1 | ||
|
|
f72182977d | ||
|
|
40d0ade2d9 | ||
|
|
7152707280 | ||
|
|
42c1f85257 | ||
|
|
0d15a46863 | ||
|
|
570edb466b | ||
|
|
7c80c81433 | ||
|
|
9202ba91cf | ||
|
|
ea8997a108 | ||
|
|
867344d0d7 | ||
|
|
e64feaba7a | ||
|
|
74633815aa | ||
|
|
e289c60c3a | ||
|
|
8c81722a0c | ||
|
|
3425edd2a5 | ||
|
|
4458e65fe7 | ||
|
|
653abad27b | ||
|
|
89a55d85a9 | ||
|
|
ef7df8f167 | ||
|
|
90a8faabba | ||
|
|
e12a588c39 | ||
|
|
547070e2d8 | ||
|
|
f2fb841b29 | ||
|
|
3256fa9ee9 | ||
|
|
dc9a9ec4c2 | ||
|
|
527845ef29 | ||
|
|
489e638b4e | ||
|
|
13ee1cffed | ||
|
|
4c148bd0ef | ||
|
|
71a7b3c062 | ||
|
|
ebab1b6c69 | ||
|
|
49773b573f | ||
|
|
32979def7d | ||
|
|
ab00c3e911 | ||
|
|
6e4efccc38 | ||
|
|
269eeec702 | ||
|
|
0e1be01b7a | ||
|
|
aff463a3c4 | ||
|
|
b3247c1d03 | ||
|
|
169635e889 | ||
|
|
cd0b2ace67 | ||
|
|
17a1ed5edf | ||
|
|
a54cd4c2fd | ||
|
|
77de45cce3 | ||
|
|
d550c33cd0 | ||
|
|
a58099022a | ||
|
|
3bc7ced97a | ||
|
|
8979b2a9d7 | ||
|
|
d4c11e58aa | ||
|
|
7f83605c81 | ||
|
|
37b41de779 | ||
|
|
bf2c10c810 | ||
|
|
4285325cb8 | ||
|
|
2a1b1107c5 | ||
|
|
cbbd606b6c | ||
|
|
a24ca9872f | ||
|
|
b8be339632 | ||
|
|
2aad8c0fce | ||
|
|
15cbd61159 | ||
|
|
2f955e0c99 | ||
|
|
af37778b0d | ||
|
|
3f5e9ea71e | ||
|
|
443275f025 | ||
|
|
8426c57a9e | ||
|
|
30ca9f9d38 | ||
|
|
46d87deb5d | ||
|
|
203508d9f3 | ||
|
|
2c7f94fdb9 | ||
|
|
ff90c9e237 | ||
|
|
b4e8861aa5 | ||
|
|
baa189f5a3 | ||
|
|
113bfb6be8 | ||
|
|
9f4688e549 | ||
|
|
3a9d0b26d5 | ||
|
|
a5e60b6a2d | ||
|
|
0df42b4426 | ||
|
|
0d96a5bf90 | ||
|
|
060f68bd90 | ||
|
|
e5739a3115 | ||
|
|
8a8797df80 | ||
|
|
8994603d46 | ||
|
|
5c0b340a4b | ||
|
|
196dcc37a8 | ||
|
|
0ab57c4139 | ||
|
|
29a7b5e064 | ||
|
|
27ae270159 | ||
|
|
2e40a8b3ca | ||
|
|
18e053546c | ||
|
|
9dbcac9af3 | ||
|
|
4a436572a8 | ||
|
|
97a4967b03 | ||
|
|
8f6a5928f7 | ||
|
|
5d89a93977 | ||
|
|
c53b72fd7b | ||
|
|
6bb739516f | ||
|
|
8d735f3e1d | ||
|
|
aca1b06747 | ||
|
|
8dcd2718aa | ||
|
|
5ad1313b8a | ||
|
|
3b3d237f07 | ||
|
|
c4c968fe69 | ||
|
|
0b6df94b12 | ||
|
|
c3d420bf75 | ||
|
|
ebb4c47155 | ||
|
|
7ea5a22657 | ||
|
|
cd76366d87 | ||
|
|
931e8830ba | ||
|
|
621374679b | ||
|
|
8be1136d03 | ||
|
|
e0b63e34fa | ||
|
|
1fd67c9000 | ||
|
|
e3406e0818 | ||
|
|
dc79116de3 | ||
|
|
8433e2ba04 | ||
|
|
86e1b44230 | ||
|
|
5d3f7d7142 | ||
|
|
648d5d0c6b | ||
|
|
dff597dcd0 | ||
|
|
076fb56f85 | ||
|
|
150537d5e0 | ||
|
|
d8c23fd39b | ||
|
|
b748576358 | ||
|
|
f555a3a76c | ||
|
|
1f4831a23f | ||
|
|
0a0d97aeb5 | ||
|
|
4a3863c2e2 | ||
|
|
d314691fd3 | ||
|
|
01e37fe892 | ||
|
|
abbdb60051 | ||
|
|
5939b336cd | ||
|
|
3d289b803d | ||
|
|
4a704bbb55 | ||
|
|
ee6c9f50a2 | ||
|
|
3181718fe0 | ||
|
|
2674f352e8 | ||
|
|
6fb46a0e79 | ||
|
|
820ea6d68f | ||
|
|
b0032ba2b3 | ||
|
|
cf9b31bd5a | ||
|
|
b68b80aec9 | ||
|
|
bd1d17e8de | ||
|
|
93306f6a5e | ||
|
|
962aede290 | ||
|
|
a8d3d329ec | ||
|
|
b256cd2a6a | ||
|
|
b6b36bc167 | ||
|
|
3dd24f8d21 | ||
|
|
794fb193ba | ||
|
|
d7e1f037d9 | ||
|
|
29ff9301d8 | ||
|
|
f3c666db3c | ||
|
|
b93aa5e35f | ||
|
|
bc66f7e43f | ||
|
|
cb6b851780 | ||
|
|
afb8a4e35d | ||
|
|
06a182386b | ||
|
|
fac07c1b3f | ||
|
|
e8c0ca4f08 | ||
|
|
ac6f257efc | ||
|
|
554cb8d09c | ||
|
|
25b504b4f0 | ||
|
|
0a6e086f9d | ||
|
|
f24c470403 | ||
|
|
52a7ccef57 | ||
|
|
bd2d0d2c3c | ||
|
|
08997279f4 | ||
|
|
205e29d843 | ||
|
|
672c901c70 | ||
|
|
c70efaa0fb | ||
|
|
d370e7788d | ||
|
|
0d83dd1b31 | ||
|
|
ed293ec3e9 | ||
|
|
2f8427bb4e | ||
|
|
94608c6110 | ||
|
|
afc607cfd8 | ||
|
|
30729b7c3c | ||
|
|
dfc5399cd7 | ||
|
|
76489d30f7 | ||
|
|
d6f75d2836 | ||
|
|
d85a3ca19a | ||
|
|
f3c9c6e8a8 | ||
|
|
67bd60d5c6 | ||
|
|
28a93b9eeb | ||
|
|
70461d1ead | ||
|
|
73eccb4c36 | ||
|
|
d7a76a4d07 | ||
|
|
924b5e2e3d | ||
|
|
d87279115d | ||
|
|
0a2735a275 | ||
|
|
c90e76c371 | ||
|
|
370daf0441 | ||
|
|
586ecea6f2 | ||
|
|
db4df5833a | ||
|
|
b17c6e5f89 | ||
|
|
c3f63ac143 | ||
|
|
3862b0b28d | ||
|
|
476d4df1b7 | ||
|
|
52449903c3 | ||
|
|
506c9c91c0 | ||
|
|
38f34e2fa1 | ||
|
|
ba41015ef6 | ||
|
|
ebfc1c49d1 | ||
|
|
45e7734b1a | ||
|
|
8ce6e39b1c | ||
|
|
c8e7d1ae34 | ||
|
|
7b5d4d01ed | ||
|
|
a4f4579f19 | ||
|
|
e4da9bacdf | ||
|
|
1e295535c3 | ||
|
|
3ca1e4b7f0 | ||
|
|
dc19624248 | ||
|
|
f8f1a52ea0 | ||
|
|
5c6d7739bc | ||
|
|
771b5c8852 | ||
|
|
fc5ec5807e | ||
|
|
fc544dc660 | ||
|
|
7792587b3f | ||
|
|
c2cd239d35 | ||
|
|
cb0ba18f53 | ||
|
|
3b85e0c3a9 | ||
|
|
73bc6bacfa | ||
|
|
8e8a953ac6 | ||
|
|
852b96714e | ||
|
|
dd64f1a4a9 | ||
|
|
41a6ec6f31 | ||
|
|
7feab7391d | ||
|
|
f0bfe9de3d | ||
|
|
596937e610 | ||
|
|
57fcd3f57e | ||
|
|
65f5383106 | ||
|
|
964c29cb93 | ||
|
|
38fb6aae78 | ||
|
|
1c5b887dfd | ||
|
|
ba209b5230 | ||
|
|
b62fd79c0c | ||
|
|
655146e522 | ||
|
|
88f2cc9b64 | ||
|
|
ed2bb9d723 | ||
|
|
2a111ff700 | ||
|
|
5c6758fde4 | ||
|
|
9bd8420a6b | ||
|
|
cbdab02d05 | ||
|
|
ce30896cd2 | ||
|
|
2e8b0a83fe | ||
|
|
369c711f14 | ||
|
|
cf0cac3b73 | ||
|
|
a9dd37f429 | ||
|
|
294729962d | ||
|
|
4de433e280 | ||
|
|
652936f47f | ||
|
|
1fe2e2cb03 | ||
|
|
e66473853c | ||
|
|
70f1e3b4b0 | ||
|
|
fdd4abb88a | ||
|
|
fdfc1946da | ||
|
|
5085aa2bce | ||
|
|
912330a7e2 | ||
|
|
88ed1ab648 | ||
|
|
191e8c6d9b | ||
|
|
6bbd741039 | ||
|
|
0f5fb7ed05 | ||
|
|
5f1a7330b2 | ||
|
|
2a75ae46c3 | ||
|
|
89cf943e04 | ||
|
|
833f33fa0c | ||
|
|
3dbac17d75 | ||
|
|
6843d45c51 | ||
|
|
4a840a7c09 | ||
|
|
9b7e4ab0c6 | ||
|
|
4ea7bbc197 | ||
|
|
4d2f05e4b9 | ||
|
|
454b60b6c5 | ||
|
|
f6be23b68b | ||
|
|
644fdc5237 | ||
|
|
4b5f0d5ffa | ||
|
|
9f7ba00611 | ||
|
|
796db80ca4 | ||
|
|
d95c8a2204 | ||
|
|
4678cbeb91 | ||
|
|
67ad0fa364 | ||
|
|
6cdd851227 | ||
|
|
c58715371c | ||
|
|
d5f038e29a | ||
|
|
6eeacaba3e | ||
|
|
6b90ad4d4b | ||
|
|
e273f5399d | ||
|
|
0de1a65644 | ||
|
|
95de11a44e | ||
|
|
05cfdcc8cb | ||
|
|
0ddd3e2839 | ||
|
|
aa9a02ec44 | ||
|
|
e97c774f8e | ||
|
|
f50ace7c0a | ||
|
|
49c0c599b0 | ||
|
|
e0d236c031 | ||
|
|
b533c2600a | ||
|
|
dc574c60ef | ||
|
|
1d26f3730f | ||
|
|
27935ebec9 | ||
|
|
378eb5d67b | ||
|
|
5e71ffab87 | ||
|
|
8df7ed12c1 | ||
|
|
f05333ab75 | ||
|
|
c8d52b633b | ||
|
|
2455f8670e | ||
|
|
3a5dbabf60 | ||
|
|
3441084bd2 | ||
|
|
8b92527d7d | ||
|
|
dab889304e | ||
|
|
7a7cb8c8c5 | ||
|
|
77bd0aa02f | ||
|
|
b0f558da44 | ||
|
|
ca1f4bc537 | ||
|
|
219b758949 | ||
|
|
6161bcff6e | ||
|
|
99a4a93dbc | ||
|
|
99ba3c9700 | ||
|
|
c19708b193 | ||
|
|
1f08d990d5 | ||
|
|
e2c59d3282 | ||
|
|
f9029f11af | ||
|
|
c7be83ac47 | ||
|
|
74aaf91653 | ||
|
|
1aeb58b531 | ||
|
|
7b3febd314 | ||
|
|
e87ff07370 | ||
|
|
a220a40350 | ||
|
|
dd6c534468 | ||
|
|
4a0aea2deb | ||
|
|
8f90812481 | ||
|
|
3b3bf9f53d | ||
|
|
54cea3d178 | ||
|
|
9628c71210 | ||
|
|
a0ad9e31da | ||
|
|
22cff038f8 | ||
|
|
685387a43e | ||
|
|
d26c1e3ad9 | ||
|
|
a6f2d2d2c9 | ||
|
|
6d3fe0b826 | ||
|
|
bdad345f99 | ||
|
|
063335a715 | ||
|
|
f074fd9ac6 | ||
|
|
6550b9911b | ||
|
|
258031afe5 | ||
|
|
259b86b6ab | ||
|
|
f0f2d2b861 | ||
|
|
d1af369800 | ||
|
|
b671c5a8bf | ||
|
|
f320686fe0 | ||
|
|
742f9cb825 | ||
|
|
99496d9e5b | ||
|
|
983a09a2d4 | ||
|
|
66fbebfc26 | ||
|
|
0108f262b1 | ||
|
|
76f3be452a | ||
|
|
c47dcaa713 | ||
|
|
b2b1eb262f | ||
|
|
9fce430c89 | ||
|
|
e114feb458 | ||
|
|
c09f69df2a | ||
|
|
3900879a5c | ||
|
|
7b1cc55add | ||
|
|
d904981bf3 | ||
|
|
f13333afce | ||
|
|
fad1fbe381 | ||
|
|
c33074c8b9 | ||
|
|
b11640c1eb | ||
|
|
03829fde8a | ||
|
|
e351976ef4 | ||
|
|
b18a9deb4c | ||
|
|
2e2f72156a | ||
|
|
22e9b006da | ||
|
|
802585cb66 | ||
|
|
d7e8eeef56 | ||
|
|
e58e8faf47 | ||
|
|
7d43225c36 | ||
|
|
460cae02b0 | ||
|
|
f3a119c06a | ||
|
|
d1aa553f73 | ||
|
|
cd747c48a4 | ||
|
|
07b2b18a01 | ||
|
|
766de67392 | ||
|
|
821f9a94d8 | ||
|
|
26019b33f8 | ||
|
|
cb30848e5a | ||
|
|
d00e2da6e9 | ||
|
|
2f993af54a | ||
|
|
af5e9238c8 | ||
|
|
8e178e9f9b | ||
|
|
8e28815d44 | ||
|
|
b27dde24d6 | ||
|
|
4a436f2255 | ||
|
|
27cea340f3 | ||
|
|
c3ba9225ef | ||
|
|
111d640bdb | ||
|
|
734c435d00 | ||
|
|
27bb2eceb4 | ||
|
|
383239cafc | ||
|
|
fd7bfa30d0 | ||
|
|
3427d27d5a | ||
|
|
def471b975 | ||
|
|
f743e95cfc | ||
|
|
4e581b637f | ||
|
|
6b86b0dbfe | ||
|
|
0b540f98b1 | ||
|
|
bdab29fa3d | ||
|
|
6821d36ca5 | ||
|
|
5631a86296 | ||
|
|
52aadcd7c1 | ||
|
|
f5e72d2f5f | ||
|
|
a5ac19cc5e | ||
|
|
14e3a5fcb9 | ||
|
|
7b608f976d | ||
|
|
fe560b7192 | ||
|
|
b61cbc4fba | ||
|
|
a3ec3df0c8 | ||
|
|
e23af009f9 | ||
|
|
531e0dcaa3 | ||
|
|
dc5f33ba5c | ||
|
|
655ab0bf8b | ||
|
|
a7199fa8ab | ||
|
|
d714c196a5 | ||
|
|
ee7e1c94d2 | ||
|
|
de9d116a49 | ||
|
|
f003914d4b | ||
|
|
1e6dc6f8e5 | ||
|
|
c03612f729 | ||
|
|
29fa9d5bff | ||
|
|
3cdbb1854f | ||
|
|
083084fcbc | ||
|
|
f7387e45ea | ||
|
|
3da28067f3 | ||
|
|
5c71151967 | ||
|
|
3f9f4be070 | ||
|
|
4cb60dac3d | ||
|
|
8c7974af01 | ||
|
|
e81b275eda | ||
|
|
537fc3c315 | ||
|
|
46cc9ab77c | ||
|
|
2d08005039 | ||
|
|
baadd569e8 | ||
|
|
11b391ff49 | ||
|
|
00d3abe6dc | ||
|
|
71c76d96d3 | ||
|
|
3676da594c | ||
|
|
843872b501 | ||
|
|
a4fd5cdcb5 | ||
|
|
ae4e596b31 | ||
|
|
cfdebb3ba4 | ||
|
|
eaf38c7239 | ||
|
|
b29a9711c4 | ||
|
|
c750a5beec | ||
|
|
df37cdf51f | ||
|
|
af75ca435b | ||
|
|
8aed5fecd9 | ||
|
|
f3261d9418 | ||
|
|
775f4a6f2f | ||
|
|
502652ff02 | ||
|
|
0e83511d6d | ||
|
|
815dd19fb4 | ||
|
|
1f3ab118fa | ||
|
|
0ec72d0745 | ||
|
|
69f3bd8336 | ||
|
|
10a3b9118b | ||
|
|
ce8c829945 | ||
|
|
ed7aa074aa | ||
|
|
66e9a79472 | ||
|
|
1480aed781 | ||
|
|
be0e2132b7 | ||
|
|
7113c76f0d | ||
|
|
ef732fc51d | ||
|
|
dd45f8ba6c | ||
|
|
c486598440 | ||
|
|
059455b45d | ||
|
|
73ff53c742 | ||
|
|
88366b393c | ||
|
|
9b0ce535c9 | ||
|
|
8a6bdb282f | ||
|
|
46e30435eb | ||
|
|
e86ba41a32 | ||
|
|
e89abe6a40 | ||
|
|
48b5c13f73 | ||
|
|
c24ffa3b4c | ||
|
|
459cc40192 | ||
|
|
e3b73682b2 | ||
|
|
8480075f01 | ||
|
|
9ad2b75038 | ||
|
|
a33650953a | ||
|
|
667e70f555 | ||
|
|
761d552814 | ||
|
|
4bc6ecb8a5 | ||
|
|
0668a6c6d3 | ||
|
|
03ce0adb79 | ||
|
|
e7a4d3d8cf | ||
|
|
9ee0a1f5c3 | ||
|
|
6b91bc88de | ||
|
|
61eb20df71 | ||
|
|
df6d5cd4e7 | ||
|
|
fbb9e9328b | ||
|
|
9824499396 | ||
|
|
59f66933cd | ||
|
|
c1aa63c0bb | ||
|
|
e4a6e52b81 | ||
|
|
06307be15d | ||
|
|
79d3353081 | ||
|
|
6690b8a444 | ||
|
|
7093d8f65e | ||
|
|
f9589f7b64 | ||
|
|
794d4585d3 | ||
|
|
d132c502e6 | ||
|
|
3b30c93f73 | ||
|
|
c0c859ce99 | ||
|
|
22f338d74d | ||
|
|
9919269ed0 | ||
|
|
87596714bf | ||
|
|
296ac5c476 | ||
|
|
ad21d5cac4 | ||
|
|
2559ec8bdb | ||
|
|
207f153ec1 | ||
|
|
3a4011585f | ||
|
|
57f66a455a | ||
|
|
e41fd52e8c | ||
|
|
08f6b5f4ea | ||
|
|
d13e17cf51 | ||
|
|
f1f6109255 | ||
|
|
87b8dc5afb | ||
|
|
fc965c1dc5 | ||
|
|
a1bd54e4ea | ||
|
|
36cceeb10e | ||
|
|
3e71a50403 | ||
|
|
98209e92ee | ||
|
|
1bea7e6985 | ||
|
|
1ba219e0da | ||
|
|
a8e3effb6c | ||
|
|
ca46f4fe2a | ||
|
|
5130f5707f | ||
|
|
6607478b23 | ||
|
|
8eafbd05ca | ||
|
|
de0d19ca09 | ||
|
|
d96869ff66 | ||
|
|
966391c77e | ||
|
|
9c8847a0cb | ||
|
|
58aaabbb10 | ||
|
|
b57a84d065 | ||
|
|
c89827b9f2 | ||
|
|
062a0e3e68 | ||
|
|
2802135741 | ||
|
|
a2da5a691a | ||
|
|
afe7966683 | ||
|
|
3ebfb881c9 | ||
|
|
bf77daa2ee | ||
|
|
9933635cf7 | ||
|
|
ac5c5cc1ef | ||
|
|
810320f591 | ||
|
|
25d2acbdb2 | ||
|
|
52c134aed3 | ||
|
|
14b6380e5f | ||
|
|
70cdfaf661 | ||
|
|
abfd9774ef | ||
|
|
e57cc55719 | ||
|
|
696c702da7 | ||
|
|
bee2c864d8 | ||
|
|
e27a0d69aa | ||
|
|
15222ceca2 | ||
|
|
3c1ca03b9c | ||
|
|
25ed4edbc7 | ||
|
|
1e93089165 | ||
|
|
b2a8e06e4f | ||
|
|
9273e11f21 | ||
|
|
09349c344e | ||
|
|
6cf515b164 | ||
|
|
6967f3070e | ||
|
|
a0c6758202 | ||
|
|
80d165475b | ||
|
|
aa6a67044f | ||
|
|
c52f87ede3 | ||
|
|
549f5c1a47 | ||
|
|
3f8ff7f090 | ||
|
|
b55a4f805f | ||
|
|
d01f08e96f | ||
|
|
ad36407747 | ||
|
|
e1f2254fc2 | ||
|
|
10d43bd3bf | ||
|
|
f825b4979b | ||
|
|
3d70727021 | ||
|
|
1fc185b640 | ||
|
|
7d59b2e350 | ||
|
|
d9992558fc | ||
|
|
d1f71b0575 | ||
|
|
de6b41e318 | ||
|
|
8d1903fed3 | ||
|
|
13eac944ae | ||
|
|
d8ecca5ebd | ||
|
|
9bbf14d0f6 | ||
|
|
c42d966a40 | ||
|
|
3dc0da9339 | ||
|
|
11ec6aeafb | ||
|
|
9d373d83ac | ||
|
|
181bd60bf9 | ||
|
|
3288c9a110 | ||
|
|
221797c609 | ||
|
|
5e00549ecc | ||
|
|
b770a32dc8 | ||
|
|
f9157b1b6b | ||
|
|
f4e811afc0 | ||
|
|
59cdef92be | ||
|
|
78a027e128 | ||
|
|
709b8b65a4 | ||
|
|
0824076e11 | ||
|
|
488bbd2aeb | ||
|
|
312891daa6 | ||
|
|
fe415e3ff8 | ||
|
|
ff35c17ecf | ||
|
|
67161ee9f8 | ||
|
|
1c891d7d97 | ||
|
|
9ab83083d1 | ||
|
|
756db2131f | ||
|
|
cb700208e8 | ||
|
|
333a9ad7fa | ||
|
|
12b1bff6c5 | ||
|
|
657976e98a | ||
|
|
a993add783 | ||
|
|
539523cfee | ||
|
|
f18780ed8a | ||
|
|
806d47b4d4 | ||
|
|
bfc9f61482 | ||
|
|
2a99d82c3b | ||
|
|
5c0feb2877 | ||
|
|
9b2753b302 | ||
|
|
5f17caa156 | ||
|
|
e9bfccdf2d | ||
|
|
7b5d26c1a8 | ||
|
|
98bf5fc9be | ||
|
|
362b1b3c4f | ||
|
|
5c0c1977e3 | ||
|
|
39331856ed | ||
|
|
dc9154e8ff | ||
|
|
021fba4e84 | ||
|
|
eb462582af | ||
|
|
fd84c886ee | ||
|
|
e6020781f6 | ||
|
|
acd3c4fbc4 | ||
|
|
c847b83d56 | ||
|
|
45d2962e97 | ||
|
|
8b322afcdb | ||
|
|
523bfa6151 | ||
|
|
cc0f2473eb | ||
|
|
76c55b31c6 | ||
|
|
a0101f024e | ||
|
|
d5f4496bdf | ||
|
|
37353a854e | ||
|
|
12e60956de | ||
|
|
15cdf137d5 | ||
|
|
9e62a31b63 | ||
|
|
2e33d9b35e | ||
|
|
dc563e4954 | ||
|
|
ad52f714a9 | ||
|
|
8969bd43c9 | ||
|
|
7703dc921c | ||
|
|
1deac2e210 | ||
|
|
02da156351 | ||
|
|
84061233ef | ||
|
|
40254b64e5 | ||
|
|
0a15edd573 | ||
|
|
51ebad76f2 | ||
|
|
d2bca93109 | ||
|
|
6e7547244b | ||
|
|
333ec8ba5a | ||
|
|
dcaeef7c10 | ||
|
|
8a2e6a8d51 | ||
|
|
74d536314f | ||
|
|
ceb016514b | ||
|
|
e90f876b34 | ||
|
|
c68a89b4a7 | ||
|
|
07dd1ca7b8 | ||
|
|
f1467f8f03 | ||
|
|
763c580a2a | ||
|
|
e1aed8cb17 | ||
|
|
713f7636e1 | ||
|
|
4cd8727379 | ||
|
|
5e0e038fec | ||
|
|
2e61f702c0 | ||
|
|
8c2319168a | ||
|
|
768edde899 | ||
|
|
be401bc2f8 | ||
|
|
06a49338b2 | ||
|
|
7a12acb6a1 | ||
|
|
5acb64be90 | ||
|
|
75e6f7717c | ||
|
|
17121960b4 | ||
|
|
7082320f3f | ||
|
|
6fe7069cbb | ||
|
|
d46006f791 | ||
|
|
f770f16294 | ||
|
|
eb1bd3449e | ||
|
|
22212c4d61 | ||
|
|
62810f61b2 | ||
|
|
e97fd5ec55 | ||
|
|
17c544e793 | ||
|
|
ddf1751e6d | ||
|
|
3d89905114 | ||
|
|
1a9bc141a5 | ||
|
|
10ded399d8 | ||
|
|
f047e078e2 | ||
|
|
f8bd693f83 | ||
|
|
a546a612bd | ||
|
|
dd294aafb3 | ||
|
|
b39f957b88 | ||
|
|
2c2cf81d0a | ||
|
|
80f4699572 | ||
|
|
57a232fc5a | ||
|
|
1851f36beb | ||
|
|
f0936d42fb | ||
|
|
d3ab1b9df4 | ||
|
|
0603d1d500 | ||
|
|
79097e84e2 | ||
|
|
1a42e26586 | ||
|
|
595ecd23fd | ||
|
|
949a1406f0 | ||
|
|
71947cb4f0 | ||
|
|
869eed9898 | ||
|
|
72531f30c0 | ||
|
|
73c6122f35 | ||
|
|
70d9f8638f | ||
|
|
d40d77432c | ||
|
|
e44284c125 | ||
|
|
dea671f8ba | ||
|
|
cdaa720bc4 | ||
|
|
d0ecfdf00f | ||
|
|
81ad185f0d | ||
|
|
d90bef44cc | ||
|
|
df12500661 | ||
|
|
43544a431c | ||
|
|
0aa2480e6a | ||
|
|
6473a81b8b | ||
|
|
af2c153324 | ||
|
|
309152d9fd | ||
|
|
d5bb2004f9 | ||
|
|
bda07d8b27 | ||
|
|
0726d9a09f | ||
|
|
61219da0e2 | ||
|
|
1b732fe361 | ||
|
|
b35554ca2b | ||
|
|
7e0553267d | ||
|
|
ebc7346be4 | ||
|
|
a3b35e1c4b | ||
|
|
4c45bc9971 | ||
|
|
495f731760 | ||
|
|
50764d9ebb | ||
|
|
6461dc9fc6 | ||
|
|
1cf826624e | ||
|
|
97e5a3c889 | ||
|
|
65b2de13a3 | ||
|
|
3d24485cae | ||
|
|
ccc4b3a501 | ||
|
|
cbceef2008 | ||
|
|
7341da1bc1 | ||
|
|
3c28a8ec1a | ||
|
|
22f54784c2 | ||
|
|
abb5d20841 | ||
|
|
da12c52347 | ||
|
|
9e3e58af60 | ||
|
|
56e6b4b501 | ||
|
|
d44565f385 | ||
|
|
24da938321 | ||
|
|
26ee2355d9 | ||
|
|
c92760dca8 | ||
|
|
61d4345ea4 | ||
|
|
1ac02b8a3b | ||
|
|
d06d97a7ac | ||
|
|
e73a2f7ad9 | ||
|
|
91b4b229aa | ||
|
|
2840634c2c | ||
|
|
eb79fa7825 | ||
|
|
d7f182ac4f | ||
|
|
2d4f1f022e | ||
|
|
2c03000b96 | ||
|
|
62556bada6 | ||
|
|
637e566d05 | ||
|
|
3a1c9c0e45 | ||
|
|
0e559c978f | ||
|
|
bd96b0aabc | ||
|
|
7b1870a94e | ||
|
|
4fd92ef9ba | ||
|
|
ac3f2207bb | ||
|
|
b2a5ec3b94 | ||
|
|
b49e8baab3 | ||
|
|
15610289ac | ||
|
|
5ae59279f4 | ||
|
|
bf259d3c93 | ||
|
|
85141a419f | ||
|
|
7d2ceb7872 | ||
|
|
b9e318866e | ||
|
|
45ac863069 | ||
|
|
7248b759e8 | ||
|
|
b840622819 | ||
|
|
17a21d540b | ||
|
|
9bad9b53d8 | ||
|
|
4730c6d99d | ||
|
|
c9a081d1a3 | ||
|
|
195a816522 | ||
|
|
eae8b41b07 | ||
|
|
8f3eb6dfc7 | ||
|
|
b226454582 | ||
|
|
4c24947785 | ||
|
|
617e510b6e | ||
|
|
4b22f270a3 | ||
|
|
2e8caefcab | ||
|
|
3fabc4d219 | ||
|
|
f640e0cb04 | ||
|
|
ebb6d0650b | ||
|
|
ba0a4d0b2e | ||
|
|
1ff54ba205 | ||
|
|
df54bf0db5 | ||
|
|
1c935db571 | ||
|
|
cf97159009 | ||
|
|
57438f3efe | ||
|
|
e855a79dd4 | ||
|
|
92e2cd9c68 | ||
|
|
051d76a63f | ||
|
|
4b20b9d8d9 | ||
|
|
425665cf25 | ||
|
|
0be97624b7 | ||
|
|
64a4b9058c | ||
|
|
8de49e8742 | ||
|
|
6146ac97d9 | ||
|
|
6af2abdb53 | ||
|
|
796ffa5123 | ||
|
|
ba9a76fdb3 | ||
|
|
cc39f41c53 | ||
|
|
2a979797ef | ||
|
|
e5169a026a | ||
|
|
3578f4e405 | ||
|
|
97fdc9a7fe | ||
|
|
771cedd3da | ||
|
|
81cec9f5e3 | ||
|
|
1485a3a902 | ||
|
|
f16c3b9568 | ||
|
|
e6b9a81ccf | ||
|
|
67fca04050 | ||
|
|
73b07e1439 | ||
|
|
b32cfc88da | ||
|
|
676c4f970d | ||
|
|
c2d49e39a2 | ||
|
|
89c73582ca | ||
|
|
d9aaab7ab2 | ||
|
|
9e0b19cce2 | ||
|
|
a87f6f84cc | ||
|
|
8a7d98fed9 | ||
|
|
cdd788085d | ||
|
|
80595115b0 | ||
|
|
bd52eebab4 | ||
|
|
91418eda3b | ||
|
|
7a9fc69435 | ||
|
|
f471eef661 | ||
|
|
ef62b86335 | ||
|
|
7cd03d7611 | ||
|
|
3667086acc | ||
|
|
db24a3b0fb | ||
|
|
221f42c5ce | ||
|
|
7a1a439049 | ||
|
|
b62aef3372 | ||
|
|
c111e9dac3 | ||
|
|
8524a57075 | ||
|
|
b63f6770a1 | ||
|
|
8a8687122d | ||
|
|
7277fbdb20 | ||
|
|
6908d93ba1 | ||
|
|
c578418791 | ||
|
|
0303d95a53 | ||
|
|
9b9fede5be | ||
|
|
9b51fc646c | ||
|
|
6eeab45a8f | ||
|
|
16df4da1f7 | ||
|
|
3de93657bd | ||
|
|
1906f8c565 | ||
|
|
655d44b413 | ||
|
|
0d0b01bded | ||
|
|
6e2b5a3f1b | ||
|
|
b3bf7fc496 | ||
|
|
bb659fcffe | ||
|
|
6de19ab7ba | ||
|
|
bab18e10eb | ||
|
|
8d5f2872d3 | ||
|
|
b0b6c355f7 | ||
|
|
23d016f114 | ||
|
|
22b7701431 | ||
|
|
1d926011a4 | ||
|
|
ff8dbd0ad8 | ||
|
|
5e832017d5 | ||
|
|
c791895c93 | ||
|
|
19b12b22e7 | ||
|
|
64ae6ae25d | ||
|
|
bdec2c8f9e | ||
|
|
4a62102b57 | ||
|
|
f2ba8d70b9 | ||
|
|
afe847ecdc | ||
|
|
9597e674d9 | ||
|
|
d6000e5ab1 | ||
|
|
4d02863b16 | ||
|
|
5d2496862a | ||
|
|
50769557e8 | ||
|
|
b41852c93b | ||
|
|
8badb47db6 | ||
|
|
8c3c4307db | ||
|
|
731c35fcab | ||
|
|
31b971d79d | ||
|
|
4e57a39067 | ||
|
|
af0344e940 | ||
|
|
97367cf773 | ||
|
|
336cf3e1f5 | ||
|
|
4e4ebbef5a | ||
|
|
b09d60c60a | ||
|
|
0908f40e43 | ||
|
|
0e73724e58 | ||
|
|
9970dea8c1 | ||
|
|
218af42325 | ||
|
|
6fa7b16482 | ||
|
|
4a992bafdb | ||
|
|
21137cf8c5 | ||
|
|
5a856b6e29 | ||
|
|
89292f08dc | ||
|
|
8c22aee256 | ||
|
|
9f3122fec6 | ||
|
|
9bd8907716 | ||
|
|
f8b2277413 | ||
|
|
6be57a3711 | ||
|
|
36251e0db4 | ||
|
|
f0541b685b | ||
|
|
536f1723ac | ||
|
|
8bb589fc5d | ||
|
|
467c526307 | ||
|
|
b2d7c26d80 | ||
|
|
7cbf265bb5 | ||
|
|
917b9a8352 | ||
|
|
2127a2378a | ||
|
|
d2db6626cf | ||
|
|
620ba5971f | ||
|
|
c67bf9d82a | ||
|
|
57e2ced969 | ||
|
|
80944e32ad | ||
|
|
54a90e9555 | ||
|
|
9d41eaedbf | ||
|
|
46d157fe07 | ||
|
|
87e4a28351 | ||
|
|
5ee9793c99 | ||
|
|
1863b7c7b2 | ||
|
|
01ed6dfc3b | ||
|
|
59b3693988 | ||
|
|
05796be21a | ||
|
|
f826b23f58 | ||
|
|
9abff7f72f | ||
|
|
f74f14f038 | ||
|
|
bcbad5b1af | ||
|
|
5d785e415e | ||
|
|
409d2f1d54 | ||
|
|
9adf513c4b | ||
|
|
cca4de20cf | ||
|
|
c98ad2a0a0 | ||
|
|
5de203195c | ||
|
|
021e843427 | ||
|
|
ac9c8fcdab | ||
|
|
3871810d1c | ||
|
|
281fcd5a58 | ||
|
|
4e6e29dbee | ||
|
|
1f0d06641a | ||
|
|
a63b34c685 | ||
|
|
522d59e844 | ||
|
|
4c62cd451a |
@@ -1,7 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
omit =
|
||||
# standlonetemplate is read dynamically and tested by test_genscript
|
||||
*standalonetemplate.py
|
||||
# oldinterpret could be removed, as it is no longer used in py26+
|
||||
*oldinterpret.py
|
||||
vendored_packages
|
||||
|
||||
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -1,15 +1,14 @@
|
||||
Thanks for submitting a PR, your contribution is really appreciated!
|
||||
|
||||
Here's a quick checklist that should be present in PRs:
|
||||
Here's a quick checklist that should be present in PRs (you can delete this text from the final description, this is
|
||||
just a guideline):
|
||||
|
||||
- [ ] Add a new news fragment into the changelog folder
|
||||
* name it `$issue_id.$type` for example (588.bug)
|
||||
* if you don't have an issue_id change it to the pr id after creating the pr
|
||||
* ensure type is one of `removal`, `feature`, `bugfix`, `vendor`, `doc` or `trivial`
|
||||
* Make sure to use full sentences with correct case and punctuation, for example: "Fix issue with non-ascii contents in doctest text files."
|
||||
- [ ] Target: for `bugfix`, `vendor`, `doc` or `trivial` fixes, target `master`; for removals or features target `features`;
|
||||
- [ ] Make sure to include reasonable tests for your change if necessary
|
||||
- [ ] Create a new changelog file in the `changelog` folder, with a name like `<ISSUE NUMBER>.<TYPE>.rst`. See [changelog/README.rst](/changelog/README.rst) for details.
|
||||
- [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes.
|
||||
- [ ] Target the `features` branch for new features and removals/deprecations.
|
||||
- [ ] Include documentation when adding new features.
|
||||
- [ ] Include new tests or update existing tests when applicable.
|
||||
|
||||
Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please:
|
||||
Unless your change is trivial or a small documentation fix (e.g., a typo or reword of a small section) please:
|
||||
|
||||
- [ ] Add yourself to `AUTHORS`;
|
||||
- [ ] Add yourself to `AUTHORS` in alphabetical order;
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -33,6 +33,7 @@ env/
|
||||
3rdparty/
|
||||
.tox
|
||||
.cache
|
||||
.pytest_cache
|
||||
.coverage
|
||||
.ropeproject
|
||||
.idea
|
||||
|
||||
54
.travis.yml
54
.travis.yml
@@ -1,42 +1,58 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- '3.5'
|
||||
# command to install dependencies
|
||||
install: "pip install -U tox"
|
||||
# # command to run tests
|
||||
- '3.6'
|
||||
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
|
||||
- TOXENV=linting
|
||||
- TOXENV=py26
|
||||
- TOXENV=py27
|
||||
- TOXENV=py33
|
||||
- TOXENV=py34
|
||||
- TOXENV=py35
|
||||
- TOXENV=pypy
|
||||
- TOXENV=py36
|
||||
- TOXENV=py27-pexpect
|
||||
- TOXENV=py27-xdist
|
||||
- TOXENV=py27-trial
|
||||
- TOXENV=py35-pexpect
|
||||
- TOXENV=py35-xdist
|
||||
- TOXENV=py35-trial
|
||||
- TOXENV=py27-numpy
|
||||
- TOXENV=py27-pluggymaster
|
||||
- TOXENV=py36-pexpect
|
||||
- TOXENV=py36-xdist
|
||||
- TOXENV=py36-trial
|
||||
- TOXENV=py36-numpy
|
||||
- TOXENV=py36-pluggymaster
|
||||
- TOXENV=py27-nobyte
|
||||
- TOXENV=doctesting
|
||||
- TOXENV=freeze
|
||||
- TOXENV=docs
|
||||
|
||||
matrix:
|
||||
jobs:
|
||||
include:
|
||||
- env: TOXENV=py36
|
||||
- env: TOXENV=pypy
|
||||
python: 'pypy-5.4'
|
||||
- env: TOXENV=py35
|
||||
python: '3.5'
|
||||
- env: TOXENV=py35-freeze
|
||||
python: '3.5'
|
||||
- env: TOXENV=py37
|
||||
python: 'nightly'
|
||||
|
||||
- stage: deploy
|
||||
python: '3.6'
|
||||
- env: TOXENV=py37
|
||||
python: 'nightly'
|
||||
allow_failures:
|
||||
- env: TOXENV=py37
|
||||
python: 'nightly'
|
||||
env:
|
||||
install: pip install -U setuptools setuptools_scm
|
||||
script: skip
|
||||
deploy:
|
||||
provider: pypi
|
||||
user: nicoddemus
|
||||
distributions: sdist bdist_wheel
|
||||
skip_upload_docs: true
|
||||
password:
|
||||
secure: xanTgTUu6XDQVqB/0bwJQXoDMnU5tkwZc5koz6mBkkqZhKdNOi2CLoC1XhiSZ+ah24l4V1E0GAqY5kBBcy9d7NVe4WNg4tD095LsHw+CRU6/HCVIFfyk2IZ+FPAlguesCcUiJSXOrlBF+Wj68wEvLoK7EoRFbJeiZ/f91Ww1sbtDlqXABWGHrmhPJL5Wva7o7+wG7JwJowqdZg1pbQExsCc7b53w4v2RBu3D6TJaTAzHiVsW+nUSI67vKI/uf+cR/OixsTfy37wlHgSwihYmrYLFls3V0bSpahCim3bCgMaFZx8S8xrdgJ++PzBCof2HeflFKvW+VCkoYzGEG4NrTWJoNz6ni4red9GdvfjGH3YCjAKS56h9x58zp2E5rpsb/kVq5/45xzV+dq6JRuhQ1nJWjBC6fSKAc/bfwnuFK3EBxNLkvBssLHvsNjj5XG++cB8DdS9wVGUqjpoK4puaXUWFqy4q3S9F86HEsKNgExtieA9qNx+pCIZVs6JCXZNjr0I5eVNzqJIyggNgJG6RyravsU35t9Zd9doL5g4Y7UKmAGTn1Sz24HQ4sMQgXdm2SyD8gEK5je4tlhUvfGtDvMSlstq71kIn9nRpFnqB6MFlbYSEAZmo8dGbCquoUc++6Rum208wcVbrzzVtGlXB/Ow9AbFMYeAGA0+N/K1e59c=
|
||||
on:
|
||||
tags: true
|
||||
repo: pytest-dev/pytest
|
||||
|
||||
script: tox --recreate
|
||||
|
||||
|
||||
34
AUTHORS
34
AUTHORS
@@ -3,12 +3,16 @@ merlinux GmbH, Germany, office at merlinux eu
|
||||
|
||||
Contributors include::
|
||||
|
||||
Aaron Coleman
|
||||
Abdeali JK
|
||||
Abhijeet Kasurde
|
||||
Ahn Ki-Wook
|
||||
Alan Velasco
|
||||
Alexander Johnson
|
||||
Alexei Kozlenok
|
||||
Anatoly Bubenkoff
|
||||
Anders Hovmöller
|
||||
Andras Tim
|
||||
Andreas Zeidler
|
||||
Andrzej Ostrowski
|
||||
Andy Freeland
|
||||
@@ -16,6 +20,7 @@ Anthon van der Neut
|
||||
Anthony Sottile
|
||||
Antony Lee
|
||||
Armin Rigo
|
||||
Aron Coyle
|
||||
Aron Curzon
|
||||
Aviv Palivoda
|
||||
Barney Gale
|
||||
@@ -24,11 +29,13 @@ Benjamin Peterson
|
||||
Bernard Pratz
|
||||
Bob Ippolito
|
||||
Brian Dorsey
|
||||
Brian Maissy
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
Bruno Oliveira
|
||||
Cal Leeming
|
||||
Carl Friedrich Bolz
|
||||
Ceridwen
|
||||
Charles Cloud
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Chris Lamb
|
||||
@@ -36,6 +43,7 @@ Christian Boelsen
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christopher Gilling
|
||||
Cyrus Maden
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Nuri
|
||||
@@ -45,6 +53,7 @@ Dave Hunt
|
||||
David Díaz-Barquero
|
||||
David Mohr
|
||||
David Vierra
|
||||
Daw-Ran Liou
|
||||
Denis Kirisov
|
||||
Diego Russo
|
||||
Dmitry Dygalo
|
||||
@@ -63,6 +72,7 @@ Feng Ma
|
||||
Florian Bruhin
|
||||
Floris Bruynooghe
|
||||
Gabriel Reis
|
||||
George Kussumoto
|
||||
Georgy Dyuldin
|
||||
Graham Horler
|
||||
Greg Price
|
||||
@@ -70,8 +80,11 @@ Grig Gheorghiu
|
||||
Grigorii Eremeev (budulianin)
|
||||
Guido Wesdorp
|
||||
Harald Armin Massa
|
||||
Henk-Jaap Wagenaar
|
||||
Hugo van Kemenade
|
||||
Hui Wang (coldnight)
|
||||
Ian Bicking
|
||||
Ian Lesperance
|
||||
Jaap Broekhuizen
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
@@ -79,10 +92,12 @@ Jason R. Coombs
|
||||
Javier Domingo Cansino
|
||||
Javier Romero
|
||||
Jeff Widman
|
||||
John Eddie Ayson
|
||||
John Towler
|
||||
Jon Sonesen
|
||||
Jonas Obrist
|
||||
Jordan Guymon
|
||||
Jordan Moldow
|
||||
Joshua Bronson
|
||||
Jurko Gospodnetić
|
||||
Justyna Janczyszyn
|
||||
@@ -90,13 +105,16 @@ Kale Kundert
|
||||
Katarzyna Jachim
|
||||
Kevin Cox
|
||||
Kodi B. Arfer
|
||||
Lawrence Mitchell
|
||||
Lee Kamentsky
|
||||
Lev Maximov
|
||||
Llandy Riveron Del Risco
|
||||
Loic Esteve
|
||||
Lukas Bednar
|
||||
Luke Murphy
|
||||
Maciek Fijalkowski
|
||||
Maho
|
||||
Maik Figura
|
||||
Mandeep Bhutani
|
||||
Manuel Krebber
|
||||
Marc Schlaich
|
||||
@@ -104,6 +122,7 @@ Marcin Bachry
|
||||
Mark Abramowitz
|
||||
Markus Unterwaditzer
|
||||
Martijn Faassen
|
||||
Martin Altmayer
|
||||
Martin K. Scherer
|
||||
Martin Prusse
|
||||
Mathieu Clabaut
|
||||
@@ -111,13 +130,16 @@ Matt Bachmann
|
||||
Matt Duck
|
||||
Matt Williams
|
||||
Matthias Hafner
|
||||
Maxim Filipenko
|
||||
mbyt
|
||||
Michael Aquilina
|
||||
Michael Birtwell
|
||||
Michael Droettboom
|
||||
Michael Seifert
|
||||
Michal Wajszczuk
|
||||
Mihai Capotă
|
||||
Mike Lundy
|
||||
Nathaniel Waisbrot
|
||||
Ned Batchelder
|
||||
Neven Mundar
|
||||
Nicolas Delaby
|
||||
@@ -133,6 +155,7 @@ Punyashloka Biswal
|
||||
Quentin Pradet
|
||||
Ralf Schmitt
|
||||
Ran Benita
|
||||
Raphael Castaneda
|
||||
Raphael Pierzina
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
@@ -143,24 +166,35 @@ Ronny Pfannschmidt
|
||||
Ross Lawley
|
||||
Russel Winder
|
||||
Ryan Wooden
|
||||
Samuel Dion-Girardeau
|
||||
Samuele Pedroni
|
||||
Segev Finer
|
||||
Simon Gomizelj
|
||||
Skylar Downes
|
||||
Srinivas Reddy Thatiparthy
|
||||
Stefan Farmbauer
|
||||
Stefan Zimmermann
|
||||
Stefano Taschini
|
||||
Steffen Allner
|
||||
Stephan Obermann
|
||||
Tarcisio Fischer
|
||||
Tareq Alayan
|
||||
Ted Xiao
|
||||
Thomas Grainger
|
||||
Thomas Hisch
|
||||
Tom Dalton
|
||||
Tom Viner
|
||||
Trevor Bekolay
|
||||
Tyler Goodlet
|
||||
Tzu-ping Chung
|
||||
Vasily Kuznetsov
|
||||
Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
Vitaly Lashmanov
|
||||
Vlad Dragos
|
||||
Wouter van Ackooy
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Zoltán Máté
|
||||
Roland Puntaier
|
||||
Allan Feldman
|
||||
|
||||
909
CHANGELOG.rst
909
CHANGELOG.rst
@@ -8,6 +8,913 @@
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
Pytest 3.4.2 (2018-03-04)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Removed progress information when capture option is ``no``. (`#3203
|
||||
<https://github.com/pytest-dev/pytest/issues/3203>`_)
|
||||
|
||||
- Refactor check of bindir from ``exists`` to ``isdir``. (`#3241
|
||||
<https://github.com/pytest-dev/pytest/issues/3241>`_)
|
||||
|
||||
- Fix ``TypeError`` issue when using ``approx`` with a ``Decimal`` value.
|
||||
(`#3247 <https://github.com/pytest-dev/pytest/issues/3247>`_)
|
||||
|
||||
- Fix reference cycle generated when using the ``request`` fixture. (`#3249
|
||||
<https://github.com/pytest-dev/pytest/issues/3249>`_)
|
||||
|
||||
- ``[tool:pytest]`` sections in ``*.cfg`` files passed by the ``-c`` option are
|
||||
now properly recognized. (`#3260
|
||||
<https://github.com/pytest-dev/pytest/issues/3260>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Add logging plugin to plugins list. (`#3209
|
||||
<https://github.com/pytest-dev/pytest/issues/3209>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Fix minor typo in fixture.rst (`#3259
|
||||
<https://github.com/pytest-dev/pytest/issues/3259>`_)
|
||||
|
||||
|
||||
Pytest 3.4.1 (2018-02-20)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Move import of ``doctest.UnexpectedException`` to top-level to avoid possible
|
||||
errors when using ``--pdb``. (`#1810
|
||||
<https://github.com/pytest-dev/pytest/issues/1810>`_)
|
||||
|
||||
- Added printing of captured stdout/stderr before entering pdb, and improved a
|
||||
test which was giving false negatives about output capturing. (`#3052
|
||||
<https://github.com/pytest-dev/pytest/issues/3052>`_)
|
||||
|
||||
- Fix ordering of tests using parametrized fixtures which can lead to fixtures
|
||||
being created more than necessary. (`#3161
|
||||
<https://github.com/pytest-dev/pytest/issues/3161>`_)
|
||||
|
||||
- Fix bug where logging happening at hooks outside of "test run" hooks would
|
||||
cause an internal error. (`#3184
|
||||
<https://github.com/pytest-dev/pytest/issues/3184>`_)
|
||||
|
||||
- Detect arguments injected by ``unittest.mock.patch`` decorator correctly when
|
||||
pypi ``mock.patch`` is installed and imported. (`#3206
|
||||
<https://github.com/pytest-dev/pytest/issues/3206>`_)
|
||||
|
||||
- Errors shown when a ``pytest.raises()`` with ``match=`` fails are now cleaner
|
||||
on what happened: When no exception was raised, the "matching '...'" part got
|
||||
removed as it falsely implies that an exception was raised but it didn't
|
||||
match. When a wrong exception was raised, it's now thrown (like
|
||||
``pytest.raised()`` without ``match=`` would) instead of complaining about
|
||||
the unmatched text. (`#3222
|
||||
<https://github.com/pytest-dev/pytest/issues/3222>`_)
|
||||
|
||||
- Fixed output capture handling in doctests on macOS. (`#985
|
||||
<https://github.com/pytest-dev/pytest/issues/985>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Add Sphinx parameter docs for ``match`` and ``message`` args to
|
||||
``pytest.raises``. (`#3202
|
||||
<https://github.com/pytest-dev/pytest/issues/3202>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- pytest has changed the publication procedure and is now being published to
|
||||
PyPI directly from Travis. (`#3060
|
||||
<https://github.com/pytest-dev/pytest/issues/3060>`_)
|
||||
|
||||
- Rename ``ParameterSet._for_parameterize()`` to ``_for_parametrize()`` in
|
||||
order to comply with the naming convention. (`#3166
|
||||
<https://github.com/pytest-dev/pytest/issues/3166>`_)
|
||||
|
||||
- Skip failing pdb/doctest test on mac. (`#985
|
||||
<https://github.com/pytest-dev/pytest/issues/985>`_)
|
||||
|
||||
|
||||
Pytest 3.4.0 (2018-01-30)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- All pytest classes now subclass ``object`` for better Python 2/3 compatibility.
|
||||
This should not affect user code except in very rare edge cases. (`#2147
|
||||
<https://github.com/pytest-dev/pytest/issues/2147>`_)
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Introduce ``empty_parameter_set_mark`` ini option to select which mark to
|
||||
apply when ``@pytest.mark.parametrize`` is given an empty set of parameters.
|
||||
Valid options are ``skip`` (default) and ``xfail``. Note that it is planned
|
||||
to change the default to ``xfail`` in future releases as this is considered
|
||||
less error prone. (`#2527
|
||||
<https://github.com/pytest-dev/pytest/issues/2527>`_)
|
||||
|
||||
- **Incompatible change**: after community feedback the `logging
|
||||
<https://docs.pytest.org/en/latest/logging.html>`_ functionality has
|
||||
undergone some changes. Please consult the `logging documentation
|
||||
<https://docs.pytest.org/en/latest/logging.html#incompatible-changes-in-pytest-3-4>`_
|
||||
for details. (`#3013 <https://github.com/pytest-dev/pytest/issues/3013>`_)
|
||||
|
||||
- Console output falls back to "classic" mode when capturing is disabled (``-s``),
|
||||
otherwise the output gets garbled to the point of being useless. (`#3038
|
||||
<https://github.com/pytest-dev/pytest/issues/3038>`_)
|
||||
|
||||
- New `pytest_runtest_logfinish
|
||||
<https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_runtest_logfinish>`_
|
||||
hook which is called when a test item has finished executing, analogous to
|
||||
`pytest_runtest_logstart
|
||||
<https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_runtest_start>`_.
|
||||
(`#3101 <https://github.com/pytest-dev/pytest/issues/3101>`_)
|
||||
|
||||
- Improve performance when collecting tests using many fixtures. (`#3107
|
||||
<https://github.com/pytest-dev/pytest/issues/3107>`_)
|
||||
|
||||
- New ``caplog.get_records(when)`` method which provides access to the captured
|
||||
records for the ``"setup"``, ``"call"`` and ``"teardown"``
|
||||
testing stages. (`#3117 <https://github.com/pytest-dev/pytest/issues/3117>`_)
|
||||
|
||||
- New fixture ``record_xml_attribute`` that allows modifying and inserting
|
||||
attributes on the ``<testcase>`` xml node in JUnit reports. (`#3130
|
||||
<https://github.com/pytest-dev/pytest/issues/3130>`_)
|
||||
|
||||
- The default cache directory has been renamed from ``.cache`` to
|
||||
``.pytest_cache`` after community feedback that the name ``.cache`` did not
|
||||
make it clear that it was used by pytest. (`#3138
|
||||
<https://github.com/pytest-dev/pytest/issues/3138>`_)
|
||||
|
||||
- Colorize the levelname column in the live-log output. (`#3142
|
||||
<https://github.com/pytest-dev/pytest/issues/3142>`_)
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fix hanging pexpect test on MacOS by using flush() instead of wait().
|
||||
(`#2022 <https://github.com/pytest-dev/pytest/issues/2022>`_)
|
||||
|
||||
- Fix restoring Python state after in-process pytest runs with the
|
||||
``pytester`` plugin; this may break tests using multiple inprocess
|
||||
pytest runs if later ones depend on earlier ones leaking global interpreter
|
||||
changes. (`#3016 <https://github.com/pytest-dev/pytest/issues/3016>`_)
|
||||
|
||||
- Fix skipping plugin reporting hook when test aborted before plugin setup
|
||||
hook. (`#3074 <https://github.com/pytest-dev/pytest/issues/3074>`_)
|
||||
|
||||
- Fix progress percentage reported when tests fail during teardown. (`#3088
|
||||
<https://github.com/pytest-dev/pytest/issues/3088>`_)
|
||||
|
||||
- **Incompatible change**: ``-o/--override`` option no longer eats all the
|
||||
remaining options, which can lead to surprising behavior: for example,
|
||||
``pytest -o foo=1 /path/to/test.py`` would fail because ``/path/to/test.py``
|
||||
would be considered as part of the ``-o`` command-line argument. One
|
||||
consequence of this is that now multiple configuration overrides need
|
||||
multiple ``-o`` flags: ``pytest -o foo=1 -o bar=2``. (`#3103
|
||||
<https://github.com/pytest-dev/pytest/issues/3103>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Document hooks (defined with ``historic=True``) which cannot be used with
|
||||
``hookwrapper=True``. (`#2423
|
||||
<https://github.com/pytest-dev/pytest/issues/2423>`_)
|
||||
|
||||
- Clarify that warning capturing doesn't change the warning filter by default.
|
||||
(`#2457 <https://github.com/pytest-dev/pytest/issues/2457>`_)
|
||||
|
||||
- Clarify a possible confusion when using pytest_fixture_setup with fixture
|
||||
functions that return None. (`#2698
|
||||
<https://github.com/pytest-dev/pytest/issues/2698>`_)
|
||||
|
||||
- Fix the wording of a sentence on doctest flags used in pytest. (`#3076
|
||||
<https://github.com/pytest-dev/pytest/issues/3076>`_)
|
||||
|
||||
- Prefer ``https://*.readthedocs.io`` over ``http://*.rtfd.org`` for links in
|
||||
the documentation. (`#3092
|
||||
<https://github.com/pytest-dev/pytest/issues/3092>`_)
|
||||
|
||||
- Improve readability (wording, grammar) of Getting Started guide (`#3131
|
||||
<https://github.com/pytest-dev/pytest/issues/3131>`_)
|
||||
|
||||
- Added note that calling pytest.main multiple times from the same process is
|
||||
not recommended because of import caching. (`#3143
|
||||
<https://github.com/pytest-dev/pytest/issues/3143>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Show a simple and easy error when keyword expressions trigger a syntax error
|
||||
(for example, ``"-k foo and import"`` will show an error that you can not use
|
||||
the ``import`` keyword in expressions). (`#2953
|
||||
<https://github.com/pytest-dev/pytest/issues/2953>`_)
|
||||
|
||||
- Change parametrized automatic test id generation to use the ``__name__``
|
||||
attribute of functions instead of the fallback argument name plus counter.
|
||||
(`#2976 <https://github.com/pytest-dev/pytest/issues/2976>`_)
|
||||
|
||||
- Replace py.std with stdlib imports. (`#3067
|
||||
<https://github.com/pytest-dev/pytest/issues/3067>`_)
|
||||
|
||||
- Corrected 'you' to 'your' in logging docs. (`#3129
|
||||
<https://github.com/pytest-dev/pytest/issues/3129>`_)
|
||||
|
||||
|
||||
Pytest 3.3.2 (2017-12-25)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- pytester: ignore files used to obtain current user metadata in the fd leak
|
||||
detector. (`#2784 <https://github.com/pytest-dev/pytest/issues/2784>`_)
|
||||
|
||||
- Fix **memory leak** where objects returned by fixtures were never destructed
|
||||
by the garbage collector. (`#2981
|
||||
<https://github.com/pytest-dev/pytest/issues/2981>`_)
|
||||
|
||||
- Fix conversion of pyargs to filename to not convert symlinks on Python 2. (`#2985
|
||||
<https://github.com/pytest-dev/pytest/issues/2985>`_)
|
||||
|
||||
- ``PYTEST_DONT_REWRITE`` is now checked for plugins too rather than only for
|
||||
test modules. (`#2995 <https://github.com/pytest-dev/pytest/issues/2995>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Add clarifying note about behavior of multiple parametrized arguments (`#3001
|
||||
<https://github.com/pytest-dev/pytest/issues/3001>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Code cleanup. (`#3015 <https://github.com/pytest-dev/pytest/issues/3015>`_,
|
||||
`#3021 <https://github.com/pytest-dev/pytest/issues/3021>`_)
|
||||
|
||||
- Clean up code by replacing imports and references of `_ast` to `ast`. (`#3018
|
||||
<https://github.com/pytest-dev/pytest/issues/3018>`_)
|
||||
|
||||
|
||||
Pytest 3.3.1 (2017-12-05)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fix issue about ``-p no:<plugin>`` having no effect. (`#2920
|
||||
<https://github.com/pytest-dev/pytest/issues/2920>`_)
|
||||
|
||||
- Fix regression with warnings that contained non-strings in their arguments in
|
||||
Python 2. (`#2956 <https://github.com/pytest-dev/pytest/issues/2956>`_)
|
||||
|
||||
- Always escape null bytes when setting ``PYTEST_CURRENT_TEST``. (`#2957
|
||||
<https://github.com/pytest-dev/pytest/issues/2957>`_)
|
||||
|
||||
- Fix ``ZeroDivisionError`` when using the ``testmon`` plugin when no tests
|
||||
were actually collected. (`#2971
|
||||
<https://github.com/pytest-dev/pytest/issues/2971>`_)
|
||||
|
||||
- Bring back ``TerminalReporter.writer`` as an alias to
|
||||
``TerminalReporter._tw``. This alias was removed by accident in the ``3.3.0``
|
||||
release. (`#2984 <https://github.com/pytest-dev/pytest/issues/2984>`_)
|
||||
|
||||
- The ``pytest-capturelog`` plugin is now also blacklisted, avoiding errors when
|
||||
running pytest with it still installed. (`#3004
|
||||
<https://github.com/pytest-dev/pytest/issues/3004>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Fix broken link to plugin ``pytest-localserver``. (`#2963
|
||||
<https://github.com/pytest-dev/pytest/issues/2963>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Update github "bugs" link in ``CONTRIBUTING.rst`` (`#2949
|
||||
<https://github.com/pytest-dev/pytest/issues/2949>`_)
|
||||
|
||||
|
||||
Pytest 3.3.0 (2017-11-23)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- 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
|
||||
which still require those versions should pin pytest to ``<3.3``. (`#2812
|
||||
<https://github.com/pytest-dev/pytest/issues/2812>`_)
|
||||
|
||||
- Remove internal ``_preloadplugins()`` function. This removal is part of the
|
||||
``pytest_namespace()`` hook deprecation. (`#2636
|
||||
<https://github.com/pytest-dev/pytest/issues/2636>`_)
|
||||
|
||||
- Internally change ``CallSpec2`` to have a list of marks instead of a broken
|
||||
mapping of keywords. This removes the keywords attribute of the internal
|
||||
``CallSpec2`` class. (`#2672
|
||||
<https://github.com/pytest-dev/pytest/issues/2672>`_)
|
||||
|
||||
- Remove ParameterSet.deprecated_arg_dict - its not a public api and the lack
|
||||
of the underscore was a naming error. (`#2675
|
||||
<https://github.com/pytest-dev/pytest/issues/2675>`_)
|
||||
|
||||
- Remove the internal multi-typed attribute ``Node._evalskip`` and replace it
|
||||
with the boolean ``Node._skipped_by_mark``. (`#2767
|
||||
<https://github.com/pytest-dev/pytest/issues/2767>`_)
|
||||
|
||||
- The ``params`` list passed to ``pytest.fixture`` is now for
|
||||
all effects considered immutable and frozen at the moment of the ``pytest.fixture``
|
||||
call. Previously the list could be changed before the first invocation of the fixture
|
||||
allowing for a form of dynamic parametrization (for example, updated from command-line options),
|
||||
but this was an unwanted implementation detail which complicated the internals and prevented
|
||||
some internal cleanup. See issue `#2959 <https://github.com/pytest-dev/pytest/issues/2959>`_
|
||||
for details and a recommended workaround.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- ``pytest_fixture_post_finalizer`` hook can now receive a ``request``
|
||||
argument. (`#2124 <https://github.com/pytest-dev/pytest/issues/2124>`_)
|
||||
|
||||
- Replace the old introspection code in compat.py that determines the available
|
||||
arguments of fixtures with inspect.signature on Python 3 and
|
||||
funcsigs.signature on Python 2. This should respect ``__signature__``
|
||||
declarations on functions. (`#2267
|
||||
<https://github.com/pytest-dev/pytest/issues/2267>`_)
|
||||
|
||||
- Report tests with global ``pytestmark`` variable only once. (`#2549
|
||||
<https://github.com/pytest-dev/pytest/issues/2549>`_)
|
||||
|
||||
- Now pytest displays the total progress percentage while running tests. The
|
||||
previous output style can be set by configuring the ``console_output_style``
|
||||
setting to ``classic``. (`#2657 <https://github.com/pytest-dev/pytest/issues/2657>`_)
|
||||
|
||||
- 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.
|
||||
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
|
||||
the captured log during specific tests (similar to ``capsys`` for example).
|
||||
For more information, please see the `logging docs
|
||||
<https://docs.pytest.org/en/latest/logging.html>`_. This feature was
|
||||
introduced by merging the popular `pytest-catchlog
|
||||
<https://pypi.org/project/pytest-catchlog/>`_ plugin, thanks to `Thomas Hisch
|
||||
<https://github.com/thisch>`_. Be advised that during the merging the
|
||||
backward compatibility interface with the defunct ``pytest-capturelog`` has
|
||||
been dropped. (`#2794 <https://github.com/pytest-dev/pytest/issues/2794>`_)
|
||||
|
||||
- Add ``allow_module_level`` kwarg to ``pytest.skip()``, enabling to skip the
|
||||
whole module. (`#2808 <https://github.com/pytest-dev/pytest/issues/2808>`_)
|
||||
|
||||
- Allow setting ``file_or_dir``, ``-c``, and ``-o`` in PYTEST_ADDOPTS. (`#2824
|
||||
<https://github.com/pytest-dev/pytest/issues/2824>`_)
|
||||
|
||||
- Return stdout/stderr capture results as a ``namedtuple``, so ``out`` and
|
||||
``err`` can be accessed by attribute. (`#2879
|
||||
<https://github.com/pytest-dev/pytest/issues/2879>`_)
|
||||
|
||||
- Add ``capfdbinary``, a version of ``capfd`` which returns bytes from
|
||||
``readouterr()``. (`#2923
|
||||
<https://github.com/pytest-dev/pytest/issues/2923>`_)
|
||||
|
||||
- Add ``capsysbinary`` a version of ``capsys`` which returns bytes from
|
||||
``readouterr()``. (`#2934
|
||||
<https://github.com/pytest-dev/pytest/issues/2934>`_)
|
||||
|
||||
- Implement feature to skip ``setup.py`` files when run with
|
||||
``--doctest-modules``. (`#502
|
||||
<https://github.com/pytest-dev/pytest/issues/502>`_)
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Resume output capturing after ``capsys/capfd.disabled()`` context manager.
|
||||
(`#1993 <https://github.com/pytest-dev/pytest/issues/1993>`_)
|
||||
|
||||
- ``pytest_fixture_setup`` and ``pytest_fixture_post_finalizer`` hooks are now
|
||||
called for all ``conftest.py`` files. (`#2124
|
||||
<https://github.com/pytest-dev/pytest/issues/2124>`_)
|
||||
|
||||
- If an exception happens while loading a plugin, pytest no longer hides the
|
||||
original traceback. In Python 2 it will show the original traceback with a new
|
||||
message that explains in which plugin. In Python 3 it will show 2 canonized
|
||||
exceptions, the original exception while loading the plugin in addition to an
|
||||
exception that pytest throws about loading a plugin. (`#2491
|
||||
<https://github.com/pytest-dev/pytest/issues/2491>`_)
|
||||
|
||||
- ``capsys`` and ``capfd`` can now be used by other fixtures. (`#2709
|
||||
<https://github.com/pytest-dev/pytest/issues/2709>`_)
|
||||
|
||||
- Internal ``pytester`` plugin properly encodes ``bytes`` arguments to
|
||||
``utf-8``. (`#2738 <https://github.com/pytest-dev/pytest/issues/2738>`_)
|
||||
|
||||
- ``testdir`` now uses use the same method used by ``tmpdir`` to create its
|
||||
temporary directory. This changes the final structure of the ``testdir``
|
||||
directory slightly, but should not affect usage in normal scenarios and
|
||||
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
|
||||
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>`_)
|
||||
|
||||
- Change return value of pytest command when ``--maxfail`` is reached from
|
||||
``2`` (interrupted) to ``1`` (failed). (`#2845
|
||||
<https://github.com/pytest-dev/pytest/issues/2845>`_)
|
||||
|
||||
- Fix issue in assertion rewriting which could lead it to rewrite modules which
|
||||
should not be rewritten. (`#2939
|
||||
<https://github.com/pytest-dev/pytest/issues/2939>`_)
|
||||
|
||||
- Handle marks without description in ``pytest.ini``. (`#2942
|
||||
<https://github.com/pytest-dev/pytest/issues/2942>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- pytest now depends on `attrs <https://pypi.org/project/attrs/>`_ for internal
|
||||
structures to ease code maintainability. (`#2641
|
||||
<https://github.com/pytest-dev/pytest/issues/2641>`_)
|
||||
|
||||
- Refactored internal Python 2/3 compatibility code to use ``six``. (`#2642
|
||||
<https://github.com/pytest-dev/pytest/issues/2642>`_)
|
||||
|
||||
- Stop vendoring ``pluggy`` - we're missing out on its latest changes for not
|
||||
much benefit (`#2719 <https://github.com/pytest-dev/pytest/issues/2719>`_)
|
||||
|
||||
- Internal refactor: simplify ascii string escaping by using the
|
||||
backslashreplace error handler in newer Python 3 versions. (`#2734
|
||||
<https://github.com/pytest-dev/pytest/issues/2734>`_)
|
||||
|
||||
- Remove unnecessary mark evaluator in unittest plugin (`#2767
|
||||
<https://github.com/pytest-dev/pytest/issues/2767>`_)
|
||||
|
||||
- Calls to ``Metafunc.addcall`` now emit a deprecation warning. This function
|
||||
is scheduled to be removed in ``pytest-4.0``. (`#2876
|
||||
<https://github.com/pytest-dev/pytest/issues/2876>`_)
|
||||
|
||||
- Internal move of the parameterset extraction to a more maintainable place.
|
||||
(`#2877 <https://github.com/pytest-dev/pytest/issues/2877>`_)
|
||||
|
||||
- Internal refactoring to simplify scope node lookup. (`#2910
|
||||
<https://github.com/pytest-dev/pytest/issues/2910>`_)
|
||||
|
||||
- Configure ``pytest`` to prevent pip from installing pytest in unsupported
|
||||
Python versions. (`#2922
|
||||
<https://github.com/pytest-dev/pytest/issues/2922>`_)
|
||||
|
||||
|
||||
Pytest 3.2.5 (2017-11-15)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Remove ``py<1.5`` restriction from ``pytest`` as this can cause version
|
||||
conflicts in some installations. (`#2926
|
||||
<https://github.com/pytest-dev/pytest/issues/2926>`_)
|
||||
|
||||
|
||||
Pytest 3.2.4 (2017-11-13)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fix the bug where running with ``--pyargs`` will result in items with
|
||||
empty ``parent.nodeid`` if run from a different root directory. (`#2775
|
||||
<https://github.com/pytest-dev/pytest/issues/2775>`_)
|
||||
|
||||
- Fix issue with ``@pytest.parametrize`` if argnames was specified as keyword arguments.
|
||||
(`#2819 <https://github.com/pytest-dev/pytest/issues/2819>`_)
|
||||
|
||||
- Strip whitespace from marker names when reading them from INI config. (`#2856
|
||||
<https://github.com/pytest-dev/pytest/issues/2856>`_)
|
||||
|
||||
- Show full context of doctest source in the pytest output, if the line number of
|
||||
failed example in the docstring is < 9. (`#2882
|
||||
<https://github.com/pytest-dev/pytest/issues/2882>`_)
|
||||
|
||||
- Match fixture paths against actual path segments in order to avoid matching folders which share a prefix.
|
||||
(`#2836 <https://github.com/pytest-dev/pytest/issues/2836>`_)
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Introduce a dedicated section about conftest.py. (`#1505
|
||||
<https://github.com/pytest-dev/pytest/issues/1505>`_)
|
||||
|
||||
- Explicitly mention ``xpass`` in the documentation of ``xfail``. (`#1997
|
||||
<https://github.com/pytest-dev/pytest/issues/1997>`_)
|
||||
|
||||
- Append example for pytest.param in the example/parametrize document. (`#2658
|
||||
<https://github.com/pytest-dev/pytest/issues/2658>`_)
|
||||
|
||||
- Clarify language of proposal for fixtures parameters (`#2893
|
||||
<https://github.com/pytest-dev/pytest/issues/2893>`_)
|
||||
|
||||
- List python 3.6 in the documented supported versions in the getting started
|
||||
document. (`#2903 <https://github.com/pytest-dev/pytest/issues/2903>`_)
|
||||
|
||||
- Clarify the documentation of available fixture scopes. (`#538
|
||||
<https://github.com/pytest-dev/pytest/issues/538>`_)
|
||||
|
||||
- Add documentation about the ``python -m pytest`` invocation adding the
|
||||
current directory to sys.path. (`#911
|
||||
<https://github.com/pytest-dev/pytest/issues/911>`_)
|
||||
|
||||
|
||||
Pytest 3.2.3 (2017-10-03)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fix crash in tab completion when no prefix is given. (`#2748
|
||||
<https://github.com/pytest-dev/pytest/issues/2748>`_)
|
||||
|
||||
- The equality checking function (``__eq__``) of ``MarkDecorator`` returns
|
||||
``False`` if one object is not an instance of ``MarkDecorator``. (`#2758
|
||||
<https://github.com/pytest-dev/pytest/issues/2758>`_)
|
||||
|
||||
- When running ``pytest --fixtures-per-test``: don't crash if an item has no
|
||||
_fixtureinfo attribute (e.g. doctests) (`#2788
|
||||
<https://github.com/pytest-dev/pytest/issues/2788>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- In help text of ``-k`` option, add example of using ``not`` to not select
|
||||
certain tests whose names match the provided expression. (`#1442
|
||||
<https://github.com/pytest-dev/pytest/issues/1442>`_)
|
||||
|
||||
- Add note in ``parametrize.rst`` about calling ``metafunc.parametrize``
|
||||
multiple times. (`#1548 <https://github.com/pytest-dev/pytest/issues/1548>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Set ``xfail_strict=True`` in pytest's own test suite to catch expected
|
||||
failures as soon as they start to pass. (`#2722
|
||||
<https://github.com/pytest-dev/pytest/issues/2722>`_)
|
||||
|
||||
- Fix typo in example of passing a callable to markers (in example/markers.rst)
|
||||
(`#2765 <https://github.com/pytest-dev/pytest/issues/2765>`_)
|
||||
|
||||
|
||||
Pytest 3.2.2 (2017-09-06)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Calling the deprecated `request.getfuncargvalue()` now shows the source of
|
||||
the call. (`#2681 <https://github.com/pytest-dev/pytest/issues/2681>`_)
|
||||
|
||||
- Allow tests declared as ``@staticmethod`` to use fixtures. (`#2699
|
||||
<https://github.com/pytest-dev/pytest/issues/2699>`_)
|
||||
|
||||
- Fixed edge-case during collection: attributes which raised ``pytest.fail``
|
||||
when accessed would abort the entire collection. (`#2707
|
||||
<https://github.com/pytest-dev/pytest/issues/2707>`_)
|
||||
|
||||
- Fix ``ReprFuncArgs`` with mixed unicode and UTF-8 args. (`#2731
|
||||
<https://github.com/pytest-dev/pytest/issues/2731>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- In examples on working with custom markers, add examples demonstrating the
|
||||
usage of ``pytest.mark.MARKER_NAME.with_args`` in comparison with
|
||||
``pytest.mark.MARKER_NAME.__call__`` (`#2604
|
||||
<https://github.com/pytest-dev/pytest/issues/2604>`_)
|
||||
|
||||
- In one of the simple examples, use `pytest_collection_modifyitems()` to skip
|
||||
tests based on a command-line option, allowing its sharing while preventing a
|
||||
user error when acessing `pytest.config` before the argument parsing. (`#2653
|
||||
<https://github.com/pytest-dev/pytest/issues/2653>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Fixed minor error in 'Good Practices/Manual Integration' code snippet.
|
||||
(`#2691 <https://github.com/pytest-dev/pytest/issues/2691>`_)
|
||||
|
||||
- Fixed typo in goodpractices.rst. (`#2721
|
||||
<https://github.com/pytest-dev/pytest/issues/2721>`_)
|
||||
|
||||
- Improve user guidance regarding ``--resultlog`` deprecation. (`#2739
|
||||
<https://github.com/pytest-dev/pytest/issues/2739>`_)
|
||||
|
||||
|
||||
Pytest 3.2.1 (2017-08-08)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fixed small terminal glitch when collecting a single test item. (`#2579
|
||||
<https://github.com/pytest-dev/pytest/issues/2579>`_)
|
||||
|
||||
- Correctly consider ``/`` as the file separator to automatically mark plugin
|
||||
files for rewrite on Windows. (`#2591 <https://github.com/pytest-
|
||||
dev/pytest/issues/2591>`_)
|
||||
|
||||
- Properly escape test names when setting ``PYTEST_CURRENT_TEST`` environment
|
||||
variable. (`#2644 <https://github.com/pytest-dev/pytest/issues/2644>`_)
|
||||
|
||||
- Fix error on Windows and Python 3.6+ when ``sys.stdout`` has been replaced
|
||||
with a stream-like object which does not implement the full ``io`` module
|
||||
buffer protocol. In particular this affects ``pytest-xdist`` users on the
|
||||
aforementioned platform. (`#2666 <https://github.com/pytest-
|
||||
dev/pytest/issues/2666>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Explicitly document which pytest features work with ``unittest``. (`#2626
|
||||
<https://github.com/pytest-dev/pytest/issues/2626>`_)
|
||||
|
||||
|
||||
Pytest 3.2.0 (2017-07-30)
|
||||
=========================
|
||||
|
||||
Deprecations and Removals
|
||||
-------------------------
|
||||
|
||||
- ``pytest.approx`` no longer supports ``>``, ``>=``, ``<`` and ``<=``
|
||||
operators to avoid surprising/inconsistent behavior. See `the approx docs
|
||||
<https://docs.pytest.org/en/latest/builtin.html#pytest.approx>`_ for more
|
||||
information. (`#2003 <https://github.com/pytest-dev/pytest/issues/2003>`_)
|
||||
|
||||
- All old-style specific behavior in current classes in the pytest's API is
|
||||
considered deprecated at this point and will be removed in a future release.
|
||||
This affects Python 2 users only and in rare situations. (`#2147
|
||||
<https://github.com/pytest-dev/pytest/issues/2147>`_)
|
||||
|
||||
- A deprecation warning is now raised when using marks for parameters
|
||||
in ``pytest.mark.parametrize``. Use ``pytest.param`` to apply marks to
|
||||
parameters instead. (`#2427 <https://github.com/pytest-dev/pytest/issues/2427>`_)
|
||||
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- Add support for numpy arrays (and dicts) to approx. (`#1994
|
||||
<https://github.com/pytest-dev/pytest/issues/1994>`_)
|
||||
|
||||
- Now test function objects have a ``pytestmark`` attribute containing a list
|
||||
of marks applied directly to the test function, as opposed to marks inherited
|
||||
from parent classes or modules. (`#2516 <https://github.com/pytest-
|
||||
dev/pytest/issues/2516>`_)
|
||||
|
||||
- Collection ignores local virtualenvs by default; `--collect-in-virtualenv`
|
||||
overrides this behavior. (`#2518 <https://github.com/pytest-
|
||||
dev/pytest/issues/2518>`_)
|
||||
|
||||
- Allow class methods decorated as ``@staticmethod`` to be candidates for
|
||||
collection as a test function. (Only for Python 2.7 and above. Python 2.6
|
||||
will still ignore static methods.) (`#2528 <https://github.com/pytest-
|
||||
dev/pytest/issues/2528>`_)
|
||||
|
||||
- Introduce ``mark.with_args`` in order to allow passing functions/classes as
|
||||
sole argument to marks. (`#2540 <https://github.com/pytest-
|
||||
dev/pytest/issues/2540>`_)
|
||||
|
||||
- New ``cache_dir`` ini option: sets the directory where the contents of the
|
||||
cache plugin are stored. Directory may be relative or absolute path: if relative path, then
|
||||
directory is created relative to ``rootdir``, otherwise it is used as is.
|
||||
Additionally path may contain environment variables which are expanded during
|
||||
runtime. (`#2543 <https://github.com/pytest-dev/pytest/issues/2543>`_)
|
||||
|
||||
- Introduce the ``PYTEST_CURRENT_TEST`` environment variable that is set with
|
||||
the ``nodeid`` and stage (``setup``, ``call`` and ``teardown``) of the test
|
||||
being currently executed. See the `documentation
|
||||
<https://docs.pytest.org/en/latest/example/simple.html#pytest-current-test-
|
||||
environment-variable>`_ for more info. (`#2583 <https://github.com/pytest-
|
||||
dev/pytest/issues/2583>`_)
|
||||
|
||||
- Introduced ``@pytest.mark.filterwarnings`` mark which allows overwriting the
|
||||
warnings filter on a per test, class or module level. See the `docs
|
||||
<https://docs.pytest.org/en/latest/warnings.html#pytest-mark-
|
||||
filterwarnings>`_ for more information. (`#2598 <https://github.com/pytest-
|
||||
dev/pytest/issues/2598>`_)
|
||||
|
||||
- ``--last-failed`` now remembers forever when a test has failed and only
|
||||
forgets it if it passes again. This makes it easy to fix a test suite by
|
||||
selectively running files and fixing tests incrementally. (`#2621
|
||||
<https://github.com/pytest-dev/pytest/issues/2621>`_)
|
||||
|
||||
- New ``pytest_report_collectionfinish`` hook which allows plugins to add
|
||||
messages to the terminal reporting after collection has been finished
|
||||
successfully. (`#2622 <https://github.com/pytest-dev/pytest/issues/2622>`_)
|
||||
|
||||
- Added support for `PEP-415's <https://www.python.org/dev/peps/pep-0415/>`_
|
||||
``Exception.__suppress_context__``. Now if a ``raise exception from None`` is
|
||||
caught by pytest, pytest will no longer chain the context in the test report.
|
||||
The behavior now matches Python's traceback behavior. (`#2631
|
||||
<https://github.com/pytest-dev/pytest/issues/2631>`_)
|
||||
|
||||
- Exceptions raised by ``pytest.fail``, ``pytest.skip`` and ``pytest.xfail``
|
||||
now subclass BaseException, making them harder to be caught unintentionally
|
||||
by normal code. (`#580 <https://github.com/pytest-dev/pytest/issues/580>`_)
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Set ``stdin`` to a closed ``PIPE`` in ``pytester.py.Testdir.popen()`` for
|
||||
avoid unwanted interactive ``pdb`` (`#2023 <https://github.com/pytest-
|
||||
dev/pytest/issues/2023>`_)
|
||||
|
||||
- Add missing ``encoding`` attribute to ``sys.std*`` streams when using
|
||||
``capsys`` capture mode. (`#2375 <https://github.com/pytest-
|
||||
dev/pytest/issues/2375>`_)
|
||||
|
||||
- Fix terminal color changing to black on Windows if ``colorama`` is imported
|
||||
in a ``conftest.py`` file. (`#2510 <https://github.com/pytest-
|
||||
dev/pytest/issues/2510>`_)
|
||||
|
||||
- Fix line number when reporting summary of skipped tests. (`#2548
|
||||
<https://github.com/pytest-dev/pytest/issues/2548>`_)
|
||||
|
||||
- capture: ensure that EncodedFile.name is a string. (`#2555
|
||||
<https://github.com/pytest-dev/pytest/issues/2555>`_)
|
||||
|
||||
- The options ``--fixtures`` and ``--fixtures-per-test`` will now keep
|
||||
indentation within docstrings. (`#2574 <https://github.com/pytest-
|
||||
dev/pytest/issues/2574>`_)
|
||||
|
||||
- doctests line numbers are now reported correctly, fixing `pytest-sugar#122
|
||||
<https://github.com/Frozenball/pytest-sugar/issues/122>`_. (`#2610
|
||||
<https://github.com/pytest-dev/pytest/issues/2610>`_)
|
||||
|
||||
- Fix non-determinism in order of fixture collection. Adds new dependency
|
||||
(ordereddict) for Python 2.6. (`#920 <https://github.com/pytest-
|
||||
dev/pytest/issues/920>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Clarify ``pytest_configure`` hook call order. (`#2539
|
||||
<https://github.com/pytest-dev/pytest/issues/2539>`_)
|
||||
|
||||
- Extend documentation for testing plugin code with the ``pytester`` plugin.
|
||||
(`#971 <https://github.com/pytest-dev/pytest/issues/971>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Update help message for ``--strict`` to make it clear it only deals with
|
||||
unregistered markers, not warnings. (`#2444 <https://github.com/pytest-
|
||||
dev/pytest/issues/2444>`_)
|
||||
|
||||
- Internal code move: move code for pytest.approx/pytest.raises to own files in
|
||||
order to cut down the size of python.py (`#2489 <https://github.com/pytest-
|
||||
dev/pytest/issues/2489>`_)
|
||||
|
||||
- Renamed the utility function ``_pytest.compat._escape_strings`` to
|
||||
``_ascii_escaped`` to better communicate the function's purpose. (`#2533
|
||||
<https://github.com/pytest-dev/pytest/issues/2533>`_)
|
||||
|
||||
- Improve error message for CollectError with skip/skipif. (`#2546
|
||||
<https://github.com/pytest-dev/pytest/issues/2546>`_)
|
||||
|
||||
- Emit warning about ``yield`` tests being deprecated only once per generator.
|
||||
(`#2562 <https://github.com/pytest-dev/pytest/issues/2562>`_)
|
||||
|
||||
- Ensure final collected line doesn't include artifacts of previous write.
|
||||
(`#2571 <https://github.com/pytest-dev/pytest/issues/2571>`_)
|
||||
|
||||
- Fixed all flake8 errors and warnings. (`#2581 <https://github.com/pytest-
|
||||
dev/pytest/issues/2581>`_)
|
||||
|
||||
- Added ``fix-lint`` tox environment to run automatic pep8 fixes on the code.
|
||||
(`#2582 <https://github.com/pytest-dev/pytest/issues/2582>`_)
|
||||
|
||||
- Turn warnings into errors in pytest's own test suite in order to catch
|
||||
regressions due to deprecations more promptly. (`#2588
|
||||
<https://github.com/pytest-dev/pytest/issues/2588>`_)
|
||||
|
||||
- Show multiple issue links in CHANGELOG entries. (`#2620
|
||||
<https://github.com/pytest-dev/pytest/issues/2620>`_)
|
||||
|
||||
|
||||
Pytest 3.1.3 (2017-07-03)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Fix decode error in Python 2 for doctests in docstrings. (`#2434
|
||||
<https://github.com/pytest-dev/pytest/issues/2434>`_)
|
||||
|
||||
- Exceptions raised during teardown by finalizers are now suppressed until all
|
||||
finalizers are called, with the initial exception reraised. (`#2440
|
||||
<https://github.com/pytest-dev/pytest/issues/2440>`_)
|
||||
|
||||
- Fix incorrect "collected items" report when specifying tests on the command-
|
||||
line. (`#2464 <https://github.com/pytest-dev/pytest/issues/2464>`_)
|
||||
|
||||
- ``deprecated_call`` in context-manager form now captures deprecation warnings
|
||||
even if the same warning has already been raised. Also, ``deprecated_call``
|
||||
will always produce the same error message (previously it would produce
|
||||
different messages in context-manager vs. function-call mode). (`#2469
|
||||
<https://github.com/pytest-dev/pytest/issues/2469>`_)
|
||||
|
||||
- Fix issue where paths collected by pytest could have triple leading ``/``
|
||||
characters. (`#2475 <https://github.com/pytest-dev/pytest/issues/2475>`_)
|
||||
|
||||
- Fix internal error when trying to detect the start of a recursive traceback.
|
||||
(`#2486 <https://github.com/pytest-dev/pytest/issues/2486>`_)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Explicitly state for which hooks the calls stop after the first non-None
|
||||
result. (`#2493 <https://github.com/pytest-dev/pytest/issues/2493>`_)
|
||||
|
||||
|
||||
Trivial/Internal Changes
|
||||
------------------------
|
||||
|
||||
- Create invoke tasks for updating the vendored packages. (`#2474
|
||||
<https://github.com/pytest-dev/pytest/issues/2474>`_)
|
||||
|
||||
- Update copyright dates in LICENSE, README.rst and in the documentation.
|
||||
(`#2499 <https://github.com/pytest-dev/pytest/issues/2499>`_)
|
||||
|
||||
|
||||
Pytest 3.1.2 (2017-06-08)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- Required options added via ``pytest_addoption`` will no longer prevent using
|
||||
--help without passing them. (#1999)
|
||||
|
||||
- Respect ``python_files`` in assertion rewriting. (#2121)
|
||||
|
||||
- Fix recursion error detection when frames in the traceback contain objects
|
||||
that can't be compared (like ``numpy`` arrays). (#2459)
|
||||
|
||||
- ``UnicodeWarning`` is issued from the internal pytest warnings plugin only
|
||||
when the message contains non-ascii unicode (Python 2 only). (#2463)
|
||||
|
||||
- Added a workaround for Python 3.6 ``WindowsConsoleIO`` breaking due to Pytests's
|
||||
``FDCapture``. Other code using console handles might still be affected by the
|
||||
very same issue and might require further workarounds/fixes, i.e. ``colorama``.
|
||||
(#2467)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Fix internal API links to ``pluggy`` objects. (#2331)
|
||||
|
||||
- Make it clear that ``pytest.xfail`` stops test execution at the calling point
|
||||
and improve overall flow of the ``skipping`` docs. (#810)
|
||||
|
||||
|
||||
Pytest 3.1.1 (2017-05-30)
|
||||
=========================
|
||||
|
||||
@@ -1658,7 +2565,7 @@ time or change existing behaviors in order to make them less surprising/more use
|
||||
- fix issue655: work around different ways that cause python2/3
|
||||
to leak sys.exc_info into fixtures/tests causing failures in 3rd party code
|
||||
|
||||
- fix issue615: assertion re-writing did not correctly escape % signs
|
||||
- fix issue615: assertion rewriting did not correctly escape % signs
|
||||
when formatting boolean operations, which tripped over mixing
|
||||
booleans with modulo operators. Thanks to Tom Viner for the report,
|
||||
triaging and fix.
|
||||
|
||||
@@ -34,13 +34,13 @@ If you are reporting a bug, please include:
|
||||
|
||||
* Your operating system name and version.
|
||||
* Any details about your local setup that might be helpful in troubleshooting,
|
||||
specifically Python interpreter version,
|
||||
installed libraries and pytest version.
|
||||
specifically the Python interpreter version, installed libraries, and pytest
|
||||
version.
|
||||
* Detailed steps to reproduce the bug.
|
||||
|
||||
If you can write a demonstration test that currently fails but should pass (xfail),
|
||||
that is a very useful commit to make as well, even if you can't find how
|
||||
to fix the bug yet.
|
||||
If you can write a demonstration test that currently fails but should pass
|
||||
(xfail), that is a very useful commit to make as well, even if you cannot
|
||||
fix the bug itself.
|
||||
|
||||
|
||||
.. _fixbugs:
|
||||
@@ -49,7 +49,7 @@ Fix bugs
|
||||
--------
|
||||
|
||||
Look through the GitHub issues for bugs. Here is a filter you can use:
|
||||
https://github.com/pytest-dev/pytest/labels/bug
|
||||
https://github.com/pytest-dev/pytest/labels/type%3A%20bug
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs.
|
||||
|
||||
@@ -120,7 +120,7 @@ the following:
|
||||
- PyPI presence with a ``setup.py`` that contains a license, ``pytest-``
|
||||
prefixed name, version number, authors, short and long description.
|
||||
|
||||
- a ``tox.ini`` for running tests using `tox <http://tox.testrun.org>`_.
|
||||
- a ``tox.ini`` for running tests using `tox <https://tox.readthedocs.io>`_.
|
||||
|
||||
- a ``README.txt`` describing how to use the plugin and on which
|
||||
platforms it runs.
|
||||
@@ -158,19 +158,41 @@ As stated, the objective is to share maintenance and avoid "plugin-abandon".
|
||||
.. _`pull requests`:
|
||||
.. _pull-requests:
|
||||
|
||||
Preparing Pull Requests on GitHub
|
||||
---------------------------------
|
||||
Preparing Pull Requests
|
||||
-----------------------
|
||||
|
||||
.. note::
|
||||
What is a "pull request"? It informs project's core developers about the
|
||||
changes you want to review and merge. Pull requests are stored on
|
||||
`GitHub servers <https://github.com/pytest-dev/pytest/pulls>`_.
|
||||
Once you send a pull request, we can discuss its potential modifications and
|
||||
even add more commits to it later on.
|
||||
Short version
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
There's an excellent tutorial on how Pull Requests work in the
|
||||
`GitHub Help Center <https://help.github.com/articles/using-pull-requests/>`_,
|
||||
but here is a simple overview:
|
||||
#. Fork the repository;
|
||||
#. Target ``master`` for bugfixes and doc changes;
|
||||
#. Target ``features`` for new features or functionality changes.
|
||||
#. Follow **PEP-8**. There's a ``tox`` command to help fixing it: ``tox -e fix-lint``.
|
||||
#. Tests are run using ``tox``::
|
||||
|
||||
tox -e linting,py27,py36
|
||||
|
||||
The test environments above are usually enough to cover most cases locally.
|
||||
|
||||
#. Write a ``changelog`` entry: ``changelog/2574.bugfix``, use issue id number
|
||||
and one of ``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or
|
||||
``trivial`` for the issue type.
|
||||
#. Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please
|
||||
add yourself to the ``AUTHORS`` file, in alphabetical order;
|
||||
|
||||
|
||||
Long version
|
||||
~~~~~~~~~~~~
|
||||
|
||||
What is a "pull request"? It informs the project's core developers about the
|
||||
changes you want to review and merge. Pull requests are stored on
|
||||
`GitHub servers <https://github.com/pytest-dev/pytest/pulls>`_.
|
||||
Once you send a pull request, we can discuss its potential modifications and
|
||||
even add more commits to it later on. There's an excellent tutorial on how Pull
|
||||
Requests work in the
|
||||
`GitHub Help Center <https://help.github.com/articles/using-pull-requests/>`_.
|
||||
|
||||
Here is a simple overview, with pytest-specific bits:
|
||||
|
||||
#. Fork the
|
||||
`pytest GitHub repository <https://github.com/pytest-dev/pytest>`__. It's
|
||||
@@ -214,12 +236,18 @@ but here is a simple overview:
|
||||
This command will run tests via the "tox" tool against Python 2.7 and 3.6
|
||||
and also perform "lint" coding-style checks.
|
||||
|
||||
#. You can now edit your local working copy.
|
||||
#. You can now edit your local working copy. Please follow PEP-8.
|
||||
|
||||
You can now make the changes you want and run the tests again as necessary.
|
||||
|
||||
To run tests on Python 2.7 and pass options to pytest (e.g. enter pdb on
|
||||
failure) to pytest you can do::
|
||||
If you have too much linting errors, try running::
|
||||
|
||||
$ tox -e fix-lint
|
||||
|
||||
To fix pep8 related errors.
|
||||
|
||||
You can pass different options to ``tox``. For example, to run tests on Python 2.7 and pass options to pytest
|
||||
(e.g. enter pdb on failure) to pytest you can do::
|
||||
|
||||
$ tox -e py27 -- --pdb
|
||||
|
||||
@@ -232,9 +260,11 @@ but here is a simple overview:
|
||||
$ git commit -a -m "<commit message>"
|
||||
$ git push -u
|
||||
|
||||
Make sure you add a message to ``CHANGELOG.rst`` and add yourself to
|
||||
``AUTHORS``. If you are unsure about either of these steps, submit your
|
||||
pull request and we'll help you fix it up.
|
||||
#. Create a new changelog entry in ``changelog``. The file should be named ``<issueid>.<type>``,
|
||||
where *issueid* is the number of the issue related to the change and *type* is one of
|
||||
``bugfix``, ``removal``, ``feature``, ``vendor``, ``doc`` or ``trivial``.
|
||||
|
||||
#. Add yourself to ``AUTHORS`` file if not there yet, in alphabetical order.
|
||||
|
||||
#. Finally, submit a pull request through the GitHub website using this data::
|
||||
|
||||
@@ -246,3 +276,15 @@ but here is a simple overview:
|
||||
base: features # if it's a feature
|
||||
|
||||
|
||||
Joining the Development Team
|
||||
----------------------------
|
||||
|
||||
Anyone who has successfully seen through a pull request which did not
|
||||
require any extra work from the development team to merge will
|
||||
themselves gain commit access if they so wish (if we forget to ask please send a friendly
|
||||
reminder). This does not mean your workflow to contribute changes,
|
||||
everyone goes through the same pull-request-and-review process and
|
||||
no-one merges their own pull requests unless already approved. It does however mean you can
|
||||
participate in the development process more fully since you can merge
|
||||
pull requests from other contributors yourself after having reviewed
|
||||
them.
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
How to release pytest
|
||||
--------------------------------------------
|
||||
Release Procedure
|
||||
-----------------
|
||||
|
||||
Our current policy for releasing is to aim for a bugfix every few weeks and a minor release every 2-3 months. The idea
|
||||
is to get fixes and new features out instead of trying to cram a ton of features into a release and by consequence
|
||||
taking a lot of time to make a new one.
|
||||
|
||||
.. important::
|
||||
|
||||
pytest releases must be prepared on **linux** because the docs and examples expect
|
||||
pytest releases must be prepared on **Linux** because the docs and examples expect
|
||||
to be executed in that platform.
|
||||
|
||||
#. Install development dependencies in a virtual environment with::
|
||||
|
||||
pip3 install -r tasks/requirements.txt
|
||||
pip3 install -U -r tasks/requirements.txt
|
||||
|
||||
#. Create a branch ``release-X.Y.Z`` with the version for the release. Make sure it is up to date
|
||||
with the latest ``master`` (for patch releases) and with the latest ``features`` merged with
|
||||
the latest ``master`` (for minor releases). Ensure your are in a clean work tree.
|
||||
#. Create a branch ``release-X.Y.Z`` with the version for the release.
|
||||
|
||||
#. Check and finalize ``CHANGELOG.rst`` (will be automated soon).
|
||||
* **patch releases**: from the latest ``master``;
|
||||
|
||||
#. Execute to automatically generate docs, announcements and upload a package to
|
||||
your ``devpi`` staging server::
|
||||
* **minor releases**: from the latest ``features``; then merge with the latest ``master``;
|
||||
|
||||
invoke generate.pre_release <VERSION> <DEVPI USER> --password <DEVPI PASSWORD>
|
||||
Ensure your are in a clean work tree.
|
||||
|
||||
If ``--password`` is not given, it is assumed the user is already logged in. If you don't have
|
||||
an account, please ask for one!
|
||||
#. Generate docs, changelog, announcements and a **local** tag::
|
||||
|
||||
#. Run from multiple machines::
|
||||
invoke generate.pre-release <VERSION>
|
||||
|
||||
devpi use https://devpi.net/USER/dev
|
||||
devpi test pytest==VERSION
|
||||
#. Open a PR for this branch targeting ``master``.
|
||||
|
||||
Alternatively, you can use `devpi-cloud-tester <https://github.com/nicoddemus/devpi-cloud-tester>`_ to test
|
||||
the package on AppVeyor and Travis (follow instructions on the ``README``).
|
||||
#. After all tests pass and the PR has been approved, publish to PyPI by pushing the tag::
|
||||
|
||||
#. Check that tests pass for relevant combinations with::
|
||||
git push git@github.com:pytest-dev/pytest.git <VERSION>
|
||||
|
||||
devpi list pytest
|
||||
Wait for the deploy to complete, then make sure it is `available on PyPI <https://pypi.org/project/pytest>`_.
|
||||
|
||||
or look at failures with "devpi list -f pytest".
|
||||
#. Send an email announcement with the contents from::
|
||||
|
||||
#. Feeling confident? Publish to PyPI::
|
||||
doc/en/announce/release-<VERSION>.rst
|
||||
|
||||
invoke generate.publish_release <VERSION> <DEVPI USER> <PYPI_NAME>
|
||||
To the following mailing lists:
|
||||
|
||||
where PYPI_NAME is the name of pypi.python.org as configured in your ``~/.pypirc``
|
||||
file `for devpi <http://doc.devpi.net/latest/quickstart-releaseprocess.html?highlight=pypirc#devpi-push-releasing-to-an-external-index>`_.
|
||||
* pytest-dev@python.org (all releases)
|
||||
* python-announce-list@python.org (all releases)
|
||||
* testing-in-python@lists.idyll.org (only major/minor releases)
|
||||
|
||||
And announce it on `Twitter <https://twitter.com/>`_ with the ``#pytest`` hashtag.
|
||||
|
||||
#. After a minor/major release, merge ``features`` into ``master`` and push (or open a PR).
|
||||
#. After a minor/major release, merge ``release-X.Y.Z`` into ``master`` and push (or open a PR).
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2004-2016 Holger Krekel and others
|
||||
Copyright (c) 2004-2017 Holger Krekel and others
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
|
||||
39
MANIFEST.in
39
MANIFEST.in
@@ -1,39 +0,0 @@
|
||||
include CHANGELOG.rst
|
||||
include LICENSE
|
||||
include AUTHORS
|
||||
include pyproject.toml
|
||||
|
||||
include README.rst
|
||||
include CONTRIBUTING.rst
|
||||
include HOWTORELEASE.rst
|
||||
|
||||
include tox.ini
|
||||
include setup.py
|
||||
|
||||
recursive-include changelog *
|
||||
recursive-include scripts *.py
|
||||
recursive-include scripts *.bat
|
||||
|
||||
include .coveragerc
|
||||
|
||||
recursive-include bench *.py
|
||||
recursive-include extra *.py
|
||||
|
||||
graft testing
|
||||
graft doc
|
||||
prune doc/en/_build
|
||||
graft tasks
|
||||
|
||||
exclude _pytest/impl
|
||||
|
||||
graft _pytest/vendored_packages
|
||||
|
||||
recursive-exclude * *.pyc *.pyo
|
||||
recursive-exclude testing/.hypothesis *
|
||||
recursive-exclude testing/freeze/~ *
|
||||
recursive-exclude testing/freeze/build *
|
||||
recursive-exclude testing/freeze/dist *
|
||||
|
||||
exclude appveyor.yml
|
||||
exclude .travis.yml
|
||||
prune .github
|
||||
@@ -23,6 +23,9 @@
|
||||
.. image:: https://ci.appveyor.com/api/projects/status/mrgbjaua7t33pg6b?svg=true
|
||||
:target: https://ci.appveyor.com/project/pytestbot/pytest
|
||||
|
||||
.. image:: https://www.codetriage.com/pytest-dev/pytest/badges/users.svg
|
||||
:target: https://www.codetriage.com/pytest-dev/pytest
|
||||
|
||||
The ``pytest`` framework makes it easy to write small tests, yet
|
||||
scales to support complex functional testing for applications and libraries.
|
||||
|
||||
@@ -76,9 +79,9 @@ Features
|
||||
- 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;
|
||||
|
||||
- Python2.6+, Python3.3+, PyPy-2.3, Jython-2.5 (untested);
|
||||
- Python 2.7, Python 3.4+, PyPy 2.3, Jython 2.5 (untested);
|
||||
|
||||
- Rich plugin architecture, with over 150+ `external plugins <http://docs.pytest.org/en/latest/plugins.html#installing-external-plugins-searching>`_ and thriving community;
|
||||
- Rich plugin architecture, with over 315+ `external plugins <http://plugincompat.herokuapp.com>`_ and thriving community;
|
||||
|
||||
|
||||
Documentation
|
||||
@@ -102,7 +105,7 @@ Consult the `Changelog <http://docs.pytest.org/en/latest/changelog.html>`__ page
|
||||
License
|
||||
-------
|
||||
|
||||
Copyright Holger Krekel and others, 2004-2016.
|
||||
Copyright Holger Krekel and others, 2004-2017.
|
||||
|
||||
Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
|
||||
|
||||
|
||||
@@ -4,9 +4,6 @@ needs argcomplete>=0.5.6 for python 3.2/3.3 (older versions fail
|
||||
to find the magic string, so _ARGCOMPLETE env. var is never set, and
|
||||
this does not need special code.
|
||||
|
||||
argcomplete does not support python 2.5 (although the changes for that
|
||||
are minor).
|
||||
|
||||
Function try_argcomplete(parser) should be called directly before
|
||||
the call to ArgumentParser.parse_args().
|
||||
|
||||
@@ -62,21 +59,24 @@ import sys
|
||||
import os
|
||||
from glob import glob
|
||||
|
||||
class FastFilesCompleter:
|
||||
|
||||
class FastFilesCompleter(object):
|
||||
'Fast file completer class'
|
||||
|
||||
def __init__(self, directories=True):
|
||||
self.directories = directories
|
||||
|
||||
def __call__(self, prefix, **kwargs):
|
||||
"""only called on non option completions"""
|
||||
if os.path.sep in prefix[1:]: #
|
||||
if os.path.sep in prefix[1:]:
|
||||
prefix_dir = len(os.path.dirname(prefix) + os.path.sep)
|
||||
else:
|
||||
prefix_dir = 0
|
||||
completion = []
|
||||
globbed = []
|
||||
if '*' not in prefix and '?' not in prefix:
|
||||
if prefix[-1] == os.path.sep: # we are on unix, otherwise no bash
|
||||
# we are on unix, otherwise no bash
|
||||
if not prefix or prefix[-1] == os.path.sep:
|
||||
globbed.extend(glob(prefix + '.*'))
|
||||
prefix += '*'
|
||||
globbed.extend(glob(prefix))
|
||||
@@ -96,7 +96,8 @@ if os.environ.get('_ARGCOMPLETE'):
|
||||
filescompleter = FastFilesCompleter()
|
||||
|
||||
def try_argcomplete(parser):
|
||||
argcomplete.autocomplete(parser)
|
||||
argcomplete.autocomplete(parser, always_complete_options=False)
|
||||
else:
|
||||
def try_argcomplete(parser): pass
|
||||
def try_argcomplete(parser):
|
||||
pass
|
||||
filescompleter = None
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import types
|
||||
|
||||
|
||||
def format_exception_only(etype, value):
|
||||
"""Format the exception part of a traceback.
|
||||
|
||||
@@ -30,7 +31,7 @@ def format_exception_only(etype, value):
|
||||
# would throw another exception and mask the original problem.
|
||||
if (isinstance(etype, BaseException) or
|
||||
isinstance(etype, types.InstanceType) or
|
||||
etype is None or type(etype) is str):
|
||||
etype is None or type(etype) is str):
|
||||
return [_format_final_exc_line(etype, value)]
|
||||
|
||||
stype = etype.__name__
|
||||
@@ -62,6 +63,7 @@ def format_exception_only(etype, value):
|
||||
lines.append(_format_final_exc_line(stype, value))
|
||||
return lines
|
||||
|
||||
|
||||
def _format_final_exc_line(etype, value):
|
||||
"""Return a list of a single line -- normal case for format_exception_only"""
|
||||
valuestr = _some_str(value)
|
||||
@@ -71,6 +73,7 @@ def _format_final_exc_line(etype, value):
|
||||
line = "%s: %s\n" % (etype, valuestr)
|
||||
return line
|
||||
|
||||
|
||||
def _some_str(value):
|
||||
try:
|
||||
return unicode(value)
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import inspect
|
||||
import sys
|
||||
import traceback
|
||||
from inspect import CO_VARARGS, CO_VARKEYWORDS
|
||||
import re
|
||||
from weakref import ref
|
||||
from _pytest.compat import _PY2, _PY3, PY35
|
||||
from _pytest.compat import _PY2, _PY3, PY35, safe_str
|
||||
|
||||
import py
|
||||
builtin_repr = repr
|
||||
|
||||
reprlib = py.builtin._tryimport('repr', 'reprlib')
|
||||
|
||||
if _PY3:
|
||||
from traceback import format_exception_only
|
||||
else:
|
||||
@@ -18,6 +18,7 @@ else:
|
||||
|
||||
class Code(object):
|
||||
""" wrapper around Python code objects """
|
||||
|
||||
def __init__(self, rawcode):
|
||||
if not hasattr(rawcode, "co_filename"):
|
||||
rawcode = getrawcode(rawcode)
|
||||
@@ -26,7 +27,7 @@ class Code(object):
|
||||
self.firstlineno = rawcode.co_firstlineno - 1
|
||||
self.name = rawcode.co_name
|
||||
except AttributeError:
|
||||
raise TypeError("not a code object: %r" %(rawcode,))
|
||||
raise TypeError("not a code object: %r" % (rawcode,))
|
||||
self.raw = rawcode
|
||||
|
||||
def __eq__(self, other):
|
||||
@@ -82,6 +83,7 @@ class Code(object):
|
||||
argcount += raw.co_flags & CO_VARKEYWORDS
|
||||
return raw.co_varnames[:argcount]
|
||||
|
||||
|
||||
class Frame(object):
|
||||
"""Wrapper around a Python frame holding f_locals and f_globals
|
||||
in which expressions can be evaluated."""
|
||||
@@ -119,7 +121,7 @@ class Frame(object):
|
||||
"""
|
||||
f_locals = self.f_locals.copy()
|
||||
f_locals.update(vars)
|
||||
py.builtin.exec_(code, self.f_globals, f_locals )
|
||||
py.builtin.exec_(code, self.f_globals, f_locals)
|
||||
|
||||
def repr(self, object):
|
||||
""" return a 'safe' (non-recursive, one-line) string repr for 'object'
|
||||
@@ -143,6 +145,7 @@ class Frame(object):
|
||||
pass # this can occur when using Psyco
|
||||
return retval
|
||||
|
||||
|
||||
class TracebackEntry(object):
|
||||
""" a single entry in a traceback """
|
||||
|
||||
@@ -168,7 +171,7 @@ class TracebackEntry(object):
|
||||
return self.lineno - self.frame.code.firstlineno
|
||||
|
||||
def __repr__(self):
|
||||
return "<TracebackEntry %s:%d>" %(self.frame.code.path, self.lineno+1)
|
||||
return "<TracebackEntry %s:%d>" % (self.frame.code.path, self.lineno + 1)
|
||||
|
||||
@property
|
||||
def statement(self):
|
||||
@@ -232,7 +235,7 @@ class TracebackEntry(object):
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if py.builtin.callable(tbh):
|
||||
if callable(tbh):
|
||||
return tbh(None if self._excinfo is None else self._excinfo())
|
||||
else:
|
||||
return tbh
|
||||
@@ -247,19 +250,21 @@ class TracebackEntry(object):
|
||||
line = str(self.statement).lstrip()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
except: # noqa
|
||||
line = "???"
|
||||
return " File %r:%d in %s\n %s\n" %(fn, self.lineno+1, name, line)
|
||||
return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line)
|
||||
|
||||
def name(self):
|
||||
return self.frame.code.raw.co_name
|
||||
name = property(name, None, None, "co_name of underlaying code")
|
||||
|
||||
|
||||
class Traceback(list):
|
||||
""" Traceback objects encapsulate and offer higher level
|
||||
access to Traceback entries.
|
||||
"""
|
||||
Entry = TracebackEntry
|
||||
|
||||
def __init__(self, tb, excinfo=None):
|
||||
""" initialize from given python traceback object and ExceptionInfo """
|
||||
self._excinfo = excinfo
|
||||
@@ -289,7 +294,7 @@ class Traceback(list):
|
||||
(excludepath is None or not hasattr(codepath, 'relto') or
|
||||
not codepath.relto(excludepath)) and
|
||||
(lineno is None or x.lineno == lineno) and
|
||||
(firstlineno is None or x.frame.code.firstlineno == firstlineno)):
|
||||
(firstlineno is None or x.frame.code.firstlineno == firstlineno)):
|
||||
return Traceback(x._rawentry, self._excinfo)
|
||||
return self
|
||||
|
||||
@@ -315,7 +320,7 @@ class Traceback(list):
|
||||
""" return last non-hidden traceback entry that lead
|
||||
to the exception of a traceback.
|
||||
"""
|
||||
for i in range(-1, -len(self)-1, -1):
|
||||
for i in range(-1, -len(self) - 1, -1):
|
||||
entry = self[i]
|
||||
if not entry.ishidden():
|
||||
return entry
|
||||
@@ -330,25 +335,26 @@ class Traceback(list):
|
||||
# id for the code.raw is needed to work around
|
||||
# the strange metaprogramming in the decorator lib from pypi
|
||||
# which generates code objects that have hash/value equality
|
||||
#XXX needs a test
|
||||
# XXX needs a test
|
||||
key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
|
||||
#print "checking for recursion at", key
|
||||
l = cache.setdefault(key, [])
|
||||
if l:
|
||||
# print "checking for recursion at", key
|
||||
values = cache.setdefault(key, [])
|
||||
if values:
|
||||
f = entry.frame
|
||||
loc = f.f_locals
|
||||
for otherloc in l:
|
||||
for otherloc in values:
|
||||
if f.is_true(f.eval(co_equal,
|
||||
__recursioncache_locals_1=loc,
|
||||
__recursioncache_locals_2=otherloc)):
|
||||
__recursioncache_locals_1=loc,
|
||||
__recursioncache_locals_2=otherloc)):
|
||||
return i
|
||||
l.append(entry.frame.f_locals)
|
||||
values.append(entry.frame.f_locals)
|
||||
return None
|
||||
|
||||
|
||||
co_equal = compile('__recursioncache_locals_1 == __recursioncache_locals_2',
|
||||
'?', 'eval')
|
||||
|
||||
|
||||
class ExceptionInfo(object):
|
||||
""" wraps sys.exc_info() objects and offers
|
||||
help for navigating the traceback.
|
||||
@@ -405,10 +411,10 @@ class ExceptionInfo(object):
|
||||
exconly = self.exconly(tryshort=True)
|
||||
entry = self.traceback.getcrashentry()
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
return ReprFileLocation(path, lineno+1, exconly)
|
||||
return ReprFileLocation(path, lineno + 1, exconly)
|
||||
|
||||
def getrepr(self, showlocals=False, style="long",
|
||||
abspath=False, tbfilter=True, funcargs=False):
|
||||
abspath=False, tbfilter=True, funcargs=False):
|
||||
""" return str()able representation of this exception info.
|
||||
showlocals: show locals per traceback entry
|
||||
style: long|short|no|native traceback style
|
||||
@@ -418,14 +424,14 @@ class ExceptionInfo(object):
|
||||
"""
|
||||
if style == 'native':
|
||||
return ReprExceptionInfo(ReprTracebackNative(
|
||||
py.std.traceback.format_exception(
|
||||
traceback.format_exception(
|
||||
self.type,
|
||||
self.value,
|
||||
self.traceback[0]._rawentry,
|
||||
)), self._getreprcrash())
|
||||
|
||||
fmt = FormattedExcinfo(showlocals=showlocals, style=style,
|
||||
abspath=abspath, tbfilter=tbfilter, funcargs=funcargs)
|
||||
abspath=abspath, tbfilter=tbfilter, funcargs=funcargs)
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
def __str__(self):
|
||||
@@ -469,15 +475,15 @@ class FormattedExcinfo(object):
|
||||
def _getindent(self, source):
|
||||
# figure out indent for given source
|
||||
try:
|
||||
s = str(source.getstatement(len(source)-1))
|
||||
s = str(source.getstatement(len(source) - 1))
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
except: # noqa
|
||||
try:
|
||||
s = str(source[-1])
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
except: # noqa
|
||||
return 0
|
||||
return 4 + (len(s) - len(s.lstrip()))
|
||||
|
||||
@@ -513,7 +519,7 @@ class FormattedExcinfo(object):
|
||||
for line in source.lines[:line_index]:
|
||||
lines.append(space_prefix + line)
|
||||
lines.append(self.flow_marker + " " + source.lines[line_index])
|
||||
for line in source.lines[line_index+1:]:
|
||||
for line in source.lines[line_index + 1:]:
|
||||
lines.append(space_prefix + line)
|
||||
if excinfo is not None:
|
||||
indent = 4 if short else self._getindent(source)
|
||||
@@ -546,13 +552,13 @@ class FormattedExcinfo(object):
|
||||
# _repr() function, which is only reprlib.Repr in
|
||||
# disguise, so is very configurable.
|
||||
str_repr = self._saferepr(value)
|
||||
#if len(str_repr) < 70 or not isinstance(value,
|
||||
# if len(str_repr) < 70 or not isinstance(value,
|
||||
# (list, tuple, dict)):
|
||||
lines.append("%-10s = %s" %(name, str_repr))
|
||||
#else:
|
||||
lines.append("%-10s = %s" % (name, str_repr))
|
||||
# else:
|
||||
# self._line("%-10s =\\" % (name,))
|
||||
# # XXX
|
||||
# py.std.pprint.pprint(value, stream=self.excinfowriter)
|
||||
# pprint.pprint(value, stream=self.excinfowriter)
|
||||
return ReprLocals(lines)
|
||||
|
||||
def repr_traceback_entry(self, entry, excinfo=None):
|
||||
@@ -575,14 +581,14 @@ class FormattedExcinfo(object):
|
||||
s = self.get_source(source, line_index, excinfo, short=short)
|
||||
lines.extend(s)
|
||||
if short:
|
||||
message = "in %s" %(entry.name)
|
||||
message = "in %s" % (entry.name)
|
||||
else:
|
||||
message = excinfo and excinfo.typename or ""
|
||||
path = self._makepath(entry.path)
|
||||
filelocrepr = ReprFileLocation(path, entry.lineno+1, message)
|
||||
filelocrepr = ReprFileLocation(path, entry.lineno + 1, message)
|
||||
localsrepr = None
|
||||
if not short:
|
||||
localsrepr = self.repr_locals(entry.locals)
|
||||
localsrepr = self.repr_locals(entry.locals)
|
||||
return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style)
|
||||
if excinfo:
|
||||
lines.extend(self.get_exconly(excinfo, indent=4))
|
||||
@@ -602,21 +608,51 @@ class FormattedExcinfo(object):
|
||||
traceback = excinfo.traceback
|
||||
if self.tbfilter:
|
||||
traceback = traceback.filter()
|
||||
recursionindex = None
|
||||
|
||||
if is_recursion_error(excinfo):
|
||||
recursionindex = traceback.recursionindex()
|
||||
traceback, extraline = self._truncate_recursive_traceback(traceback)
|
||||
else:
|
||||
extraline = None
|
||||
|
||||
last = traceback[-1]
|
||||
entries = []
|
||||
extraline = None
|
||||
for index, entry in enumerate(traceback):
|
||||
einfo = (last == entry) and excinfo or None
|
||||
reprentry = self.repr_traceback_entry(entry, einfo)
|
||||
entries.append(reprentry)
|
||||
if index == recursionindex:
|
||||
extraline = "!!! Recursion detected (same locals & position)"
|
||||
break
|
||||
return ReprTraceback(entries, extraline, style=self.style)
|
||||
|
||||
def _truncate_recursive_traceback(self, traceback):
|
||||
"""
|
||||
Truncate the given recursive traceback trying to find the starting point
|
||||
of the recursion.
|
||||
|
||||
The detection is done by going through each traceback entry and finding the
|
||||
point in which the locals of the frame are equal to the locals of a previous frame (see ``recursionindex()``.
|
||||
|
||||
Handle the situation where the recursion process might raise an exception (for example
|
||||
comparing numpy arrays using equality raises a TypeError), in which case we do our best to
|
||||
warn the user of the error and show a limited traceback.
|
||||
"""
|
||||
try:
|
||||
recursionindex = traceback.recursionindex()
|
||||
except Exception as e:
|
||||
max_frames = 10
|
||||
extraline = (
|
||||
'!!! Recursion error detected, but an error occurred locating the origin of recursion.\n'
|
||||
' The following exception happened when comparing locals in the stack frame:\n'
|
||||
' {exc_type}: {exc_msg}\n'
|
||||
' Displaying first and last {max_frames} stack frames out of {total}.'
|
||||
).format(exc_type=type(e).__name__, exc_msg=safe_str(e), max_frames=max_frames, total=len(traceback))
|
||||
traceback = traceback[:max_frames] + traceback[-max_frames:]
|
||||
else:
|
||||
if recursionindex is not None:
|
||||
extraline = "!!! Recursion detected (same locals & position)"
|
||||
traceback = traceback[:recursionindex + 1]
|
||||
else:
|
||||
extraline = None
|
||||
|
||||
return traceback, extraline
|
||||
|
||||
def repr_excinfo(self, excinfo):
|
||||
if _PY2:
|
||||
@@ -635,7 +671,7 @@ class FormattedExcinfo(object):
|
||||
else:
|
||||
# fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work
|
||||
reprtraceback = ReprTracebackNative(py.std.traceback.format_exception(type(e), e, None))
|
||||
reprtraceback = ReprTracebackNative(traceback.format_exception(type(e), e, None))
|
||||
reprcrash = None
|
||||
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
@@ -643,7 +679,7 @@ class FormattedExcinfo(object):
|
||||
e = e.__cause__
|
||||
excinfo = ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None
|
||||
descr = 'The above exception was the direct cause of the following exception:'
|
||||
elif e.__context__ is not None:
|
||||
elif (e.__context__ is not None and not e.__suppress_context__):
|
||||
e = e.__context__
|
||||
excinfo = ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None
|
||||
descr = 'During handling of the above exception, another exception occurred:'
|
||||
@@ -669,7 +705,7 @@ class TerminalRepr(object):
|
||||
return io.getvalue().strip()
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s instance at %0x>" %(self.__class__, id(self))
|
||||
return "<%s instance at %0x>" % (self.__class__, id(self))
|
||||
|
||||
|
||||
class ExceptionRepr(TerminalRepr):
|
||||
@@ -713,6 +749,7 @@ class ReprExceptionInfo(ExceptionRepr):
|
||||
self.reprtraceback.toterminal(tw)
|
||||
super(ReprExceptionInfo, self).toterminal(tw)
|
||||
|
||||
|
||||
class ReprTraceback(TerminalRepr):
|
||||
entrysep = "_ "
|
||||
|
||||
@@ -728,7 +765,7 @@ class ReprTraceback(TerminalRepr):
|
||||
tw.line("")
|
||||
entry.toterminal(tw)
|
||||
if i < len(self.reprentries) - 1:
|
||||
next_entry = self.reprentries[i+1]
|
||||
next_entry = self.reprentries[i + 1]
|
||||
if entry.style == "long" or \
|
||||
entry.style == "short" and next_entry.style == "long":
|
||||
tw.sep(self.entrysep)
|
||||
@@ -736,12 +773,14 @@ class ReprTraceback(TerminalRepr):
|
||||
if self.extraline:
|
||||
tw.line(self.extraline)
|
||||
|
||||
|
||||
class ReprTracebackNative(ReprTraceback):
|
||||
def __init__(self, tblines):
|
||||
self.style = "native"
|
||||
self.reprentries = [ReprEntryNative(tblines)]
|
||||
self.extraline = None
|
||||
|
||||
|
||||
class ReprEntryNative(TerminalRepr):
|
||||
style = "native"
|
||||
|
||||
@@ -751,6 +790,7 @@ class ReprEntryNative(TerminalRepr):
|
||||
def toterminal(self, tw):
|
||||
tw.write("".join(self.lines))
|
||||
|
||||
|
||||
class ReprEntry(TerminalRepr):
|
||||
localssep = "_ "
|
||||
|
||||
@@ -767,7 +807,7 @@ class ReprEntry(TerminalRepr):
|
||||
for line in self.lines:
|
||||
red = line.startswith("E ")
|
||||
tw.line(line, bold=True, red=red)
|
||||
#tw.line("")
|
||||
# tw.line("")
|
||||
return
|
||||
if self.reprfuncargs:
|
||||
self.reprfuncargs.toterminal(tw)
|
||||
@@ -775,7 +815,7 @@ class ReprEntry(TerminalRepr):
|
||||
red = line.startswith("E ")
|
||||
tw.line(line, bold=True, red=red)
|
||||
if self.reprlocals:
|
||||
#tw.sep(self.localssep, "Locals")
|
||||
# tw.sep(self.localssep, "Locals")
|
||||
tw.line("")
|
||||
self.reprlocals.toterminal(tw)
|
||||
if self.reprfileloc:
|
||||
@@ -788,6 +828,7 @@ class ReprEntry(TerminalRepr):
|
||||
self.reprlocals,
|
||||
self.reprfileloc)
|
||||
|
||||
|
||||
class ReprFileLocation(TerminalRepr):
|
||||
def __init__(self, path, lineno, message):
|
||||
self.path = str(path)
|
||||
@@ -804,6 +845,7 @@ class ReprFileLocation(TerminalRepr):
|
||||
tw.write(self.path, bold=True, red=True)
|
||||
tw.line(":%s: %s" % (self.lineno, msg))
|
||||
|
||||
|
||||
class ReprLocals(TerminalRepr):
|
||||
def __init__(self, lines):
|
||||
self.lines = lines
|
||||
@@ -812,6 +854,7 @@ class ReprLocals(TerminalRepr):
|
||||
for line in self.lines:
|
||||
tw.line(line)
|
||||
|
||||
|
||||
class ReprFuncArgs(TerminalRepr):
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
@@ -820,11 +863,11 @@ class ReprFuncArgs(TerminalRepr):
|
||||
if self.args:
|
||||
linesofar = ""
|
||||
for name, value in self.args:
|
||||
ns = "%s = %s" %(name, value)
|
||||
ns = "%s = %s" % (safe_str(name), safe_str(value))
|
||||
if len(ns) + len(linesofar) + 2 > tw.fullwidth:
|
||||
if linesofar:
|
||||
tw.line(linesofar)
|
||||
linesofar = ns
|
||||
linesofar = ns
|
||||
else:
|
||||
if linesofar:
|
||||
linesofar += ", " + ns
|
||||
@@ -845,7 +888,7 @@ def getrawcode(obj, trycall=True):
|
||||
obj = getattr(obj, 'f_code', obj)
|
||||
obj = getattr(obj, '__code__', obj)
|
||||
if trycall and not hasattr(obj, 'co_firstlineno'):
|
||||
if hasattr(obj, '__call__') and not py.std.inspect.isclass(obj):
|
||||
if hasattr(obj, '__call__') and not inspect.isclass(obj):
|
||||
x = getrawcode(obj.__call__, trycall=False)
|
||||
if hasattr(x, 'co_firstlineno'):
|
||||
return x
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
from __future__ import absolute_import, division, generators, print_function
|
||||
|
||||
import ast
|
||||
from ast import PyCF_ONLY_AST as _AST_FLAG
|
||||
from bisect import bisect_right
|
||||
import linecache
|
||||
import sys
|
||||
import inspect, tokenize
|
||||
import six
|
||||
import inspect
|
||||
import tokenize
|
||||
import py
|
||||
cpy_compile = compile
|
||||
|
||||
try:
|
||||
import _ast
|
||||
from _ast import PyCF_ONLY_AST as _AST_FLAG
|
||||
except ImportError:
|
||||
_AST_FLAG = 0
|
||||
_ast = None
|
||||
cpy_compile = compile
|
||||
|
||||
|
||||
class Source(object):
|
||||
@@ -19,6 +18,7 @@ class Source(object):
|
||||
possibly deindenting it.
|
||||
"""
|
||||
_compilecounter = 0
|
||||
|
||||
def __init__(self, *parts, **kwargs):
|
||||
self.lines = lines = []
|
||||
de = kwargs.get('deindent', True)
|
||||
@@ -30,7 +30,7 @@ class Source(object):
|
||||
partlines = part.lines
|
||||
elif isinstance(part, (tuple, list)):
|
||||
partlines = [x.rstrip("\n") for x in part]
|
||||
elif isinstance(part, py.builtin._basestring):
|
||||
elif isinstance(part, six.string_types):
|
||||
partlines = part.split('\n')
|
||||
if rstrip:
|
||||
while partlines:
|
||||
@@ -73,7 +73,7 @@ class Source(object):
|
||||
start, end = 0, len(self)
|
||||
while start < end and not self.lines[start].strip():
|
||||
start += 1
|
||||
while end > start and not self.lines[end-1].strip():
|
||||
while end > start and not self.lines[end - 1].strip():
|
||||
end -= 1
|
||||
source = Source()
|
||||
source.lines[:] = self.lines[start:end]
|
||||
@@ -86,8 +86,8 @@ class Source(object):
|
||||
before = Source(before)
|
||||
after = Source(after)
|
||||
newsource = Source()
|
||||
lines = [ (indent + line) for line in self.lines]
|
||||
newsource.lines = before.lines + lines + after.lines
|
||||
lines = [(indent + line) for line in self.lines]
|
||||
newsource.lines = before.lines + lines + after.lines
|
||||
return newsource
|
||||
|
||||
def indent(self, indent=' ' * 4):
|
||||
@@ -95,7 +95,7 @@ class Source(object):
|
||||
all lines indented by the given indent-string.
|
||||
"""
|
||||
newsource = Source()
|
||||
newsource.lines = [(indent+line) for line in self.lines]
|
||||
newsource.lines = [(indent + line) for line in self.lines]
|
||||
return newsource
|
||||
|
||||
def getstatement(self, lineno, assertion=False):
|
||||
@@ -134,7 +134,8 @@ class Source(object):
|
||||
try:
|
||||
import parser
|
||||
except ImportError:
|
||||
syntax_checker = lambda x: compile(x, 'asd', 'exec')
|
||||
def syntax_checker(x):
|
||||
return compile(x, 'asd', 'exec')
|
||||
else:
|
||||
syntax_checker = parser.suite
|
||||
|
||||
@@ -143,8 +144,8 @@ class Source(object):
|
||||
else:
|
||||
source = str(self)
|
||||
try:
|
||||
#compile(source+'\n', "x", "exec")
|
||||
syntax_checker(source+'\n')
|
||||
# compile(source+'\n', "x", "exec")
|
||||
syntax_checker(source + '\n')
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
@@ -164,8 +165,8 @@ class Source(object):
|
||||
"""
|
||||
if not filename or py.path.local(filename).check(file=0):
|
||||
if _genframe is None:
|
||||
_genframe = sys._getframe(1) # the caller
|
||||
fn,lineno = _genframe.f_code.co_filename, _genframe.f_lineno
|
||||
_genframe = sys._getframe(1) # the caller
|
||||
fn, lineno = _genframe.f_code.co_filename, _genframe.f_lineno
|
||||
base = "<%d-codegen " % self._compilecounter
|
||||
self.__class__._compilecounter += 1
|
||||
if not filename:
|
||||
@@ -180,7 +181,7 @@ class Source(object):
|
||||
# re-represent syntax errors from parsing python strings
|
||||
msglines = self.lines[:ex.lineno]
|
||||
if ex.offset:
|
||||
msglines.append(" "*ex.offset + '^')
|
||||
msglines.append(" " * ex.offset + '^')
|
||||
msglines.append("(code was compiled probably from here: %s)" % filename)
|
||||
newex = SyntaxError('\n'.join(msglines))
|
||||
newex.offset = ex.offset
|
||||
@@ -191,24 +192,24 @@ class Source(object):
|
||||
if flag & _AST_FLAG:
|
||||
return co
|
||||
lines = [(x + "\n") for x in self.lines]
|
||||
py.std.linecache.cache[filename] = (1, None, lines, filename)
|
||||
linecache.cache[filename] = (1, None, lines, filename)
|
||||
return co
|
||||
|
||||
#
|
||||
# public API shortcut functions
|
||||
#
|
||||
|
||||
def compile_(source, filename=None, mode='exec', flags=
|
||||
generators.compiler_flag, dont_inherit=0):
|
||||
|
||||
def compile_(source, filename=None, mode='exec', flags=generators.compiler_flag, dont_inherit=0):
|
||||
""" compile the given source to a raw code object,
|
||||
and maintain an internal cache which allows later
|
||||
retrieval of the source code for the code object
|
||||
and any recursively created code objects.
|
||||
"""
|
||||
if _ast is not None and isinstance(source, _ast.AST):
|
||||
if isinstance(source, ast.AST):
|
||||
# XXX should Source support having AST?
|
||||
return cpy_compile(source, filename, mode, flags, dont_inherit)
|
||||
_genframe = sys._getframe(1) # the caller
|
||||
_genframe = sys._getframe(1) # the caller
|
||||
s = Source(source)
|
||||
co = s.compile(filename, mode, flags, _genframe=_genframe)
|
||||
return co
|
||||
@@ -223,8 +224,7 @@ def getfslineno(obj):
|
||||
code = _pytest._code.Code(obj)
|
||||
except TypeError:
|
||||
try:
|
||||
fn = (py.std.inspect.getsourcefile(obj) or
|
||||
py.std.inspect.getfile(obj))
|
||||
fn = inspect.getsourcefile(obj) or inspect.getfile(obj)
|
||||
except TypeError:
|
||||
return "", -1
|
||||
|
||||
@@ -245,12 +245,13 @@ def getfslineno(obj):
|
||||
# helper functions
|
||||
#
|
||||
|
||||
|
||||
def findsource(obj):
|
||||
try:
|
||||
sourcelines, lineno = py.std.inspect.findsource(obj)
|
||||
sourcelines, lineno = inspect.findsource(obj)
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
except: # noqa
|
||||
return None, -1
|
||||
source = Source()
|
||||
source.lines = [line.rstrip() for line in sourcelines]
|
||||
@@ -274,7 +275,7 @@ def deindent(lines, offset=None):
|
||||
line = line.expandtabs()
|
||||
s = line.lstrip()
|
||||
if s:
|
||||
offset = len(line)-len(s)
|
||||
offset = len(line) - len(s)
|
||||
break
|
||||
else:
|
||||
offset = 0
|
||||
@@ -293,11 +294,11 @@ def deindent(lines, offset=None):
|
||||
try:
|
||||
for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(lambda: next(it)):
|
||||
if sline > len(lines):
|
||||
break # End of input reached
|
||||
break # End of input reached
|
||||
if sline > len(newlines):
|
||||
line = lines[sline - 1].expandtabs()
|
||||
if line.lstrip() and line[:offset].isspace():
|
||||
line = line[offset:] # Deindent
|
||||
line = line[offset:] # Deindent
|
||||
newlines.append(line)
|
||||
|
||||
for i in range(sline, eline):
|
||||
@@ -315,30 +316,28 @@ def get_statement_startend2(lineno, node):
|
||||
import ast
|
||||
# flatten all statements and except handlers into one lineno-list
|
||||
# AST's line numbers start indexing at 1
|
||||
l = []
|
||||
values = []
|
||||
for x in ast.walk(node):
|
||||
if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler):
|
||||
l.append(x.lineno - 1)
|
||||
if isinstance(x, ast.stmt) or isinstance(x, ast.ExceptHandler):
|
||||
values.append(x.lineno - 1)
|
||||
for name in "finalbody", "orelse":
|
||||
val = getattr(x, name, None)
|
||||
if val:
|
||||
# treat the finally/orelse part as its own statement
|
||||
l.append(val[0].lineno - 1 - 1)
|
||||
l.sort()
|
||||
insert_index = bisect_right(l, lineno)
|
||||
start = l[insert_index - 1]
|
||||
if insert_index >= len(l):
|
||||
values.append(val[0].lineno - 1 - 1)
|
||||
values.sort()
|
||||
insert_index = bisect_right(values, lineno)
|
||||
start = values[insert_index - 1]
|
||||
if insert_index >= len(values):
|
||||
end = None
|
||||
else:
|
||||
end = l[insert_index]
|
||||
end = values[insert_index]
|
||||
return start, end
|
||||
|
||||
|
||||
def getstatementrange_ast(lineno, source, assertion=False, astnode=None):
|
||||
if astnode is None:
|
||||
content = str(source)
|
||||
if sys.version_info < (2,7):
|
||||
content += "\n"
|
||||
try:
|
||||
astnode = compile(content, "source", "exec", 1024) # 1024 for AST
|
||||
except ValueError:
|
||||
@@ -393,7 +392,7 @@ def getstatementrange_old(lineno, source, assertion=False):
|
||||
raise IndexError("likely a subclass")
|
||||
if "assert" not in line and "raise" not in line:
|
||||
continue
|
||||
trylines = source.lines[start:lineno+1]
|
||||
trylines = source.lines[start:lineno + 1]
|
||||
# quick hack to prepare parsing an indented line with
|
||||
# compile_command() (which errors on "return" outside defs)
|
||||
trylines.insert(0, 'def xxx():')
|
||||
@@ -405,10 +404,8 @@ def getstatementrange_old(lineno, source, assertion=False):
|
||||
continue
|
||||
|
||||
# 2. find the end of the statement
|
||||
for end in range(lineno+1, len(source)+1):
|
||||
for end in range(lineno + 1, len(source) + 1):
|
||||
trysource = source[start:end]
|
||||
if trysource.isparseable():
|
||||
return start, end
|
||||
raise SyntaxError("no valid source range around line %d " % (lineno,))
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
"""
|
||||
imports symbols from vendored "pluggy" if available, otherwise
|
||||
falls back to importing "pluggy" from the default namespace.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
try:
|
||||
from _pytest.vendored_packages.pluggy import * # noqa
|
||||
from _pytest.vendored_packages.pluggy import __version__ # noqa
|
||||
except ImportError:
|
||||
from pluggy import * # noqa
|
||||
from pluggy import __version__ # noqa
|
||||
@@ -2,8 +2,8 @@
|
||||
support for presenting detailed information in failing assertions.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import py
|
||||
import sys
|
||||
import six
|
||||
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion import rewrite
|
||||
@@ -25,7 +25,6 @@ def pytest_addoption(parser):
|
||||
expression information.""")
|
||||
|
||||
|
||||
|
||||
def register_assert_rewrite(*names):
|
||||
"""Register one or more module names to be rewritten on import.
|
||||
|
||||
@@ -57,7 +56,7 @@ class DummyRewriteHook(object):
|
||||
pass
|
||||
|
||||
|
||||
class AssertionState:
|
||||
class AssertionState(object):
|
||||
"""State for the assertion plugin."""
|
||||
|
||||
def __init__(self, config, mode):
|
||||
@@ -68,10 +67,8 @@ class AssertionState:
|
||||
|
||||
def install_importhook(config):
|
||||
"""Try to install the rewrite hook, raise SystemError if it fails."""
|
||||
# Both Jython and CPython 2.6.0 have AST bugs that make the
|
||||
# assertion rewriting hook malfunction.
|
||||
if (sys.platform.startswith('java') or
|
||||
sys.version_info[:3] == (2, 6, 0)):
|
||||
# Jython has an AST bug that make the assertion rewriting hook malfunction.
|
||||
if (sys.platform.startswith('java')):
|
||||
raise SystemError('rewrite not supported')
|
||||
|
||||
config._assertstate = AssertionState(config, 'rewrite')
|
||||
@@ -127,7 +124,7 @@ def pytest_runtest_setup(item):
|
||||
if new_expl:
|
||||
new_expl = truncate.truncate_if_required(new_expl, item)
|
||||
new_expl = [line.replace("\n", "\\n") for line in new_expl]
|
||||
res = py.builtin._totext("\n~").join(new_expl)
|
||||
res = six.text_type("\n~").join(new_expl)
|
||||
if item.config.getvalue("assertmode") == "rewrite":
|
||||
res = res.replace("%", "%%")
|
||||
return res
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
"""Rewrite assertion AST to produce nice error messages"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import ast
|
||||
import _ast
|
||||
import errno
|
||||
import itertools
|
||||
import imp
|
||||
import marshal
|
||||
import os
|
||||
import re
|
||||
import six
|
||||
import struct
|
||||
import sys
|
||||
import types
|
||||
from fnmatch import fnmatch
|
||||
|
||||
import py
|
||||
from _pytest.assertion import util
|
||||
@@ -34,13 +33,13 @@ else:
|
||||
PYC_EXT = ".py" + (__debug__ and "c" or "o")
|
||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
|
||||
REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2)
|
||||
ASCII_IS_DEFAULT_ENCODING = sys.version_info[0] < 3
|
||||
|
||||
if sys.version_info >= (3,5):
|
||||
if sys.version_info >= (3, 5):
|
||||
ast_Call = ast.Call
|
||||
else:
|
||||
ast_Call = lambda a,b,c: ast.Call(a, b, c, None, None)
|
||||
def ast_Call(a, b, c):
|
||||
return ast.Call(a, b, c, None, None)
|
||||
|
||||
|
||||
class AssertionRewritingHook(object):
|
||||
@@ -163,38 +162,36 @@ class AssertionRewritingHook(object):
|
||||
# modules not passed explicitly on the command line are only
|
||||
# rewritten if they match the naming convention for test files
|
||||
for pat in self.fnpats:
|
||||
# use fnmatch instead of fn_pypath.fnmatch because the
|
||||
# latter might trigger an import to fnmatch.fnmatch
|
||||
# internally, which would cause this method to be
|
||||
# called recursively
|
||||
if fnmatch(fn_pypath.basename, pat):
|
||||
if fn_pypath.fnmatch(pat):
|
||||
state.trace("matched test file %r" % (fn,))
|
||||
return True
|
||||
|
||||
for marked in self._must_rewrite:
|
||||
if name.startswith(marked):
|
||||
if name == marked or name.startswith(marked + '.'):
|
||||
state.trace("matched marked file %r (from %r)" % (name, marked))
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def mark_rewrite(self, *names):
|
||||
"""Mark import names as needing to be re-written.
|
||||
"""Mark import names as needing to be rewritten.
|
||||
|
||||
The named module or package as well as any nested modules will
|
||||
be re-written on import.
|
||||
be rewritten on import.
|
||||
"""
|
||||
already_imported = set(names).intersection(set(sys.modules))
|
||||
if already_imported:
|
||||
for name in already_imported:
|
||||
if name not in self._rewritten_names:
|
||||
self._warn_already_imported(name)
|
||||
already_imported = (set(names)
|
||||
.intersection(sys.modules)
|
||||
.difference(self._rewritten_names))
|
||||
for name in already_imported:
|
||||
if not AssertionRewriter.is_rewrite_disabled(
|
||||
sys.modules[name].__doc__ or ""):
|
||||
self._warn_already_imported(name)
|
||||
self._must_rewrite.update(names)
|
||||
|
||||
def _warn_already_imported(self, name):
|
||||
self.config.warn(
|
||||
'P1',
|
||||
'Module already imported so can not be re-written: %s' % name)
|
||||
'Module already imported so cannot be rewritten: %s' % name)
|
||||
|
||||
def load_module(self, name):
|
||||
# If there is an existing module object named 'fullname' in
|
||||
@@ -214,14 +211,12 @@ class AssertionRewritingHook(object):
|
||||
mod.__cached__ = pyc
|
||||
mod.__loader__ = self
|
||||
py.builtin.exec_(co, mod.__dict__)
|
||||
except:
|
||||
except: # noqa
|
||||
if name in sys.modules:
|
||||
del sys.modules[name]
|
||||
raise
|
||||
return sys.modules[name]
|
||||
|
||||
|
||||
|
||||
def is_package(self, name):
|
||||
try:
|
||||
fd, fn, desc = imp.find_module(name)
|
||||
@@ -266,7 +261,7 @@ def _write_pyc(state, co, source_stat, pyc):
|
||||
fp = open(pyc, "wb")
|
||||
except IOError:
|
||||
err = sys.exc_info()[1].errno
|
||||
state.trace("error writing pyc file at %s: errno=%s" %(pyc, err))
|
||||
state.trace("error writing pyc file at %s: errno=%s" % (pyc, err))
|
||||
# we ignore any failure to write the cache file
|
||||
# there are many reasons, permission-denied, __pycache__ being a
|
||||
# file etc.
|
||||
@@ -288,6 +283,7 @@ N = "\n".encode("utf-8")
|
||||
cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+")
|
||||
BOM_UTF8 = '\xef\xbb\xbf'
|
||||
|
||||
|
||||
def _rewrite_test(config, fn):
|
||||
"""Try to read and rewrite *fn* and return the code object."""
|
||||
state = config._assertstate
|
||||
@@ -312,7 +308,7 @@ def _rewrite_test(config, fn):
|
||||
end2 = source.find("\n", end1 + 1)
|
||||
if (not source.startswith(BOM_UTF8) and
|
||||
cookie_re.match(source[0:end1]) is None and
|
||||
cookie_re.match(source[end1 + 1:end2]) is None):
|
||||
cookie_re.match(source[end1 + 1:end2]) is None):
|
||||
if hasattr(state, "_indecode"):
|
||||
# encodings imported us again, so don't rewrite.
|
||||
return None, None
|
||||
@@ -325,10 +321,6 @@ def _rewrite_test(config, fn):
|
||||
return None, None
|
||||
finally:
|
||||
del state._indecode
|
||||
# On Python versions which are not 2.7 and less than or equal to 3.1, the
|
||||
# parser expects *nix newlines.
|
||||
if REWRITE_NEWLINES:
|
||||
source = source.replace(RN, N) + N
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
@@ -345,6 +337,7 @@ def _rewrite_test(config, fn):
|
||||
return None, None
|
||||
return stat, co
|
||||
|
||||
|
||||
def _make_rewritten_pyc(state, source_stat, pyc, co):
|
||||
"""Try to dump rewritten code to *pyc*."""
|
||||
if sys.platform.startswith("win"):
|
||||
@@ -358,6 +351,7 @@ def _make_rewritten_pyc(state, source_stat, pyc, co):
|
||||
if _write_pyc(state, co, source_stat, proc_pyc):
|
||||
os.rename(proc_pyc, pyc)
|
||||
|
||||
|
||||
def _read_pyc(source, pyc, trace=lambda x: None):
|
||||
"""Possibly read a pytest pyc containing rewritten code.
|
||||
|
||||
@@ -408,14 +402,15 @@ def _saferepr(obj):
|
||||
|
||||
"""
|
||||
repr = py.io.saferepr(obj)
|
||||
if py.builtin._istext(repr):
|
||||
t = py.builtin.text
|
||||
if isinstance(repr, six.text_type):
|
||||
t = six.text_type
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
t = six.binary_type
|
||||
return repr.replace(t("\n"), t("\\n"))
|
||||
|
||||
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation # noqa
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation # noqa
|
||||
|
||||
|
||||
def _format_assertmsg(obj):
|
||||
"""Format the custom assertion message given.
|
||||
@@ -429,32 +424,35 @@ 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 py.builtin._istext(obj) or py.builtin._isbytes(obj):
|
||||
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 py.builtin._istext(s):
|
||||
t = py.builtin.text
|
||||
if isinstance(s, six.text_type):
|
||||
t = six.text_type
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
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
|
||||
|
||||
|
||||
def _should_repr_global_name(obj):
|
||||
return not hasattr(obj, "__name__") and not py.builtin.callable(obj)
|
||||
return not hasattr(obj, "__name__") and not callable(obj)
|
||||
|
||||
|
||||
def _format_boolop(explanations, is_or):
|
||||
explanation = "(" + (is_or and " or " or " and ").join(explanations) + ")"
|
||||
if py.builtin._istext(explanation):
|
||||
t = py.builtin.text
|
||||
if isinstance(explanation, six.text_type):
|
||||
t = six.text_type
|
||||
else:
|
||||
t = py.builtin.bytes
|
||||
t = six.binary_type
|
||||
return explanation.replace(t('%'), t('%%'))
|
||||
|
||||
|
||||
def _call_reprcompare(ops, results, expls, each_obj):
|
||||
for i, res, expl in zip(range(len(ops)), results, expls):
|
||||
try:
|
||||
@@ -488,7 +486,7 @@ binop_map = {
|
||||
ast.Mult: "*",
|
||||
ast.Div: "/",
|
||||
ast.FloorDiv: "//",
|
||||
ast.Mod: "%%", # escaped for string formatting
|
||||
ast.Mod: "%%", # escaped for string formatting
|
||||
ast.Eq: "==",
|
||||
ast.NotEq: "!=",
|
||||
ast.Lt: "<",
|
||||
@@ -532,7 +530,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
"""Assertion rewriting implementation.
|
||||
|
||||
The main entrypoint is to call .run() with an ast.Module instance,
|
||||
this will then find all the assert statements and re-write them to
|
||||
this will then find all the assert statements and rewrite them to
|
||||
provide intermediate values and a detailed assertion error. See
|
||||
http://pybites.blogspot.be/2011/07/behind-scenes-of-pytests-new-assertion.html
|
||||
for an overview of how this works.
|
||||
@@ -541,7 +539,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
statements in an ast.Module and for each ast.Assert statement it
|
||||
finds call .visit() with it. Then .visit_Assert() takes over and
|
||||
is responsible for creating new ast statements to replace the
|
||||
original assert statement: it re-writes the test of an assertion
|
||||
original assert statement: it rewrites the test of an assertion
|
||||
to provide intermediate values and replace it with an if statement
|
||||
which raises an assertion error with a detailed explanation in
|
||||
case the expression is false.
|
||||
@@ -594,23 +592,26 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
# docstrings and __future__ imports.
|
||||
aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"),
|
||||
ast.alias("_pytest.assertion.rewrite", "@pytest_ar")]
|
||||
expect_docstring = True
|
||||
doc = getattr(mod, "docstring", None)
|
||||
expect_docstring = doc is None
|
||||
if doc is not None and self.is_rewrite_disabled(doc):
|
||||
return
|
||||
pos = 0
|
||||
lineno = 0
|
||||
lineno = 1
|
||||
for item in mod.body:
|
||||
if (expect_docstring and isinstance(item, ast.Expr) and
|
||||
isinstance(item.value, ast.Str)):
|
||||
doc = item.value.s
|
||||
if "PYTEST_DONT_REWRITE" in doc:
|
||||
# The module has disabled assertion rewriting.
|
||||
if self.is_rewrite_disabled(doc):
|
||||
return
|
||||
lineno += len(doc) - 1
|
||||
expect_docstring = False
|
||||
elif (not isinstance(item, ast.ImportFrom) or item.level > 0 or
|
||||
item.module != "__future__"):
|
||||
lineno = item.lineno
|
||||
break
|
||||
pos += 1
|
||||
else:
|
||||
lineno = item.lineno
|
||||
imports = [ast.Import([alias], lineno=lineno, col_offset=0)
|
||||
for alias in aliases]
|
||||
mod.body[pos:pos] = imports
|
||||
@@ -636,6 +637,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
not isinstance(field, ast.expr)):
|
||||
nodes.append(field)
|
||||
|
||||
@staticmethod
|
||||
def is_rewrite_disabled(docstring):
|
||||
return "PYTEST_DONT_REWRITE" in docstring
|
||||
|
||||
def variable(self):
|
||||
"""Get a new variable."""
|
||||
# Use a character invalid in python identifiers to avoid clashing.
|
||||
@@ -719,7 +724,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Assert(self, assert_):
|
||||
"""Return the AST statements to replace the ast.Assert instance.
|
||||
|
||||
This re-writes the test of an assertion to provide
|
||||
This rewrites the test of an assertion to provide
|
||||
intermediate values and replace it with an if statement which
|
||||
raises an assertion error with a detailed explanation in case
|
||||
the expression is false.
|
||||
@@ -728,7 +733,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
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)
|
||||
'remove parentheses?', fslocation=fslocation)
|
||||
self.statements = []
|
||||
self.variables = []
|
||||
self.variable_counter = itertools.count()
|
||||
@@ -792,7 +797,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
if i:
|
||||
fail_inner = []
|
||||
# cond is set in a prior loop iteration below
|
||||
self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa
|
||||
self.on_failure.append(ast.If(cond, fail_inner, [])) # noqa
|
||||
self.on_failure = fail_inner
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
@@ -844,7 +849,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
if keyword.arg:
|
||||
arg_expls.append(keyword.arg + "=" + expl)
|
||||
else: ## **args have `arg` keywords with an .arg of None
|
||||
else: # **args have `arg` keywords with an .arg of None
|
||||
arg_expls.append("**" + expl)
|
||||
|
||||
expl = "%s(%s)" % (func_expl, ', '.join(arg_expls))
|
||||
@@ -898,7 +903,6 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
else:
|
||||
visit_Call = visit_Call_legacy
|
||||
|
||||
|
||||
def visit_Attribute(self, attr):
|
||||
if not isinstance(attr.ctx, ast.Load):
|
||||
return self.generic_visit(attr)
|
||||
@@ -912,7 +916,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Compare(self, comp):
|
||||
self.push_format_context()
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
if isinstance(comp.left, (_ast.Compare, _ast.BoolOp)):
|
||||
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
|
||||
left_expl = "({0})".format(left_expl)
|
||||
res_variables = [self.variable() for i in range(len(comp.ops))]
|
||||
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
|
||||
@@ -923,7 +927,7 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
results = [left_res]
|
||||
for i, op, next_operand in it:
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
if isinstance(next_operand, (_ast.Compare, _ast.BoolOp)):
|
||||
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
|
||||
next_expl = "({0})".format(next_expl)
|
||||
results.append(next_res)
|
||||
sym = binop_map[op.__class__]
|
||||
|
||||
@@ -7,7 +7,7 @@ Current default behaviour is to truncate assertion explanations at
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
|
||||
import py
|
||||
import six
|
||||
|
||||
|
||||
DEFAULT_MAX_LINES = 8
|
||||
@@ -74,8 +74,8 @@ def _truncate_explanation(input_lines, max_lines=None, max_chars=None):
|
||||
msg += ' ({0} lines hidden)'.format(truncated_line_count)
|
||||
msg += ", {0}" .format(USAGE_MSG)
|
||||
truncated_explanation.extend([
|
||||
py.builtin._totext(""),
|
||||
py.builtin._totext(msg),
|
||||
six.text_type(""),
|
||||
six.text_type(msg),
|
||||
])
|
||||
return truncated_explanation
|
||||
|
||||
|
||||
@@ -4,13 +4,10 @@ import pprint
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
try:
|
||||
from collections import Sequence
|
||||
except ImportError:
|
||||
Sequence = list
|
||||
import six
|
||||
from collections import Sequence
|
||||
|
||||
|
||||
u = py.builtin._totext
|
||||
u = six.text_type
|
||||
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
# interpretation code and assertion rewriter to detect this plugin was
|
||||
@@ -53,11 +50,11 @@ def _split_explanation(explanation):
|
||||
"""
|
||||
raw_lines = (explanation or u('')).split('\n')
|
||||
lines = [raw_lines[0]]
|
||||
for l in raw_lines[1:]:
|
||||
if l and l[0] in ['{', '}', '~', '>']:
|
||||
lines.append(l)
|
||||
for values in raw_lines[1:]:
|
||||
if values and values[0] in ['{', '}', '~', '>']:
|
||||
lines.append(values)
|
||||
else:
|
||||
lines[-1] += '\\n' + l
|
||||
lines[-1] += '\\n' + values
|
||||
return lines
|
||||
|
||||
|
||||
@@ -82,7 +79,7 @@ def _format_lines(lines):
|
||||
stack.append(len(result))
|
||||
stackcnt[-1] += 1
|
||||
stackcnt.append(0)
|
||||
result.append(u(' +') + u(' ')*(len(stack)-1) + s + line[1:])
|
||||
result.append(u(' +') + u(' ') * (len(stack) - 1) + s + line[1:])
|
||||
elif line.startswith('}'):
|
||||
stack.pop()
|
||||
stackcnt.pop()
|
||||
@@ -91,7 +88,7 @@ def _format_lines(lines):
|
||||
assert line[0] in ['~', '>']
|
||||
stack[-1] += 1
|
||||
indent = len(stack) if line.startswith('~') else len(stack) - 1
|
||||
result.append(u(' ')*indent + line[1:])
|
||||
result.append(u(' ') * indent + line[1:])
|
||||
assert len(stack) == 1
|
||||
return result
|
||||
|
||||
@@ -106,16 +103,22 @@ except NameError:
|
||||
def assertrepr_compare(config, op, left, right):
|
||||
"""Return specialised explanations for some operators/operands"""
|
||||
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
|
||||
left_repr = py.io.saferepr(left, maxsize=int(width//2))
|
||||
right_repr = py.io.saferepr(right, maxsize=width-len(left_repr))
|
||||
left_repr = py.io.saferepr(left, maxsize=int(width // 2))
|
||||
right_repr = py.io.saferepr(right, maxsize=width - len(left_repr))
|
||||
|
||||
summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr))
|
||||
|
||||
issequence = lambda x: (isinstance(x, (list, tuple, Sequence)) and
|
||||
not isinstance(x, basestring))
|
||||
istext = lambda x: isinstance(x, basestring)
|
||||
isdict = lambda x: isinstance(x, dict)
|
||||
isset = lambda x: isinstance(x, (set, frozenset))
|
||||
def issequence(x):
|
||||
return isinstance(x, Sequence) and not isinstance(x, basestring)
|
||||
|
||||
def istext(x):
|
||||
return isinstance(x, basestring)
|
||||
|
||||
def isdict(x):
|
||||
return isinstance(x, dict)
|
||||
|
||||
def isset(x):
|
||||
return isinstance(x, (set, frozenset))
|
||||
|
||||
def isiterable(obj):
|
||||
try:
|
||||
@@ -168,9 +171,9 @@ def _diff_text(left, right, verbose=False):
|
||||
"""
|
||||
from difflib import ndiff
|
||||
explanation = []
|
||||
if isinstance(left, py.builtin.bytes):
|
||||
if isinstance(left, six.binary_type):
|
||||
left = u(repr(left)[1:-1]).replace(r'\n', '\n')
|
||||
if isinstance(right, py.builtin.bytes):
|
||||
if isinstance(right, six.binary_type):
|
||||
right = u(repr(right)[1:-1]).replace(r'\n', '\n')
|
||||
if not verbose:
|
||||
i = 0 # just in case left or right has zero length
|
||||
@@ -285,7 +288,7 @@ def _compare_eq_dict(left, right, verbose=False):
|
||||
def _notin_text(term, text, verbose=False):
|
||||
index = text.find(term)
|
||||
head = text[:index]
|
||||
tail = text[index+len(term):]
|
||||
tail = text[index + len(term):]
|
||||
correct_text = head + tail
|
||||
diff = _diff_text(correct_text, text, verbose)
|
||||
newdiff = [u('%s is contained here:') % py.io.saferepr(term, maxsize=42)]
|
||||
|
||||
@@ -8,20 +8,31 @@ from __future__ import absolute_import, division, print_function
|
||||
import py
|
||||
import pytest
|
||||
import json
|
||||
import os
|
||||
from os.path import sep as _sep, altsep as _altsep
|
||||
|
||||
|
||||
class Cache(object):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self._cachedir = config.rootdir.join(".cache")
|
||||
self._cachedir = Cache.cache_dir_from_config(config)
|
||||
self.trace = config.trace.root.get("cache")
|
||||
if config.getvalue("cacheclear"):
|
||||
if config.getoption("cacheclear"):
|
||||
self.trace("clearing cachedir")
|
||||
if self._cachedir.check():
|
||||
self._cachedir.remove()
|
||||
self._cachedir.mkdir()
|
||||
|
||||
@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)
|
||||
|
||||
def makedir(self, name):
|
||||
""" return a directory path object with the given name. If the
|
||||
directory does not yet exist, it will be created. You can use it
|
||||
@@ -87,33 +98,34 @@ class Cache(object):
|
||||
json.dump(value, f, indent=2, sort_keys=True)
|
||||
|
||||
|
||||
class LFPlugin:
|
||||
class LFPlugin(object):
|
||||
""" Plugin which implements the --lf (run last-failing) option """
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
active_keys = 'lf', 'failedfirst'
|
||||
self.active = any(config.getvalue(key) for key in active_keys)
|
||||
if self.active:
|
||||
self.lastfailed = config.cache.get("cache/lastfailed", {})
|
||||
else:
|
||||
self.lastfailed = {}
|
||||
self.active = any(config.getoption(key) for key in active_keys)
|
||||
self.lastfailed = config.cache.get("cache/lastfailed", {})
|
||||
self._previously_failed_count = None
|
||||
|
||||
def pytest_report_header(self):
|
||||
def pytest_report_collectionfinish(self):
|
||||
if self.active:
|
||||
if not self.lastfailed:
|
||||
if not self._previously_failed_count:
|
||||
mode = "run all (no recorded failures)"
|
||||
else:
|
||||
mode = "rerun last %d failures%s" % (
|
||||
len(self.lastfailed),
|
||||
" first" if self.config.getvalue("failedfirst") 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 "run-last-failure: %s" % mode
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.failed and "xfail" not in report.keywords:
|
||||
if (report.when == 'call' and report.passed) or report.skipped:
|
||||
self.lastfailed.pop(report.nodeid, None)
|
||||
elif report.failed:
|
||||
self.lastfailed[report.nodeid] = True
|
||||
elif not report.failed:
|
||||
if report.when == "call":
|
||||
self.lastfailed.pop(report.nodeid, None)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
passed = report.outcome in ('passed', 'skipped')
|
||||
@@ -135,11 +147,12 @@ class LFPlugin:
|
||||
previously_failed.append(item)
|
||||
else:
|
||||
previously_passed.append(item)
|
||||
if not previously_failed and previously_passed:
|
||||
self._previously_failed_count = len(previously_failed)
|
||||
if not previously_failed:
|
||||
# running a subset of all tests with recorded failures outside
|
||||
# of the set of tests currently executing
|
||||
pass
|
||||
elif self.config.getvalue("lf"):
|
||||
return
|
||||
if self.config.getoption("lf"):
|
||||
items[:] = previously_failed
|
||||
config.hook.pytest_deselected(items=previously_passed)
|
||||
else:
|
||||
@@ -147,10 +160,11 @@ class LFPlugin:
|
||||
|
||||
def pytest_sessionfinish(self, session):
|
||||
config = self.config
|
||||
if config.getvalue("cacheshow") or hasattr(config, "slaveinput"):
|
||||
if config.getoption("cacheshow") or hasattr(config, "slaveinput"):
|
||||
return
|
||||
prev_failed = config.cache.get("cache/lastfailed", None) is not None
|
||||
if (session.testscollected and prev_failed) or self.lastfailed:
|
||||
|
||||
saved_lastfailed = config.cache.get("cache/lastfailed", {})
|
||||
if saved_lastfailed != self.lastfailed:
|
||||
config.cache.set("cache/lastfailed", self.lastfailed)
|
||||
|
||||
|
||||
@@ -171,6 +185,9 @@ def pytest_addoption(parser):
|
||||
group.addoption(
|
||||
'--cache-clear', action='store_true', dest="cacheclear",
|
||||
help="remove all cache contents at start of test run.")
|
||||
parser.addini(
|
||||
"cache_dir", default='.pytest_cache',
|
||||
help="cache directory path.")
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
@@ -179,7 +196,6 @@ def pytest_cmdline_main(config):
|
||||
return wrap_session(config, cacheshow)
|
||||
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_configure(config):
|
||||
config.cache = Cache(config)
|
||||
@@ -224,7 +240,7 @@ def cacheshow(config, session):
|
||||
val = config.cache.get(key, dummy)
|
||||
if val is dummy:
|
||||
tw.line("%s contains unreadable content, "
|
||||
"will be ignored" % key)
|
||||
"will be ignored" % key)
|
||||
else:
|
||||
tw.line("%s contains:" % key)
|
||||
stream = py.io.TextIO()
|
||||
@@ -236,7 +252,7 @@ def cacheshow(config, session):
|
||||
if ddir.isdir() and ddir.listdir():
|
||||
tw.sep("-", "cache directories")
|
||||
for p in sorted(basedir.join("d").visit()):
|
||||
#if p.check(dir=1):
|
||||
# if p.check(dir=1):
|
||||
# print("%s/" % p.relto(basedir))
|
||||
if p.isfile():
|
||||
key = p.relto(basedir)
|
||||
|
||||
@@ -4,17 +4,18 @@ per-test stdout/stderr capturing mechanism.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import collections
|
||||
import contextlib
|
||||
import sys
|
||||
import os
|
||||
import io
|
||||
from io import UnsupportedOperation
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
import py
|
||||
import six
|
||||
import pytest
|
||||
from _pytest.compat import CaptureIO
|
||||
|
||||
unicode = py.builtin.text
|
||||
|
||||
patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'}
|
||||
|
||||
@@ -33,14 +34,17 @@ def pytest_addoption(parser):
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
_readline_workaround()
|
||||
ns = early_config.known_args_namespace
|
||||
if ns.capture == "fd":
|
||||
_py36_windowsconsoleio_workaround(sys.stdout)
|
||||
_colorama_workaround()
|
||||
_readline_workaround()
|
||||
pluginmanager = early_config.pluginmanager
|
||||
capman = CaptureManager(ns.capture)
|
||||
pluginmanager.register(capman, "capturemanager")
|
||||
|
||||
# make sure that capturemanager is properly reset at final shutdown
|
||||
early_config.add_cleanup(capman.reset_capturings)
|
||||
early_config.add_cleanup(capman.stop_global_capturing)
|
||||
|
||||
# make sure logging does not raise exceptions at the end
|
||||
def silence_logging_at_shutdown():
|
||||
@@ -49,17 +53,30 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||
early_config.add_cleanup(silence_logging_at_shutdown)
|
||||
|
||||
# finally trigger conftest loading but while capturing (issue93)
|
||||
capman.init_capturings()
|
||||
capman.start_global_capturing()
|
||||
outcome = yield
|
||||
out, err = capman.suspendcapture()
|
||||
out, err = capman.suspend_global_capture()
|
||||
if outcome.excinfo is not None:
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
|
||||
class CaptureManager:
|
||||
class CaptureManager(object):
|
||||
"""
|
||||
Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each
|
||||
test phase (setup, call, teardown). After each of those points, the captured output is obtained and
|
||||
attached to the collection/runtest report.
|
||||
|
||||
There are two levels of capture:
|
||||
* global: which is enabled by default and can be suppressed by the ``-s`` option. This is always enabled/disabled
|
||||
during collection and each test phase.
|
||||
* fixture: when a test function or one of its fixture depend on the ``capsys`` or ``capfd`` fixtures. In this
|
||||
case special handling is needed to ensure the fixtures take precedence over the global capture.
|
||||
"""
|
||||
|
||||
def __init__(self, method):
|
||||
self._method = method
|
||||
self._global_capturing = None
|
||||
|
||||
def _getcapture(self, method):
|
||||
if method == "fd":
|
||||
@@ -71,23 +88,24 @@ class CaptureManager:
|
||||
else:
|
||||
raise ValueError("unknown capturing method: %r" % method)
|
||||
|
||||
def init_capturings(self):
|
||||
assert not hasattr(self, "_capturing")
|
||||
self._capturing = self._getcapture(self._method)
|
||||
self._capturing.start_capturing()
|
||||
def start_global_capturing(self):
|
||||
assert self._global_capturing is None
|
||||
self._global_capturing = self._getcapture(self._method)
|
||||
self._global_capturing.start_capturing()
|
||||
|
||||
def reset_capturings(self):
|
||||
cap = self.__dict__.pop("_capturing", None)
|
||||
if cap is not None:
|
||||
cap.pop_outerr_to_orig()
|
||||
cap.stop_capturing()
|
||||
def stop_global_capturing(self):
|
||||
if self._global_capturing is not None:
|
||||
self._global_capturing.pop_outerr_to_orig()
|
||||
self._global_capturing.stop_capturing()
|
||||
self._global_capturing = None
|
||||
|
||||
def resumecapture(self):
|
||||
self._capturing.resume_capturing()
|
||||
def resume_global_capture(self):
|
||||
self._global_capturing.resume_capturing()
|
||||
|
||||
def suspendcapture(self, in_=False):
|
||||
self.deactivate_funcargs()
|
||||
cap = getattr(self, "_capturing", None)
|
||||
def suspend_global_capture(self, item=None, in_=False):
|
||||
if item is not None:
|
||||
self.deactivate_fixture(item)
|
||||
cap = getattr(self, "_global_capturing", None)
|
||||
if cap is not None:
|
||||
try:
|
||||
outerr = cap.readouterr()
|
||||
@@ -95,23 +113,26 @@ class CaptureManager:
|
||||
cap.suspend_capturing(in_=in_)
|
||||
return outerr
|
||||
|
||||
def activate_funcargs(self, pyfuncitem):
|
||||
capfuncarg = pyfuncitem.__dict__.pop("_capfuncarg", None)
|
||||
if capfuncarg is not None:
|
||||
capfuncarg._start()
|
||||
self._capfuncarg = capfuncarg
|
||||
def activate_fixture(self, item):
|
||||
"""If the current item is using ``capsys`` or ``capfd``, activate them so they take precedence over
|
||||
the global capture.
|
||||
"""
|
||||
fixture = getattr(item, "_capture_fixture", None)
|
||||
if fixture is not None:
|
||||
fixture._start()
|
||||
|
||||
def deactivate_funcargs(self):
|
||||
capfuncarg = self.__dict__.pop("_capfuncarg", None)
|
||||
if capfuncarg is not None:
|
||||
capfuncarg.close()
|
||||
def deactivate_fixture(self, item):
|
||||
"""Deactivates the ``capsys`` or ``capfd`` fixture of this item, if any."""
|
||||
fixture = getattr(item, "_capture_fixture", None)
|
||||
if fixture is not None:
|
||||
fixture.close()
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_make_collect_report(self, collector):
|
||||
if isinstance(collector, pytest.File):
|
||||
self.resumecapture()
|
||||
self.resume_global_capture()
|
||||
outcome = yield
|
||||
out, err = self.suspendcapture()
|
||||
out, err = self.suspend_global_capture()
|
||||
rep = outcome.get_result()
|
||||
if out:
|
||||
rep.sections.append(("Captured stdout", out))
|
||||
@@ -122,67 +143,135 @@ class CaptureManager:
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
self.resumecapture()
|
||||
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
|
||||
yield
|
||||
self.suspendcapture_item(item, "setup")
|
||||
self.suspend_capture_item(item, "setup")
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(self, item):
|
||||
self.resumecapture()
|
||||
self.activate_funcargs(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.deactivate_funcargs() called from suspendcapture()
|
||||
self.suspendcapture_item(item, "call")
|
||||
self.suspend_capture_item(item, "call")
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.resumecapture()
|
||||
self.resume_global_capture()
|
||||
self.activate_fixture(item)
|
||||
yield
|
||||
self.suspendcapture_item(item, "teardown")
|
||||
self.suspend_capture_item(item, "teardown")
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
self.reset_capturings()
|
||||
self.stop_global_capturing()
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_internalerror(self, excinfo):
|
||||
self.reset_capturings()
|
||||
self.stop_global_capturing()
|
||||
|
||||
def suspendcapture_item(self, item, when, in_=False):
|
||||
out, err = self.suspendcapture(in_=in_)
|
||||
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)
|
||||
|
||||
|
||||
error_capsysfderror = "cannot use capsys and capfd at the same time"
|
||||
capture_fixtures = {'capfd', 'capfdbinary', 'capsys', 'capsysbinary'}
|
||||
|
||||
|
||||
def _ensure_only_one_capture_fixture(request, name):
|
||||
fixtures = set(request.fixturenames) & capture_fixtures - set((name,))
|
||||
if fixtures:
|
||||
fixtures = sorted(fixtures)
|
||||
fixtures = fixtures[0] if len(fixtures) == 1 else fixtures
|
||||
raise request.raiseerror(
|
||||
"cannot use {0} and {1} at the same time".format(
|
||||
fixtures, name,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def capsys(request):
|
||||
"""Enable capturing of writes to sys.stdout/sys.stderr and make
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
|
||||
objects.
|
||||
"""
|
||||
if "capfd" in request.fixturenames:
|
||||
raise request.raiseerror(error_capsysfderror)
|
||||
request.node._capfuncarg = c = CaptureFixture(SysCapture, request)
|
||||
return c
|
||||
_ensure_only_one_capture_fixture(request, 'capsys')
|
||||
with _install_capture_fixture_on_item(request, SysCapture) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def capsysbinary(request):
|
||||
"""Enable capturing of writes to sys.stdout/sys.stderr and make
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``bytes``
|
||||
objects.
|
||||
"""
|
||||
_ensure_only_one_capture_fixture(request, 'capsysbinary')
|
||||
# Currently, the implementation uses the python3 specific `.buffer`
|
||||
# property of CaptureIO.
|
||||
if sys.version_info < (3,):
|
||||
raise request.raiseerror('capsysbinary is only supported on python 3')
|
||||
with _install_capture_fixture_on_item(request, SysCaptureBinary) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def capfd(request):
|
||||
"""Enable capturing of writes to file descriptors 1 and 2 and make
|
||||
captured output available via ``capfd.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
|
||||
objects.
|
||||
"""
|
||||
if "capsys" in request.fixturenames:
|
||||
request.raiseerror(error_capsysfderror)
|
||||
_ensure_only_one_capture_fixture(request, 'capfd')
|
||||
if not hasattr(os, 'dup'):
|
||||
pytest.skip("capfd funcarg needs os.dup")
|
||||
request.node._capfuncarg = c = CaptureFixture(FDCapture, request)
|
||||
return c
|
||||
pytest.skip("capfd fixture needs os.dup function which is not available in this system")
|
||||
with _install_capture_fixture_on_item(request, FDCapture) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
class CaptureFixture:
|
||||
@pytest.fixture
|
||||
def capfdbinary(request):
|
||||
"""Enable capturing of write to file descriptors 1 and 2 and make
|
||||
captured output available via ``capfdbinary.readouterr`` method calls
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be
|
||||
``bytes`` objects.
|
||||
"""
|
||||
_ensure_only_one_capture_fixture(request, 'capfdbinary')
|
||||
if not hasattr(os, 'dup'):
|
||||
pytest.skip("capfdbinary fixture needs os.dup function which is not available in this system")
|
||||
with _install_capture_fixture_on_item(request, FDCaptureBinary) as fixture:
|
||||
yield fixture
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _install_capture_fixture_on_item(request, capture_class):
|
||||
"""
|
||||
Context manager which creates a ``CaptureFixture`` instance and "installs" it on
|
||||
the item/node of the given request. Used by ``capsys`` and ``capfd``.
|
||||
|
||||
The CaptureFixture is added as attribute of the item because it needs to accessed
|
||||
by ``CaptureManager`` during its ``pytest_runtest_*`` hooks.
|
||||
"""
|
||||
request.node._capture_fixture = fixture = CaptureFixture(capture_class, request)
|
||||
capmanager = request.config.pluginmanager.getplugin('capturemanager')
|
||||
# need to active this fixture right away in case it is being used by another fixture (setup phase)
|
||||
# if this fixture is being used only by a test function (call phase), then we wouldn't need this
|
||||
# activation, but it doesn't hurt
|
||||
capmanager.activate_fixture(request.node)
|
||||
yield fixture
|
||||
fixture.close()
|
||||
del request.node._capture_fixture
|
||||
|
||||
|
||||
class CaptureFixture(object):
|
||||
def __init__(self, captureclass, request):
|
||||
self.captureclass = captureclass
|
||||
self.request = request
|
||||
@@ -206,12 +295,14 @@ class CaptureFixture:
|
||||
|
||||
@contextlib.contextmanager
|
||||
def disabled(self):
|
||||
self._capture.suspend_capturing()
|
||||
capmanager = self.request.config.pluginmanager.getplugin('capturemanager')
|
||||
capmanager.suspendcapture_item(self.request.node, "call", in_=True)
|
||||
capmanager.suspend_global_capture(item=None, in_=False)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
capmanager.resumecapture()
|
||||
capmanager.resume_global_capture()
|
||||
self._capture.resume_capturing()
|
||||
|
||||
|
||||
def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
||||
@@ -235,12 +326,13 @@ def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
||||
|
||||
class EncodedFile(object):
|
||||
errors = "strict" # possibly needed by py3 code (issue555)
|
||||
|
||||
def __init__(self, buffer, encoding):
|
||||
self.buffer = buffer
|
||||
self.encoding = encoding
|
||||
|
||||
def write(self, obj):
|
||||
if isinstance(obj, unicode):
|
||||
if isinstance(obj, six.text_type):
|
||||
obj = obj.encode(self.encoding, "replace")
|
||||
self.buffer.write(obj)
|
||||
|
||||
@@ -248,10 +340,18 @@ class EncodedFile(object):
|
||||
data = ''.join(linelist)
|
||||
self.write(data)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Ensure that file.name is a string."""
|
||||
return repr(self.buffer)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(object.__getattribute__(self, "buffer"), name)
|
||||
|
||||
|
||||
CaptureResult = collections.namedtuple("CaptureResult", ["out", "err"])
|
||||
|
||||
|
||||
class MultiCapture(object):
|
||||
out = err = in_ = None
|
||||
|
||||
@@ -312,14 +412,19 @@ class MultiCapture(object):
|
||||
|
||||
def readouterr(self):
|
||||
""" return snapshot unicode value of stdout/stderr capturings. """
|
||||
return (self.out.snap() if self.out is not None else "",
|
||||
self.err.snap() if self.err is not None else "")
|
||||
return CaptureResult(self.out.snap() if self.out is not None else "",
|
||||
self.err.snap() if self.err is not None else "")
|
||||
|
||||
class NoCapture:
|
||||
|
||||
class NoCapture(object):
|
||||
__init__ = start = done = suspend = resume = lambda *args: None
|
||||
|
||||
class FDCapture:
|
||||
""" Capture IO to/from a given os-level filedescriptor. """
|
||||
|
||||
class FDCaptureBinary(object):
|
||||
"""Capture IO to/from a given os-level filedescriptor.
|
||||
|
||||
snap() produces `bytes`
|
||||
"""
|
||||
|
||||
def __init__(self, targetfd, tmpfile=None):
|
||||
self.targetfd = targetfd
|
||||
@@ -358,17 +463,11 @@ class FDCapture:
|
||||
self.syscapture.start()
|
||||
|
||||
def snap(self):
|
||||
f = self.tmpfile
|
||||
f.seek(0)
|
||||
res = f.read()
|
||||
if res:
|
||||
enc = getattr(f, "encoding", None)
|
||||
if enc and isinstance(res, bytes):
|
||||
res = py.builtin._totext(res, enc, "replace")
|
||||
f.truncate(0)
|
||||
f.seek(0)
|
||||
return res
|
||||
return ''
|
||||
self.tmpfile.seek(0)
|
||||
res = self.tmpfile.read()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def done(self):
|
||||
""" stop capturing, restore streams, return original capture file,
|
||||
@@ -389,12 +488,25 @@ class FDCapture:
|
||||
|
||||
def writeorg(self, data):
|
||||
""" write to original file descriptor. """
|
||||
if py.builtin._istext(data):
|
||||
data = data.encode("utf8") # XXX use encoding of original stream
|
||||
if isinstance(data, six.text_type):
|
||||
data = data.encode("utf8") # XXX use encoding of original stream
|
||||
os.write(self.targetfd_save, data)
|
||||
|
||||
|
||||
class SysCapture:
|
||||
class FDCapture(FDCaptureBinary):
|
||||
"""Capture IO to/from a given os-level filedescriptor.
|
||||
|
||||
snap() produces text
|
||||
"""
|
||||
def snap(self):
|
||||
res = FDCaptureBinary.snap(self)
|
||||
enc = getattr(self.tmpfile, "encoding", None)
|
||||
if enc and isinstance(res, bytes):
|
||||
res = six.text_type(res, enc, "replace")
|
||||
return res
|
||||
|
||||
|
||||
class SysCapture(object):
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
name = patchsysdict[fd]
|
||||
self._old = getattr(sys, name)
|
||||
@@ -410,10 +522,9 @@ class SysCapture:
|
||||
setattr(sys, self.name, self.tmpfile)
|
||||
|
||||
def snap(self):
|
||||
f = self.tmpfile
|
||||
res = f.getvalue()
|
||||
f.truncate(0)
|
||||
f.seek(0)
|
||||
res = self.tmpfile.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
def done(self):
|
||||
@@ -432,7 +543,15 @@ class SysCapture:
|
||||
self._old.flush()
|
||||
|
||||
|
||||
class DontReadFromInput:
|
||||
class SysCaptureBinary(SysCapture):
|
||||
def snap(self):
|
||||
res = self.tmpfile.buffer.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
|
||||
class DontReadFromInput(object):
|
||||
"""Temporary stub class. Ideally when stdin is accessed, the
|
||||
capturing should be turned off, with possibly all data captured
|
||||
so far sent to the screen. This should be configurable, though,
|
||||
@@ -460,12 +579,30 @@ class DontReadFromInput:
|
||||
|
||||
@property
|
||||
def buffer(self):
|
||||
if sys.version_info >= (3,0):
|
||||
if sys.version_info >= (3, 0):
|
||||
return self
|
||||
else:
|
||||
raise AttributeError('redirected stdin has no attribute buffer')
|
||||
|
||||
|
||||
def _colorama_workaround():
|
||||
"""
|
||||
Ensure colorama is imported so that it attaches to the correct stdio
|
||||
handles on Windows.
|
||||
|
||||
colorama uses the terminal on import time. So if something does the
|
||||
first import of colorama while I/O capture is active, colorama will
|
||||
fail in various ways.
|
||||
"""
|
||||
|
||||
if not sys.platform.startswith('win32'):
|
||||
return
|
||||
try:
|
||||
import colorama # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def _readline_workaround():
|
||||
"""
|
||||
Ensure readline is imported so that it attaches to the correct stdio
|
||||
@@ -491,3 +628,56 @@ def _readline_workaround():
|
||||
import readline # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def _py36_windowsconsoleio_workaround(stream):
|
||||
"""
|
||||
Python 3.6 implemented unicode console handling for Windows. This works
|
||||
by reading/writing to the raw console handle using
|
||||
``{Read,Write}ConsoleW``.
|
||||
|
||||
The problem is that we are going to ``dup2`` over the stdio file
|
||||
descriptors when doing ``FDCapture`` and this will ``CloseHandle`` the
|
||||
handles used by Python to write to the console. Though there is still some
|
||||
weirdness and the console handle seems to only be closed randomly and not
|
||||
on the first call to ``CloseHandle``, or maybe it gets reopened with the
|
||||
same handle value when we suspend capturing.
|
||||
|
||||
The workaround in this case will reopen stdio with a different fd which
|
||||
also means a different handle by replicating the logic in
|
||||
"Py_lifecycle.c:initstdio/create_stdio".
|
||||
|
||||
:param stream: in practice ``sys.stdout`` or ``sys.stderr``, but given
|
||||
here as parameter for unittesting purposes.
|
||||
|
||||
See https://github.com/pytest-dev/py/issues/103
|
||||
"""
|
||||
if not sys.platform.startswith('win32') or sys.version_info[:2] < (3, 6):
|
||||
return
|
||||
|
||||
# bail out if ``stream`` doesn't seem like a proper ``io`` stream (#2666)
|
||||
if not hasattr(stream, 'buffer'):
|
||||
return
|
||||
|
||||
buffered = hasattr(stream.buffer, 'raw')
|
||||
raw_stdout = stream.buffer.raw if buffered else stream.buffer
|
||||
|
||||
if not isinstance(raw_stdout, io._WindowsConsoleIO):
|
||||
return
|
||||
|
||||
def _reopen_stdio(f, mode):
|
||||
if not buffered and mode[0] == 'w':
|
||||
buffering = 0
|
||||
else:
|
||||
buffering = -1
|
||||
|
||||
return io.TextIOWrapper(
|
||||
open(os.dup(f.fileno()), mode, buffering),
|
||||
f.encoding,
|
||||
f.errors,
|
||||
f.newlines,
|
||||
f.line_buffering)
|
||||
|
||||
sys.__stdin__ = sys.stdin = _reopen_stdio(sys.stdin, 'rb')
|
||||
sys.__stdout__ = sys.stdout = _reopen_stdio(sys.stdout, 'wb')
|
||||
sys.__stderr__ = sys.stderr = _reopen_stdio(sys.stderr, 'wb')
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
python version compatibility code
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import sys
|
||||
import inspect
|
||||
import types
|
||||
import re
|
||||
|
||||
import codecs
|
||||
import functools
|
||||
import inspect
|
||||
import re
|
||||
import sys
|
||||
|
||||
import py
|
||||
|
||||
import _pytest
|
||||
|
||||
|
||||
import _pytest
|
||||
from _pytest.outcomes import TEST_OUTCOME
|
||||
|
||||
try:
|
||||
import enum
|
||||
@@ -25,6 +25,12 @@ _PY3 = sys.version_info > (3, 0)
|
||||
_PY2 = not _PY3
|
||||
|
||||
|
||||
if _PY3:
|
||||
from inspect import signature, Parameter as Parameter
|
||||
else:
|
||||
from funcsigs import signature, Parameter as Parameter
|
||||
|
||||
|
||||
NoneType = type(None)
|
||||
NOTSET = object()
|
||||
|
||||
@@ -32,12 +38,10 @@ PY35 = sys.version_info[:2] >= (3, 5)
|
||||
PY36 = sys.version_info[:2] >= (3, 6)
|
||||
MODULE_NOT_FOUND_ERROR = 'ModuleNotFoundError' if PY36 else 'ImportError'
|
||||
|
||||
if hasattr(inspect, 'signature'):
|
||||
def _format_args(func):
|
||||
return str(inspect.signature(func))
|
||||
else:
|
||||
def _format_args(func):
|
||||
return inspect.formatargspec(*inspect.getargspec(func))
|
||||
|
||||
def _format_args(func):
|
||||
return str(signature(func))
|
||||
|
||||
|
||||
isfunction = inspect.isfunction
|
||||
isclass = inspect.isclass
|
||||
@@ -59,16 +63,15 @@ def iscoroutinefunction(func):
|
||||
which in turns also initializes the "logging" module as side-effect (see issue #8).
|
||||
"""
|
||||
return (getattr(func, '_is_coroutine', False) or
|
||||
(hasattr(inspect, 'iscoroutinefunction') and inspect.iscoroutinefunction(func)))
|
||||
(hasattr(inspect, 'iscoroutinefunction') and inspect.iscoroutinefunction(func)))
|
||||
|
||||
|
||||
def getlocation(function, curdir):
|
||||
import inspect
|
||||
fn = py.path.local(inspect.getfile(function))
|
||||
lineno = py.builtin._getcode(function).co_firstlineno
|
||||
if fn.relto(curdir):
|
||||
fn = fn.relto(curdir)
|
||||
return "%s:%d" %(fn, lineno+1)
|
||||
return "%s:%d" % (fn, lineno + 1)
|
||||
|
||||
|
||||
def num_mock_patch_args(function):
|
||||
@@ -76,59 +79,72 @@ def num_mock_patch_args(function):
|
||||
patchings = getattr(function, "patchings", None)
|
||||
if not patchings:
|
||||
return 0
|
||||
mock = sys.modules.get("mock", sys.modules.get("unittest.mock", None))
|
||||
if mock is not None:
|
||||
mock_modules = [sys.modules.get("mock"), sys.modules.get("unittest.mock")]
|
||||
if any(mock_modules):
|
||||
sentinels = [m.DEFAULT for m in mock_modules if m is not None]
|
||||
return len([p for p in patchings
|
||||
if not p.attribute_name and p.new is mock.DEFAULT])
|
||||
if not p.attribute_name and p.new in sentinels])
|
||||
return len(patchings)
|
||||
|
||||
|
||||
def getfuncargnames(function, startindex=None):
|
||||
# XXX merge with main.py's varnames
|
||||
#assert not isclass(function)
|
||||
realfunction = function
|
||||
while hasattr(realfunction, "__wrapped__"):
|
||||
realfunction = realfunction.__wrapped__
|
||||
if startindex is None:
|
||||
startindex = inspect.ismethod(function) and 1 or 0
|
||||
if realfunction != function:
|
||||
startindex += num_mock_patch_args(function)
|
||||
function = realfunction
|
||||
if isinstance(function, functools.partial):
|
||||
argnames = inspect.getargs(_pytest._code.getrawcode(function.func))[0]
|
||||
partial = function
|
||||
argnames = argnames[len(partial.args):]
|
||||
if partial.keywords:
|
||||
for kw in partial.keywords:
|
||||
argnames.remove(kw)
|
||||
else:
|
||||
argnames = inspect.getargs(_pytest._code.getrawcode(function))[0]
|
||||
defaults = getattr(function, 'func_defaults',
|
||||
getattr(function, '__defaults__', None)) or ()
|
||||
numdefaults = len(defaults)
|
||||
if numdefaults:
|
||||
return tuple(argnames[startindex:-numdefaults])
|
||||
return tuple(argnames[startindex:])
|
||||
def getfuncargnames(function, is_method=False, cls=None):
|
||||
"""Returns the names of a function's mandatory arguments.
|
||||
|
||||
This should return the names of all function arguments that:
|
||||
* Aren't bound to an instance or type as in instance or class methods.
|
||||
* Don't have default values.
|
||||
* Aren't bound with functools.partial.
|
||||
* Aren't replaced with mocks.
|
||||
|
||||
The is_method and cls arguments indicate that the function should
|
||||
be treated as a bound method even though it's not unless, only in
|
||||
the case of cls, the function is a static method.
|
||||
|
||||
if sys.version_info[:2] == (2, 6):
|
||||
def isclass(object):
|
||||
""" Return true if the object is a class. Overrides inspect.isclass for
|
||||
python 2.6 because it will return True for objects which always return
|
||||
something on __getattr__ calls (see #1035).
|
||||
Backport of https://hg.python.org/cpython/rev/35bf8f7a8edc
|
||||
"""
|
||||
return isinstance(object, (type, types.ClassType))
|
||||
@RonnyPfannschmidt: This function should be refactored when we
|
||||
revisit fixtures. The fixture mechanism should ask the node for
|
||||
the fixture names, and not try to obtain directly from the
|
||||
function object well after collection has occurred.
|
||||
|
||||
"""
|
||||
# The parameters attribute of a Signature object contains an
|
||||
# ordered mapping of parameter names to Parameter instances. This
|
||||
# creates a tuple of the names of the parameters that don't have
|
||||
# defaults.
|
||||
arg_names = tuple(p.name for p in signature(function).parameters.values()
|
||||
if (p.kind is Parameter.POSITIONAL_OR_KEYWORD or
|
||||
p.kind is Parameter.KEYWORD_ONLY) and
|
||||
p.default is Parameter.empty)
|
||||
# If this function should be treated as a bound method even though
|
||||
# it's passed as an unbound method or function, remove the first
|
||||
# parameter name.
|
||||
if (is_method or
|
||||
(cls and not isinstance(cls.__dict__.get(function.__name__, None),
|
||||
staticmethod))):
|
||||
arg_names = arg_names[1:]
|
||||
# Remove any names that will be replaced with mocks.
|
||||
if hasattr(function, "__wrapped__"):
|
||||
arg_names = arg_names[num_mock_patch_args(function):]
|
||||
return arg_names
|
||||
|
||||
|
||||
if _PY3:
|
||||
import codecs
|
||||
imap = map
|
||||
STRING_TYPES = bytes, str
|
||||
UNICODE_TYPES = str,
|
||||
|
||||
def _escape_strings(val):
|
||||
if PY35:
|
||||
def _bytes_to_ascii(val):
|
||||
return val.decode('ascii', 'backslashreplace')
|
||||
else:
|
||||
def _bytes_to_ascii(val):
|
||||
if val:
|
||||
# source: http://goo.gl/bGsnwC
|
||||
encoded_bytes, _ = codecs.escape_encode(val)
|
||||
return encoded_bytes.decode('ascii')
|
||||
else:
|
||||
# empty bytes crashes codecs.escape_encode (#1087)
|
||||
return ''
|
||||
|
||||
def ascii_escaped(val):
|
||||
"""If val is pure ascii, returns it as a str(). Otherwise, escapes
|
||||
bytes objects into a sequence of escaped bytes:
|
||||
|
||||
@@ -147,22 +163,14 @@ if _PY3:
|
||||
|
||||
"""
|
||||
if isinstance(val, bytes):
|
||||
if val:
|
||||
# source: http://goo.gl/bGsnwC
|
||||
encoded_bytes, _ = codecs.escape_encode(val)
|
||||
return encoded_bytes.decode('ascii')
|
||||
else:
|
||||
# empty bytes crashes codecs.escape_encode (#1087)
|
||||
return ''
|
||||
return _bytes_to_ascii(val)
|
||||
else:
|
||||
return val.encode('unicode_escape').decode('ascii')
|
||||
else:
|
||||
STRING_TYPES = bytes, str, unicode
|
||||
UNICODE_TYPES = unicode,
|
||||
|
||||
from itertools import imap # NOQA
|
||||
|
||||
def _escape_strings(val):
|
||||
def ascii_escaped(val):
|
||||
"""In py2 bytes and str are the same type, so return if it's a bytes
|
||||
object, return it unchanged if it is a full ascii string,
|
||||
otherwise escape it into its binary form.
|
||||
@@ -215,21 +223,20 @@ def getimfunc(func):
|
||||
try:
|
||||
return func.__func__
|
||||
except AttributeError:
|
||||
try:
|
||||
return func.im_func
|
||||
except AttributeError:
|
||||
return func
|
||||
return func
|
||||
|
||||
|
||||
def safe_getattr(object, name, default):
|
||||
""" Like getattr but return default upon any Exception.
|
||||
""" Like getattr but return default upon any Exception or any OutcomeException.
|
||||
|
||||
Attribute access can potentially fail for 'evil' Python objects.
|
||||
See issue #214.
|
||||
It catches OutcomeException because of #2490 (issue #580), new outcomes are derived from BaseException
|
||||
instead of Exception (for more details check #2707)
|
||||
"""
|
||||
try:
|
||||
return getattr(object, name, default)
|
||||
except Exception:
|
||||
except TEST_OUTCOME:
|
||||
return default
|
||||
|
||||
|
||||
@@ -283,7 +290,15 @@ def _setup_collect_fakemodule():
|
||||
|
||||
|
||||
if _PY2:
|
||||
from py.io import TextIO as CaptureIO
|
||||
# Without this the test_dupfile_on_textio will fail, otherwise CaptureIO could directly inherit from StringIO.
|
||||
from py.io import TextIO
|
||||
|
||||
class CaptureIO(TextIO):
|
||||
|
||||
@property
|
||||
def encoding(self):
|
||||
return getattr(self, '_encoding', 'UTF-8')
|
||||
|
||||
else:
|
||||
import io
|
||||
|
||||
@@ -297,6 +312,7 @@ else:
|
||||
def getvalue(self):
|
||||
return self.buffer.getvalue().decode('UTF-8')
|
||||
|
||||
|
||||
class FuncargnamesCompatAttr(object):
|
||||
""" helper class so that Metafunc, Function and FixtureRequest
|
||||
don't need to each define the "funcargnames" compatibility attribute.
|
||||
|
||||
@@ -6,6 +6,7 @@ import traceback
|
||||
import types
|
||||
import warnings
|
||||
|
||||
import six
|
||||
import py
|
||||
# DON't import pytest here because it causes import cycle troubles
|
||||
import sys
|
||||
@@ -13,7 +14,7 @@ import os
|
||||
import _pytest._code
|
||||
import _pytest.hookspec # the extension point definitions
|
||||
import _pytest.assertion
|
||||
from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker
|
||||
from pluggy import PluginManager, HookimplMarker, HookspecMarker
|
||||
from _pytest.compat import safe_str
|
||||
|
||||
hookimpl = HookimplMarker("pytest")
|
||||
@@ -59,11 +60,13 @@ def main(args=None, plugins=None):
|
||||
finally:
|
||||
config._ensure_unconfigure()
|
||||
except UsageError as e:
|
||||
tw = py.io.TerminalWriter(sys.stderr)
|
||||
for msg in e.args:
|
||||
sys.stderr.write("ERROR: %s\n" %(msg,))
|
||||
tw.line("ERROR: {}\n".format(msg), red=True)
|
||||
return 4
|
||||
|
||||
class cmdline: # compatibility namespace
|
||||
|
||||
class cmdline(object): # compatibility namespace
|
||||
main = staticmethod(main)
|
||||
|
||||
|
||||
@@ -71,6 +74,12 @@ class UsageError(Exception):
|
||||
""" error in pytest usage or invocation"""
|
||||
|
||||
|
||||
class PrintHelp(Exception):
|
||||
"""Raised when pytest should print it's help to skip the rest of the
|
||||
argument parsing and validation."""
|
||||
pass
|
||||
|
||||
|
||||
def filename_arg(path, optname):
|
||||
""" Argparse type validator for filename arguments.
|
||||
|
||||
@@ -93,26 +102,18 @@ def directory_arg(path, optname):
|
||||
return path
|
||||
|
||||
|
||||
_preinit = []
|
||||
|
||||
default_plugins = (
|
||||
"mark main terminal runner python fixtures debugging unittest capture skipping "
|
||||
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion "
|
||||
"junitxml resultlog doctest cacheprovider freeze_support "
|
||||
"setuponly setupplan warnings").split()
|
||||
"mark main terminal runner python fixtures debugging unittest capture skipping "
|
||||
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion "
|
||||
"junitxml resultlog doctest cacheprovider freeze_support "
|
||||
"setuponly setupplan warnings logging").split()
|
||||
|
||||
|
||||
builtin_plugins = set(default_plugins)
|
||||
builtin_plugins.add("pytester")
|
||||
|
||||
|
||||
def _preloadplugins():
|
||||
assert not _preinit
|
||||
_preinit.append(get_config())
|
||||
|
||||
def get_config():
|
||||
if _preinit:
|
||||
return _preinit.pop(0)
|
||||
# subsequent calls to main will create a fresh instance
|
||||
pluginmanager = PytestPluginManager()
|
||||
config = Config(pluginmanager)
|
||||
@@ -120,6 +121,7 @@ def get_config():
|
||||
pluginmanager.import_plugin(spec)
|
||||
return config
|
||||
|
||||
|
||||
def get_plugin_manager():
|
||||
"""
|
||||
Obtain a new instance of the
|
||||
@@ -131,6 +133,7 @@ def get_plugin_manager():
|
||||
"""
|
||||
return get_config().pluginmanager
|
||||
|
||||
|
||||
def _prepareconfig(args=None, plugins=None):
|
||||
warning = None
|
||||
if args is None:
|
||||
@@ -148,14 +151,14 @@ def _prepareconfig(args=None, plugins=None):
|
||||
try:
|
||||
if plugins:
|
||||
for plugin in plugins:
|
||||
if isinstance(plugin, py.builtin._basestring):
|
||||
if isinstance(plugin, six.string_types):
|
||||
pluginmanager.consider_pluginarg(plugin)
|
||||
else:
|
||||
pluginmanager.register(plugin)
|
||||
if warning:
|
||||
config.warn('C1', warning)
|
||||
return pluginmanager.hook.pytest_cmdline_parse(
|
||||
pluginmanager=pluginmanager, args=args)
|
||||
pluginmanager=pluginmanager, args=args)
|
||||
except BaseException:
|
||||
config._ensure_unconfigure()
|
||||
raise
|
||||
@@ -163,13 +166,14 @@ def _prepareconfig(args=None, plugins=None):
|
||||
|
||||
class PytestPluginManager(PluginManager):
|
||||
"""
|
||||
Overwrites :py:class:`pluggy.PluginManager` to add pytest-specific
|
||||
Overwrites :py:class:`pluggy.PluginManager <pluggy.PluginManager>` to add pytest-specific
|
||||
functionality:
|
||||
|
||||
* loading plugins from the command line, ``PYTEST_PLUGIN`` env variable and
|
||||
``pytest_plugins`` global variables found in plugins being loaded;
|
||||
* ``conftest.py`` loading during start-up;
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super(PytestPluginManager, self).__init__("pytest", implprefix="pytest_")
|
||||
self._conftest_plugins = set()
|
||||
@@ -200,7 +204,8 @@ class PytestPluginManager(PluginManager):
|
||||
"""
|
||||
.. deprecated:: 2.8
|
||||
|
||||
Use :py:meth:`pluggy.PluginManager.add_hookspecs` instead.
|
||||
Use :py:meth:`pluggy.PluginManager.add_hookspecs <PluginManager.add_hookspecs>`
|
||||
instead.
|
||||
"""
|
||||
warning = dict(code="I2",
|
||||
fslocation=_pytest._code.getfslineno(sys._getframe(1)),
|
||||
@@ -229,7 +234,7 @@ class PytestPluginManager(PluginManager):
|
||||
|
||||
def parse_hookspec_opts(self, module_or_class, name):
|
||||
opts = super(PytestPluginManager, self).parse_hookspec_opts(
|
||||
module_or_class, name)
|
||||
module_or_class, name)
|
||||
if opts is None:
|
||||
method = getattr(module_or_class, name)
|
||||
if name.startswith("pytest_"):
|
||||
@@ -237,22 +242,16 @@ class PytestPluginManager(PluginManager):
|
||||
"historic": hasattr(method, "historic")}
|
||||
return opts
|
||||
|
||||
def _verify_hook(self, hook, hookmethod):
|
||||
super(PytestPluginManager, self)._verify_hook(hook, hookmethod)
|
||||
if "__multicall__" in hookmethod.argnames:
|
||||
fslineno = _pytest._code.getfslineno(hookmethod.function)
|
||||
warning = dict(code="I1",
|
||||
fslocation=fslineno,
|
||||
nodeid=None,
|
||||
message="%r hook uses deprecated __multicall__ "
|
||||
"argument" % (hook.name))
|
||||
self._warn(warning)
|
||||
|
||||
def register(self, plugin, name=None):
|
||||
if name in ['pytest_catchlog', 'pytest_capturelog']:
|
||||
self._warn('{0} plugin has been merged into the core, '
|
||||
'please remove it from your requirements.'.format(
|
||||
name.replace('_', '-')))
|
||||
return
|
||||
ret = super(PytestPluginManager, self).register(plugin, name)
|
||||
if ret:
|
||||
self.hook.pytest_plugin_registered.call_historic(
|
||||
kwargs=dict(plugin=plugin, manager=self))
|
||||
kwargs=dict(plugin=plugin, manager=self))
|
||||
|
||||
if isinstance(plugin, types.ModuleType):
|
||||
self.consider_module(plugin)
|
||||
@@ -270,11 +269,11 @@ class PytestPluginManager(PluginManager):
|
||||
# XXX now that the pluginmanager exposes hookimpl(tryfirst...)
|
||||
# we should remove tryfirst/trylast as markers
|
||||
config.addinivalue_line("markers",
|
||||
"tryfirst: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it first/as early as possible.")
|
||||
"tryfirst: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it first/as early as possible.")
|
||||
config.addinivalue_line("markers",
|
||||
"trylast: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it last/as late as possible.")
|
||||
"trylast: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it last/as late as possible.")
|
||||
|
||||
def _warn(self, message):
|
||||
kwargs = message if isinstance(message, dict) else {
|
||||
@@ -298,7 +297,7 @@ class PytestPluginManager(PluginManager):
|
||||
"""
|
||||
current = py.path.local()
|
||||
self._confcutdir = current.join(namespace.confcutdir, abs=True) \
|
||||
if namespace.confcutdir else None
|
||||
if namespace.confcutdir else None
|
||||
self._noconftest = namespace.noconftest
|
||||
testpaths = namespace.file_or_dir
|
||||
foundanchor = False
|
||||
@@ -309,7 +308,7 @@ class PytestPluginManager(PluginManager):
|
||||
if i != -1:
|
||||
path = path[:i]
|
||||
anchor = current.join(path, abs=1)
|
||||
if exists(anchor): # we found some file object
|
||||
if exists(anchor): # we found some file object
|
||||
self._try_load_conftest(anchor)
|
||||
foundanchor = True
|
||||
if not foundanchor:
|
||||
@@ -376,7 +375,7 @@ class PytestPluginManager(PluginManager):
|
||||
if path and path.relto(dirpath) or path == dirpath:
|
||||
assert mod not in mods
|
||||
mods.append(mod)
|
||||
self.trace("loaded conftestmodule %r" %(mod))
|
||||
self.trace("loaded conftestmodule %r" % (mod))
|
||||
self.consider_conftest(mod)
|
||||
return mod
|
||||
|
||||
@@ -386,7 +385,7 @@ class PytestPluginManager(PluginManager):
|
||||
#
|
||||
|
||||
def consider_preparse(self, args):
|
||||
for opt1,opt2 in zip(args, args[1:]):
|
||||
for opt1, opt2 in zip(args, args[1:]):
|
||||
if opt1 == "-p":
|
||||
self.consider_pluginarg(opt2)
|
||||
|
||||
@@ -418,9 +417,9 @@ class PytestPluginManager(PluginManager):
|
||||
# "terminal" or "capture". Those plugins are registered under their
|
||||
# basename for historic purposes but must be imported with the
|
||||
# _pytest prefix.
|
||||
assert isinstance(modname, (py.builtin.text, str)), "module name as text required, got %r" % modname
|
||||
assert isinstance(modname, (six.text_type, str)), "module name as text required, got %r" % modname
|
||||
modname = str(modname)
|
||||
if self.get_plugin(modname) is not None:
|
||||
if self.is_blocked(modname) or self.get_plugin(modname) is not None:
|
||||
return
|
||||
if modname in builtin_plugins:
|
||||
importspec = "_pytest." + modname
|
||||
@@ -430,17 +429,17 @@ class PytestPluginManager(PluginManager):
|
||||
try:
|
||||
__import__(importspec)
|
||||
except ImportError as e:
|
||||
new_exc = ImportError('Error importing plugin "%s": %s' % (modname, safe_str(e.args[0])))
|
||||
# copy over name and path attributes
|
||||
for attr in ('name', 'path'):
|
||||
if hasattr(e, attr):
|
||||
setattr(new_exc, attr, getattr(e, attr))
|
||||
raise new_exc
|
||||
new_exc_type = ImportError
|
||||
new_exc_message = 'Error importing plugin "%s": %s' % (modname, safe_str(e.args[0]))
|
||||
new_exc = new_exc_type(new_exc_message)
|
||||
|
||||
six.reraise(new_exc_type, new_exc, sys.exc_info()[2])
|
||||
|
||||
except Exception as e:
|
||||
import pytest
|
||||
if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception):
|
||||
raise
|
||||
self._warn("skipped plugin %r: %s" %((modname, e.msg)))
|
||||
self._warn("skipped plugin %r: %s" % ((modname, e.msg)))
|
||||
else:
|
||||
mod = sys.modules[importspec]
|
||||
self.register(mod, modname)
|
||||
@@ -464,7 +463,7 @@ def _get_plugin_specs_as_list(specs):
|
||||
return []
|
||||
|
||||
|
||||
class Parser:
|
||||
class Parser(object):
|
||||
""" Parser for command line arguments and ini-file values.
|
||||
|
||||
:ivar extra_info: dict of generic param -> value to display in case
|
||||
@@ -505,7 +504,7 @@ class Parser:
|
||||
for i, grp in enumerate(self._groups):
|
||||
if grp.name == after:
|
||||
break
|
||||
self._groups.insert(i+1, group)
|
||||
self._groups.insert(i + 1, group)
|
||||
return group
|
||||
|
||||
def addoption(self, *opts, **attrs):
|
||||
@@ -543,7 +542,7 @@ class Parser:
|
||||
a = option.attrs()
|
||||
arggroup.add_argument(*n, **a)
|
||||
# bash like autocompletion for dirs (appending '/')
|
||||
optparser.add_argument(FILE_OR_DIR, nargs='*').completer=filescompleter
|
||||
optparser.add_argument(FILE_OR_DIR, nargs='*').completer = filescompleter
|
||||
return optparser
|
||||
|
||||
def parse_setoption(self, args, option, namespace=None):
|
||||
@@ -599,7 +598,7 @@ class ArgumentError(Exception):
|
||||
return self.msg
|
||||
|
||||
|
||||
class Argument:
|
||||
class Argument(object):
|
||||
"""class that mimics the necessary behaviour of optparse.Option
|
||||
|
||||
its currently a least effort implementation
|
||||
@@ -631,7 +630,7 @@ class Argument:
|
||||
pass
|
||||
else:
|
||||
# this might raise a keyerror as well, don't want to catch that
|
||||
if isinstance(typ, py.builtin._basestring):
|
||||
if isinstance(typ, six.string_types):
|
||||
if typ == 'choice':
|
||||
warnings.warn(
|
||||
'type argument to addoption() is a string %r.'
|
||||
@@ -687,7 +686,7 @@ class Argument:
|
||||
if self._attrs.get('help'):
|
||||
a = self._attrs['help']
|
||||
a = a.replace('%default', '%(default)s')
|
||||
#a = a.replace('%prog', '%(prog)s')
|
||||
# a = a.replace('%prog', '%(prog)s')
|
||||
self._attrs['help'] = a
|
||||
return self._attrs
|
||||
|
||||
@@ -729,7 +728,7 @@ class Argument:
|
||||
return 'Argument({0})'.format(', '.join(args))
|
||||
|
||||
|
||||
class OptionGroup:
|
||||
class OptionGroup(object):
|
||||
def __init__(self, name, description="", parser=None):
|
||||
self.name = name
|
||||
self.description = description
|
||||
@@ -771,7 +770,7 @@ class MyOptionParser(argparse.ArgumentParser):
|
||||
extra_info = {}
|
||||
self._parser = parser
|
||||
argparse.ArgumentParser.__init__(self, usage=parser._usage,
|
||||
add_help=False, formatter_class=DropShorterLongHelpFormatter)
|
||||
add_help=False, formatter_class=DropShorterLongHelpFormatter)
|
||||
# extra_info is a dict of (param -> value) to display if there's
|
||||
# an usage error to provide more contextual information to the user
|
||||
self.extra_info = extra_info
|
||||
@@ -799,9 +798,10 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
- shortcut if there are only two options and one of them is a short one
|
||||
- cache result on action object as this is called at least 2 times
|
||||
"""
|
||||
|
||||
def _format_action_invocation(self, action):
|
||||
orgstr = argparse.HelpFormatter._format_action_invocation(self, action)
|
||||
if orgstr and orgstr[0] != '-': # only optional arguments
|
||||
if orgstr and orgstr[0] != '-': # only optional arguments
|
||||
return orgstr
|
||||
res = getattr(action, '_formatted_action_invocation', None)
|
||||
if res:
|
||||
@@ -812,7 +812,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
action._formatted_action_invocation = orgstr
|
||||
return orgstr
|
||||
return_list = []
|
||||
option_map = getattr(action, 'map_long_option', {})
|
||||
option_map = getattr(action, 'map_long_option', {})
|
||||
if option_map is None:
|
||||
option_map = {}
|
||||
short_long = {}
|
||||
@@ -830,7 +830,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
short_long[shortened] = xxoption
|
||||
# now short_long has been filled out to the longest with dashes
|
||||
# **and** we keep the right option ordering from add_argument
|
||||
for option in options: #
|
||||
for option in options:
|
||||
if len(option) == 2 or option[2] == ' ':
|
||||
return_list.append(option)
|
||||
if option[2:] == short_long.get(option.replace('-', '')):
|
||||
@@ -839,23 +839,27 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
return action._formatted_action_invocation
|
||||
|
||||
|
||||
|
||||
def _ensure_removed_sysmodule(modname):
|
||||
try:
|
||||
del sys.modules[modname]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
class CmdOptions(object):
|
||||
""" holds cmdline options as attributes."""
|
||||
|
||||
def __init__(self, values=()):
|
||||
self.__dict__.update(values)
|
||||
|
||||
def __repr__(self):
|
||||
return "<CmdOptions %r>" %(self.__dict__,)
|
||||
return "<CmdOptions %r>" % (self.__dict__,)
|
||||
|
||||
def copy(self):
|
||||
return CmdOptions(self.__dict__)
|
||||
|
||||
class Notset:
|
||||
|
||||
class Notset(object):
|
||||
def __repr__(self):
|
||||
return "<NOTSET>"
|
||||
|
||||
@@ -864,6 +868,18 @@ notset = Notset()
|
||||
FILE_OR_DIR = 'file_or_dir'
|
||||
|
||||
|
||||
def _iter_rewritable_modules(package_files):
|
||||
for fn in package_files:
|
||||
is_simple_module = '/' not in fn and fn.endswith('.py')
|
||||
is_package = fn.count('/') == 1 and fn.endswith('__init__.py')
|
||||
if is_simple_module:
|
||||
module_name, _ = os.path.splitext(fn)
|
||||
yield module_name
|
||||
elif is_package:
|
||||
package_name = os.path.dirname(fn)
|
||||
yield package_name
|
||||
|
||||
|
||||
class Config(object):
|
||||
""" access to configuration values, pluginmanager and plugin hooks. """
|
||||
|
||||
@@ -934,14 +950,14 @@ class Config(object):
|
||||
else:
|
||||
style = "native"
|
||||
excrepr = excinfo.getrepr(funcargs=True,
|
||||
showlocals=getattr(option, 'showlocals', False),
|
||||
style=style,
|
||||
)
|
||||
showlocals=getattr(option, 'showlocals', False),
|
||||
style=style,
|
||||
)
|
||||
res = self.hook.pytest_internalerror(excrepr=excrepr,
|
||||
excinfo=excinfo)
|
||||
if not py.builtin.any(res):
|
||||
if not any(res):
|
||||
for line in str(excrepr).split("\n"):
|
||||
sys.stderr.write("INTERNALERROR> %s\n" %line)
|
||||
sys.stderr.write("INTERNALERROR> %s\n" % line)
|
||||
sys.stderr.flush()
|
||||
|
||||
def cwd_relative_nodeid(self, nodeid):
|
||||
@@ -985,10 +1001,10 @@ class Config(object):
|
||||
self._override_ini = ns.override_ini or ()
|
||||
|
||||
def _consider_importhook(self, args):
|
||||
"""Install the PEP 302 import hook if using assertion re-writing.
|
||||
"""Install the PEP 302 import hook if using assertion rewriting.
|
||||
|
||||
Needs to parse the --assert=<mode> option from the commandline
|
||||
and find all the installed plugins to mark them for re-writing
|
||||
and find all the installed plugins to mark them for rewriting
|
||||
by the importhook.
|
||||
"""
|
||||
ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
|
||||
@@ -1024,15 +1040,8 @@ class Config(object):
|
||||
for entry in entrypoint.dist._get_metadata(metadata)
|
||||
)
|
||||
|
||||
for fn in package_files:
|
||||
is_simple_module = os.sep not in fn and fn.endswith('.py')
|
||||
is_package = fn.count(os.sep) == 1 and fn.endswith('__init__.py')
|
||||
if is_simple_module:
|
||||
module_name, ext = os.path.splitext(fn)
|
||||
hook.mark_rewrite(module_name)
|
||||
elif is_package:
|
||||
package_name = os.path.dirname(fn)
|
||||
hook.mark_rewrite(package_name)
|
||||
for name in _iter_rewritable_modules(package_files):
|
||||
hook.mark_rewrite(name)
|
||||
|
||||
def _warn_about_missing_assertion(self, mode):
|
||||
try:
|
||||
@@ -1052,9 +1061,10 @@ class Config(object):
|
||||
"(are you using python -O?)\n")
|
||||
|
||||
def _preparse(self, args, addopts=True):
|
||||
self._initini(args)
|
||||
if addopts:
|
||||
args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args
|
||||
self._initini(args)
|
||||
if addopts:
|
||||
args[:] = self.getini("addopts") + args
|
||||
self._checkversion()
|
||||
self._consider_importhook(args)
|
||||
@@ -1062,13 +1072,12 @@ class Config(object):
|
||||
self.pluginmanager.load_setuptools_entrypoints('pytest11')
|
||||
self.pluginmanager.consider_env()
|
||||
self.known_args_namespace = ns = self._parser.parse_known_args(args, namespace=self.option.copy())
|
||||
confcutdir = self.known_args_namespace.confcutdir
|
||||
if self.known_args_namespace.confcutdir is None and self.inifile:
|
||||
confcutdir = py.path.local(self.inifile).dirname
|
||||
self.known_args_namespace.confcutdir = confcutdir
|
||||
try:
|
||||
self.hook.pytest_load_initial_conftests(early_config=self,
|
||||
args=args, parser=self._parser)
|
||||
args=args, parser=self._parser)
|
||||
except ConftestImportFailure:
|
||||
e = sys.exc_info()[1]
|
||||
if ns.help or ns.version:
|
||||
@@ -1086,28 +1095,32 @@ class Config(object):
|
||||
myver = pytest.__version__.split(".")
|
||||
if myver < ver:
|
||||
raise pytest.UsageError(
|
||||
"%s:%d: requires pytest-%s, actual pytest-%s'" %(
|
||||
self.inicfg.config.path, self.inicfg.lineof('minversion'),
|
||||
minver, pytest.__version__))
|
||||
"%s:%d: requires pytest-%s, actual pytest-%s'" % (
|
||||
self.inicfg.config.path, self.inicfg.lineof('minversion'),
|
||||
minver, pytest.__version__))
|
||||
|
||||
def parse(self, args, addopts=True):
|
||||
# parse given cmdline arguments into this config object.
|
||||
assert not hasattr(self, 'args'), (
|
||||
"can only parse cmdline args at most once per Config object")
|
||||
"can only parse cmdline args at most once per Config object")
|
||||
self._origargs = args
|
||||
self.hook.pytest_addhooks.call_historic(
|
||||
kwargs=dict(pluginmanager=self.pluginmanager))
|
||||
kwargs=dict(pluginmanager=self.pluginmanager))
|
||||
self._preparse(args, addopts=addopts)
|
||||
# XXX deprecated hook:
|
||||
self.hook.pytest_cmdline_preparse(config=self, args=args)
|
||||
args = self._parser.parse_setoption(args, self.option, namespace=self.option)
|
||||
if not args:
|
||||
cwd = os.getcwd()
|
||||
if cwd == self.rootdir:
|
||||
args = self.getini('testpaths')
|
||||
self._parser.after_preparse = True
|
||||
try:
|
||||
args = self._parser.parse_setoption(args, self.option, namespace=self.option)
|
||||
if not args:
|
||||
args = [cwd]
|
||||
self.args = args
|
||||
cwd = os.getcwd()
|
||||
if cwd == self.rootdir:
|
||||
args = self.getini('testpaths')
|
||||
if not args:
|
||||
args = [cwd]
|
||||
self.args = args
|
||||
except PrintHelp:
|
||||
pass
|
||||
|
||||
def addinivalue_line(self, name, line):
|
||||
""" add a line to an ini-file option. The option must have been
|
||||
@@ -1115,12 +1128,12 @@ class Config(object):
|
||||
the first line in its value. """
|
||||
x = self.getini(name)
|
||||
assert isinstance(x, list)
|
||||
x.append(line) # modifies the cached list inline
|
||||
x.append(line) # modifies the cached list inline
|
||||
|
||||
def getini(self, name):
|
||||
""" return configuration value from an :ref:`ini file <inifiles>`. If the
|
||||
specified name hasn't been registered through a prior
|
||||
:py:func:`parser.addini <pytest.config.Parser.addini>`
|
||||
:py:func:`parser.addini <_pytest.config.Parser.addini>`
|
||||
call (usually from a plugin), a ValueError is raised. """
|
||||
try:
|
||||
return self._inicache[name]
|
||||
@@ -1132,7 +1145,7 @@ class Config(object):
|
||||
try:
|
||||
description, type, default = self._parser._inidict[name]
|
||||
except KeyError:
|
||||
raise ValueError("unknown configuration value: %r" %(name,))
|
||||
raise ValueError("unknown configuration value: %r" % (name,))
|
||||
value = self._get_override_ini_value(name)
|
||||
if value is None:
|
||||
try:
|
||||
@@ -1145,10 +1158,10 @@ class Config(object):
|
||||
return []
|
||||
if type == "pathlist":
|
||||
dp = py.path.local(self.inicfg.config.path).dirpath()
|
||||
l = []
|
||||
values = []
|
||||
for relpath in shlex.split(value):
|
||||
l.append(dp.join(relpath, abs=True))
|
||||
return l
|
||||
values.append(dp.join(relpath, abs=True))
|
||||
return values
|
||||
elif type == "args":
|
||||
return shlex.split(value)
|
||||
elif type == "linelist":
|
||||
@@ -1165,26 +1178,25 @@ class Config(object):
|
||||
except KeyError:
|
||||
return None
|
||||
modpath = py.path.local(mod.__file__).dirpath()
|
||||
l = []
|
||||
values = []
|
||||
for relroot in relroots:
|
||||
if not isinstance(relroot, py.path.local):
|
||||
relroot = relroot.replace("/", py.path.local.sep)
|
||||
relroot = modpath.join(relroot, abs=True)
|
||||
l.append(relroot)
|
||||
return l
|
||||
values.append(relroot)
|
||||
return values
|
||||
|
||||
def _get_override_ini_value(self, name):
|
||||
value = None
|
||||
# override_ini is a list of list, to support both -o foo1=bar1 foo2=bar2 and
|
||||
# and -o foo1=bar1 -o foo2=bar2 options
|
||||
# always use the last item if multiple value set for same ini-name,
|
||||
# override_ini is a list of "ini=value" options
|
||||
# always use the last item if multiple values are set for same ini-name,
|
||||
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2
|
||||
for ini_config_list in self._override_ini:
|
||||
for ini_config in ini_config_list:
|
||||
try:
|
||||
(key, user_ini_value) = ini_config.split("=", 1)
|
||||
except ValueError:
|
||||
raise UsageError("-o/--override-ini expects option=value style.")
|
||||
for ini_config in self._override_ini:
|
||||
try:
|
||||
key, user_ini_value = ini_config.split("=", 1)
|
||||
except ValueError:
|
||||
raise UsageError("-o/--override-ini expects option=value style.")
|
||||
else:
|
||||
if key == name:
|
||||
value = user_ini_value
|
||||
return value
|
||||
@@ -1209,7 +1221,7 @@ class Config(object):
|
||||
return default
|
||||
if skip:
|
||||
import pytest
|
||||
pytest.skip("no %r option found" %(name,))
|
||||
pytest.skip("no %r option found" % (name,))
|
||||
raise ValueError("no option named %r" % (name,))
|
||||
|
||||
def getvalue(self, name, path=None):
|
||||
@@ -1220,12 +1232,14 @@ class Config(object):
|
||||
""" (deprecated, use getoption(skip=True)) """
|
||||
return self.getoption(name, skip=True)
|
||||
|
||||
|
||||
def exists(path, ignore=EnvironmentError):
|
||||
try:
|
||||
return path.check()
|
||||
except ignore:
|
||||
return False
|
||||
|
||||
|
||||
def getcfg(args, warnfunc=None):
|
||||
"""
|
||||
Search the list of arguments for a valid ini-file for pytest,
|
||||
@@ -1313,10 +1327,14 @@ def determine_setup(inifile, args, warnfunc=None):
|
||||
dirs = get_dirs_from_args(args)
|
||||
if inifile:
|
||||
iniconfig = py.iniconfig.IniConfig(inifile)
|
||||
try:
|
||||
inicfg = iniconfig["pytest"]
|
||||
except KeyError:
|
||||
inicfg = None
|
||||
is_cfg_file = str(inifile).endswith('.cfg')
|
||||
sections = ['tool:pytest', 'pytest'] if is_cfg_file else ['pytest']
|
||||
for section in sections:
|
||||
try:
|
||||
inicfg = iniconfig[section]
|
||||
break
|
||||
except KeyError:
|
||||
inicfg = None
|
||||
rootdir = get_common_ancestor(dirs)
|
||||
else:
|
||||
ancestor = get_common_ancestor(dirs)
|
||||
@@ -1329,7 +1347,7 @@ def determine_setup(inifile, args, warnfunc=None):
|
||||
rootdir, inifile, inicfg = getcfg(dirs, warnfunc=warnfunc)
|
||||
if rootdir is None:
|
||||
rootdir = get_common_ancestor([py.path.local(), ancestor])
|
||||
is_fs_root = os.path.splitdrive(str(rootdir))[1] == os.sep
|
||||
is_fs_root = os.path.splitdrive(str(rootdir))[1] == '/'
|
||||
if is_fs_root:
|
||||
rootdir = ancestor
|
||||
return rootdir, inifile, inicfg or {}
|
||||
@@ -1351,7 +1369,7 @@ def setns(obj, dic):
|
||||
else:
|
||||
setattr(obj, name, value)
|
||||
obj.__all__.append(name)
|
||||
#if obj != pytest:
|
||||
# if obj != pytest:
|
||||
# pytest.__all__.append(name)
|
||||
setattr(pytest, name, value)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import pdb
|
||||
import sys
|
||||
|
||||
from doctest import UnexpectedException
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -40,7 +40,8 @@ def pytest_configure(config):
|
||||
pytestPDB._pdb_cls = pdb_cls
|
||||
config._cleanup.append(fin)
|
||||
|
||||
class pytestPDB:
|
||||
|
||||
class pytestPDB(object):
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
_pluginmanager = None
|
||||
_config = None
|
||||
@@ -54,7 +55,7 @@ class pytestPDB:
|
||||
if cls._pluginmanager is not None:
|
||||
capman = cls._pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
capman.suspendcapture(in_=True)
|
||||
capman.suspend_global_capture(in_=True)
|
||||
tw = _pytest.config.create_terminal_writer(cls._config)
|
||||
tw.line()
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
@@ -62,11 +63,11 @@ class pytestPDB:
|
||||
cls._pdb_cls().set_trace(frame)
|
||||
|
||||
|
||||
class PdbInvoke:
|
||||
class PdbInvoke(object):
|
||||
def pytest_exception_interact(self, node, call, report):
|
||||
capman = node.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
out, err = capman.suspendcapture(in_=True)
|
||||
out, err = capman.suspend_global_capture(in_=True)
|
||||
sys.stdout.write(out)
|
||||
sys.stdout.write(err)
|
||||
_enter_pdb(node, call.excinfo, report)
|
||||
@@ -85,6 +86,17 @@ def _enter_pdb(node, excinfo, rep):
|
||||
# for not completely clear reasons.
|
||||
tw = node.config.pluginmanager.getplugin("terminalreporter")._tw
|
||||
tw.line()
|
||||
|
||||
captured_stdout = rep.capstdout
|
||||
if len(captured_stdout) > 0:
|
||||
tw.sep(">", "captured stdout")
|
||||
tw.line(captured_stdout)
|
||||
|
||||
captured_stderr = rep.capstderr
|
||||
if len(captured_stderr) > 0:
|
||||
tw.sep(">", "captured stderr")
|
||||
tw.line(captured_stderr)
|
||||
|
||||
tw.sep(">", "traceback")
|
||||
rep.toterminal(tw)
|
||||
tw.sep(">", "entering PDB")
|
||||
@@ -95,10 +107,9 @@ def _enter_pdb(node, excinfo, rep):
|
||||
|
||||
|
||||
def _postmortem_traceback(excinfo):
|
||||
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
from doctest import UnexpectedException
|
||||
if isinstance(excinfo.value, UnexpectedException):
|
||||
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
return excinfo.value.exc_info[2]
|
||||
else:
|
||||
return excinfo._excinfo[2]
|
||||
|
||||
@@ -7,8 +7,13 @@ be removed when the time comes.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
|
||||
class RemovedInPytest4Warning(DeprecationWarning):
|
||||
"""warning class for features removed in pytest 4.0"""
|
||||
|
||||
|
||||
MAIN_STR_ARGS = 'passing a string to pytest.main() is deprecated, ' \
|
||||
'pass a list of arguments instead.'
|
||||
'pass a list of arguments instead.'
|
||||
|
||||
YIELD_TESTS = 'yield tests are deprecated, and scheduled to be removed in pytest 4.0'
|
||||
|
||||
@@ -21,4 +26,27 @@ SETUP_CFG_PYTEST = '[pytest] section in setup.cfg files is deprecated, use [tool
|
||||
|
||||
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue"
|
||||
|
||||
RESULT_LOG = '--result-log is deprecated and scheduled for removal in pytest 4.0'
|
||||
RESULT_LOG = (
|
||||
'--result-log is deprecated and scheduled for removal in pytest 4.0.\n'
|
||||
'See https://docs.pytest.org/en/latest/usage.html#creating-resultlog-format-files for more information.'
|
||||
)
|
||||
|
||||
MARK_INFO_ATTRIBUTE = RemovedInPytest4Warning(
|
||||
"MarkInfo objects are deprecated as they contain the merged marks"
|
||||
)
|
||||
|
||||
MARK_PARAMETERSET_UNPACKING = RemovedInPytest4Warning(
|
||||
"Applying marks directly to parameters is deprecated,"
|
||||
" please use pytest.param(..., marks=...) instead.\n"
|
||||
"For more details, see: https://docs.pytest.org/en/latest/parametrize.html"
|
||||
)
|
||||
|
||||
COLLECTOR_MAKEITEM = RemovedInPytest4Warning(
|
||||
"pycollector makeitem was removed "
|
||||
"as it is an accidentially leaked internal api"
|
||||
)
|
||||
|
||||
METAFUNC_ADD_CALL = (
|
||||
"Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.\n"
|
||||
"Please use Metafunc.parametrize instead."
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import traceback
|
||||
import sys
|
||||
import platform
|
||||
|
||||
import pytest
|
||||
from _pytest._code.code import ExceptionInfo, ReprFileLocation, TerminalRepr
|
||||
@@ -22,39 +24,47 @@ DOCTEST_REPORT_CHOICES = (
|
||||
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
|
||||
)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addini('doctest_optionflags', 'option flags for doctests',
|
||||
type="args", default=["ELLIPSIS"])
|
||||
type="args", default=["ELLIPSIS"])
|
||||
parser.addini("doctest_encoding", 'encoding used for doctest files', default="utf-8")
|
||||
group = parser.getgroup("collect")
|
||||
group.addoption("--doctest-modules",
|
||||
action="store_true", default=False,
|
||||
help="run doctests in all .py modules",
|
||||
dest="doctestmodules")
|
||||
action="store_true", default=False,
|
||||
help="run doctests in all .py modules",
|
||||
dest="doctestmodules")
|
||||
group.addoption("--doctest-report",
|
||||
type=str.lower, default="udiff",
|
||||
help="choose another output format for diffs on doctest failure",
|
||||
choices=DOCTEST_REPORT_CHOICES,
|
||||
dest="doctestreport")
|
||||
type=str.lower, default="udiff",
|
||||
help="choose another output format for diffs on doctest failure",
|
||||
choices=DOCTEST_REPORT_CHOICES,
|
||||
dest="doctestreport")
|
||||
group.addoption("--doctest-glob",
|
||||
action="append", default=[], metavar="pat",
|
||||
help="doctests file matching pattern, default: test*.txt",
|
||||
dest="doctestglob")
|
||||
action="append", default=[], metavar="pat",
|
||||
help="doctests file matching pattern, default: test*.txt",
|
||||
dest="doctestglob")
|
||||
group.addoption("--doctest-ignore-import-errors",
|
||||
action="store_true", default=False,
|
||||
help="ignore doctest ImportErrors",
|
||||
dest="doctest_ignore_import_errors")
|
||||
action="store_true", default=False,
|
||||
help="ignore doctest ImportErrors",
|
||||
dest="doctest_ignore_import_errors")
|
||||
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
config = parent.config
|
||||
if path.ext == ".py":
|
||||
if config.option.doctestmodules:
|
||||
if config.option.doctestmodules and not _is_setup_py(config, path, parent):
|
||||
return DoctestModule(path, parent)
|
||||
elif _is_doctest(config, path, parent):
|
||||
return DoctestTextfile(path, parent)
|
||||
|
||||
|
||||
def _is_setup_py(config, path, parent):
|
||||
if path.basename != "setup.py":
|
||||
return False
|
||||
contents = path.read()
|
||||
return 'setuptools' in contents or 'distutils' in contents
|
||||
|
||||
|
||||
def _is_doctest(config, path, parent):
|
||||
if path.ext in ('.txt', '.rst') and parent.session.isinitpath(path):
|
||||
return True
|
||||
@@ -95,8 +105,21 @@ class DoctestItem(pytest.Item):
|
||||
|
||||
def runtest(self):
|
||||
_check_all_skipped(self.dtest)
|
||||
self._disable_output_capturing_for_darwin()
|
||||
self.runner.run(self.dtest)
|
||||
|
||||
def _disable_output_capturing_for_darwin(self):
|
||||
"""
|
||||
Disable output capturing. Otherwise, stdout is lost to doctest (#985)
|
||||
"""
|
||||
if platform.system() != 'Darwin':
|
||||
return
|
||||
capman = self.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
out, err = capman.suspend_global_capture(in_=True)
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
import doctest
|
||||
if excinfo.errisinstance((doctest.DocTestFailure,
|
||||
@@ -119,7 +142,7 @@ class DoctestItem(pytest.Item):
|
||||
lines = ["%03d %s" % (i + test.lineno + 1, x)
|
||||
for (i, x) in enumerate(lines)]
|
||||
# trim docstring error lines to 10
|
||||
lines = lines[example.lineno - 9:example.lineno + 1]
|
||||
lines = lines[max(example.lineno - 9, 0):example.lineno + 1]
|
||||
else:
|
||||
lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example']
|
||||
indent = '>>>'
|
||||
@@ -128,18 +151,18 @@ class DoctestItem(pytest.Item):
|
||||
indent = '...'
|
||||
if excinfo.errisinstance(doctest.DocTestFailure):
|
||||
lines += checker.output_difference(example,
|
||||
doctestfailure.got, report_choice).split("\n")
|
||||
doctestfailure.got, report_choice).split("\n")
|
||||
else:
|
||||
inner_excinfo = ExceptionInfo(excinfo.value.exc_info)
|
||||
lines += ["UNEXPECTED EXCEPTION: %s" %
|
||||
repr(inner_excinfo.value)]
|
||||
repr(inner_excinfo.value)]
|
||||
lines += traceback.format_exception(*excinfo.value.exc_info)
|
||||
return ReprFailDoctest(reprlocation, lines)
|
||||
else:
|
||||
return super(DoctestItem, self).repr_failure(excinfo)
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, "[doctest] %s" % self.name
|
||||
return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name
|
||||
|
||||
|
||||
def _get_flag_lookup():
|
||||
@@ -163,6 +186,7 @@ def get_optionflags(parent):
|
||||
flag_acc |= flag_lookup_table[flag]
|
||||
return flag_acc
|
||||
|
||||
|
||||
class DoctestTextfile(pytest.Module):
|
||||
obj = None
|
||||
|
||||
@@ -177,7 +201,6 @@ class DoctestTextfile(pytest.Module):
|
||||
name = self.fspath.basename
|
||||
globs = {'__name__': '__main__'}
|
||||
|
||||
|
||||
optionflags = get_optionflags(self)
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
|
||||
checker=_get_checker())
|
||||
@@ -218,9 +241,6 @@ class DoctestModule(pytest.Module):
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
|
||||
checker=_get_checker())
|
||||
|
||||
encoding = self.config.getini("doctest_encoding")
|
||||
_fix_spoof_python2(runner, encoding)
|
||||
|
||||
for test in finder.find(module, module.__name__):
|
||||
if test.examples: # skip empty doctests
|
||||
yield DoctestItem(test.name, self, runner, test)
|
||||
@@ -332,8 +352,11 @@ def _get_report_choice(key):
|
||||
|
||||
def _fix_spoof_python2(runner, encoding):
|
||||
"""
|
||||
Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output.
|
||||
|
||||
Installs a "SpoofOut" into the given DebugRunner so it properly deals with unicode output. This
|
||||
should patch only doctests for text files because they don't have a way to declare their
|
||||
encoding. Doctests in docstrings from Python modules don't have the same problem given that
|
||||
Python already decoded the strings.
|
||||
|
||||
This fixes the problem related in issue #2434.
|
||||
"""
|
||||
from _pytest.compat import _PY2
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import sys
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import sys
|
||||
import warnings
|
||||
from collections import OrderedDict, deque, defaultdict
|
||||
|
||||
import attr
|
||||
import py
|
||||
from py._code.code import FormattedExcinfo
|
||||
|
||||
import py
|
||||
import warnings
|
||||
|
||||
import inspect
|
||||
import _pytest
|
||||
from _pytest import nodes
|
||||
from _pytest._code.code import TerminalRepr
|
||||
from _pytest.compat import (
|
||||
NOTSET, exc_clear, _format_args,
|
||||
@@ -15,16 +19,26 @@ from _pytest.compat import (
|
||||
is_generator, isclass, getimfunc,
|
||||
getlocation, getfuncargnames,
|
||||
safe_getattr,
|
||||
FuncargnamesCompatAttr,
|
||||
)
|
||||
from _pytest.runner import fail
|
||||
from _pytest.compat import FuncargnamesCompatAttr
|
||||
from _pytest.outcomes import fail, TEST_OUTCOME
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class PseudoFixtureDef(object):
|
||||
cached_result = attr.ib()
|
||||
scope = attr.ib()
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
import _pytest.python
|
||||
import _pytest.nodes
|
||||
|
||||
scopename2class.update({
|
||||
'class': _pytest.python.Class,
|
||||
'module': _pytest.python.Module,
|
||||
'function': _pytest.main.Item,
|
||||
'function': _pytest.nodes.Item,
|
||||
'session': _pytest.main.Session,
|
||||
})
|
||||
session._fixturemanager = FixtureManager(session)
|
||||
|
||||
@@ -38,6 +52,7 @@ scope2props["class"] = scope2props["module"] + ("cls",)
|
||||
scope2props["instance"] = scope2props["class"] + ("instance", )
|
||||
scope2props["function"] = scope2props["instance"] + ("function", "keywords")
|
||||
|
||||
|
||||
def scopeproperty(name=None, doc=None):
|
||||
def decoratescope(func):
|
||||
scopename = name or func.__name__
|
||||
@@ -55,8 +70,6 @@ def scopeproperty(name=None, doc=None):
|
||||
def get_scope_node(node, scope):
|
||||
cls = scopename2class.get(scope)
|
||||
if cls is None:
|
||||
if scope == "session":
|
||||
return node.session
|
||||
raise ValueError("unknown scope")
|
||||
return node.getparent(cls)
|
||||
|
||||
@@ -69,7 +82,7 @@ def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager):
|
||||
# XXX we can probably avoid this algorithm if we modify CallSpec2
|
||||
# to directly care for creating the fixturedefs within its methods.
|
||||
if not metafunc._calls[0].funcargs:
|
||||
return # this function call does not have direct parametrization
|
||||
return # this function call does not have direct parametrization
|
||||
# collect funcargs of all callspecs into a list of values
|
||||
arg2params = {}
|
||||
arg2scope = {}
|
||||
@@ -105,28 +118,26 @@ def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager):
|
||||
if node and argname in node._name2pseudofixturedef:
|
||||
arg2fixturedefs[argname] = [node._name2pseudofixturedef[argname]]
|
||||
else:
|
||||
fixturedef = FixtureDef(fixturemanager, '', argname,
|
||||
get_direct_param_fixture_func,
|
||||
arg2scope[argname],
|
||||
valuelist, False, False)
|
||||
fixturedef = FixtureDef(fixturemanager, '', argname,
|
||||
get_direct_param_fixture_func,
|
||||
arg2scope[argname],
|
||||
valuelist, False, False)
|
||||
arg2fixturedefs[argname] = [fixturedef]
|
||||
if node is not None:
|
||||
node._name2pseudofixturedef[argname] = fixturedef
|
||||
|
||||
|
||||
|
||||
def getfixturemarker(obj):
|
||||
""" return fixturemarker or None if it doesn't exist or raised
|
||||
exceptions."""
|
||||
try:
|
||||
return getattr(obj, "_pytestfixturefunction", None)
|
||||
except Exception:
|
||||
except TEST_OUTCOME:
|
||||
# some objects raise errors like request (from flask import request)
|
||||
# we don't expect them to be fixture functions
|
||||
return None
|
||||
|
||||
|
||||
|
||||
def get_parametrized_fixture_keys(item, scopenum):
|
||||
""" return list of keys for all parametrized arguments which match
|
||||
the specified scope. """
|
||||
@@ -136,10 +147,10 @@ def get_parametrized_fixture_keys(item, scopenum):
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
# cs.indictes.items() is random order of argnames but
|
||||
# then again different functions (items) can change order of
|
||||
# arguments so it doesn't matter much probably
|
||||
for argname, param_index in cs.indices.items():
|
||||
# cs.indices.items() is random order of argnames. Need to
|
||||
# sort this so that different calls to
|
||||
# get_parametrized_fixture_keys will be deterministic.
|
||||
for argname, param_index in sorted(cs.indices.items()):
|
||||
if cs._arg2scopenum[argname] != scopenum:
|
||||
continue
|
||||
if scopenum == 0: # session
|
||||
@@ -158,61 +169,59 @@ def get_parametrized_fixture_keys(item, scopenum):
|
||||
|
||||
def reorder_items(items):
|
||||
argkeys_cache = {}
|
||||
items_by_argkey = {}
|
||||
for scopenum in range(0, scopenum_function):
|
||||
argkeys_cache[scopenum] = d = {}
|
||||
items_by_argkey[scopenum] = item_d = defaultdict(deque)
|
||||
for item in items:
|
||||
keys = set(get_parametrized_fixture_keys(item, scopenum))
|
||||
keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum))
|
||||
if keys:
|
||||
d[item] = keys
|
||||
return reorder_items_atscope(items, set(), argkeys_cache, 0)
|
||||
for key in keys:
|
||||
item_d[key].append(item)
|
||||
items = OrderedDict.fromkeys(items)
|
||||
return list(reorder_items_atscope(items, argkeys_cache, items_by_argkey, 0))
|
||||
|
||||
def reorder_items_atscope(items, ignore, argkeys_cache, scopenum):
|
||||
|
||||
def fix_cache_order(item, argkeys_cache, items_by_argkey):
|
||||
for scopenum in range(0, scopenum_function):
|
||||
for key in argkeys_cache[scopenum].get(item, []):
|
||||
items_by_argkey[scopenum][key].appendleft(item)
|
||||
|
||||
|
||||
def reorder_items_atscope(items, argkeys_cache, items_by_argkey, scopenum):
|
||||
if scopenum >= scopenum_function or len(items) < 3:
|
||||
return items
|
||||
items_done = []
|
||||
while 1:
|
||||
items_before, items_same, items_other, newignore = \
|
||||
slice_items(items, ignore, argkeys_cache[scopenum])
|
||||
items_before = reorder_items_atscope(
|
||||
items_before, ignore, argkeys_cache,scopenum+1)
|
||||
if items_same is None:
|
||||
# nothing to reorder in this scope
|
||||
assert items_other is None
|
||||
return items_done + items_before
|
||||
items_done.extend(items_before)
|
||||
items = items_same + items_other
|
||||
ignore = newignore
|
||||
|
||||
|
||||
def slice_items(items, ignore, scoped_argkeys_cache):
|
||||
# we pick the first item which uses a fixture instance in the
|
||||
# requested scope and which we haven't seen yet. We slice the input
|
||||
# items list into a list of items_nomatch, items_same and
|
||||
# items_other
|
||||
if scoped_argkeys_cache: # do we need to do work at all?
|
||||
it = iter(items)
|
||||
# first find a slicing key
|
||||
for i, item in enumerate(it):
|
||||
argkeys = scoped_argkeys_cache.get(item)
|
||||
if argkeys is not None:
|
||||
argkeys = argkeys.difference(ignore)
|
||||
if argkeys: # found a slicing key
|
||||
slicing_argkey = argkeys.pop()
|
||||
items_before = items[:i]
|
||||
items_same = [item]
|
||||
items_other = []
|
||||
# now slice the remainder of the list
|
||||
for item in it:
|
||||
argkeys = scoped_argkeys_cache.get(item)
|
||||
if argkeys and slicing_argkey in argkeys and \
|
||||
slicing_argkey not in ignore:
|
||||
items_same.append(item)
|
||||
else:
|
||||
items_other.append(item)
|
||||
newignore = ignore.copy()
|
||||
newignore.add(slicing_argkey)
|
||||
return (items_before, items_same, items_other, newignore)
|
||||
return items, None, None, None
|
||||
ignore = set()
|
||||
items_deque = deque(items)
|
||||
items_done = OrderedDict()
|
||||
scoped_items_by_argkey = items_by_argkey[scopenum]
|
||||
scoped_argkeys_cache = argkeys_cache[scopenum]
|
||||
while items_deque:
|
||||
no_argkey_group = OrderedDict()
|
||||
slicing_argkey = None
|
||||
while items_deque:
|
||||
item = items_deque.popleft()
|
||||
if item in items_done or item in no_argkey_group:
|
||||
continue
|
||||
argkeys = OrderedDict.fromkeys(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore)
|
||||
if not argkeys:
|
||||
no_argkey_group[item] = None
|
||||
else:
|
||||
slicing_argkey, _ = argkeys.popitem()
|
||||
# we don't have to remove relevant items from later in the deque because they'll just be ignored
|
||||
matching_items = [i for i in scoped_items_by_argkey[slicing_argkey] if i in items]
|
||||
for i in reversed(matching_items):
|
||||
fix_cache_order(i, argkeys_cache, items_by_argkey)
|
||||
items_deque.appendleft(i)
|
||||
break
|
||||
if no_argkey_group:
|
||||
no_argkey_group = reorder_items_atscope(
|
||||
no_argkey_group, argkeys_cache, items_by_argkey, scopenum + 1)
|
||||
for item in no_argkey_group:
|
||||
items_done[item] = None
|
||||
ignore.add(slicing_argkey)
|
||||
return items_done
|
||||
|
||||
|
||||
def fillfixtures(function):
|
||||
@@ -237,11 +246,11 @@ def fillfixtures(function):
|
||||
request._fillfixtures()
|
||||
|
||||
|
||||
|
||||
def get_direct_param_fixture_func(request):
|
||||
return request.param
|
||||
|
||||
class FuncFixtureInfo:
|
||||
|
||||
class FuncFixtureInfo(object):
|
||||
def __init__(self, argnames, names_closure, name2fixturedefs):
|
||||
self.argnames = argnames
|
||||
self.names_closure = names_closure
|
||||
@@ -262,7 +271,6 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
self.fixturename = None
|
||||
#: Scope string, one of "function", "class", "module", "session"
|
||||
self.scope = "function"
|
||||
self._fixture_values = {} # argname -> fixture value
|
||||
self._fixture_defs = {} # argname -> FixtureDef
|
||||
fixtureinfo = pyfuncitem._fixtureinfo
|
||||
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
|
||||
@@ -279,7 +287,6 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
""" underlying collection node (depends on current request scope)"""
|
||||
return self._getscopeitem(self.scope)
|
||||
|
||||
|
||||
def _getnextfixturedef(self, argname):
|
||||
fixturedefs = self._arg2fixturedefs.get(argname, None)
|
||||
if fixturedefs is None:
|
||||
@@ -301,7 +308,6 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
""" the pytest config object associated with this request. """
|
||||
return self._pyfuncitem.config
|
||||
|
||||
|
||||
@scopeproperty()
|
||||
def function(self):
|
||||
""" test function object if the request has a per-function scope. """
|
||||
@@ -397,7 +403,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
:arg extrakey: added to internal caching key of (funcargname, scope).
|
||||
"""
|
||||
if not hasattr(self.config, '_setupcache'):
|
||||
self.config._setupcache = {} # XXX weakref?
|
||||
self.config._setupcache = {} # XXX weakref?
|
||||
cachekey = (self.fixturename, self._getscopeitem(scope), extrakey)
|
||||
cache = self.config._setupcache
|
||||
try:
|
||||
@@ -428,7 +434,8 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
from _pytest import deprecated
|
||||
warnings.warn(
|
||||
deprecated.GETFUNCARGVALUE,
|
||||
DeprecationWarning)
|
||||
DeprecationWarning,
|
||||
stacklevel=2)
|
||||
return self.getfixturevalue(argname)
|
||||
|
||||
def _get_active_fixturedef(self, argname):
|
||||
@@ -439,30 +446,35 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
fixturedef = self._getnextfixturedef(argname)
|
||||
except FixtureLookupError:
|
||||
if argname == "request":
|
||||
class PseudoFixtureDef:
|
||||
cached_result = (self, [0], None)
|
||||
scope = "function"
|
||||
return PseudoFixtureDef
|
||||
cached_result = (self, [0], None)
|
||||
scope = "function"
|
||||
return PseudoFixtureDef(cached_result, scope)
|
||||
raise
|
||||
# remove indent to prevent the python3 exception
|
||||
# from leaking into the call
|
||||
result = self._getfixturevalue(fixturedef)
|
||||
self._fixture_values[argname] = result
|
||||
self._compute_fixture_value(fixturedef)
|
||||
self._fixture_defs[argname] = fixturedef
|
||||
return fixturedef
|
||||
|
||||
def _get_fixturestack(self):
|
||||
current = self
|
||||
l = []
|
||||
values = []
|
||||
while 1:
|
||||
fixturedef = getattr(current, "_fixturedef", None)
|
||||
if fixturedef is None:
|
||||
l.reverse()
|
||||
return l
|
||||
l.append(fixturedef)
|
||||
values.reverse()
|
||||
return values
|
||||
values.append(fixturedef)
|
||||
current = current._parent_request
|
||||
|
||||
def _getfixturevalue(self, fixturedef):
|
||||
def _compute_fixture_value(self, fixturedef):
|
||||
"""
|
||||
Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will
|
||||
force the FixtureDef object to throw away any previous results and compute a new fixture value, which
|
||||
will be stored into the FixtureDef object itself.
|
||||
|
||||
:param FixtureDef fixturedef:
|
||||
"""
|
||||
# prepare a subrequest object before calling fixture function
|
||||
# (latter managed by fixturedef)
|
||||
argname = fixturedef.argname
|
||||
@@ -511,12 +523,11 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
exc_clear()
|
||||
try:
|
||||
# call the fixture function
|
||||
val = fixturedef.execute(request=subrequest)
|
||||
fixturedef.execute(request=subrequest)
|
||||
finally:
|
||||
# if fixture function failed it might have registered finalizers
|
||||
self.session._setupstate.addfinalizer(fixturedef.finish,
|
||||
self.session._setupstate.addfinalizer(functools.partial(fixturedef.finish, request=subrequest),
|
||||
subrequest.node)
|
||||
return val
|
||||
|
||||
def _check_scope(self, argname, invoking_scope, requested_scope):
|
||||
if argname == "request":
|
||||
@@ -527,8 +538,8 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
fail("ScopeMismatch: You tried to access the %r scoped "
|
||||
"fixture %r with a %r scoped request object, "
|
||||
"involved factories\n%s" % (
|
||||
(requested_scope, argname, invoking_scope, "\n".join(lines))),
|
||||
pytrace=False)
|
||||
(requested_scope, argname, invoking_scope, "\n".join(lines))),
|
||||
pytrace=False)
|
||||
|
||||
def _factorytraceback(self):
|
||||
lines = []
|
||||
@@ -549,16 +560,17 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
if node is None and scope == "class":
|
||||
# fallback to function item itself
|
||||
node = self._pyfuncitem
|
||||
assert node
|
||||
assert node, 'Could not obtain a node for scope "{}" for function {!r}'.format(scope, self._pyfuncitem)
|
||||
return node
|
||||
|
||||
def __repr__(self):
|
||||
return "<FixtureRequest for %r>" %(self.node)
|
||||
return "<FixtureRequest for %r>" % (self.node)
|
||||
|
||||
|
||||
class SubRequest(FixtureRequest):
|
||||
""" a sub request for handling getting a fixture from a
|
||||
test function/fixture. """
|
||||
|
||||
def __init__(self, request, scope, param, param_index, fixturedef):
|
||||
self._parent_request = request
|
||||
self.fixturename = fixturedef.argname
|
||||
@@ -567,9 +579,7 @@ class SubRequest(FixtureRequest):
|
||||
self.param_index = param_index
|
||||
self.scope = scope
|
||||
self._fixturedef = fixturedef
|
||||
self.addfinalizer = fixturedef.addfinalizer
|
||||
self._pyfuncitem = request._pyfuncitem
|
||||
self._fixture_values = request._fixture_values
|
||||
self._fixture_defs = request._fixture_defs
|
||||
self._arg2fixturedefs = request._arg2fixturedefs
|
||||
self._arg2index = request._arg2index
|
||||
@@ -578,6 +588,9 @@ class SubRequest(FixtureRequest):
|
||||
def __repr__(self):
|
||||
return "<SubRequest %r for %r>" % (self.fixturename, self._pyfuncitem)
|
||||
|
||||
def addfinalizer(self, finalizer):
|
||||
self._fixturedef.addfinalizer(finalizer)
|
||||
|
||||
|
||||
class ScopeMismatchError(Exception):
|
||||
""" A fixture function tries to use a different fixture function which
|
||||
@@ -609,6 +622,7 @@ def scope2index(scope, descr, where=None):
|
||||
|
||||
class FixtureLookupError(LookupError):
|
||||
""" could not return a requested Fixture (missing or invalid). """
|
||||
|
||||
def __init__(self, argname, request, msg=None):
|
||||
self.argname = argname
|
||||
self.request = request
|
||||
@@ -631,9 +645,9 @@ class FixtureLookupError(LookupError):
|
||||
lines, _ = inspect.getsourcelines(get_real_func(function))
|
||||
except (IOError, IndexError, TypeError):
|
||||
error_msg = "file %s, line %s: source code not available"
|
||||
addline(error_msg % (fspath, lineno+1))
|
||||
addline(error_msg % (fspath, lineno + 1))
|
||||
else:
|
||||
addline("file %s, line %s" % (fspath, lineno+1))
|
||||
addline("file %s, line %s" % (fspath, lineno + 1))
|
||||
for i, line in enumerate(lines):
|
||||
line = line.rstrip()
|
||||
addline(" " + line)
|
||||
@@ -649,7 +663,7 @@ class FixtureLookupError(LookupError):
|
||||
if faclist and name not in available:
|
||||
available.append(name)
|
||||
msg = "fixture %r not found" % (self.argname,)
|
||||
msg += "\n available fixtures: %s" %(", ".join(sorted(available)),)
|
||||
msg += "\n available fixtures: %s" % (", ".join(sorted(available)),)
|
||||
msg += "\n use 'pytest --fixtures [testpath]' for help on them."
|
||||
|
||||
return FixtureLookupErrorRepr(fspath, lineno, tblines, msg, self.argname)
|
||||
@@ -675,12 +689,12 @@ class FixtureLookupErrorRepr(TerminalRepr):
|
||||
tw.line('{0} {1}'.format(FormattedExcinfo.flow_marker,
|
||||
line.strip()), red=True)
|
||||
tw.line()
|
||||
tw.line("%s:%d" % (self.filename, self.firstlineno+1))
|
||||
tw.line("%s:%d" % (self.filename, self.firstlineno + 1))
|
||||
|
||||
|
||||
def fail_fixturefunc(fixturefunc, msg):
|
||||
fs, lineno = getfslineno(fixturefunc)
|
||||
location = "%s:%s" % (fs, lineno+1)
|
||||
location = "%s:%s" % (fs, lineno + 1)
|
||||
source = _pytest._code.Source(fixturefunc)
|
||||
fail(msg + ":\n\n" + str(source.indent()) + "\n" + location,
|
||||
pytrace=False)
|
||||
@@ -699,7 +713,7 @@ def call_fixture_func(fixturefunc, request, kwargs):
|
||||
pass
|
||||
else:
|
||||
fail_fixturefunc(fixturefunc,
|
||||
"yield_fixture function has more than one 'yield'")
|
||||
"yield_fixture function has more than one 'yield'")
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
else:
|
||||
@@ -707,8 +721,9 @@ def call_fixture_func(fixturefunc, request, kwargs):
|
||||
return res
|
||||
|
||||
|
||||
class FixtureDef:
|
||||
class FixtureDef(object):
|
||||
""" A container for a factory definition. """
|
||||
|
||||
def __init__(self, fixturemanager, baseid, argname, func, scope, params,
|
||||
unittest=False, ids=None):
|
||||
self._fixturemanager = fixturemanager
|
||||
@@ -723,27 +738,38 @@ class FixtureDef:
|
||||
where=baseid
|
||||
)
|
||||
self.params = params
|
||||
startindex = unittest and 1 or None
|
||||
self.argnames = getfuncargnames(func, startindex=startindex)
|
||||
self.argnames = getfuncargnames(func, is_method=unittest)
|
||||
self.unittest = unittest
|
||||
self.ids = ids
|
||||
self._finalizer = []
|
||||
self._finalizers = []
|
||||
|
||||
def addfinalizer(self, finalizer):
|
||||
self._finalizer.append(finalizer)
|
||||
self._finalizers.append(finalizer)
|
||||
|
||||
def finish(self):
|
||||
def finish(self, request):
|
||||
exceptions = []
|
||||
try:
|
||||
while self._finalizer:
|
||||
func = self._finalizer.pop()
|
||||
func()
|
||||
while self._finalizers:
|
||||
try:
|
||||
func = self._finalizers.pop()
|
||||
func()
|
||||
except: # noqa
|
||||
exceptions.append(sys.exc_info())
|
||||
if exceptions:
|
||||
e = exceptions[0]
|
||||
del exceptions # ensure we don't keep all frames alive because of the traceback
|
||||
py.builtin._reraise(*e)
|
||||
|
||||
finally:
|
||||
ihook = self._fixturemanager.session.ihook
|
||||
ihook.pytest_fixture_post_finalizer(fixturedef=self)
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
||||
hook.pytest_fixture_post_finalizer(fixturedef=self, request=request)
|
||||
# even if finalization fails, we invalidate
|
||||
# the cached fixture value
|
||||
# the cached fixture value and remove
|
||||
# all finalizers because they may be bound methods which will
|
||||
# keep instances alive
|
||||
if hasattr(self, "cached_result"):
|
||||
del self.cached_result
|
||||
self._finalizers = []
|
||||
|
||||
def execute(self, request):
|
||||
# get required arguments and register our own finish()
|
||||
@@ -751,7 +777,7 @@ class FixtureDef:
|
||||
for argname in self.argnames:
|
||||
fixturedef = request._get_active_fixturedef(argname)
|
||||
if argname != "request":
|
||||
fixturedef.addfinalizer(self.finish)
|
||||
fixturedef.addfinalizer(functools.partial(self.finish, request=request))
|
||||
|
||||
my_cache_key = request.param_index
|
||||
cached_result = getattr(self, "cached_result", None)
|
||||
@@ -764,16 +790,17 @@ class FixtureDef:
|
||||
return result
|
||||
# we have a previous but differently parametrized fixture instance
|
||||
# so we need to tear it down before creating a new one
|
||||
self.finish()
|
||||
self.finish(request)
|
||||
assert not hasattr(self, "cached_result")
|
||||
|
||||
ihook = self._fixturemanager.session.ihook
|
||||
return ihook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
hook = self._fixturemanager.session.gethookproxy(request.node.fspath)
|
||||
return hook.pytest_fixture_setup(fixturedef=self, request=request)
|
||||
|
||||
def __repr__(self):
|
||||
return ("<FixtureDef name=%r scope=%r baseid=%r >" %
|
||||
(self.argname, self.scope, self.baseid))
|
||||
|
||||
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
""" Execution of fixture setup. """
|
||||
kwargs = {}
|
||||
@@ -799,30 +826,37 @@ def pytest_fixture_setup(fixturedef, request):
|
||||
my_cache_key = request.param_index
|
||||
try:
|
||||
result = call_fixture_func(fixturefunc, request, kwargs)
|
||||
except Exception:
|
||||
except TEST_OUTCOME:
|
||||
fixturedef.cached_result = (None, my_cache_key, sys.exc_info())
|
||||
raise
|
||||
fixturedef.cached_result = (result, my_cache_key, None)
|
||||
return result
|
||||
|
||||
|
||||
class FixtureFunctionMarker:
|
||||
def __init__(self, scope, params, autouse=False, ids=None, name=None):
|
||||
self.scope = scope
|
||||
self.params = params
|
||||
self.autouse = autouse
|
||||
self.ids = ids
|
||||
self.name = name
|
||||
def _ensure_immutable_ids(ids):
|
||||
if ids is None:
|
||||
return
|
||||
if callable(ids):
|
||||
return ids
|
||||
return tuple(ids)
|
||||
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class FixtureFunctionMarker(object):
|
||||
scope = attr.ib()
|
||||
params = attr.ib(convert=attr.converters.optional(tuple))
|
||||
autouse = attr.ib(default=False)
|
||||
ids = attr.ib(default=None, convert=_ensure_immutable_ids)
|
||||
name = attr.ib(default=None)
|
||||
|
||||
def __call__(self, function):
|
||||
if isclass(function):
|
||||
raise ValueError(
|
||||
"class fixtures not supported (may be in the future)")
|
||||
"class fixtures not supported (may be in the future)")
|
||||
function._pytestfixturefunction = self
|
||||
return function
|
||||
|
||||
|
||||
|
||||
def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
|
||||
""" (return a) decorator to mark a fixture factory function.
|
||||
|
||||
@@ -861,10 +895,10 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
|
||||
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 == False:
|
||||
if callable(scope) and params is None and autouse is False:
|
||||
# direct decoration
|
||||
return FixtureFunctionMarker(
|
||||
"function", params, autouse, name=name)(scope)
|
||||
"function", params, autouse, name=name)(scope)
|
||||
if params is not None and not isinstance(params, (list, tuple)):
|
||||
params = list(params)
|
||||
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
|
||||
@@ -879,7 +913,7 @@ def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=N
|
||||
if callable(scope) and params is None and not autouse:
|
||||
# direct decoration
|
||||
return FixtureFunctionMarker(
|
||||
"function", params, autouse, ids=ids, name=name)(scope)
|
||||
"function", params, autouse, ids=ids, name=name)(scope)
|
||||
else:
|
||||
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
|
||||
|
||||
@@ -893,7 +927,7 @@ def pytestconfig(request):
|
||||
return request.config
|
||||
|
||||
|
||||
class FixtureManager:
|
||||
class FixtureManager(object):
|
||||
"""
|
||||
pytest fixtures definitions and information is stored and managed
|
||||
from this class.
|
||||
@@ -938,14 +972,9 @@ class FixtureManager:
|
||||
self._nodeid_and_autousenames = [("", self.config.getini("usefixtures"))]
|
||||
session.config.pluginmanager.register(self, "funcmanage")
|
||||
|
||||
|
||||
def getfixtureinfo(self, node, func, cls, funcargs=True):
|
||||
if funcargs and not hasattr(node, "nofuncargs"):
|
||||
if cls is not None:
|
||||
startindex = 1
|
||||
else:
|
||||
startindex = None
|
||||
argnames = getfuncargnames(func, startindex)
|
||||
argnames = getfuncargnames(func, cls=cls)
|
||||
else:
|
||||
argnames = ()
|
||||
usefixtures = getattr(func, "usefixtures", None)
|
||||
@@ -969,8 +998,8 @@ class FixtureManager:
|
||||
# by their test id)
|
||||
if p.basename.startswith("conftest.py"):
|
||||
nodeid = p.dirpath().relto(self.config.rootdir)
|
||||
if p.sep != "/":
|
||||
nodeid = nodeid.replace(p.sep, "/")
|
||||
if p.sep != nodes.SEP:
|
||||
nodeid = nodeid.replace(p.sep, nodes.SEP)
|
||||
self.parsefactories(plugin, nodeid)
|
||||
|
||||
def _getautousenames(self, nodeid):
|
||||
@@ -980,7 +1009,7 @@ class FixtureManager:
|
||||
if nodeid.startswith(baseid):
|
||||
if baseid:
|
||||
i = len(baseid)
|
||||
nextchar = nodeid[i:i+1]
|
||||
nextchar = nodeid[i:i + 1]
|
||||
if nextchar and nextchar not in ":/":
|
||||
continue
|
||||
autousenames.extend(basenames)
|
||||
@@ -1025,9 +1054,14 @@ class FixtureManager:
|
||||
if faclist:
|
||||
fixturedef = faclist[-1]
|
||||
if fixturedef.params is not None:
|
||||
func_params = getattr(getattr(metafunc.function, 'parametrize', None), 'args', [[None]])
|
||||
parametrize_func = getattr(metafunc.function, 'parametrize', None)
|
||||
func_params = getattr(parametrize_func, 'args', [[None]])
|
||||
func_kwargs = getattr(parametrize_func, 'kwargs', {})
|
||||
# skip directly parametrized arguments
|
||||
argnames = func_params[0]
|
||||
if "argnames" in func_kwargs:
|
||||
argnames = parametrize_func.kwargs["argnames"]
|
||||
else:
|
||||
argnames = func_params[0]
|
||||
if not isinstance(argnames, (tuple, list)):
|
||||
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
|
||||
if argname not in func_params and argname not in argnames:
|
||||
@@ -1115,6 +1149,5 @@ class FixtureManager:
|
||||
|
||||
def _matchfactories(self, fixturedefs, nodeid):
|
||||
for fixturedef in fixturedefs:
|
||||
if nodeid.startswith(fixturedef.baseid):
|
||||
if nodes.ischildnode(fixturedef.baseid, nodeid):
|
||||
yield fixturedef
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ pytest
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
|
||||
|
||||
def freeze_includes():
|
||||
"""
|
||||
Returns a list of module names used by py.test that should be
|
||||
|
||||
@@ -3,29 +3,63 @@ from __future__ import absolute_import, division, print_function
|
||||
|
||||
import py
|
||||
import pytest
|
||||
import os, sys
|
||||
from _pytest.config import PrintHelp
|
||||
import os
|
||||
import sys
|
||||
from argparse import Action
|
||||
|
||||
|
||||
class HelpAction(Action):
|
||||
"""This is an argparse Action that will raise an exception in
|
||||
order to skip the rest of the argument parsing when --help is passed.
|
||||
This prevents argparse from quitting due to missing required arguments
|
||||
when any are defined, for example by ``pytest_addoption``.
|
||||
This is similar to the way that the builtin argparse --help option is
|
||||
implemented by raising SystemExit.
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
option_strings,
|
||||
dest=None,
|
||||
default=False,
|
||||
help=None):
|
||||
super(HelpAction, self).__init__(
|
||||
option_strings=option_strings,
|
||||
dest=dest,
|
||||
const=True,
|
||||
default=default,
|
||||
nargs=0,
|
||||
help=help)
|
||||
|
||||
def __call__(self, parser, namespace, values, option_string=None):
|
||||
setattr(namespace, self.dest, self.const)
|
||||
|
||||
# We should only skip the rest of the parsing after preparse is done
|
||||
if getattr(parser._parser, 'after_preparse', False):
|
||||
raise PrintHelp
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup('debugconfig')
|
||||
group.addoption('--version', action="store_true",
|
||||
help="display pytest lib version and import information.")
|
||||
group._addoption("-h", "--help", action="store_true", dest="help",
|
||||
help="show help message and configuration info")
|
||||
group._addoption('-p', action="append", dest="plugins", default = [],
|
||||
metavar="name",
|
||||
help="early-load given plugin (multi-allowed). "
|
||||
"To avoid loading of plugins, use the `no:` prefix, e.g. "
|
||||
"`no:doctest`.")
|
||||
help="display pytest lib version and import information.")
|
||||
group._addoption("-h", "--help", action=HelpAction, dest="help",
|
||||
help="show help message and configuration info")
|
||||
group._addoption('-p', action="append", dest="plugins", default=[],
|
||||
metavar="name",
|
||||
help="early-load given plugin (multi-allowed). "
|
||||
"To avoid loading of plugins, use the `no:` prefix, e.g. "
|
||||
"`no:doctest`.")
|
||||
group.addoption('--traceconfig', '--trace-config',
|
||||
action="store_true", default=False,
|
||||
help="trace considerations of conftest.py files."),
|
||||
action="store_true", default=False,
|
||||
help="trace considerations of conftest.py files."),
|
||||
group.addoption('--debug',
|
||||
action="store_true", dest="debug", default=False,
|
||||
help="store internal tracing debug information in 'pytestdebug.log'.")
|
||||
action="store_true", dest="debug", default=False,
|
||||
help="store internal tracing debug information in 'pytestdebug.log'.")
|
||||
group._addoption(
|
||||
'-o', '--override-ini', nargs='*', dest="override_ini",
|
||||
'-o', '--override-ini', dest="override_ini",
|
||||
action="append",
|
||||
help="override config option with option=value style, e.g. `-o xfail_strict=True`.")
|
||||
help='override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`.')
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@@ -36,10 +70,10 @@ def pytest_cmdline_parse():
|
||||
path = os.path.abspath("pytestdebug.log")
|
||||
debugfile = open(path, 'w')
|
||||
debugfile.write("versions pytest-%s, py-%s, "
|
||||
"python-%s\ncwd=%s\nargs=%s\n\n" %(
|
||||
pytest.__version__, py.__version__,
|
||||
".".join(map(str, sys.version_info)),
|
||||
os.getcwd(), config._origargs))
|
||||
"python-%s\ncwd=%s\nargs=%s\n\n" % (
|
||||
pytest.__version__, py.__version__,
|
||||
".".join(map(str, sys.version_info)),
|
||||
os.getcwd(), config._origargs))
|
||||
config.trace.root.setwriter(debugfile.write)
|
||||
undo_tracing = config.pluginmanager.enable_tracing()
|
||||
sys.stderr.write("writing pytestdebug information to %s\n" % path)
|
||||
@@ -53,11 +87,12 @@ def pytest_cmdline_parse():
|
||||
|
||||
config.add_cleanup(unset_tracing)
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
if config.option.version:
|
||||
p = py.path.local(pytest.__file__)
|
||||
sys.stderr.write("This is pytest version %s, imported from %s\n" %
|
||||
(pytest.__version__, p))
|
||||
(pytest.__version__, p))
|
||||
plugininfo = getpluginversioninfo(config)
|
||||
if plugininfo:
|
||||
for line in plugininfo:
|
||||
@@ -69,6 +104,7 @@ def pytest_cmdline_main(config):
|
||||
config._ensure_unconfigure()
|
||||
return 0
|
||||
|
||||
|
||||
def showhelp(config):
|
||||
reporter = config.pluginmanager.get_plugin('terminalreporter')
|
||||
tw = reporter._tw
|
||||
@@ -84,7 +120,7 @@ def showhelp(config):
|
||||
if type is None:
|
||||
type = "string"
|
||||
spec = "%s (%s)" % (name, type)
|
||||
line = " %-24s %s" %(spec, help)
|
||||
line = " %-24s %s" % (spec, help)
|
||||
tw.line(line[:tw.fullwidth])
|
||||
|
||||
tw.line()
|
||||
@@ -113,6 +149,7 @@ conftest_options = [
|
||||
('pytest_plugins', 'list of plugin names to load'),
|
||||
]
|
||||
|
||||
|
||||
def getpluginversioninfo(config):
|
||||
lines = []
|
||||
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
||||
@@ -124,11 +161,12 @@ def getpluginversioninfo(config):
|
||||
lines.append(" " + content)
|
||||
return lines
|
||||
|
||||
|
||||
def pytest_report_header(config):
|
||||
lines = []
|
||||
if config.option.debug or config.option.traceconfig:
|
||||
lines.append("using: pytest-%s pylib-%s" %
|
||||
(pytest.__version__,py.__version__))
|
||||
(pytest.__version__, py.__version__))
|
||||
|
||||
verinfo = getpluginversioninfo(config)
|
||||
if verinfo:
|
||||
@@ -142,5 +180,5 @@ def pytest_report_header(config):
|
||||
r = plugin.__file__
|
||||
else:
|
||||
r = repr(plugin)
|
||||
lines.append(" %-20s: %s" %(name, r))
|
||||
lines.append(" %-20s: %s" % (name, r))
|
||||
return lines
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """
|
||||
|
||||
from _pytest._pluggy import HookspecMarker
|
||||
from pluggy import HookspecMarker
|
||||
|
||||
hookspec = HookspecMarker("pytest")
|
||||
|
||||
@@ -8,24 +8,44 @@ hookspec = HookspecMarker("pytest")
|
||||
# Initialization hooks called for every plugin
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_addhooks(pluginmanager):
|
||||
"""called at plugin registration time to allow adding new hooks via a call to
|
||||
pluginmanager.add_hookspecs(module_or_class, prefix)."""
|
||||
``pluginmanager.add_hookspecs(module_or_class, prefix)``.
|
||||
|
||||
|
||||
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_namespace():
|
||||
"""
|
||||
DEPRECATED: this hook causes direct monkeypatching on pytest, its use is strongly discouraged
|
||||
(**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. This hook is called at plugin registration
|
||||
time.
|
||||
the pytest namespace.
|
||||
|
||||
This hook is called at plugin registration time.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_plugin_registered(plugin, manager):
|
||||
""" a new pytest plugin got registered. """
|
||||
""" a new pytest plugin got registered.
|
||||
|
||||
:param plugin: the plugin module or instance
|
||||
:param _pytest.config.PytestPluginManager manager: pytest plugin manager
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
@@ -39,7 +59,7 @@ def pytest_addoption(parser):
|
||||
files situated at the tests root directory due to how pytest
|
||||
:ref:`discovers plugins during startup <pluginorder>`.
|
||||
|
||||
:arg parser: To add command line options, call
|
||||
:arg _pytest.config.Parser parser: To add command line options, call
|
||||
:py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`.
|
||||
To add ini-file values call :py:func:`parser.addini(...)
|
||||
<_pytest.config.Parser.addini>`.
|
||||
@@ -54,38 +74,89 @@ def pytest_addoption(parser):
|
||||
a value read from an ini-style file.
|
||||
|
||||
The config object is passed around on many internal objects via the ``.config``
|
||||
attribute or can be retrieved as the ``pytestconfig`` fixture or accessed
|
||||
via (deprecated) ``pytest.config``.
|
||||
attribute or can be retrieved as the ``pytestconfig`` fixture.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_configure(config):
|
||||
""" called after command line options have been parsed
|
||||
and all plugins and initial conftest files been loaded.
|
||||
This hook is called for every plugin.
|
||||
"""
|
||||
Allows plugins and conftest files to perform initial configuration.
|
||||
|
||||
This hook is called for every plugin and initial conftest file
|
||||
after command line options have been parsed.
|
||||
|
||||
After that, the hook is called for other conftest files as they are
|
||||
imported.
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
|
||||
:arg _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Bootstrapping hooks called for plugins registered early enough:
|
||||
# internal and 3rd party plugins as well as directly
|
||||
# discoverable conftest.py local plugins.
|
||||
# internal and 3rd party plugins.
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_cmdline_parse(pluginmanager, args):
|
||||
"""return initialized config object, parsing the specified args. """
|
||||
"""return initialized config object, parsing the specified args.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
|
||||
:param list[str] args: list of arguments passed on the command line
|
||||
"""
|
||||
|
||||
|
||||
def pytest_cmdline_preparse(config, args):
|
||||
"""(deprecated) modify command line arguments before option parsing. """
|
||||
"""(**Deprecated**) modify command line arguments before option parsing.
|
||||
|
||||
This hook is considered deprecated and will be removed in a future pytest version. Consider
|
||||
using :func:`pytest_load_initial_conftests` instead.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param list[str] args: list of arguments passed on the command line
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_cmdline_main(config):
|
||||
""" called for performing the main command line action. The default
|
||||
implementation will invoke the configure hooks and runtest_mainloop. """
|
||||
implementation will invoke the configure hooks and runtest_mainloop.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
""" implements the loading of initial conftest files ahead
|
||||
of command line option parsing. """
|
||||
of command line option parsing.
|
||||
|
||||
.. note::
|
||||
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
|
||||
|
||||
:param _pytest.config.Config early_config: pytest config object
|
||||
:param list[str] args: list of arguments passed on the command line
|
||||
:param _pytest.config.Parser parser: to add command line options
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -94,88 +165,151 @@ def pytest_load_initial_conftests(early_config, parser, args):
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_collection(session):
|
||||
""" perform the collection protocol for the given session. """
|
||||
"""Perform the collection protocol for the given session.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(session, config, items):
|
||||
""" called after collection has been performed, may filter or re-order
|
||||
the items in-place."""
|
||||
the items in-place.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param List[_pytest.nodes.Item] items: list of item objects
|
||||
"""
|
||||
|
||||
|
||||
def pytest_collection_finish(session):
|
||||
""" called after collection has been performed and modified. """
|
||||
""" called after collection has been performed and modified.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_ignore_collect(path, config):
|
||||
""" return True to prevent considering this path for collection.
|
||||
This hook is consulted for all files and directories prior to calling
|
||||
more specific hooks.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param str path: the path to analyze
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_collect_directory(path, parent):
|
||||
""" called before traversing a directory for collection files. """
|
||||
""" called before traversing a directory for collection files.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param str path: the path to analyze
|
||||
"""
|
||||
|
||||
|
||||
def pytest_collect_file(path, parent):
|
||||
""" return collection Node or None for the given path. Any new node
|
||||
needs to have the specified ``parent`` as a parent."""
|
||||
needs to have the specified ``parent`` as a parent.
|
||||
|
||||
:param str path: the path to collect
|
||||
"""
|
||||
|
||||
# logging hooks for collection
|
||||
|
||||
|
||||
def pytest_collectstart(collector):
|
||||
""" collector starts collecting. """
|
||||
|
||||
|
||||
def pytest_itemcollected(item):
|
||||
""" we just collected a test item. """
|
||||
|
||||
|
||||
def pytest_collectreport(report):
|
||||
""" collector finished collecting. """
|
||||
|
||||
|
||||
def pytest_deselected(items):
|
||||
""" called for test items deselected by keyword. """
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_make_collect_report(collector):
|
||||
""" perform ``collector.collect()`` and return a CollectReport. """
|
||||
""" perform ``collector.collect()`` and return a CollectReport.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Python test function related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_pycollect_makemodule(path, parent):
|
||||
""" return a Module collector or None for the given path.
|
||||
This hook will be called for each matching test module path.
|
||||
The pytest_collect_file hook needs to be used if you want to
|
||||
create test modules for files that do not match as a test module.
|
||||
"""
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
""" return custom item/collector for a python object in a module, or None. """
|
||||
""" return custom item/collector for a python object in a module, or None.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
""" call underlying test function. """
|
||||
""" call underlying test function.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
""" generate (multiple) parametrized calls to a test function."""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_make_parametrize_id(config, val, argname):
|
||||
"""Return a user-friendly string representation of the given ``val`` that will be used
|
||||
by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``.
|
||||
The parameter name is available as ``argname``, if required.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param val: the parametrized value
|
||||
:param str argname: the automatic parameter name produced by pytest
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# generic runtest related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_runtestloop(session):
|
||||
""" called for performing the main runtest loop
|
||||
(after collection finished). """
|
||||
(after collection finished).
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_itemstart(item, node):
|
||||
""" (deprecated, use pytest_runtest_logstart). """
|
||||
"""(**Deprecated**) use pytest_runtest_logstart. """
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_runtest_protocol(item, nextitem):
|
||||
@@ -190,17 +324,41 @@ def pytest_runtest_protocol(item, nextitem):
|
||||
:py:func:`pytest_runtest_teardown`.
|
||||
|
||||
:return boolean: True if no further hook implementations should be invoked.
|
||||
"""
|
||||
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
|
||||
|
||||
def pytest_runtest_logstart(nodeid, location):
|
||||
""" signal the start of running a single test item. """
|
||||
""" signal the start of running a single test item.
|
||||
|
||||
This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and
|
||||
:func:`pytest_runtest_teardown` hooks.
|
||||
|
||||
:param str nodeid: full id of the item
|
||||
:param location: a triple of ``(filename, linenum, testname)``
|
||||
"""
|
||||
|
||||
|
||||
def pytest_runtest_logfinish(nodeid, location):
|
||||
""" signal the complete finish of running a single test item.
|
||||
|
||||
This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and
|
||||
:func:`pytest_runtest_teardown` hooks.
|
||||
|
||||
:param str nodeid: full id of the item
|
||||
:param location: a triple of ``(filename, linenum, testname)``
|
||||
"""
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
""" called before ``pytest_runtest_call(item)``. """
|
||||
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
""" called to execute the test ``item``. """
|
||||
|
||||
|
||||
def pytest_runtest_teardown(item, nextitem):
|
||||
""" called after ``pytest_runtest_call``.
|
||||
|
||||
@@ -210,12 +368,15 @@ def pytest_runtest_teardown(item, nextitem):
|
||||
so that nextitem only needs to call setup-functions.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
""" return a :py:class:`_pytest.runner.TestReport` object
|
||||
for the given :py:class:`pytest.Item` and
|
||||
for the given :py:class:`pytest.Item <_pytest.main.Item>` and
|
||||
:py:class:`_pytest.runner.CallInfo`.
|
||||
"""
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
|
||||
|
||||
def pytest_runtest_logreport(report):
|
||||
""" process a test setup/call/teardown report relating to
|
||||
@@ -225,11 +386,23 @@ def pytest_runtest_logreport(report):
|
||||
# Fixture related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
""" performs fixture setup execution. """
|
||||
""" performs fixture setup execution.
|
||||
|
||||
def pytest_fixture_post_finalizer(fixturedef):
|
||||
:return: The return value of the call to the fixture function
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult`
|
||||
|
||||
.. note::
|
||||
If the fixture function returns None, other implementations of
|
||||
this hook function will continue to be called, according to the
|
||||
behavior of the :ref:`firstresult` option.
|
||||
"""
|
||||
|
||||
|
||||
def pytest_fixture_post_finalizer(fixturedef, request):
|
||||
""" called after fixture teardown, but before the cache is cleared so
|
||||
the fixture result cache ``fixturedef.cached_result`` can
|
||||
still be accessed."""
|
||||
@@ -238,14 +411,27 @@ def pytest_fixture_post_finalizer(fixturedef):
|
||||
# test session related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
""" before session.main() is called. """
|
||||
""" before session.main() is called.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
"""
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
""" whole test run finishes. """
|
||||
""" whole test run finishes.
|
||||
|
||||
:param _pytest.main.Session session: the pytest session object
|
||||
:param int exitstatus: the status which pytest will return to the system
|
||||
"""
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
""" called before test process is exited. """
|
||||
""" called before test process is exited.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -259,14 +445,20 @@ def pytest_assertrepr_compare(config, op, left, right):
|
||||
of strings. The strings will be joined by newlines but any newlines
|
||||
*in* a string will be escaped. Note that all but the first line will
|
||||
be indented slightly, the intention is for the first line to be a summary.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# hooks for influencing reporting (invoked from _pytest_terminal)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
""" return a string to be displayed as header info for terminal reporting.
|
||||
""" return a string or list of strings to be displayed as header info for terminal reporting.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param startdir: py.path object with the starting dir
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -275,9 +467,27 @@ def pytest_report_header(config, startdir):
|
||||
:ref:`discovers plugins during startup <pluginorder>`.
|
||||
"""
|
||||
|
||||
|
||||
def pytest_report_collectionfinish(config, startdir, items):
|
||||
"""
|
||||
.. versionadded:: 3.2
|
||||
|
||||
return a string or list of strings to be displayed after collection has finished successfully.
|
||||
|
||||
This strings will be displayed after the standard "collected X items" message.
|
||||
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
:param startdir: py.path object with the starting dir
|
||||
:param items: list of pytest items that are going to be executed; this list should not be modified.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_report_teststatus(report):
|
||||
""" return result-category, shortletter and verbose word for reporting."""
|
||||
""" return result-category, shortletter and verbose word for reporting.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
|
||||
|
||||
def pytest_terminal_summary(terminalreporter, exitstatus):
|
||||
""" add additional section in terminal summary reporting. """
|
||||
@@ -287,26 +497,36 @@ def pytest_terminal_summary(terminalreporter, exitstatus):
|
||||
def pytest_logwarning(message, code, nodeid, fslocation):
|
||||
""" 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 partilar node/location)."""
|
||||
if the warning is not tied to a particular node/location).
|
||||
|
||||
.. note::
|
||||
This hook is incompatible with ``hookwrapper=True``.
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# doctest hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_doctest_prepare_content(content):
|
||||
""" return processed content for a given doctest"""
|
||||
""" return processed content for a given doctest
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# error handling and internal debugging hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_internalerror(excrepr, excinfo):
|
||||
""" called for internal errors. """
|
||||
|
||||
|
||||
def pytest_keyboard_interrupt(excinfo):
|
||||
""" called for keyboard interrupt. """
|
||||
|
||||
|
||||
def pytest_exception_interact(node, call, report):
|
||||
"""called when an exception was raised which can potentially be
|
||||
interactively handled.
|
||||
@@ -315,10 +535,10 @@ def pytest_exception_interact(node, call, report):
|
||||
that is not an internal exception like ``skip.Exception``.
|
||||
"""
|
||||
|
||||
|
||||
def pytest_enter_pdb(config):
|
||||
""" called upon pdb.set_trace(), can be used by plugins to take special
|
||||
action just before the python debugger enters in interactive mode.
|
||||
|
||||
:arg config: pytest config object
|
||||
:type config: _pytest.config.Config
|
||||
:param _pytest.config.Config config: pytest config object
|
||||
"""
|
||||
|
||||
254
_pytest/impl
254
_pytest/impl
@@ -1,254 +0,0 @@
|
||||
Sorting per-resource
|
||||
-----------------------------
|
||||
|
||||
for any given set of items:
|
||||
|
||||
- collect items per session-scoped parametrized funcarg
|
||||
- re-order until items no parametrizations are mixed
|
||||
|
||||
examples:
|
||||
|
||||
test()
|
||||
test1(s1)
|
||||
test1(s2)
|
||||
test2()
|
||||
test3(s1)
|
||||
test3(s2)
|
||||
|
||||
gets sorted to:
|
||||
|
||||
test()
|
||||
test2()
|
||||
test1(s1)
|
||||
test3(s1)
|
||||
test1(s2)
|
||||
test3(s2)
|
||||
|
||||
|
||||
the new @setup functions
|
||||
--------------------------------------
|
||||
|
||||
Consider a given @setup-marked function::
|
||||
|
||||
@pytest.mark.setup(maxscope=SCOPE)
|
||||
def mysetup(request, arg1, arg2, ...)
|
||||
...
|
||||
request.addfinalizer(fin)
|
||||
...
|
||||
|
||||
then FUNCARGSET denotes the set of (arg1, arg2, ...) funcargs and
|
||||
all of its dependent funcargs. The mysetup function will execute
|
||||
for any matching test item once per scope.
|
||||
|
||||
The scope is determined as the minimum scope of all scopes of the args
|
||||
in FUNCARGSET and the given "maxscope".
|
||||
|
||||
If mysetup has been called and no finalizers have been called it is
|
||||
called "active".
|
||||
|
||||
Furthermore the following rules apply:
|
||||
|
||||
- if an arg value in FUNCARGSET is about to be torn down, the
|
||||
mysetup-registered finalizers will execute as well.
|
||||
|
||||
- There will never be two active mysetup invocations.
|
||||
|
||||
Example 1, session scope::
|
||||
|
||||
@pytest.mark.funcarg(scope="session", params=[1,2])
|
||||
def db(request):
|
||||
request.addfinalizer(db_finalize)
|
||||
|
||||
@pytest.mark.setup
|
||||
def mysetup(request, db):
|
||||
request.addfinalizer(mysetup_finalize)
|
||||
...
|
||||
|
||||
And a given test module:
|
||||
|
||||
def test_something():
|
||||
...
|
||||
def test_otherthing():
|
||||
pass
|
||||
|
||||
Here is what happens::
|
||||
|
||||
db(request) executes with request.param == 1
|
||||
mysetup(request, db) executes
|
||||
test_something() executes
|
||||
test_otherthing() executes
|
||||
mysetup_finalize() executes
|
||||
db_finalize() executes
|
||||
db(request) executes with request.param == 2
|
||||
mysetup(request, db) executes
|
||||
test_something() executes
|
||||
test_otherthing() executes
|
||||
mysetup_finalize() executes
|
||||
db_finalize() executes
|
||||
|
||||
Example 2, session/function scope::
|
||||
|
||||
@pytest.mark.funcarg(scope="session", params=[1,2])
|
||||
def db(request):
|
||||
request.addfinalizer(db_finalize)
|
||||
|
||||
@pytest.mark.setup(scope="function")
|
||||
def mysetup(request, db):
|
||||
...
|
||||
request.addfinalizer(mysetup_finalize)
|
||||
...
|
||||
|
||||
And a given test module:
|
||||
|
||||
def test_something():
|
||||
...
|
||||
def test_otherthing():
|
||||
pass
|
||||
|
||||
Here is what happens::
|
||||
|
||||
db(request) executes with request.param == 1
|
||||
mysetup(request, db) executes
|
||||
test_something() executes
|
||||
mysetup_finalize() executes
|
||||
mysetup(request, db) executes
|
||||
test_otherthing() executes
|
||||
mysetup_finalize() executes
|
||||
db_finalize() executes
|
||||
db(request) executes with request.param == 2
|
||||
mysetup(request, db) executes
|
||||
test_something() executes
|
||||
mysetup_finalize() executes
|
||||
mysetup(request, db) executes
|
||||
test_otherthing() executes
|
||||
mysetup_finalize() executes
|
||||
db_finalize() executes
|
||||
|
||||
|
||||
Example 3 - funcargs session-mix
|
||||
----------------------------------------
|
||||
|
||||
Similar with funcargs, an example::
|
||||
|
||||
@pytest.mark.funcarg(scope="session", params=[1,2])
|
||||
def db(request):
|
||||
request.addfinalizer(db_finalize)
|
||||
|
||||
@pytest.mark.funcarg(scope="function")
|
||||
def table(request, db):
|
||||
...
|
||||
request.addfinalizer(table_finalize)
|
||||
...
|
||||
|
||||
And a given test module:
|
||||
|
||||
def test_something(table):
|
||||
...
|
||||
def test_otherthing(table):
|
||||
pass
|
||||
def test_thirdthing():
|
||||
pass
|
||||
|
||||
Here is what happens::
|
||||
|
||||
db(request) executes with param == 1
|
||||
table(request, db)
|
||||
test_something(table)
|
||||
table_finalize()
|
||||
table(request, db)
|
||||
test_otherthing(table)
|
||||
table_finalize()
|
||||
db_finalize
|
||||
db(request) executes with param == 2
|
||||
table(request, db)
|
||||
test_something(table)
|
||||
table_finalize()
|
||||
table(request, db)
|
||||
test_otherthing(table)
|
||||
table_finalize()
|
||||
db_finalize
|
||||
test_thirdthing()
|
||||
|
||||
Data structures
|
||||
--------------------
|
||||
|
||||
pytest internally maintains a dict of active funcargs with cache, param,
|
||||
finalizer, (scopeitem?) information:
|
||||
|
||||
active_funcargs = dict()
|
||||
|
||||
if a parametrized "db" is activated:
|
||||
|
||||
active_funcargs["db"] = FuncargInfo(dbvalue, paramindex,
|
||||
FuncargFinalize(...), scopeitem)
|
||||
|
||||
if a test is torn down and the next test requires a differently
|
||||
parametrized "db":
|
||||
|
||||
for argname in item.callspec.params:
|
||||
if argname in active_funcargs:
|
||||
funcarginfo = active_funcargs[argname]
|
||||
if funcarginfo.param != item.callspec.params[argname]:
|
||||
funcarginfo.callfinalizer()
|
||||
del node2funcarg[funcarginfo.scopeitem]
|
||||
del active_funcargs[argname]
|
||||
nodes_to_be_torn_down = ...
|
||||
for node in nodes_to_be_torn_down:
|
||||
if node in node2funcarg:
|
||||
argname = node2funcarg[node]
|
||||
active_funcargs[argname].callfinalizer()
|
||||
del node2funcarg[node]
|
||||
del active_funcargs[argname]
|
||||
|
||||
if a test is setup requiring a "db" funcarg:
|
||||
|
||||
if "db" in active_funcargs:
|
||||
return active_funcargs["db"][0]
|
||||
funcarginfo = setup_funcarg()
|
||||
active_funcargs["db"] = funcarginfo
|
||||
node2funcarg[funcarginfo.scopeitem] = "db"
|
||||
|
||||
Implementation plan for resources
|
||||
------------------------------------------
|
||||
|
||||
1. Revert FuncargRequest to the old form, unmerge item/request
|
||||
(done)
|
||||
2. make funcarg factories be discovered at collection time
|
||||
3. Introduce funcarg marker
|
||||
4. Introduce funcarg scope parameter
|
||||
5. Introduce funcarg parametrize parameter
|
||||
6. make setup functions be discovered at collection time
|
||||
7. (Introduce a pytest_fixture_protocol/setup_funcargs hook)
|
||||
|
||||
methods and data structures
|
||||
--------------------------------
|
||||
|
||||
A FuncarcManager holds all information about funcarg definitions
|
||||
including parametrization and scope definitions. It implements
|
||||
a pytest_generate_tests hook which performs parametrization as appropriate.
|
||||
|
||||
as a simple example, let's consider a tree where a test function requires
|
||||
a "abc" funcarg and its factory defines it as parametrized and scoped
|
||||
for Modules. When collections hits the function item, it creates
|
||||
the metafunc object, and calls funcargdb.pytest_generate_tests(metafunc)
|
||||
which looks up available funcarg factories and their scope and parametrization.
|
||||
This information is equivalent to what can be provided today directly
|
||||
at the function site and it should thus be relatively straight forward
|
||||
to implement the additional way of defining parametrization/scoping.
|
||||
|
||||
conftest loading:
|
||||
each funcarg-factory will populate the session.funcargmanager
|
||||
|
||||
When a test item is collected, it grows a dictionary
|
||||
(funcargname2factorycalllist). A factory lookup is performed
|
||||
for each required funcarg. The resulting factory call is stored
|
||||
with the item. If a function is parametrized multiple items are
|
||||
created with respective factory calls. Else if a factory is parametrized
|
||||
multiple items and calls to the factory function are created as well.
|
||||
|
||||
At setup time, an item populates a funcargs mapping, mapping names
|
||||
to values. If a value is funcarg factories are queried for a given item
|
||||
test functions and setup functions are put in a class
|
||||
which looks up required funcarg factories.
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import re
|
||||
import sys
|
||||
import time
|
||||
import pytest
|
||||
from _pytest import nodes
|
||||
from _pytest.config import filename_arg
|
||||
|
||||
# Python 2.X and 3.X compatibility
|
||||
@@ -84,6 +85,9 @@ class _NodeReporter(object):
|
||||
def add_property(self, name, value):
|
||||
self.properties.append((str(name), bin_xml_escape(value)))
|
||||
|
||||
def add_attribute(self, name, value):
|
||||
self.attrs[str(name)] = bin_xml_escape(value)
|
||||
|
||||
def make_properties_node(self):
|
||||
"""Return a Junit node containing custom properties, if any.
|
||||
"""
|
||||
@@ -97,6 +101,7 @@ class _NodeReporter(object):
|
||||
def record_testreport(self, testreport):
|
||||
assert not self.testcase
|
||||
names = mangle_test_address(testreport.nodeid)
|
||||
existing_attrs = self.attrs
|
||||
classnames = names[:-1]
|
||||
if self.xml.prefix:
|
||||
classnames.insert(0, self.xml.prefix)
|
||||
@@ -110,6 +115,7 @@ class _NodeReporter(object):
|
||||
if hasattr(testreport, "url"):
|
||||
attrs["url"] = testreport.url
|
||||
self.attrs = attrs
|
||||
self.attrs.update(existing_attrs) # restore any user-defined attributes
|
||||
|
||||
def to_xml(self):
|
||||
testcase = Junit.testcase(time=self.duration, **self.attrs)
|
||||
@@ -210,6 +216,27 @@ def record_xml_property(request):
|
||||
return add_property_noop
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def record_xml_attribute(request):
|
||||
"""Add extra xml attributes to the tag for the calling test.
|
||||
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',
|
||||
)
|
||||
xml = getattr(request.config, "_xml", None)
|
||||
if xml is not None:
|
||||
node_reporter = xml.node_reporter(request.node.nodeid)
|
||||
return node_reporter.add_attribute
|
||||
else:
|
||||
def add_attr_noop(name, value):
|
||||
pass
|
||||
|
||||
return add_attr_noop
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group.addoption(
|
||||
@@ -252,7 +279,7 @@ def mangle_test_address(address):
|
||||
except ValueError:
|
||||
pass
|
||||
# convert file path to dotted path
|
||||
names[0] = names[0].replace("/", '.')
|
||||
names[0] = names[0].replace(nodes.SEP, '.')
|
||||
names[0] = _py_ext_re.sub("", names[0])
|
||||
# put any params back
|
||||
names[-1] += possible_open_bracket + params
|
||||
@@ -378,8 +405,8 @@ class LogXML(object):
|
||||
close_report = next(
|
||||
(rep for rep in self.open_reports
|
||||
if (rep.nodeid == report.nodeid and
|
||||
getattr(rep, "item_index", None) == report_ii and
|
||||
getattr(rep, "worker_id", None) == report_wid
|
||||
getattr(rep, "item_index", None) == report_ii and
|
||||
getattr(rep, "worker_id", None) == report_wid
|
||||
)
|
||||
), None)
|
||||
if close_report:
|
||||
@@ -444,9 +471,9 @@ class LogXML(object):
|
||||
"""
|
||||
if self.global_properties:
|
||||
return Junit.properties(
|
||||
[
|
||||
Junit.property(name=name, value=value)
|
||||
for name, value in self.global_properties
|
||||
]
|
||||
[
|
||||
Junit.property(name=name, value=value)
|
||||
for name, value in self.global_properties
|
||||
]
|
||||
)
|
||||
return ''
|
||||
|
||||
487
_pytest/logging.py
Normal file
487
_pytest/logging.py
Normal file
@@ -0,0 +1,487 @@
|
||||
""" Access and control log capturing. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import logging
|
||||
from contextlib import closing, contextmanager
|
||||
import re
|
||||
import six
|
||||
|
||||
from _pytest.config import create_terminal_writer
|
||||
import pytest
|
||||
import py
|
||||
|
||||
|
||||
DEFAULT_LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
|
||||
DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S'
|
||||
|
||||
|
||||
class ColoredLevelFormatter(logging.Formatter):
|
||||
"""
|
||||
Colorize the %(levelname)..s part of the log format passed to __init__.
|
||||
"""
|
||||
|
||||
LOGLEVEL_COLOROPTS = {
|
||||
logging.CRITICAL: {'red'},
|
||||
logging.ERROR: {'red', 'bold'},
|
||||
logging.WARNING: {'yellow'},
|
||||
logging.WARN: {'yellow'},
|
||||
logging.INFO: {'green'},
|
||||
logging.DEBUG: {'purple'},
|
||||
logging.NOTSET: set(),
|
||||
}
|
||||
LEVELNAME_FMT_REGEX = re.compile(r'%\(levelname\)([+-]?\d*s)')
|
||||
|
||||
def __init__(self, terminalwriter, *args, **kwargs):
|
||||
super(ColoredLevelFormatter, self).__init__(
|
||||
*args, **kwargs)
|
||||
if six.PY2:
|
||||
self._original_fmt = self._fmt
|
||||
else:
|
||||
self._original_fmt = self._style._fmt
|
||||
self._level_to_fmt_mapping = {}
|
||||
|
||||
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
|
||||
if not levelname_fmt_match:
|
||||
return
|
||||
levelname_fmt = levelname_fmt_match.group()
|
||||
|
||||
for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
|
||||
formatted_levelname = levelname_fmt % {
|
||||
'levelname': logging.getLevelName(level)}
|
||||
|
||||
# add ANSI escape sequences around the formatted levelname
|
||||
color_kwargs = {name: True for name in color_opts}
|
||||
colorized_formatted_levelname = terminalwriter.markup(
|
||||
formatted_levelname, **color_kwargs)
|
||||
self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
|
||||
colorized_formatted_levelname,
|
||||
self._fmt)
|
||||
|
||||
def format(self, record):
|
||||
fmt = self._level_to_fmt_mapping.get(
|
||||
record.levelno, self._original_fmt)
|
||||
if six.PY2:
|
||||
self._fmt = fmt
|
||||
else:
|
||||
self._style._fmt = fmt
|
||||
return super(ColoredLevelFormatter, self).format(record)
|
||||
|
||||
|
||||
def get_option_ini(config, *names):
|
||||
for name in names:
|
||||
ret = config.getoption(name) # 'default' arg won't work as expected
|
||||
if ret is None:
|
||||
ret = config.getini(name)
|
||||
if ret:
|
||||
return ret
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
"""Add options to control log capturing."""
|
||||
group = parser.getgroup('logging')
|
||||
|
||||
def add_option_ini(option, dest, default=None, type=None, **kwargs):
|
||||
parser.addini(dest, default=default, type=type,
|
||||
help='default value for ' + option)
|
||||
group.addoption(option, dest=dest, **kwargs)
|
||||
|
||||
add_option_ini(
|
||||
'--no-print-logs',
|
||||
dest='log_print', action='store_const', const=False, default=True,
|
||||
type='bool',
|
||||
help='disable printing caught logs on failed tests.')
|
||||
add_option_ini(
|
||||
'--log-level',
|
||||
dest='log_level', default=None,
|
||||
help='logging level used by the logging module')
|
||||
add_option_ini(
|
||||
'--log-format',
|
||||
dest='log_format', default=DEFAULT_LOG_FORMAT,
|
||||
help='log format as used by the logging module.')
|
||||
add_option_ini(
|
||||
'--log-date-format',
|
||||
dest='log_date_format', default=DEFAULT_LOG_DATE_FORMAT,
|
||||
help='log date format as used by the logging module.')
|
||||
parser.addini(
|
||||
'log_cli', default=False, type='bool',
|
||||
help='enable log display during test run (also known as "live logging").')
|
||||
add_option_ini(
|
||||
'--log-cli-level',
|
||||
dest='log_cli_level', default=None,
|
||||
help='cli logging level.')
|
||||
add_option_ini(
|
||||
'--log-cli-format',
|
||||
dest='log_cli_format', default=None,
|
||||
help='log format as used by the logging module.')
|
||||
add_option_ini(
|
||||
'--log-cli-date-format',
|
||||
dest='log_cli_date_format', default=None,
|
||||
help='log date format as used by the logging module.')
|
||||
add_option_ini(
|
||||
'--log-file',
|
||||
dest='log_file', default=None,
|
||||
help='path to a file when logging will be written to.')
|
||||
add_option_ini(
|
||||
'--log-file-level',
|
||||
dest='log_file_level', default=None,
|
||||
help='log file logging level.')
|
||||
add_option_ini(
|
||||
'--log-file-format',
|
||||
dest='log_file_format', default=DEFAULT_LOG_FORMAT,
|
||||
help='log format as used by the logging module.')
|
||||
add_option_ini(
|
||||
'--log-file-date-format',
|
||||
dest='log_file_date_format', default=DEFAULT_LOG_DATE_FORMAT,
|
||||
help='log date format as used by the logging module.')
|
||||
|
||||
|
||||
@contextmanager
|
||||
def catching_logs(handler, formatter=None, level=None):
|
||||
"""Context manager that prepares the whole logging machinery properly."""
|
||||
root_logger = logging.getLogger()
|
||||
|
||||
if formatter is not None:
|
||||
handler.setFormatter(formatter)
|
||||
if level is not None:
|
||||
handler.setLevel(level)
|
||||
|
||||
# Adding the same handler twice would confuse logging system.
|
||||
# Just don't do that.
|
||||
add_new_handler = handler not in root_logger.handlers
|
||||
|
||||
if add_new_handler:
|
||||
root_logger.addHandler(handler)
|
||||
if level is not None:
|
||||
orig_level = root_logger.level
|
||||
root_logger.setLevel(level)
|
||||
try:
|
||||
yield handler
|
||||
finally:
|
||||
if level is not None:
|
||||
root_logger.setLevel(orig_level)
|
||||
if add_new_handler:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
|
||||
class LogCaptureHandler(logging.StreamHandler):
|
||||
"""A logging handler that stores log records and the log text."""
|
||||
|
||||
def __init__(self):
|
||||
"""Creates a new log handler."""
|
||||
logging.StreamHandler.__init__(self, py.io.TextIO())
|
||||
self.records = []
|
||||
|
||||
def emit(self, record):
|
||||
"""Keep the log records in a list in addition to the log text."""
|
||||
self.records.append(record)
|
||||
logging.StreamHandler.emit(self, record)
|
||||
|
||||
|
||||
class LogCaptureFixture(object):
|
||||
"""Provides access and control of log capturing."""
|
||||
|
||||
def __init__(self, item):
|
||||
"""Creates a new funcarg."""
|
||||
self._item = item
|
||||
self._initial_log_levels = {} # type: Dict[str, int] # dict of log name -> log level
|
||||
|
||||
def _finalize(self):
|
||||
"""Finalizes the fixture.
|
||||
|
||||
This restores the log levels changed by :meth:`set_level`.
|
||||
"""
|
||||
# restore log levels
|
||||
for logger_name, level in self._initial_log_levels.items():
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(level)
|
||||
|
||||
@property
|
||||
def handler(self):
|
||||
return self._item.catch_log_handler
|
||||
|
||||
def get_records(self, when):
|
||||
"""
|
||||
Get the logging records for one of the possible test phases.
|
||||
|
||||
:param str when:
|
||||
Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
|
||||
|
||||
:rtype: List[logging.LogRecord]
|
||||
:return: the list of captured records at the given stage
|
||||
|
||||
.. versionadded:: 3.4
|
||||
"""
|
||||
handler = self._item.catch_log_handlers.get(when)
|
||||
if handler:
|
||||
return handler.records
|
||||
else:
|
||||
return []
|
||||
|
||||
@property
|
||||
def text(self):
|
||||
"""Returns the log text."""
|
||||
return self.handler.stream.getvalue()
|
||||
|
||||
@property
|
||||
def records(self):
|
||||
"""Returns the list of log records."""
|
||||
return self.handler.records
|
||||
|
||||
@property
|
||||
def record_tuples(self):
|
||||
"""Returns a list of a striped down version of log records intended
|
||||
for use in assertion comparison.
|
||||
|
||||
The format of the tuple is:
|
||||
|
||||
(logger_name, log_level, message)
|
||||
"""
|
||||
return [(r.name, r.levelno, r.getMessage()) for r in self.records]
|
||||
|
||||
def clear(self):
|
||||
"""Reset the list of log records."""
|
||||
self.handler.records = []
|
||||
|
||||
def set_level(self, level, logger=None):
|
||||
"""Sets the level for capturing of logs. The level will be restored to its previous value at the end of
|
||||
the test.
|
||||
|
||||
:param int level: the logger to level.
|
||||
:param str logger: the logger to update the level. If not given, the root logger level is updated.
|
||||
|
||||
.. versionchanged:: 3.4
|
||||
The levels of the loggers changed by this function will be restored to their initial values at the
|
||||
end of the test.
|
||||
"""
|
||||
logger_name = logger
|
||||
logger = logging.getLogger(logger_name)
|
||||
# save the original log-level to restore it during teardown
|
||||
self._initial_log_levels.setdefault(logger_name, logger.level)
|
||||
logger.setLevel(level)
|
||||
|
||||
@contextmanager
|
||||
def at_level(self, level, logger=None):
|
||||
"""Context manager that sets the level for capturing of logs. After the end of the 'with' statement the
|
||||
level is restored to its original value.
|
||||
|
||||
:param int level: the logger to level.
|
||||
:param str logger: the logger to update the level. If not given, the root logger level is updated.
|
||||
"""
|
||||
logger = logging.getLogger(logger)
|
||||
orig_level = logger.level
|
||||
logger.setLevel(level)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
logger.setLevel(orig_level)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def caplog(request):
|
||||
"""Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following methods::
|
||||
|
||||
* caplog.text() -> string containing formatted log output
|
||||
* caplog.records() -> list of logging.LogRecord instances
|
||||
* caplog.record_tuples() -> list of (logger_name, level, message) tuples
|
||||
"""
|
||||
result = LogCaptureFixture(request.node)
|
||||
yield result
|
||||
result._finalize()
|
||||
|
||||
|
||||
def get_actual_log_level(config, *setting_names):
|
||||
"""Return the actual logging level."""
|
||||
|
||||
for setting_name in setting_names:
|
||||
log_level = config.getoption(setting_name)
|
||||
if log_level is None:
|
||||
log_level = config.getini(setting_name)
|
||||
if log_level:
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
if isinstance(log_level, six.string_types):
|
||||
log_level = log_level.upper()
|
||||
try:
|
||||
return int(getattr(logging, log_level, log_level))
|
||||
except ValueError:
|
||||
# Python logging does not recognise this as a logging level
|
||||
raise pytest.UsageError(
|
||||
"'{0}' is not recognized as a logging level name for "
|
||||
"'{1}'. Please consider passing the "
|
||||
"logging level num instead.".format(
|
||||
log_level,
|
||||
setting_name))
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def __init__(self, config):
|
||||
"""Creates a new plugin to capture log messages.
|
||||
|
||||
The formatter can be safely shared across all handlers so
|
||||
create a single one for the entire test session here.
|
||||
"""
|
||||
self._config = config
|
||||
|
||||
# enable verbose output automatically if live logging is enabled
|
||||
if self._config.getini('log_cli') and not config.getoption('verbose'):
|
||||
# sanity check: terminal reporter should not have been loaded at this point
|
||||
assert self._config.pluginmanager.get_plugin('terminalreporter') is None
|
||||
config.option.verbose = 1
|
||||
|
||||
self.print_logs = get_option_ini(config, 'log_print')
|
||||
self.formatter = logging.Formatter(get_option_ini(config, 'log_format'),
|
||||
get_option_ini(config, 'log_date_format'))
|
||||
self.log_level = get_actual_log_level(config, 'log_level')
|
||||
|
||||
log_file = get_option_ini(config, 'log_file')
|
||||
if log_file:
|
||||
self.log_file_level = get_actual_log_level(config, 'log_file_level')
|
||||
|
||||
log_file_format = get_option_ini(config, 'log_file_format', 'log_format')
|
||||
log_file_date_format = get_option_ini(config, 'log_file_date_format', 'log_date_format')
|
||||
# Each pytest runtests session will write to a clean logfile
|
||||
self.log_file_handler = logging.FileHandler(log_file, mode='w')
|
||||
log_file_formatter = logging.Formatter(log_file_format, datefmt=log_file_date_format)
|
||||
self.log_file_handler.setFormatter(log_file_formatter)
|
||||
else:
|
||||
self.log_file_handler = None
|
||||
|
||||
# initialized during pytest_runtestloop
|
||||
self.log_cli_handler = None
|
||||
|
||||
@contextmanager
|
||||
def _runtest_for(self, item, when):
|
||||
"""Implements the internals of pytest_runtest_xxx() hook."""
|
||||
with catching_logs(LogCaptureHandler(),
|
||||
formatter=self.formatter, level=self.log_level) as log_handler:
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.set_when(when)
|
||||
if not hasattr(item, 'catch_log_handlers'):
|
||||
item.catch_log_handlers = {}
|
||||
item.catch_log_handlers[when] = log_handler
|
||||
item.catch_log_handler = log_handler
|
||||
try:
|
||||
yield # run test
|
||||
finally:
|
||||
del item.catch_log_handler
|
||||
if when == 'teardown':
|
||||
del item.catch_log_handlers
|
||||
|
||||
if self.print_logs:
|
||||
# Add a captured log section to the report.
|
||||
log = log_handler.stream.getvalue().strip()
|
||||
item.add_report_section(when, 'log', log)
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_setup(self, item):
|
||||
with self._runtest_for(item, 'setup'):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_call(self, item):
|
||||
with self._runtest_for(item, 'call'):
|
||||
yield
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item):
|
||||
with self._runtest_for(item, 'teardown'):
|
||||
yield
|
||||
|
||||
def pytest_runtest_logstart(self):
|
||||
if self.log_cli_handler:
|
||||
self.log_cli_handler.reset()
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtestloop(self, session):
|
||||
"""Runs all collected test items."""
|
||||
self._setup_cli_logging()
|
||||
with self.live_logs_context:
|
||||
if self.log_file_handler is not None:
|
||||
with closing(self.log_file_handler):
|
||||
with catching_logs(self.log_file_handler,
|
||||
level=self.log_file_level):
|
||||
yield # run all the tests
|
||||
else:
|
||||
yield # run all the tests
|
||||
|
||||
def _setup_cli_logging(self):
|
||||
"""Sets up the handler and logger for the Live Logs feature, if enabled.
|
||||
|
||||
This must be done right before starting the loop so we can access the terminal reporter plugin.
|
||||
"""
|
||||
terminal_reporter = self._config.pluginmanager.get_plugin('terminalreporter')
|
||||
if self._config.getini('log_cli') and terminal_reporter is not None:
|
||||
capture_manager = self._config.pluginmanager.get_plugin('capturemanager')
|
||||
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
|
||||
log_cli_format = get_option_ini(self._config, 'log_cli_format', 'log_format')
|
||||
log_cli_date_format = get_option_ini(self._config, 'log_cli_date_format', 'log_date_format')
|
||||
if self._config.option.color != 'no' and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format):
|
||||
log_cli_formatter = ColoredLevelFormatter(create_terminal_writer(self._config),
|
||||
log_cli_format, datefmt=log_cli_date_format)
|
||||
else:
|
||||
log_cli_formatter = logging.Formatter(log_cli_format, datefmt=log_cli_date_format)
|
||||
log_cli_level = get_actual_log_level(self._config, 'log_cli_level', 'log_level')
|
||||
self.log_cli_handler = log_cli_handler
|
||||
self.live_logs_context = catching_logs(log_cli_handler, formatter=log_cli_formatter, level=log_cli_level)
|
||||
else:
|
||||
self.live_logs_context = _dummy_context_manager()
|
||||
|
||||
|
||||
class _LiveLoggingStreamHandler(logging.StreamHandler):
|
||||
"""
|
||||
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
|
||||
in each test.
|
||||
|
||||
During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
|
||||
and won't appear in the terminal.
|
||||
"""
|
||||
|
||||
def __init__(self, terminal_reporter, capture_manager):
|
||||
"""
|
||||
:param _pytest.terminal.TerminalReporter terminal_reporter:
|
||||
:param _pytest.capture.CaptureManager capture_manager:
|
||||
"""
|
||||
logging.StreamHandler.__init__(self, stream=terminal_reporter)
|
||||
self.capture_manager = capture_manager
|
||||
self.reset()
|
||||
self.set_when(None)
|
||||
|
||||
def reset(self):
|
||||
"""Reset the handler; should be called before the start of each test"""
|
||||
self._first_record_emitted = False
|
||||
|
||||
def set_when(self, when):
|
||||
"""Prepares for the given test phase (setup/call/teardown)"""
|
||||
self._when = when
|
||||
self._section_name_shown = False
|
||||
|
||||
def emit(self, record):
|
||||
if self.capture_manager is not None:
|
||||
self.capture_manager.suspend_global_capture()
|
||||
try:
|
||||
if not self._first_record_emitted or self._when == 'teardown':
|
||||
self.stream.write('\n')
|
||||
self._first_record_emitted = True
|
||||
if not self._section_name_shown and self._when:
|
||||
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()
|
||||
532
_pytest/main.py
532
_pytest/main.py
@@ -1,22 +1,22 @@
|
||||
""" core implementation of testing process: init, session, runtest loop. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import contextlib
|
||||
import functools
|
||||
import os
|
||||
import pkgutil
|
||||
import six
|
||||
import sys
|
||||
|
||||
import _pytest
|
||||
from _pytest import nodes
|
||||
import _pytest._code
|
||||
import py
|
||||
try:
|
||||
from collections import MutableMapping as MappingMixin
|
||||
except ImportError:
|
||||
from UserDict import DictMixin as MappingMixin
|
||||
|
||||
from _pytest.config import directory_arg, UsageError, hookimpl
|
||||
from _pytest.runner import collect_one_node, exit
|
||||
from _pytest.outcomes import exit
|
||||
from _pytest.runner import collect_one_node
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
# exitcodes for the command line
|
||||
EXIT_OK = 0
|
||||
@@ -29,66 +29,61 @@ EXIT_NOTESTSCOLLECTED = 5
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addini("norecursedirs", "directory patterns to avoid for recursion",
|
||||
type="args", default=['.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv'])
|
||||
parser.addini("testpaths", "directories to search for tests when no files or directories are given in the command line.",
|
||||
type="args", default=[])
|
||||
#parser.addini("dirpatterns",
|
||||
type="args", default=['.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv'])
|
||||
parser.addini("testpaths", "directories to search for tests when no files or directories are given in the "
|
||||
"command line.",
|
||||
type="args", default=[])
|
||||
# parser.addini("dirpatterns",
|
||||
# "patterns specifying possible locations of test files",
|
||||
# type="linelist", default=["**/test_*.txt",
|
||||
# "**/test_*.py", "**/*_test.py"]
|
||||
#)
|
||||
# )
|
||||
group = parser.getgroup("general", "running and selection options")
|
||||
group._addoption('-x', '--exitfirst', action="store_const",
|
||||
dest="maxfail", const=1,
|
||||
help="exit instantly on first error or failed test."),
|
||||
dest="maxfail", const=1,
|
||||
help="exit instantly on first error or failed test."),
|
||||
group._addoption('--maxfail', metavar="num",
|
||||
action="store", type=int, dest="maxfail", default=0,
|
||||
help="exit after first num failures or errors.")
|
||||
action="store", type=int, dest="maxfail", default=0,
|
||||
help="exit after first num failures or errors.")
|
||||
group._addoption('--strict', action="store_true",
|
||||
help="run pytest in strict mode, warnings become errors.")
|
||||
help="marks not registered in configuration file raise errors.")
|
||||
group._addoption("-c", metavar="file", type=str, dest="inifilename",
|
||||
help="load configuration from `file` instead of trying to locate one of the implicit configuration files.")
|
||||
help="load configuration from `file` instead of trying to locate one of the implicit "
|
||||
"configuration files.")
|
||||
group._addoption("--continue-on-collection-errors", action="store_true",
|
||||
default=False, dest="continue_on_collection_errors",
|
||||
help="Force test execution even if collection errors occur.")
|
||||
default=False, dest="continue_on_collection_errors",
|
||||
help="Force test execution even if collection errors occur.")
|
||||
|
||||
group = parser.getgroup("collect", "collection")
|
||||
group.addoption('--collectonly', '--collect-only', action="store_true",
|
||||
help="only collect tests, don't execute them."),
|
||||
help="only collect tests, don't execute them."),
|
||||
group.addoption('--pyargs', action="store_true",
|
||||
help="try to interpret all arguments as python packages.")
|
||||
help="try to interpret all arguments as python packages.")
|
||||
group.addoption("--ignore", action="append", metavar="path",
|
||||
help="ignore path during collection (multi-allowed).")
|
||||
help="ignore path during collection (multi-allowed).")
|
||||
# when changing this to --conf-cut-dir, config.py Conftest.setinitial
|
||||
# needs upgrading as well
|
||||
group.addoption('--confcutdir', dest="confcutdir", default=None,
|
||||
metavar="dir", type=functools.partial(directory_arg, optname="--confcutdir"),
|
||||
help="only load conftest.py's relative to specified dir.")
|
||||
metavar="dir", type=functools.partial(directory_arg, optname="--confcutdir"),
|
||||
help="only load conftest.py's relative to specified dir.")
|
||||
group.addoption('--noconftest', action="store_true",
|
||||
dest="noconftest", default=False,
|
||||
help="Don't load any conftest.py files.")
|
||||
dest="noconftest", default=False,
|
||||
help="Don't load any conftest.py files.")
|
||||
group.addoption('--keepduplicates', '--keep-duplicates', action="store_true",
|
||||
dest="keepduplicates", default=False,
|
||||
help="Keep duplicate tests.")
|
||||
dest="keepduplicates", default=False,
|
||||
help="Keep duplicate tests.")
|
||||
group.addoption('--collect-in-virtualenv', action='store_true',
|
||||
dest='collect_in_virtualenv', default=False,
|
||||
help="Don't ignore tests in a local virtualenv directory")
|
||||
|
||||
group = parser.getgroup("debugconfig",
|
||||
"test session debugging and configuration")
|
||||
"test session debugging and configuration")
|
||||
group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir",
|
||||
help="base temporary directory for this test run.")
|
||||
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
"""keeping this one works around a deeper startup issue in pytest
|
||||
|
||||
i tried to find it for a while but the amount of time turned unsustainable,
|
||||
so i put a hack in to revisit later
|
||||
"""
|
||||
return {}
|
||||
help="base temporary directory for this test run.")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
__import__('pytest').config = config # compatibiltiy
|
||||
__import__('pytest').config = config # compatibiltiy
|
||||
|
||||
|
||||
def wrap_session(config, doit):
|
||||
@@ -105,6 +100,8 @@ def wrap_session(config, doit):
|
||||
session.exitstatus = doit(config, session) or 0
|
||||
except UsageError:
|
||||
raise
|
||||
except Failed:
|
||||
session.exitstatus = EXIT_TESTSFAILED
|
||||
except KeyboardInterrupt:
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
if initstate < 2 and isinstance(excinfo.value, exit.Exception):
|
||||
@@ -112,7 +109,7 @@ def wrap_session(config, doit):
|
||||
excinfo.typename, excinfo.value.msg))
|
||||
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
|
||||
session.exitstatus = EXIT_INTERRUPTED
|
||||
except:
|
||||
except: # noqa
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
config.notify_exception(excinfo, config.option)
|
||||
session.exitstatus = EXIT_INTERNALERROR
|
||||
@@ -160,22 +157,38 @@ def pytest_runtestloop(session):
|
||||
return True
|
||||
|
||||
for i, item in enumerate(session.items):
|
||||
nextitem = session.items[i+1] if i+1 < len(session.items) else None
|
||||
nextitem = session.items[i + 1] if i + 1 < len(session.items) else None
|
||||
item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
|
||||
if session.shouldfail:
|
||||
raise session.Failed(session.shouldfail)
|
||||
if session.shouldstop:
|
||||
raise session.Interrupted(session.shouldstop)
|
||||
return True
|
||||
|
||||
|
||||
def _in_venv(path):
|
||||
"""Attempts to detect if ``path`` is the root of a Virtual Environment by
|
||||
checking for the existence of the appropriate activate script"""
|
||||
bindir = path.join('Scripts' if sys.platform.startswith('win') else 'bin')
|
||||
if not bindir.isdir():
|
||||
return False
|
||||
activates = ('activate', 'activate.csh', 'activate.fish',
|
||||
'Activate', 'Activate.bat', 'Activate.ps1')
|
||||
return any([fname.basename in activates for fname in bindir.listdir()])
|
||||
|
||||
|
||||
def pytest_ignore_collect(path, config):
|
||||
p = path.dirpath()
|
||||
ignore_paths = config._getconftest_pathlist("collect_ignore", path=p)
|
||||
ignore_paths = config._getconftest_pathlist("collect_ignore", path=path.dirpath())
|
||||
ignore_paths = ignore_paths or []
|
||||
excludeopt = config.getoption("ignore")
|
||||
if excludeopt:
|
||||
ignore_paths.extend([py.path.local(x) for x in excludeopt])
|
||||
|
||||
if path in ignore_paths:
|
||||
if py.path.local(path) in ignore_paths:
|
||||
return True
|
||||
|
||||
allow_in_venv = config.getoption("collect_in_virtualenv")
|
||||
if _in_venv(path) and not allow_in_venv:
|
||||
return True
|
||||
|
||||
# Skip duplicate paths.
|
||||
@@ -190,7 +203,47 @@ def pytest_ignore_collect(path, config):
|
||||
return False
|
||||
|
||||
|
||||
class FSHookProxy:
|
||||
@contextlib.contextmanager
|
||||
def _patched_find_module():
|
||||
"""Patch bug in pkgutil.ImpImporter.find_module
|
||||
|
||||
When using pkgutil.find_loader on python<3.4 it removes symlinks
|
||||
from the path due to a call to os.path.realpath. This is not consistent
|
||||
with actually doing the import (in these versions, pkgutil and __import__
|
||||
did not share the same underlying code). This can break conftest
|
||||
discovery for pytest where symlinks are involved.
|
||||
|
||||
The only supported python<3.4 by pytest is python 2.7.
|
||||
"""
|
||||
if six.PY2: # python 3.4+ uses importlib instead
|
||||
def find_module_patched(self, fullname, path=None):
|
||||
# Note: we ignore 'path' argument since it is only used via meta_path
|
||||
subname = fullname.split(".")[-1]
|
||||
if subname != fullname and self.path is None:
|
||||
return None
|
||||
if self.path is None:
|
||||
path = None
|
||||
else:
|
||||
# original: path = [os.path.realpath(self.path)]
|
||||
path = [self.path]
|
||||
try:
|
||||
file, filename, etc = pkgutil.imp.find_module(subname,
|
||||
path)
|
||||
except ImportError:
|
||||
return None
|
||||
return pkgutil.ImpLoader(fullname, file, filename, etc)
|
||||
|
||||
old_find_module = pkgutil.ImpImporter.find_module
|
||||
pkgutil.ImpImporter.find_module = find_module_patched
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
pkgutil.ImpImporter.find_module = old_find_module
|
||||
else:
|
||||
yield
|
||||
|
||||
|
||||
class FSHookProxy(object):
|
||||
def __init__(self, fspath, pm, remove_mods):
|
||||
self.fspath = fspath
|
||||
self.pm = pm
|
||||
@@ -201,363 +254,32 @@ class FSHookProxy:
|
||||
self.__dict__[name] = x
|
||||
return x
|
||||
|
||||
class _CompatProperty(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn(
|
||||
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
|
||||
# name=self.name, owner=type(owner).__name__),
|
||||
# PendingDeprecationWarning, stacklevel=2)
|
||||
return getattr(__import__('pytest'), self.name)
|
||||
|
||||
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
self._markers = {node.name: True}
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self._markers[key]
|
||||
except KeyError:
|
||||
if self.parent is None:
|
||||
raise
|
||||
return self.parent.keywords[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._markers[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise ValueError("cannot delete key in keywords dict")
|
||||
|
||||
def __iter__(self):
|
||||
seen = set(self._markers)
|
||||
if self.parent is not None:
|
||||
seen.update(self.parent.keywords)
|
||||
return iter(seen)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__iter__())
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<NodeKeywords for node %s>" % (self.node, )
|
||||
|
||||
|
||||
class Node(object):
|
||||
""" base class for Collector and Item the test collection tree.
|
||||
Collector subclasses have children, Items are terminal nodes."""
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
#: a unique name within the scope of the parent node
|
||||
self.name = name
|
||||
|
||||
#: the parent collector node.
|
||||
self.parent = parent
|
||||
|
||||
#: the pytest config object
|
||||
self.config = config or parent.config
|
||||
|
||||
#: the session this node is part of
|
||||
self.session = session or parent.session
|
||||
|
||||
#: filesystem path where this node was collected from (can be None)
|
||||
self.fspath = getattr(parent, 'fspath', None)
|
||||
|
||||
#: keywords/markers collected from all scopes
|
||||
self.keywords = NodeKeywords(self)
|
||||
|
||||
#: allow adding of extra keywords to use for matching
|
||||
self.extra_keyword_matches = set()
|
||||
|
||||
# used for storing artificial fixturedefs for direct parametrization
|
||||
self._name2pseudofixturedef = {}
|
||||
|
||||
@property
|
||||
def ihook(self):
|
||||
""" fspath sensitive hook proxy used to call pytest hooks"""
|
||||
return self.session.gethookproxy(self.fspath)
|
||||
|
||||
Module = _CompatProperty("Module")
|
||||
Class = _CompatProperty("Class")
|
||||
Instance = _CompatProperty("Instance")
|
||||
Function = _CompatProperty("Function")
|
||||
File = _CompatProperty("File")
|
||||
Item = _CompatProperty("Item")
|
||||
|
||||
def _getcustomclass(self, name):
|
||||
maybe_compatprop = getattr(type(self), name)
|
||||
if isinstance(maybe_compatprop, _CompatProperty):
|
||||
return getattr(__import__('pytest'), name)
|
||||
else:
|
||||
cls = getattr(self, name)
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn("use of node.%s is deprecated, "
|
||||
# "use pytest_pycollect_makeitem(...) to create custom "
|
||||
# "collection nodes" % name, category=DeprecationWarning)
|
||||
return cls
|
||||
|
||||
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. """
|
||||
assert isinstance(code, str)
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
|
||||
code=code, message=message,
|
||||
nodeid=self.nodeid, fslocation=fslocation))
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
""" a ::-separated string denoting its collection tree address. """
|
||||
try:
|
||||
return self._nodeid
|
||||
except AttributeError:
|
||||
self._nodeid = x = self._makeid()
|
||||
return x
|
||||
|
||||
def _makeid(self):
|
||||
return self.parent.nodeid + "::" + self.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.nodeid)
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
def _memoizedcall(self, attrname, function):
|
||||
exattrname = "_ex_" + attrname
|
||||
failure = getattr(self, exattrname, None)
|
||||
if failure is not None:
|
||||
py.builtin._reraise(failure[0], failure[1], failure[2])
|
||||
if hasattr(self, attrname):
|
||||
return getattr(self, attrname)
|
||||
try:
|
||||
res = function()
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
failure = sys.exc_info()
|
||||
setattr(self, exattrname, failure)
|
||||
raise
|
||||
setattr(self, attrname, res)
|
||||
return res
|
||||
|
||||
def listchain(self):
|
||||
""" return list of all parent collectors up to self,
|
||||
starting from root of collection tree. """
|
||||
chain = []
|
||||
item = self
|
||||
while item is not None:
|
||||
chain.append(item)
|
||||
item = item.parent
|
||||
chain.reverse()
|
||||
return chain
|
||||
|
||||
def add_marker(self, marker):
|
||||
""" dynamically add a marker object to the node.
|
||||
|
||||
``marker`` can be a string or pytest.mark.* instance.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator, MARK_GEN
|
||||
if isinstance(marker, py.builtin._basestring):
|
||||
marker = getattr(MARK_GEN, marker)
|
||||
elif not isinstance(marker, MarkDecorator):
|
||||
raise ValueError("is not a string or pytest.mark.* Marker")
|
||||
self.keywords[marker.name] = marker
|
||||
|
||||
def get_marker(self, name):
|
||||
""" get a marker object from this node or None if
|
||||
the node doesn't have a marker with that name. """
|
||||
val = self.keywords.get(name, None)
|
||||
if val is not None:
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
if isinstance(val, (MarkDecorator, MarkInfo)):
|
||||
return val
|
||||
|
||||
def listextrakeywords(self):
|
||||
""" Return a set of all extra keywords in self and any parents."""
|
||||
extra_keywords = set()
|
||||
item = self
|
||||
for item in self.listchain():
|
||||
extra_keywords.update(item.extra_keyword_matches)
|
||||
return extra_keywords
|
||||
|
||||
def listnames(self):
|
||||
return [x.name for x in self.listchain()]
|
||||
|
||||
def addfinalizer(self, fin):
|
||||
""" register a function to be called when this node is finalized.
|
||||
|
||||
This method can only be called when this node is active
|
||||
in a setup chain, for example during self.setup().
|
||||
"""
|
||||
self.session._setupstate.addfinalizer(fin, self)
|
||||
|
||||
def getparent(self, cls):
|
||||
""" get the next parent node (including ourself)
|
||||
which is an instance of the given class"""
|
||||
current = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
return current
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
pass
|
||||
|
||||
def _repr_failure_py(self, excinfo, style=None):
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
tbfilter = True
|
||||
if self.config.option.fulltrace:
|
||||
style="long"
|
||||
else:
|
||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
||||
self._prunetraceback(excinfo)
|
||||
if len(excinfo.traceback) == 0:
|
||||
excinfo.traceback = tb
|
||||
tbfilter = False # prunetraceback already does it
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||
if style is None:
|
||||
if self.config.option.tbstyle == "short":
|
||||
style = "short"
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
try:
|
||||
os.getcwd()
|
||||
abspath = False
|
||||
except OSError:
|
||||
abspath = True
|
||||
|
||||
return excinfo.getrepr(funcargs=True, abspath=abspath,
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style, tbfilter=tbfilter)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
"""
|
||||
|
||||
class CollectError(Exception):
|
||||
""" an error during collection, contains a custom message. """
|
||||
|
||||
def collect(self):
|
||||
""" returns a list of children (items and collectors)
|
||||
for this collection node.
|
||||
"""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
""" represent a collection failure. """
|
||||
if excinfo.errisinstance(self.CollectError):
|
||||
exc = excinfo.value
|
||||
return str(exc.args[0])
|
||||
return self._repr_failure_py(excinfo, style="short")
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
if hasattr(self, 'fspath'):
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.fspath)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
|
||||
class FSCollector(Collector):
|
||||
def __init__(self, fspath, parent=None, config=None, session=None):
|
||||
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
|
||||
name = fspath.basename
|
||||
if parent is not None:
|
||||
rel = fspath.relto(parent.fspath)
|
||||
if rel:
|
||||
name = rel
|
||||
name = name.replace(os.sep, "/")
|
||||
super(FSCollector, self).__init__(name, parent, config, session)
|
||||
self.fspath = fspath
|
||||
|
||||
def _makeid(self):
|
||||
relpath = self.fspath.relto(self.config.rootdir)
|
||||
if os.sep != "/":
|
||||
relpath = relpath.replace(os.sep, "/")
|
||||
return relpath
|
||||
|
||||
class File(FSCollector):
|
||||
""" base class for collecting tests from a file. """
|
||||
|
||||
class Item(Node):
|
||||
""" a basic test invocation item. Note that for a single function
|
||||
there might be multiple test invocation items.
|
||||
"""
|
||||
nextitem = None
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
super(Item, self).__init__(name, parent, config, session)
|
||||
self._report_sections = []
|
||||
|
||||
def add_report_section(self, when, key, content):
|
||||
if content:
|
||||
self._report_sections.append((when, key, content))
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, ""
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
try:
|
||||
return self._location
|
||||
except AttributeError:
|
||||
location = self.reportinfo()
|
||||
# bestrelpath is a quite slow function
|
||||
cache = self.config.__dict__.setdefault("_bestrelpathcache", {})
|
||||
try:
|
||||
fspath = cache[location[0]]
|
||||
except KeyError:
|
||||
fspath = self.session.fspath.bestrelpath(location[0])
|
||||
cache[location[0]] = fspath
|
||||
location = (fspath, location[1], str(location[2]))
|
||||
self._location = location
|
||||
return location
|
||||
|
||||
class NoMatch(Exception):
|
||||
""" raised if matching cannot locate a matching names. """
|
||||
|
||||
|
||||
class Interrupted(KeyboardInterrupt):
|
||||
""" signals an interrupted test run. """
|
||||
__module__ = 'builtins' # for py3
|
||||
__module__ = 'builtins' # for py3
|
||||
|
||||
class Session(FSCollector):
|
||||
|
||||
class Failed(Exception):
|
||||
""" signals an stop as failed test run. """
|
||||
|
||||
|
||||
class Session(nodes.FSCollector):
|
||||
Interrupted = Interrupted
|
||||
Failed = Failed
|
||||
|
||||
def __init__(self, config):
|
||||
FSCollector.__init__(self, config.rootdir, parent=None,
|
||||
config=config, session=self)
|
||||
nodes.FSCollector.__init__(
|
||||
self, config.rootdir, parent=None,
|
||||
config=config, session=self)
|
||||
self.testsfailed = 0
|
||||
self.testscollected = 0
|
||||
self.shouldstop = False
|
||||
self.shouldfail = False
|
||||
self.trace = config.trace.root.get("collection")
|
||||
self._norecursepatterns = config.getini("norecursedirs")
|
||||
self.startdir = py.path.local()
|
||||
@@ -568,6 +290,8 @@ class Session(FSCollector):
|
||||
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_collectstart(self):
|
||||
if self.shouldfail:
|
||||
raise self.Failed(self.shouldfail)
|
||||
if self.shouldstop:
|
||||
raise self.Interrupted(self.shouldstop)
|
||||
|
||||
@@ -577,7 +301,7 @@ class Session(FSCollector):
|
||||
self.testsfailed += 1
|
||||
maxfail = self.config.getvalue("maxfail")
|
||||
if maxfail and self.testsfailed >= maxfail:
|
||||
self.shouldstop = "stopping after %d failures" % (
|
||||
self.shouldfail = "stopping after %d failures" % (
|
||||
self.testsfailed)
|
||||
pytest_collectreport = pytest_runtest_logreport
|
||||
|
||||
@@ -604,7 +328,7 @@ class Session(FSCollector):
|
||||
items = self._perform_collect(args, genitems)
|
||||
self.config.pluginmanager.check_pending()
|
||||
hook.pytest_collection_modifyitems(session=self,
|
||||
config=self.config, items=items)
|
||||
config=self.config, items=items)
|
||||
finally:
|
||||
hook.pytest_collection_finish(session=self)
|
||||
self.testscollected = len(items)
|
||||
@@ -692,9 +416,10 @@ class Session(FSCollector):
|
||||
"""Convert a dotted module name to path.
|
||||
|
||||
"""
|
||||
import pkgutil
|
||||
|
||||
try:
|
||||
loader = pkgutil.find_loader(x)
|
||||
with _patched_find_module():
|
||||
loader = pkgutil.find_loader(x)
|
||||
except ImportError:
|
||||
return x
|
||||
if loader is None:
|
||||
@@ -702,7 +427,8 @@ class Session(FSCollector):
|
||||
# This method is sometimes invoked when AssertionRewritingHook, which
|
||||
# does not define a get_filename method, is already in place:
|
||||
try:
|
||||
path = loader.get_filename(x)
|
||||
with _patched_find_module():
|
||||
path = loader.get_filename(x)
|
||||
except AttributeError:
|
||||
# Retrieve path from AssertionRewritingHook:
|
||||
path = loader.modules[x][0].co_filename
|
||||
@@ -746,11 +472,11 @@ class Session(FSCollector):
|
||||
nextnames = names[1:]
|
||||
resultnodes = []
|
||||
for node in matching:
|
||||
if isinstance(node, Item):
|
||||
if isinstance(node, nodes.Item):
|
||||
if not names:
|
||||
resultnodes.append(node)
|
||||
continue
|
||||
assert isinstance(node, Collector)
|
||||
assert isinstance(node, nodes.Collector)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
has_matched = False
|
||||
@@ -763,16 +489,20 @@ class Session(FSCollector):
|
||||
if not has_matched and len(rep.result) == 1 and x.name == "()":
|
||||
nextnames.insert(0, name)
|
||||
resultnodes.extend(self.matchnodes([x], nextnames))
|
||||
node.ihook.pytest_collectreport(report=rep)
|
||||
else:
|
||||
# report collection failures here to avoid failing to run some test
|
||||
# specified in the command line because the module could not be
|
||||
# imported (#134)
|
||||
node.ihook.pytest_collectreport(report=rep)
|
||||
return resultnodes
|
||||
|
||||
def genitems(self, node):
|
||||
self.trace("genitems", node)
|
||||
if isinstance(node, Item):
|
||||
if isinstance(node, nodes.Item):
|
||||
node.ihook.pytest_itemcollected(item=node)
|
||||
yield node
|
||||
else:
|
||||
assert isinstance(node, Collector)
|
||||
assert isinstance(node, nodes.Collector)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
for subnode in rep.result:
|
||||
|
||||
250
_pytest/mark.py
250
_pytest/mark.py
@@ -2,13 +2,28 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import inspect
|
||||
import keyword
|
||||
import warnings
|
||||
import attr
|
||||
from collections import namedtuple
|
||||
from operator import attrgetter
|
||||
from .compat import imap
|
||||
from six.moves import map
|
||||
|
||||
from _pytest.config import UsageError
|
||||
from .deprecated import MARK_PARAMETERSET_UNPACKING
|
||||
from .compat import NOTSET, getfslineno
|
||||
|
||||
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
|
||||
|
||||
|
||||
def alias(name):
|
||||
return property(attrgetter(name), doc='alias for ' + name)
|
||||
def alias(name, warning=None):
|
||||
getter = attrgetter(name)
|
||||
|
||||
def warned(self):
|
||||
warnings.warn(warning, stacklevel=2)
|
||||
return getter(self)
|
||||
|
||||
return property(getter if warning is None else warned, doc='alias for ' + name)
|
||||
|
||||
|
||||
class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
||||
@@ -54,11 +69,45 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
|
||||
if legacy_force_tuple:
|
||||
argval = argval,
|
||||
|
||||
if newmarks:
|
||||
warnings.warn(MARK_PARAMETERSET_UNPACKING)
|
||||
|
||||
return cls(argval, marks=newmarks, id=None)
|
||||
|
||||
@property
|
||||
def deprecated_arg_dict(self):
|
||||
return dict((mark.name, mark) for mark in self.marks)
|
||||
@classmethod
|
||||
def _for_parametrize(cls, argnames, argvalues, function, config):
|
||||
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)
|
||||
for x in argvalues]
|
||||
del argvalues
|
||||
|
||||
if not parameters:
|
||||
mark = get_empty_parameterset_mark(config, argnames, function)
|
||||
parameters.append(ParameterSet(
|
||||
values=(NOTSET,) * len(argnames),
|
||||
marks=[mark],
|
||||
id=None,
|
||||
))
|
||||
return argnames, parameters
|
||||
|
||||
|
||||
def get_empty_parameterset_mark(config, argnames, function):
|
||||
requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
if requested_mark in ('', None, 'skip'):
|
||||
mark = MARK_GEN.skip
|
||||
elif requested_mark == 'xfail':
|
||||
mark = MARK_GEN.xfail(run=False)
|
||||
else:
|
||||
raise LookupError(requested_mark)
|
||||
fs, lineno = getfslineno(function)
|
||||
reason = "got empty parameter set %r, function %s at %s:%d" % (
|
||||
argnames, function.__name__, fs, lineno)
|
||||
return mark(reason=reason)
|
||||
|
||||
|
||||
class MarkerError(Exception):
|
||||
@@ -80,7 +129,8 @@ def pytest_addoption(parser):
|
||||
"where all names are substring-matched against test names "
|
||||
"and their parent classes. Example: -k 'test_method or test_"
|
||||
"other' matches all test functions and classes whose name "
|
||||
"contains 'test_method' or 'test_other'. "
|
||||
"contains 'test_method' or 'test_other', while -k 'not test_method' "
|
||||
"matches those that don't contain 'test_method' in their names. "
|
||||
"Additionally keywords are matched to classes and functions "
|
||||
"containing extra names in their 'extra_keyword_matches' set, "
|
||||
"as well as functions which have names assigned directly to them."
|
||||
@@ -99,6 +149,9 @@ def pytest_addoption(parser):
|
||||
)
|
||||
|
||||
parser.addini("markers", "markers for test functions", 'linelist')
|
||||
parser.addini(
|
||||
EMPTY_PARAMETERSET_OPTION,
|
||||
"default marker for empty parametersets")
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
@@ -107,7 +160,9 @@ def pytest_cmdline_main(config):
|
||||
config._do_configure()
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
for line in config.getini("markers"):
|
||||
name, rest = line.split(":", 1)
|
||||
parts = line.split(":", 1)
|
||||
name = parts[0]
|
||||
rest = parts[1] if len(parts) == 2 else ''
|
||||
tw.write("@pytest.mark.%s:" % name, bold=True)
|
||||
tw.line(rest)
|
||||
tw.line()
|
||||
@@ -152,24 +207,30 @@ def pytest_collection_modifyitems(items, config):
|
||||
items[:] = remaining
|
||||
|
||||
|
||||
class MarkMapping:
|
||||
@attr.s
|
||||
class MarkMapping(object):
|
||||
"""Provides a local mapping for markers where item access
|
||||
resolves to True if the marker is present. """
|
||||
def __init__(self, keywords):
|
||||
mymarks = set()
|
||||
|
||||
own_mark_names = attr.ib()
|
||||
|
||||
@classmethod
|
||||
def from_keywords(cls, keywords):
|
||||
mark_names = set()
|
||||
for key, value in keywords.items():
|
||||
if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator):
|
||||
mymarks.add(key)
|
||||
self._mymarks = mymarks
|
||||
mark_names.add(key)
|
||||
return cls(mark_names)
|
||||
|
||||
def __getitem__(self, name):
|
||||
return name in self._mymarks
|
||||
return name in self.own_mark_names
|
||||
|
||||
|
||||
class KeywordMapping:
|
||||
class KeywordMapping(object):
|
||||
"""Provides a local mapping for keywords.
|
||||
Given a list of names, map any substring of one of these names to True.
|
||||
"""
|
||||
|
||||
def __init__(self, names):
|
||||
self._names = names
|
||||
|
||||
@@ -180,9 +241,12 @@ class KeywordMapping:
|
||||
return False
|
||||
|
||||
|
||||
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(colitem.keywords))
|
||||
return eval(markexpr, {}, MarkMapping.from_keywords(colitem.keywords))
|
||||
|
||||
|
||||
def matchkeyword(colitem, keywordexpr):
|
||||
@@ -217,7 +281,13 @@ def matchkeyword(colitem, keywordexpr):
|
||||
return mapping[keywordexpr]
|
||||
elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]:
|
||||
return not mapping[keywordexpr[4:]]
|
||||
return eval(keywordexpr, {}, mapping)
|
||||
for kwd in keywordexpr.split():
|
||||
if keyword.iskeyword(kwd) and kwd not in python_keywords_allowed_list:
|
||||
raise UsageError("Python keyword '{}' not accepted in expressions passed to '-k'".format(kwd))
|
||||
try:
|
||||
return eval(keywordexpr, {}, mapping)
|
||||
except SyntaxError:
|
||||
raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr))
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
@@ -225,12 +295,19 @@ def pytest_configure(config):
|
||||
if config.option.strict:
|
||||
MARK_GEN._config = config
|
||||
|
||||
empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
|
||||
|
||||
if empty_parameterset not in ('skip', 'xfail', None, ''):
|
||||
raise UsageError(
|
||||
"{!s} must be one of skip and xfail,"
|
||||
" but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset))
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
MARK_GEN._config = getattr(config, '_old_mark_config', None)
|
||||
|
||||
|
||||
class MarkGenerator:
|
||||
class MarkGenerator(object):
|
||||
""" Factory for :class:`MarkDecorator` objects - exposed as
|
||||
a ``pytest.mark`` singleton instance. Example::
|
||||
|
||||
@@ -243,7 +320,6 @@ class MarkGenerator:
|
||||
on the ``test_function`` object. """
|
||||
_config = None
|
||||
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name[0] == "_":
|
||||
raise AttributeError("Marker name must NOT start with underscore")
|
||||
@@ -257,11 +333,12 @@ class MarkGenerator:
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
self._markers = l = set()
|
||||
self._markers = values = set()
|
||||
for line in self._config.getini("markers"):
|
||||
beginning = line.split(":", 1)
|
||||
x = beginning[0].split("(", 1)[0]
|
||||
l.add(x)
|
||||
marker = line.split(":", 1)[0]
|
||||
marker = marker.rstrip()
|
||||
x = marker.split("(", 1)[0]
|
||||
values.add(x)
|
||||
if name not in self._markers:
|
||||
raise AttributeError("%r not a registered marker" % (name,))
|
||||
|
||||
@@ -270,7 +347,22 @@ def istestfunc(func):
|
||||
return hasattr(func, "__call__") and \
|
||||
getattr(func, "__name__", "<lambda>") != "<lambda>"
|
||||
|
||||
class MarkDecorator:
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class Mark(object):
|
||||
name = attr.ib()
|
||||
args = attr.ib()
|
||||
kwargs = attr.ib()
|
||||
|
||||
def combined_with(self, other):
|
||||
assert self.name == other.name
|
||||
return Mark(
|
||||
self.name, self.args + other.args,
|
||||
dict(self.kwargs, **other.kwargs))
|
||||
|
||||
|
||||
@attr.s
|
||||
class MarkDecorator(object):
|
||||
""" A decorator for test functions and test classes. When applied
|
||||
it will create :class:`MarkInfo` objects which may be
|
||||
:ref:`retrieved by hooks as item keywords <excontrolskip>`.
|
||||
@@ -303,9 +395,8 @@ class MarkDecorator:
|
||||
additional keyword or positional arguments.
|
||||
|
||||
"""
|
||||
def __init__(self, mark):
|
||||
assert isinstance(mark, Mark), repr(mark)
|
||||
self.mark = mark
|
||||
|
||||
mark = attr.ib(validator=attr.validators.instance_of(Mark))
|
||||
|
||||
name = alias('mark.name')
|
||||
args = alias('mark.args')
|
||||
@@ -313,14 +404,25 @@ class MarkDecorator:
|
||||
|
||||
@property
|
||||
def markname(self):
|
||||
return self.name # for backward-compat (2.4.1 had this attr)
|
||||
return self.name # for backward-compat (2.4.1 had this attr)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.mark == other.mark
|
||||
return self.mark == other.mark if isinstance(other, MarkDecorator) else False
|
||||
|
||||
def __repr__(self):
|
||||
return "<MarkDecorator %r>" % (self.mark,)
|
||||
|
||||
def with_args(self, *args, **kwargs):
|
||||
""" return a MarkDecorator with extra arguments added
|
||||
|
||||
unlike call this can be used even if the sole argument is a callable/class
|
||||
|
||||
:return: MarkDecorator
|
||||
"""
|
||||
|
||||
mark = Mark(self.name, args, kwargs)
|
||||
return self.__class__(self.mark.combined_with(mark))
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
""" if passed a single callable argument: decorate it with mark info.
|
||||
otherwise add *args/**kwargs in-place to mark information. """
|
||||
@@ -329,43 +431,54 @@ class MarkDecorator:
|
||||
is_class = inspect.isclass(func)
|
||||
if len(args) == 1 and (istestfunc(func) or is_class):
|
||||
if is_class:
|
||||
if hasattr(func, 'pytestmark'):
|
||||
mark_list = func.pytestmark
|
||||
if not isinstance(mark_list, list):
|
||||
mark_list = [mark_list]
|
||||
# always work on a copy to avoid updating pytestmark
|
||||
# from a superclass by accident
|
||||
mark_list = mark_list + [self]
|
||||
func.pytestmark = mark_list
|
||||
else:
|
||||
func.pytestmark = [self]
|
||||
store_mark(func, self.mark)
|
||||
else:
|
||||
holder = getattr(func, self.name, None)
|
||||
if holder is None:
|
||||
holder = MarkInfo(self.mark)
|
||||
setattr(func, self.name, holder)
|
||||
else:
|
||||
holder.add_mark(self.mark)
|
||||
store_legacy_markinfo(func, self.mark)
|
||||
store_mark(func, self.mark)
|
||||
return func
|
||||
|
||||
mark = Mark(self.name, args, kwargs)
|
||||
return self.__class__(self.mark.combined_with(mark))
|
||||
return self.with_args(*args, **kwargs)
|
||||
|
||||
|
||||
def get_unpacked_marks(obj):
|
||||
"""
|
||||
obtain the unpacked marks that are stored on a object
|
||||
"""
|
||||
mark_list = getattr(obj, 'pytestmark', [])
|
||||
|
||||
if not isinstance(mark_list, list):
|
||||
mark_list = [mark_list]
|
||||
return [
|
||||
getattr(mark, 'mark', mark) # unpack MarkDecorator
|
||||
for mark in mark_list
|
||||
]
|
||||
|
||||
|
||||
def store_mark(obj, mark):
|
||||
"""store a Mark on a object
|
||||
this is used to implement the Mark declarations/decorators correctly
|
||||
"""
|
||||
assert isinstance(mark, Mark), mark
|
||||
# always reassign name to avoid updating pytestmark
|
||||
# in a reference that was only borrowed
|
||||
obj.pytestmark = get_unpacked_marks(obj) + [mark]
|
||||
|
||||
class Mark(namedtuple('Mark', 'name, args, kwargs')):
|
||||
|
||||
def combined_with(self, other):
|
||||
assert self.name == other.name
|
||||
return Mark(
|
||||
self.name, self.args + other.args,
|
||||
dict(self.kwargs, **other.kwargs))
|
||||
def store_legacy_markinfo(func, mark):
|
||||
"""create the legacy MarkInfo objects and put them onto the function
|
||||
"""
|
||||
if not isinstance(mark, Mark):
|
||||
raise TypeError("got {mark!r} instead of a Mark".format(mark=mark))
|
||||
holder = getattr(func, mark.name, None)
|
||||
if holder is None:
|
||||
holder = MarkInfo(mark)
|
||||
setattr(func, mark.name, holder)
|
||||
else:
|
||||
holder.add_mark(mark)
|
||||
|
||||
|
||||
class MarkInfo(object):
|
||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||
|
||||
def __init__(self, mark):
|
||||
assert isinstance(mark, Mark), repr(mark)
|
||||
self.combined = mark
|
||||
@@ -385,7 +498,34 @@ class MarkInfo(object):
|
||||
|
||||
def __iter__(self):
|
||||
""" yield MarkInfo objects each relating to a marking-call. """
|
||||
return imap(MarkInfo, self._marks)
|
||||
return map(MarkInfo, self._marks)
|
||||
|
||||
|
||||
MARK_GEN = MarkGenerator()
|
||||
|
||||
|
||||
def _marked(func, mark):
|
||||
""" Returns True if :func: is already marked with :mark:, False otherwise.
|
||||
This can happen if marker is applied to class and the test file is
|
||||
invoked more than once.
|
||||
"""
|
||||
try:
|
||||
func_mark = getattr(func, mark.name)
|
||||
except AttributeError:
|
||||
return False
|
||||
return mark.args == func_mark.args and mark.kwargs == func_mark.kwargs
|
||||
|
||||
|
||||
def transfer_markers(funcobj, cls, mod):
|
||||
"""
|
||||
this function transfers class level markers and module level markers
|
||||
into function level markinfo objects
|
||||
|
||||
this is the main reason why marks are so broken
|
||||
the resolution will involve phasing out function level MarkInfo objects
|
||||
|
||||
"""
|
||||
for obj in (cls, mod):
|
||||
for mark in get_unpacked_marks(obj):
|
||||
if not _marked(funcobj, mark):
|
||||
store_legacy_markinfo(funcobj, mark)
|
||||
|
||||
@@ -4,8 +4,7 @@ from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
|
||||
from py.builtin import _basestring
|
||||
import six
|
||||
from _pytest.fixtures import fixture
|
||||
|
||||
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
|
||||
@@ -71,15 +70,15 @@ def annotated_getattr(obj, name, ann):
|
||||
obj = getattr(obj, name)
|
||||
except AttributeError:
|
||||
raise AttributeError(
|
||||
'%r object at %s has no attribute %r' % (
|
||||
type(obj).__name__, ann, name
|
||||
)
|
||||
'%r object at %s has no attribute %r' % (
|
||||
type(obj).__name__, ann, name
|
||||
)
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
def derive_importpath(import_path, raising):
|
||||
if not isinstance(import_path, _basestring) or "." not in import_path:
|
||||
if not isinstance(import_path, six.string_types) or "." not in import_path:
|
||||
raise TypeError("must be absolute import path string, not %r" %
|
||||
(import_path,))
|
||||
module, attr = import_path.rsplit('.', 1)
|
||||
@@ -89,7 +88,7 @@ def derive_importpath(import_path, raising):
|
||||
return attr, target
|
||||
|
||||
|
||||
class Notset:
|
||||
class Notset(object):
|
||||
def __repr__(self):
|
||||
return "<notset>"
|
||||
|
||||
@@ -97,7 +96,7 @@ class Notset:
|
||||
notset = Notset()
|
||||
|
||||
|
||||
class MonkeyPatch:
|
||||
class MonkeyPatch(object):
|
||||
""" Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
|
||||
"""
|
||||
|
||||
@@ -125,7 +124,7 @@ class MonkeyPatch:
|
||||
import inspect
|
||||
|
||||
if value is notset:
|
||||
if not isinstance(target, _basestring):
|
||||
if not isinstance(target, six.string_types):
|
||||
raise TypeError("use setattr(target, name, value) or "
|
||||
"setattr(target, value) with target being a dotted "
|
||||
"import string")
|
||||
@@ -155,7 +154,7 @@ class MonkeyPatch:
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if name is notset:
|
||||
if not isinstance(target, _basestring):
|
||||
if not isinstance(target, six.string_types):
|
||||
raise TypeError("use delattr(target, name) or "
|
||||
"delattr(target) with target being a dotted "
|
||||
"import string")
|
||||
|
||||
400
_pytest/nodes.py
Normal file
400
_pytest/nodes.py
Normal file
@@ -0,0 +1,400 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
from collections import MutableMapping as MappingMixin
|
||||
import os
|
||||
|
||||
import six
|
||||
import py
|
||||
import attr
|
||||
|
||||
import _pytest
|
||||
|
||||
|
||||
SEP = "/"
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
|
||||
def _splitnode(nodeid):
|
||||
"""Split a nodeid into constituent 'parts'.
|
||||
|
||||
Node IDs are strings, and can be things like:
|
||||
''
|
||||
'testing/code'
|
||||
'testing/code/test_excinfo.py'
|
||||
'testing/code/test_excinfo.py::TestFormattedExcinfo::()'
|
||||
|
||||
Return values are lists e.g.
|
||||
[]
|
||||
['testing', 'code']
|
||||
['testing', 'code', 'test_excinfo.py']
|
||||
['testing', 'code', 'test_excinfo.py', 'TestFormattedExcinfo', '()']
|
||||
"""
|
||||
if nodeid == '':
|
||||
# If there is no root node at all, return an empty list so the caller's logic can remain sane
|
||||
return []
|
||||
parts = nodeid.split(SEP)
|
||||
# Replace single last element 'test_foo.py::Bar::()' with multiple elements 'test_foo.py', 'Bar', '()'
|
||||
parts[-1:] = parts[-1].split("::")
|
||||
return parts
|
||||
|
||||
|
||||
def ischildnode(baseid, nodeid):
|
||||
"""Return True if the nodeid is a child node of the baseid.
|
||||
|
||||
E.g. 'foo/bar::Baz::()' is a child of 'foo', 'foo/bar' and 'foo/bar::Baz', but not of 'foo/blorp'
|
||||
"""
|
||||
base_parts = _splitnode(baseid)
|
||||
node_parts = _splitnode(nodeid)
|
||||
if len(node_parts) < len(base_parts):
|
||||
return False
|
||||
return node_parts[:len(base_parts)] == base_parts
|
||||
|
||||
|
||||
@attr.s
|
||||
class _CompatProperty(object):
|
||||
name = attr.ib()
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn(
|
||||
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
|
||||
# name=self.name, owner=type(owner).__name__),
|
||||
# PendingDeprecationWarning, stacklevel=2)
|
||||
return getattr(__import__('pytest'), self.name)
|
||||
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
def __init__(self, node):
|
||||
self.node = node
|
||||
self.parent = node.parent
|
||||
self._markers = {node.name: True}
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return self._markers[key]
|
||||
except KeyError:
|
||||
if self.parent is None:
|
||||
raise
|
||||
return self.parent.keywords[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._markers[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise ValueError("cannot delete key in keywords dict")
|
||||
|
||||
def __iter__(self):
|
||||
seen = set(self._markers)
|
||||
if self.parent is not None:
|
||||
seen.update(self.parent.keywords)
|
||||
return iter(seen)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__iter__())
|
||||
|
||||
def keys(self):
|
||||
return list(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "<NodeKeywords for node %s>" % (self.node, )
|
||||
|
||||
|
||||
class Node(object):
|
||||
""" base class for Collector and Item the test collection tree.
|
||||
Collector subclasses have children, Items are terminal nodes."""
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
#: a unique name within the scope of the parent node
|
||||
self.name = name
|
||||
|
||||
#: the parent collector node.
|
||||
self.parent = parent
|
||||
|
||||
#: the pytest config object
|
||||
self.config = config or parent.config
|
||||
|
||||
#: the session this node is part of
|
||||
self.session = session or parent.session
|
||||
|
||||
#: filesystem path where this node was collected from (can be None)
|
||||
self.fspath = getattr(parent, 'fspath', None)
|
||||
|
||||
#: keywords/markers collected from all scopes
|
||||
self.keywords = NodeKeywords(self)
|
||||
|
||||
#: allow adding of extra keywords to use for matching
|
||||
self.extra_keyword_matches = set()
|
||||
|
||||
# used for storing artificial fixturedefs for direct parametrization
|
||||
self._name2pseudofixturedef = {}
|
||||
|
||||
@property
|
||||
def ihook(self):
|
||||
""" fspath sensitive hook proxy used to call pytest hooks"""
|
||||
return self.session.gethookproxy(self.fspath)
|
||||
|
||||
Module = _CompatProperty("Module")
|
||||
Class = _CompatProperty("Class")
|
||||
Instance = _CompatProperty("Instance")
|
||||
Function = _CompatProperty("Function")
|
||||
File = _CompatProperty("File")
|
||||
Item = _CompatProperty("Item")
|
||||
|
||||
def _getcustomclass(self, name):
|
||||
maybe_compatprop = getattr(type(self), name)
|
||||
if isinstance(maybe_compatprop, _CompatProperty):
|
||||
return getattr(__import__('pytest'), name)
|
||||
else:
|
||||
cls = getattr(self, name)
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn("use of node.%s is deprecated, "
|
||||
# "use pytest_pycollect_makeitem(...) to create custom "
|
||||
# "collection nodes" % name, category=DeprecationWarning)
|
||||
return cls
|
||||
|
||||
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. """
|
||||
assert isinstance(code, str)
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
|
||||
code=code, message=message,
|
||||
nodeid=self.nodeid, fslocation=fslocation))
|
||||
|
||||
# methods for ordering nodes
|
||||
@property
|
||||
def nodeid(self):
|
||||
""" a ::-separated string denoting its collection tree address. """
|
||||
try:
|
||||
return self._nodeid
|
||||
except AttributeError:
|
||||
self._nodeid = x = self._makeid()
|
||||
return x
|
||||
|
||||
def _makeid(self):
|
||||
return self.parent.nodeid + "::" + self.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.nodeid)
|
||||
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def teardown(self):
|
||||
pass
|
||||
|
||||
def listchain(self):
|
||||
""" return list of all parent collectors up to self,
|
||||
starting from root of collection tree. """
|
||||
chain = []
|
||||
item = self
|
||||
while item is not None:
|
||||
chain.append(item)
|
||||
item = item.parent
|
||||
chain.reverse()
|
||||
return chain
|
||||
|
||||
def add_marker(self, marker):
|
||||
""" dynamically add a marker object to the node.
|
||||
|
||||
``marker`` can be a string or pytest.mark.* instance.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator, MARK_GEN
|
||||
if isinstance(marker, six.string_types):
|
||||
marker = getattr(MARK_GEN, marker)
|
||||
elif not isinstance(marker, MarkDecorator):
|
||||
raise ValueError("is not a string or pytest.mark.* Marker")
|
||||
self.keywords[marker.name] = marker
|
||||
|
||||
def get_marker(self, name):
|
||||
""" get a marker object from this node or None if
|
||||
the node doesn't have a marker with that name. """
|
||||
val = self.keywords.get(name, None)
|
||||
if val is not None:
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
if isinstance(val, (MarkDecorator, MarkInfo)):
|
||||
return val
|
||||
|
||||
def listextrakeywords(self):
|
||||
""" Return a set of all extra keywords in self and any parents."""
|
||||
extra_keywords = set()
|
||||
item = self
|
||||
for item in self.listchain():
|
||||
extra_keywords.update(item.extra_keyword_matches)
|
||||
return extra_keywords
|
||||
|
||||
def listnames(self):
|
||||
return [x.name for x in self.listchain()]
|
||||
|
||||
def addfinalizer(self, fin):
|
||||
""" register a function to be called when this node is finalized.
|
||||
|
||||
This method can only be called when this node is active
|
||||
in a setup chain, for example during self.setup().
|
||||
"""
|
||||
self.session._setupstate.addfinalizer(fin, self)
|
||||
|
||||
def getparent(self, cls):
|
||||
""" get the next parent node (including ourself)
|
||||
which is an instance of the given class"""
|
||||
current = self
|
||||
while current and not isinstance(current, cls):
|
||||
current = current.parent
|
||||
return current
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
pass
|
||||
|
||||
def _repr_failure_py(self, excinfo, style=None):
|
||||
fm = self.session._fixturemanager
|
||||
if excinfo.errisinstance(fm.FixtureLookupError):
|
||||
return excinfo.value.formatrepr()
|
||||
tbfilter = True
|
||||
if self.config.option.fulltrace:
|
||||
style = "long"
|
||||
else:
|
||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
||||
self._prunetraceback(excinfo)
|
||||
if len(excinfo.traceback) == 0:
|
||||
excinfo.traceback = tb
|
||||
tbfilter = False # prunetraceback already does it
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
# XXX should excinfo.getrepr record all data and toterminal() process it?
|
||||
if style is None:
|
||||
if self.config.option.tbstyle == "short":
|
||||
style = "short"
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
try:
|
||||
os.getcwd()
|
||||
abspath = False
|
||||
except OSError:
|
||||
abspath = True
|
||||
|
||||
return excinfo.getrepr(funcargs=True, abspath=abspath,
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style, tbfilter=tbfilter)
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
|
||||
class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
"""
|
||||
|
||||
class CollectError(Exception):
|
||||
""" an error during collection, contains a custom message. """
|
||||
|
||||
def collect(self):
|
||||
""" returns a list of children (items and collectors)
|
||||
for this collection node.
|
||||
"""
|
||||
raise NotImplementedError("abstract")
|
||||
|
||||
def repr_failure(self, excinfo):
|
||||
""" represent a collection failure. """
|
||||
if excinfo.errisinstance(self.CollectError):
|
||||
exc = excinfo.value
|
||||
return str(exc.args[0])
|
||||
return self._repr_failure_py(excinfo, style="short")
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
if hasattr(self, 'fspath'):
|
||||
traceback = excinfo.traceback
|
||||
ntraceback = traceback.cut(path=self.fspath)
|
||||
if ntraceback == traceback:
|
||||
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
|
||||
excinfo.traceback = ntraceback.filter()
|
||||
|
||||
|
||||
class FSCollector(Collector):
|
||||
def __init__(self, fspath, parent=None, config=None, session=None):
|
||||
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
|
||||
name = fspath.basename
|
||||
if parent is not None:
|
||||
rel = fspath.relto(parent.fspath)
|
||||
if rel:
|
||||
name = rel
|
||||
name = name.replace(os.sep, SEP)
|
||||
super(FSCollector, self).__init__(name, parent, config, session)
|
||||
self.fspath = fspath
|
||||
|
||||
def _check_initialpaths_for_relpath(self):
|
||||
for initialpath in self.session._initialpaths:
|
||||
if self.fspath.common(initialpath) == initialpath:
|
||||
return self.fspath.relto(initialpath.dirname)
|
||||
|
||||
def _makeid(self):
|
||||
relpath = self.fspath.relto(self.config.rootdir)
|
||||
|
||||
if not relpath:
|
||||
relpath = self._check_initialpaths_for_relpath()
|
||||
if os.sep != SEP:
|
||||
relpath = relpath.replace(os.sep, SEP)
|
||||
return relpath
|
||||
|
||||
|
||||
class File(FSCollector):
|
||||
""" base class for collecting tests from a file. """
|
||||
|
||||
|
||||
class Item(Node):
|
||||
""" a basic test invocation item. Note that for a single function
|
||||
there might be multiple test invocation items.
|
||||
"""
|
||||
nextitem = None
|
||||
|
||||
def __init__(self, name, parent=None, config=None, session=None):
|
||||
super(Item, self).__init__(name, parent, config, session)
|
||||
self._report_sections = []
|
||||
|
||||
def add_report_section(self, when, key, content):
|
||||
"""
|
||||
Adds a new report section, similar to what's done internally to add stdout and
|
||||
stderr captured output::
|
||||
|
||||
item.add_report_section("call", "stdout", "report section contents")
|
||||
|
||||
:param str when:
|
||||
One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
|
||||
:param str key:
|
||||
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
|
||||
``"stderr"`` internally.
|
||||
|
||||
:param str content:
|
||||
The full contents as a string.
|
||||
"""
|
||||
if content:
|
||||
self._report_sections.append((when, key, content))
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, ""
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
try:
|
||||
return self._location
|
||||
except AttributeError:
|
||||
location = self.reportinfo()
|
||||
# bestrelpath is a quite slow function
|
||||
cache = self.config.__dict__.setdefault("_bestrelpathcache", {})
|
||||
try:
|
||||
fspath = cache[location[0]]
|
||||
except KeyError:
|
||||
fspath = self.session.fspath.bestrelpath(location[0])
|
||||
cache[location[0]] = fspath
|
||||
location = (fspath, location[1], str(location[2]))
|
||||
self._location = location
|
||||
return location
|
||||
@@ -3,7 +3,6 @@ from __future__ import absolute_import, division, print_function
|
||||
|
||||
import sys
|
||||
|
||||
import py
|
||||
from _pytest import unittest, runner, python
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
@@ -38,14 +37,15 @@ def pytest_runtest_setup(item):
|
||||
if not call_optional(item.obj, 'setup'):
|
||||
# call module level setup if there is no object level one
|
||||
call_optional(item.parent.obj, 'setup')
|
||||
#XXX this implies we only call teardown when setup worked
|
||||
# XXX this implies we only call teardown when setup worked
|
||||
item.session._setupstate.addfinalizer((lambda: teardown_nose(item)), item)
|
||||
|
||||
|
||||
def teardown_nose(item):
|
||||
if is_potential_nosetest(item):
|
||||
if not call_optional(item.obj, 'teardown'):
|
||||
call_optional(item.parent.obj, 'teardown')
|
||||
#if hasattr(item.parent, '_nosegensetup'):
|
||||
# if hasattr(item.parent, '_nosegensetup'):
|
||||
# #call_optional(item._nosegensetup, 'teardown')
|
||||
# del item.parent._nosegensetup
|
||||
|
||||
@@ -65,7 +65,7 @@ def is_potential_nosetest(item):
|
||||
def call_optional(obj, name):
|
||||
method = getattr(obj, name, None)
|
||||
isfixture = hasattr(method, "_pytestfixturefunction")
|
||||
if method is not None and not isfixture and py.builtin.callable(method):
|
||||
if method is not None and not isfixture and callable(method):
|
||||
# If there's any problems allow the exception to raise rather than
|
||||
# silently ignoring them
|
||||
method()
|
||||
|
||||
147
_pytest/outcomes.py
Normal file
147
_pytest/outcomes.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
class OutcomeException(BaseException):
|
||||
""" OutcomeException and its subclass instances indicate and
|
||||
contain info about test and collection outcomes.
|
||||
"""
|
||||
def __init__(self, msg=None, pytrace=True):
|
||||
BaseException.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.pytrace = pytrace
|
||||
|
||||
def __repr__(self):
|
||||
if self.msg:
|
||||
val = self.msg
|
||||
if isinstance(val, bytes):
|
||||
val = py._builtin._totext(val, errors='replace')
|
||||
return val
|
||||
return "<%s instance>" % (self.__class__.__name__,)
|
||||
__str__ = __repr__
|
||||
|
||||
|
||||
TEST_OUTCOME = (OutcomeException, Exception)
|
||||
|
||||
|
||||
class Skipped(OutcomeException):
|
||||
# XXX hackish: on 3k we fake to live in the builtins
|
||||
# in order to have Skipped exception printing shorter/nicer
|
||||
__module__ = 'builtins'
|
||||
|
||||
def __init__(self, msg=None, pytrace=True, allow_module_level=False):
|
||||
OutcomeException.__init__(self, msg=msg, pytrace=pytrace)
|
||||
self.allow_module_level = allow_module_level
|
||||
|
||||
|
||||
class Failed(OutcomeException):
|
||||
""" raised from an explicit call to pytest.fail() """
|
||||
__module__ = 'builtins'
|
||||
|
||||
|
||||
class Exit(KeyboardInterrupt):
|
||||
""" raised for immediate program exits (no tracebacks/summaries)"""
|
||||
def __init__(self, msg="unknown reason"):
|
||||
self.msg = msg
|
||||
KeyboardInterrupt.__init__(self, msg)
|
||||
|
||||
# exposed helper methods
|
||||
|
||||
|
||||
def exit(msg):
|
||||
""" exit testing process as if KeyboardInterrupt was triggered. """
|
||||
__tracebackhide__ = True
|
||||
raise Exit(msg)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
:kwarg bool allow_module_level: allows this function to be called at
|
||||
module level, skipping the rest of the module. Default to False.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
allow_module_level = kwargs.pop('allow_module_level', False)
|
||||
if kwargs:
|
||||
keys = [k for k in kwargs.keys()]
|
||||
raise TypeError('unexpected keyword arguments: {0}'.format(keys))
|
||||
raise Skipped(msg=msg, allow_module_level=allow_module_level)
|
||||
|
||||
|
||||
skip.Exception = Skipped
|
||||
|
||||
|
||||
def fail(msg="", pytrace=True):
|
||||
""" explicitly fail an currently-executing test with the given Message.
|
||||
|
||||
:arg 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)
|
||||
|
||||
|
||||
fail.Exception = Failed
|
||||
|
||||
|
||||
class XFailed(fail.Exception):
|
||||
""" raised from an explicit call to pytest.xfail() """
|
||||
|
||||
|
||||
def xfail(reason=""):
|
||||
""" xfail an executing test or setup functions with the given reason."""
|
||||
__tracebackhide__ = True
|
||||
raise XFailed(reason)
|
||||
|
||||
|
||||
xfail.Exception = XFailed
|
||||
|
||||
|
||||
def importorskip(modname, minversion=None):
|
||||
""" return imported module if it has at least "minversion" as its
|
||||
__version__ attribute. If no minversion is specified the a skip
|
||||
is only triggered if the module can not be imported.
|
||||
"""
|
||||
import warnings
|
||||
__tracebackhide__ = True
|
||||
compile(modname, '', 'eval') # to catch syntaxerrors
|
||||
should_skip = False
|
||||
|
||||
with warnings.catch_warnings():
|
||||
# make sure to ignore ImportWarnings that might happen because
|
||||
# of existing directories with the same name we're trying to
|
||||
# import but without a __init__.py file
|
||||
warnings.simplefilter('ignore')
|
||||
try:
|
||||
__import__(modname)
|
||||
except ImportError:
|
||||
# Do not raise chained exception here(#1485)
|
||||
should_skip = True
|
||||
if should_skip:
|
||||
raise Skipped("could not import %r" % (modname,), allow_module_level=True)
|
||||
mod = sys.modules[modname]
|
||||
if minversion is None:
|
||||
return mod
|
||||
verattr = getattr(mod, '__version__', None)
|
||||
if minversion is not None:
|
||||
try:
|
||||
from pkg_resources import parse_version as pv
|
||||
except ImportError:
|
||||
raise Skipped("we have a required version for %r but can not import "
|
||||
"pkg_resources to parse version strings." % (modname,),
|
||||
allow_module_level=True)
|
||||
if verattr is None or pv(verattr) < pv(minversion):
|
||||
raise Skipped("module %r has __version__ %r, required is: %r" % (
|
||||
modname, verattr, minversion), allow_module_level=True)
|
||||
return mod
|
||||
@@ -2,6 +2,7 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import pytest
|
||||
import six
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
@@ -9,14 +10,13 @@ import tempfile
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group._addoption('--pastebin', metavar="mode",
|
||||
action='store', dest="pastebin", default=None,
|
||||
choices=['failed', 'all'],
|
||||
help="send failed|all info to bpaste.net pastebin service.")
|
||||
action='store', dest="pastebin", default=None,
|
||||
choices=['failed', 'all'],
|
||||
help="send failed|all info to bpaste.net pastebin service.")
|
||||
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_configure(config):
|
||||
import py
|
||||
if config.option.pastebin == "all":
|
||||
tr = config.pluginmanager.getplugin('terminalreporter')
|
||||
# if no terminal reporter plugin is present, nothing we can do here;
|
||||
@@ -29,7 +29,7 @@ def pytest_configure(config):
|
||||
|
||||
def tee_write(s, **kwargs):
|
||||
oldwrite(s, **kwargs)
|
||||
if py.builtin._istext(s):
|
||||
if isinstance(s, six.text_type):
|
||||
s = s.encode('utf-8')
|
||||
config._pastebinfile.write(s)
|
||||
|
||||
@@ -97,4 +97,4 @@ def pytest_terminal_summary(terminalreporter):
|
||||
s = tw.stringio.getvalue()
|
||||
assert len(s)
|
||||
pastebinurl = create_new_paste(s)
|
||||
tr.write_line("%s --> %s" %(msg, pastebinurl))
|
||||
tr.write_line("%s --> %s" % (msg, pastebinurl))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
634
_pytest/python_api.py
Normal file
634
_pytest/python_api.py
Normal file
@@ -0,0 +1,634 @@
|
||||
import math
|
||||
import sys
|
||||
|
||||
import py
|
||||
from six.moves import zip
|
||||
|
||||
from _pytest.compat import isclass
|
||||
from _pytest.outcomes import fail
|
||||
import _pytest._code
|
||||
|
||||
|
||||
def _cmp_raises_type_error(self, other):
|
||||
"""__cmp__ implementation which raises TypeError. Used
|
||||
by Approx base classes to implement only == and != and raise a
|
||||
TypeError for other comparisons.
|
||||
|
||||
Needed in Python 2 only, Python 3 all it takes is not implementing the
|
||||
other operators at all.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise TypeError('Comparison operators other than == and != not supported by approx objects')
|
||||
|
||||
|
||||
# builtin pytest.approx helper
|
||||
|
||||
|
||||
class ApproxBase(object):
|
||||
"""
|
||||
Provide shared utilities for making approximate comparisons between numbers
|
||||
or sequences of numbers.
|
||||
"""
|
||||
|
||||
def __init__(self, expected, rel=None, abs=None, nan_ok=False):
|
||||
self.expected = expected
|
||||
self.abs = abs
|
||||
self.rel = rel
|
||||
self.nan_ok = nan_ok
|
||||
|
||||
def __repr__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def __eq__(self, actual):
|
||||
return all(
|
||||
a == self._approx_scalar(x)
|
||||
for a, x in self._yield_comparisons(actual))
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, actual):
|
||||
return not (actual == self)
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
__cmp__ = _cmp_raises_type_error
|
||||
|
||||
def _approx_scalar(self, x):
|
||||
return ApproxScalar(x, rel=self.rel, abs=self.abs, nan_ok=self.nan_ok)
|
||||
|
||||
def _yield_comparisons(self, actual):
|
||||
"""
|
||||
Yield all the pairs of numbers to be compared. This is used to
|
||||
implement the `__eq__` method.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class ApproxNumpy(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for numpy arrays.
|
||||
"""
|
||||
|
||||
# Tell numpy to use our `__eq__` operator instead of its.
|
||||
__array_priority__ = 100
|
||||
|
||||
def __repr__(self):
|
||||
# It might be nice to rewrite this function to account for the
|
||||
# shape of the array...
|
||||
return "approx({0!r})".format(list(
|
||||
self._approx_scalar(x) for x in self.expected))
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
__cmp__ = _cmp_raises_type_error
|
||||
|
||||
def __eq__(self, actual):
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
actual = np.asarray(actual)
|
||||
except: # noqa
|
||||
raise TypeError("cannot compare '{0}' to numpy.ndarray".format(actual))
|
||||
|
||||
if actual.shape != self.expected.shape:
|
||||
return False
|
||||
|
||||
return ApproxBase.__eq__(self, actual)
|
||||
|
||||
def _yield_comparisons(self, actual):
|
||||
import numpy as np
|
||||
|
||||
# We can be sure that `actual` is a numpy array, because it's
|
||||
# casted in `__eq__` before being passed to `ApproxBase.__eq__`,
|
||||
# which is the only method that calls this one.
|
||||
for i in np.ndindex(self.expected.shape):
|
||||
yield actual[i], self.expected[i]
|
||||
|
||||
|
||||
class ApproxMapping(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for mappings where the values are numbers
|
||||
(the keys can be anything).
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
return "approx({0!r})".format(dict(
|
||||
(k, self._approx_scalar(v))
|
||||
for k, v in self.expected.items()))
|
||||
|
||||
def __eq__(self, actual):
|
||||
if set(actual.keys()) != set(self.expected.keys()):
|
||||
return False
|
||||
|
||||
return ApproxBase.__eq__(self, actual)
|
||||
|
||||
def _yield_comparisons(self, actual):
|
||||
for k in self.expected.keys():
|
||||
yield actual[k], self.expected[k]
|
||||
|
||||
|
||||
class ApproxSequence(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for sequences of numbers.
|
||||
"""
|
||||
|
||||
# Tell numpy to use our `__eq__` operator instead of its.
|
||||
__array_priority__ = 100
|
||||
|
||||
def __repr__(self):
|
||||
seq_type = type(self.expected)
|
||||
if seq_type not in (tuple, list, set):
|
||||
seq_type = list
|
||||
return "approx({0!r})".format(seq_type(
|
||||
self._approx_scalar(x) for x in self.expected))
|
||||
|
||||
def __eq__(self, actual):
|
||||
if len(actual) != len(self.expected):
|
||||
return False
|
||||
return ApproxBase.__eq__(self, actual)
|
||||
|
||||
def _yield_comparisons(self, actual):
|
||||
return zip(actual, self.expected)
|
||||
|
||||
|
||||
class ApproxScalar(ApproxBase):
|
||||
"""
|
||||
Perform approximate comparisons for single numbers only.
|
||||
"""
|
||||
DEFAULT_ABSOLUTE_TOLERANCE = 1e-12
|
||||
DEFAULT_RELATIVE_TOLERANCE = 1e-6
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Return a string communicating both the expected value and the tolerance
|
||||
for the comparison being made, e.g. '1.0 +- 1e-6'. Use the unicode
|
||||
plus/minus symbol if this is python3 (it's too hard to get right for
|
||||
python2).
|
||||
"""
|
||||
if isinstance(self.expected, complex):
|
||||
return str(self.expected)
|
||||
|
||||
# Infinities aren't compared using tolerances, so don't show a
|
||||
# tolerance.
|
||||
if math.isinf(self.expected):
|
||||
return str(self.expected)
|
||||
|
||||
# If a sensible tolerance can't be calculated, self.tolerance will
|
||||
# raise a ValueError. In this case, display '???'.
|
||||
try:
|
||||
vetted_tolerance = '{:.1e}'.format(self.tolerance)
|
||||
except ValueError:
|
||||
vetted_tolerance = '???'
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
return '{0} +- {1}'.format(self.expected, vetted_tolerance)
|
||||
else:
|
||||
return u'{0} \u00b1 {1}'.format(self.expected, vetted_tolerance)
|
||||
|
||||
def __eq__(self, actual):
|
||||
"""
|
||||
Return true if the given value is equal to the expected value within
|
||||
the pre-specified tolerance.
|
||||
"""
|
||||
|
||||
# Short-circuit exact equality.
|
||||
if actual == self.expected:
|
||||
return True
|
||||
|
||||
# Allow the user to control whether NaNs are considered equal to each
|
||||
# other or not. The abs() calls are for compatibility with complex
|
||||
# numbers.
|
||||
if math.isnan(abs(self.expected)):
|
||||
return self.nan_ok and math.isnan(abs(actual))
|
||||
|
||||
# Infinity shouldn't be approximately equal to anything but itself, but
|
||||
# if there's a relative tolerance, it will be infinite and infinity
|
||||
# will seem approximately equal to everything. The equal-to-itself
|
||||
# case would have been short circuited above, so here we can just
|
||||
# return false if the expected value is infinite. The abs() call is
|
||||
# for compatibility with complex numbers.
|
||||
if math.isinf(abs(self.expected)):
|
||||
return False
|
||||
|
||||
# Return true if the two numbers are within the tolerance.
|
||||
return abs(self.expected - actual) <= self.tolerance
|
||||
|
||||
__hash__ = None
|
||||
|
||||
@property
|
||||
def tolerance(self):
|
||||
"""
|
||||
Return the tolerance for the comparison. This could be either an
|
||||
absolute tolerance or a relative tolerance, depending on what the user
|
||||
specified or which would be larger.
|
||||
"""
|
||||
def set_default(x, default):
|
||||
return x if x is not None else default
|
||||
|
||||
# Figure out what the absolute tolerance should be. ``self.abs`` is
|
||||
# either None or a value specified by the user.
|
||||
absolute_tolerance = set_default(self.abs, self.DEFAULT_ABSOLUTE_TOLERANCE)
|
||||
|
||||
if absolute_tolerance < 0:
|
||||
raise ValueError("absolute tolerance can't be negative: {}".format(absolute_tolerance))
|
||||
if math.isnan(absolute_tolerance):
|
||||
raise ValueError("absolute tolerance can't be NaN.")
|
||||
|
||||
# If the user specified an absolute tolerance but not a relative one,
|
||||
# just return the absolute tolerance.
|
||||
if self.rel is None:
|
||||
if self.abs is not None:
|
||||
return absolute_tolerance
|
||||
|
||||
# Figure out what the relative tolerance should be. ``self.rel`` is
|
||||
# either None or a value specified by the user. This is done after
|
||||
# we've made sure the user didn't ask for an absolute tolerance only,
|
||||
# because we don't want to raise errors about the relative tolerance if
|
||||
# we aren't even going to use it.
|
||||
relative_tolerance = set_default(self.rel, self.DEFAULT_RELATIVE_TOLERANCE) * abs(self.expected)
|
||||
|
||||
if relative_tolerance < 0:
|
||||
raise ValueError("relative tolerance can't be negative: {}".format(absolute_tolerance))
|
||||
if math.isnan(relative_tolerance):
|
||||
raise ValueError("relative tolerance can't be NaN.")
|
||||
|
||||
# Return the larger of the relative and absolute tolerances.
|
||||
return max(relative_tolerance, absolute_tolerance)
|
||||
|
||||
|
||||
class ApproxDecimal(ApproxScalar):
|
||||
from decimal import Decimal
|
||||
|
||||
DEFAULT_ABSOLUTE_TOLERANCE = Decimal('1e-12')
|
||||
DEFAULT_RELATIVE_TOLERANCE = Decimal('1e-6')
|
||||
|
||||
|
||||
def approx(expected, rel=None, abs=None, nan_ok=False):
|
||||
"""
|
||||
Assert that two numbers (or two sets of numbers) are equal to each other
|
||||
within some tolerance.
|
||||
|
||||
Due to the `intricacies of floating-point arithmetic`__, numbers that we
|
||||
would intuitively expect to be equal are not always so::
|
||||
|
||||
>>> 0.1 + 0.2 == 0.3
|
||||
False
|
||||
|
||||
__ https://docs.python.org/3/tutorial/floatingpoint.html
|
||||
|
||||
This problem is commonly encountered when writing tests, e.g. when making
|
||||
sure that floating-point values are what you expect them to be. One way to
|
||||
deal with this problem is to assert that two floating-point numbers are
|
||||
equal to within some appropriate tolerance::
|
||||
|
||||
>>> abs((0.1 + 0.2) - 0.3) < 1e-6
|
||||
True
|
||||
|
||||
However, comparisons like this are tedious to write and difficult to
|
||||
understand. Furthermore, absolute comparisons like the one above are
|
||||
usually discouraged because there's no tolerance that works well for all
|
||||
situations. ``1e-6`` is good for numbers around ``1``, but too small for
|
||||
very big numbers and too big for very small ones. It's better to express
|
||||
the tolerance as a fraction of the expected value, but relative comparisons
|
||||
like that are even more difficult to write correctly and concisely.
|
||||
|
||||
The ``approx`` class performs floating-point comparisons using a syntax
|
||||
that's as intuitive as possible::
|
||||
|
||||
>>> from pytest import approx
|
||||
>>> 0.1 + 0.2 == approx(0.3)
|
||||
True
|
||||
|
||||
The same syntax also works for sequences of numbers::
|
||||
|
||||
>>> (0.1 + 0.2, 0.2 + 0.4) == approx((0.3, 0.6))
|
||||
True
|
||||
|
||||
Dictionary *values*::
|
||||
|
||||
>>> {'a': 0.1 + 0.2, 'b': 0.2 + 0.4} == approx({'a': 0.3, 'b': 0.6})
|
||||
True
|
||||
|
||||
And ``numpy`` arrays::
|
||||
|
||||
>>> import numpy as np # doctest: +SKIP
|
||||
>>> np.array([0.1, 0.2]) + np.array([0.2, 0.4]) == approx(np.array([0.3, 0.6])) # doctest: +SKIP
|
||||
True
|
||||
|
||||
By default, ``approx`` considers numbers within a relative tolerance of
|
||||
``1e-6`` (i.e. one part in a million) of its expected value to be equal.
|
||||
This treatment would lead to surprising results if the expected value was
|
||||
``0.0``, because nothing but ``0.0`` itself is relatively close to ``0.0``.
|
||||
To handle this case less surprisingly, ``approx`` also considers numbers
|
||||
within an absolute tolerance of ``1e-12`` of its expected value to be
|
||||
equal. Infinity and NaN are special cases. Infinity is only considered
|
||||
equal to itself, regardless of the relative tolerance. NaN is not
|
||||
considered equal to anything by default, but you can make it be equal to
|
||||
itself by setting the ``nan_ok`` argument to True. (This is meant to
|
||||
facilitate comparing arrays that use NaN to mean "no data".)
|
||||
|
||||
Both the relative and absolute tolerances can be changed by passing
|
||||
arguments to the ``approx`` constructor::
|
||||
|
||||
>>> 1.0001 == approx(1)
|
||||
False
|
||||
>>> 1.0001 == approx(1, rel=1e-3)
|
||||
True
|
||||
>>> 1.0001 == approx(1, abs=1e-3)
|
||||
True
|
||||
|
||||
If you specify ``abs`` but not ``rel``, the comparison will not consider
|
||||
the relative tolerance at all. In other words, two numbers that are within
|
||||
the default relative tolerance of ``1e-6`` will still be considered unequal
|
||||
if they exceed the specified absolute tolerance. If you specify both
|
||||
``abs`` and ``rel``, the numbers will be considered equal if either
|
||||
tolerance is met::
|
||||
|
||||
>>> 1 + 1e-8 == approx(1)
|
||||
True
|
||||
>>> 1 + 1e-8 == approx(1, abs=1e-12)
|
||||
False
|
||||
>>> 1 + 1e-8 == approx(1, rel=1e-6, abs=1e-12)
|
||||
True
|
||||
|
||||
If you're thinking about using ``approx``, then you might want to know how
|
||||
it compares to other good ways of comparing floating-point numbers. All of
|
||||
these algorithms are based on relative and absolute tolerances and should
|
||||
agree for the most part, but they do have meaningful differences:
|
||||
|
||||
- ``math.isclose(a, b, rel_tol=1e-9, abs_tol=0.0)``: True if the relative
|
||||
tolerance is met w.r.t. either ``a`` or ``b`` or if the absolute
|
||||
tolerance is met. Because the relative tolerance is calculated w.r.t.
|
||||
both ``a`` and ``b``, this test is symmetric (i.e. neither ``a`` nor
|
||||
``b`` is a "reference value"). You have to specify an absolute tolerance
|
||||
if you want to compare to ``0.0`` because there is no tolerance by
|
||||
default. Only available in python>=3.5. `More information...`__
|
||||
|
||||
__ https://docs.python.org/3/library/math.html#math.isclose
|
||||
|
||||
- ``numpy.isclose(a, b, rtol=1e-5, atol=1e-8)``: True if the difference
|
||||
between ``a`` and ``b`` is less that the sum of the relative tolerance
|
||||
w.r.t. ``b`` and the absolute tolerance. Because the relative tolerance
|
||||
is only calculated w.r.t. ``b``, this test is asymmetric and you can
|
||||
think of ``b`` as the reference value. Support for comparing sequences
|
||||
is provided by ``numpy.allclose``. `More information...`__
|
||||
|
||||
__ http://docs.scipy.org/doc/numpy-1.10.0/reference/generated/numpy.isclose.html
|
||||
|
||||
- ``unittest.TestCase.assertAlmostEqual(a, b)``: True if ``a`` and ``b``
|
||||
are within an absolute tolerance of ``1e-7``. No relative tolerance is
|
||||
considered and the absolute tolerance cannot be changed, so this function
|
||||
is not appropriate for very large or very small numbers. Also, it's only
|
||||
available in subclasses of ``unittest.TestCase`` and it's ugly because it
|
||||
doesn't follow PEP8. `More information...`__
|
||||
|
||||
__ https://docs.python.org/3/library/unittest.html#unittest.TestCase.assertAlmostEqual
|
||||
|
||||
- ``a == pytest.approx(b, rel=1e-6, abs=1e-12)``: True if the relative
|
||||
tolerance is met w.r.t. ``b`` or if the absolute tolerance is met.
|
||||
Because the relative tolerance is only calculated w.r.t. ``b``, this test
|
||||
is asymmetric and you can think of ``b`` as the reference value. In the
|
||||
special case that you explicitly specify an absolute tolerance but not a
|
||||
relative tolerance, only the absolute tolerance is considered.
|
||||
|
||||
.. warning::
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
In order to avoid inconsistent behavior, ``TypeError`` is
|
||||
raised for ``>``, ``>=``, ``<`` and ``<=`` comparisons.
|
||||
The example below illustrates the problem::
|
||||
|
||||
assert approx(0.1) > 0.1 + 1e-10 # calls approx(0.1).__gt__(0.1 + 1e-10)
|
||||
assert 0.1 + 1e-10 > approx(0.1) # calls approx(0.1).__lt__(0.1 + 1e-10)
|
||||
|
||||
In the second example one expects ``approx(0.1).__le__(0.1 + 1e-10)``
|
||||
to be called. But instead, ``approx(0.1).__lt__(0.1 + 1e-10)`` is used to
|
||||
comparison. This is because the call hierarchy of rich comparisons
|
||||
follows a fixed behavior. `More information...`__
|
||||
|
||||
__ https://docs.python.org/3/reference/datamodel.html#object.__ge__
|
||||
"""
|
||||
|
||||
from collections import Mapping, Sequence
|
||||
from _pytest.compat import STRING_TYPES as String
|
||||
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.
|
||||
#
|
||||
# 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.
|
||||
|
||||
if _is_numpy_array(expected):
|
||||
cls = ApproxNumpy
|
||||
elif isinstance(expected, Mapping):
|
||||
cls = ApproxMapping
|
||||
elif isinstance(expected, Sequence) and not isinstance(expected, String):
|
||||
cls = ApproxSequence
|
||||
elif isinstance(expected, Decimal):
|
||||
cls = ApproxDecimal
|
||||
else:
|
||||
cls = ApproxScalar
|
||||
|
||||
return cls(expected, rel, abs, nan_ok)
|
||||
|
||||
|
||||
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
|
||||
|
||||
return False
|
||||
|
||||
|
||||
# builtin pytest.raises helper
|
||||
|
||||
def raises(expected_exception, *args, **kwargs):
|
||||
"""
|
||||
Assert that a code block/function call raises ``expected_exception``
|
||||
and raise a failure exception otherwise.
|
||||
|
||||
:arg message: if specified, provides a custom failure message if the
|
||||
exception is not raised
|
||||
:arg match: if specified, asserts that the exception matches a text or regex
|
||||
|
||||
This helper produces a ``ExceptionInfo()`` object (see below).
|
||||
|
||||
You may use this function as a context manager::
|
||||
|
||||
>>> with raises(ZeroDivisionError):
|
||||
... 1/0
|
||||
|
||||
.. versionchanged:: 2.10
|
||||
|
||||
In the context manager form you may use the keyword argument
|
||||
``message`` to specify a custom failure message::
|
||||
|
||||
>>> with raises(ZeroDivisionError, message="Expecting ZeroDivisionError"):
|
||||
... pass
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Failed: Expecting ZeroDivisionError
|
||||
|
||||
.. note::
|
||||
|
||||
When using ``pytest.raises`` as a context manager, it's worthwhile to
|
||||
note that normal context manager rules apply and that the exception
|
||||
raised *must* be the final line in the scope of the context manager.
|
||||
Lines of code after that, within the scope of the context manager will
|
||||
not be executed. For example::
|
||||
|
||||
>>> value = 15
|
||||
>>> with raises(ValueError) as exc_info:
|
||||
... if value > 10:
|
||||
... raise ValueError("value must be <= 10")
|
||||
... assert exc_info.type == ValueError # this will not execute
|
||||
|
||||
Instead, the following approach must be taken (note the difference in
|
||||
scope)::
|
||||
|
||||
>>> with raises(ValueError) as exc_info:
|
||||
... if value > 10:
|
||||
... raise ValueError("value must be <= 10")
|
||||
...
|
||||
>>> assert exc_info.type == ValueError
|
||||
|
||||
|
||||
Since version ``3.1`` you can use the keyword argument ``match`` to assert that the
|
||||
exception matches a text or regex::
|
||||
|
||||
>>> with raises(ValueError, match='must be 0 or None'):
|
||||
... raise ValueError("value must be 0 or None")
|
||||
|
||||
>>> with raises(ValueError, match=r'must be \d+$'):
|
||||
... raise ValueError("value must be 42")
|
||||
|
||||
**Legacy forms**
|
||||
|
||||
The forms below are fully supported but are discouraged for new code because the
|
||||
context manager form is regarded as more readable and less error-prone.
|
||||
|
||||
It is possible to specify a callable by passing a to-be-called lambda::
|
||||
|
||||
>>> raises(ZeroDivisionError, lambda: 1/0)
|
||||
<ExceptionInfo ...>
|
||||
|
||||
or you can specify an arbitrary callable with arguments::
|
||||
|
||||
>>> def f(x): return 1/x
|
||||
...
|
||||
>>> raises(ZeroDivisionError, f, 0)
|
||||
<ExceptionInfo ...>
|
||||
>>> raises(ZeroDivisionError, f, x=0)
|
||||
<ExceptionInfo ...>
|
||||
|
||||
It is also possible to pass a string to be evaluated at runtime::
|
||||
|
||||
>>> raises(ZeroDivisionError, "f(0)")
|
||||
<ExceptionInfo ...>
|
||||
|
||||
The string will be evaluated using the same ``locals()`` and ``globals()``
|
||||
at the moment of the ``raises`` call.
|
||||
|
||||
.. autoclass:: _pytest._code.ExceptionInfo
|
||||
:members:
|
||||
|
||||
.. note::
|
||||
Similar to caught exception objects in Python, explicitly clearing
|
||||
local references to returned ``ExceptionInfo`` objects can
|
||||
help the Python interpreter speed up its garbage collection.
|
||||
|
||||
Clearing those references breaks a reference cycle
|
||||
(``ExceptionInfo`` --> caught exception --> frame stack raising
|
||||
the exception --> current frame stack --> local variables -->
|
||||
``ExceptionInfo``) which makes Python keep all objects referenced
|
||||
from that cycle (including all local variables in the current
|
||||
frame) alive until the next cyclic garbage collection run. See the
|
||||
official Python ``try`` statement documentation for more detailed
|
||||
information.
|
||||
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
msg = ("exceptions must be old-style classes or"
|
||||
" derived from BaseException, not %s")
|
||||
if isinstance(expected_exception, tuple):
|
||||
for exc in expected_exception:
|
||||
if not isclass(exc):
|
||||
raise TypeError(msg % type(exc))
|
||||
elif not isclass(expected_exception):
|
||||
raise TypeError(msg % type(expected_exception))
|
||||
|
||||
message = "DID NOT RAISE {0}".format(expected_exception)
|
||||
match_expr = None
|
||||
|
||||
if not args:
|
||||
if "message" in kwargs:
|
||||
message = kwargs.pop("message")
|
||||
if "match" in kwargs:
|
||||
match_expr = kwargs.pop("match")
|
||||
return RaisesContext(expected_exception, message, match_expr)
|
||||
elif isinstance(args[0], str):
|
||||
code, = args
|
||||
assert isinstance(code, str)
|
||||
frame = sys._getframe(1)
|
||||
loc = frame.f_locals.copy()
|
||||
loc.update(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?
|
||||
# this is destroyed here ...
|
||||
except expected_exception:
|
||||
return _pytest._code.ExceptionInfo()
|
||||
else:
|
||||
func = args[0]
|
||||
try:
|
||||
func(*args[1:], **kwargs)
|
||||
except expected_exception:
|
||||
return _pytest._code.ExceptionInfo()
|
||||
fail(message)
|
||||
|
||||
|
||||
raises.Exception = fail.Exception
|
||||
|
||||
|
||||
class RaisesContext(object):
|
||||
def __init__(self, expected_exception, message, match_expr):
|
||||
self.expected_exception = expected_exception
|
||||
self.message = message
|
||||
self.match_expr = match_expr
|
||||
self.excinfo = None
|
||||
|
||||
def __enter__(self):
|
||||
self.excinfo = object.__new__(_pytest._code.ExceptionInfo)
|
||||
return self.excinfo
|
||||
|
||||
def __exit__(self, *tp):
|
||||
__tracebackhide__ = True
|
||||
if tp[0] is None:
|
||||
fail(self.message)
|
||||
self.excinfo.__init__(tp)
|
||||
suppress_exception = issubclass(self.excinfo.type, self.expected_exception)
|
||||
if sys.version_info[0] == 2 and suppress_exception:
|
||||
sys.exc_clear()
|
||||
if self.match_expr and suppress_exception:
|
||||
self.excinfo.match(self.match_expr)
|
||||
return suppress_exception
|
||||
@@ -7,7 +7,11 @@ import _pytest._code
|
||||
import py
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import re
|
||||
|
||||
from _pytest.fixtures import yield_fixture
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
|
||||
@yield_fixture
|
||||
@@ -27,10 +31,8 @@ def recwarn():
|
||||
|
||||
|
||||
def deprecated_call(func=None, *args, **kwargs):
|
||||
""" assert that calling ``func(*args, **kwargs)`` triggers a
|
||||
``DeprecationWarning`` or ``PendingDeprecationWarning``.
|
||||
|
||||
This function can be used as a context manager::
|
||||
"""context manager that can be used to ensure a block of code triggers a
|
||||
``DeprecationWarning`` or ``PendingDeprecationWarning``::
|
||||
|
||||
>>> import warnings
|
||||
>>> def api_call_v2():
|
||||
@@ -40,38 +42,47 @@ def deprecated_call(func=None, *args, **kwargs):
|
||||
>>> with deprecated_call():
|
||||
... assert api_call_v2() == 200
|
||||
|
||||
Note: we cannot use WarningsRecorder here because it is still subject
|
||||
to the mechanism that prevents warnings of the same type from being
|
||||
triggered twice for the same module. See #1190.
|
||||
``deprecated_call`` can also be used by passing a function and ``*args`` and ``*kwargs``,
|
||||
in which case it will ensure calling ``func(*args, **kwargs)`` produces one of the warnings
|
||||
types above.
|
||||
"""
|
||||
if not func:
|
||||
return WarningsChecker(expected_warning=(DeprecationWarning, PendingDeprecationWarning))
|
||||
|
||||
categories = []
|
||||
|
||||
def warn_explicit(message, category, *args, **kwargs):
|
||||
categories.append(category)
|
||||
|
||||
def warn(message, category=None, *args, **kwargs):
|
||||
if isinstance(message, Warning):
|
||||
categories.append(message.__class__)
|
||||
else:
|
||||
categories.append(category)
|
||||
|
||||
old_warn = warnings.warn
|
||||
old_warn_explicit = warnings.warn_explicit
|
||||
warnings.warn_explicit = warn_explicit
|
||||
warnings.warn = warn
|
||||
try:
|
||||
ret = func(*args, **kwargs)
|
||||
finally:
|
||||
warnings.warn_explicit = old_warn_explicit
|
||||
warnings.warn = old_warn
|
||||
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
|
||||
if not any(issubclass(c, deprecation_categories) for c in categories):
|
||||
return _DeprecatedCallContext()
|
||||
else:
|
||||
__tracebackhide__ = True
|
||||
raise AssertionError("%r did not produce DeprecationWarning" % (func,))
|
||||
return ret
|
||||
with _DeprecatedCallContext():
|
||||
return func(*args, **kwargs)
|
||||
|
||||
|
||||
class _DeprecatedCallContext(object):
|
||||
"""Implements the logic to capture deprecation warnings as a context manager."""
|
||||
|
||||
def __enter__(self):
|
||||
self._captured_categories = []
|
||||
self._old_warn = warnings.warn
|
||||
self._old_warn_explicit = warnings.warn_explicit
|
||||
warnings.warn_explicit = self._warn_explicit
|
||||
warnings.warn = self._warn
|
||||
|
||||
def _warn_explicit(self, message, category, *args, **kwargs):
|
||||
self._captured_categories.append(category)
|
||||
|
||||
def _warn(self, message, category=None, *args, **kwargs):
|
||||
if isinstance(message, Warning):
|
||||
self._captured_categories.append(message.__class__)
|
||||
else:
|
||||
self._captured_categories.append(category)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
warnings.warn_explicit = self._old_warn_explicit
|
||||
warnings.warn = self._old_warn
|
||||
|
||||
if exc_type is None:
|
||||
deprecation_categories = (DeprecationWarning, PendingDeprecationWarning)
|
||||
if not any(issubclass(c, deprecation_categories) for c in self._captured_categories):
|
||||
__tracebackhide__ = True
|
||||
msg = "Did not produce DeprecationWarning or PendingDeprecationWarning"
|
||||
raise AssertionError(msg)
|
||||
|
||||
|
||||
def warns(expected_warning, *args, **kwargs):
|
||||
@@ -89,10 +100,28 @@ def warns(expected_warning, *args, **kwargs):
|
||||
|
||||
>>> with warns(RuntimeWarning):
|
||||
... warnings.warn("my warning", RuntimeWarning)
|
||||
|
||||
In the context manager form you may use the keyword argument ``match`` to assert
|
||||
that the exception matches a text or regex::
|
||||
|
||||
>>> with warns(UserWarning, match='must be 0 or None'):
|
||||
... warnings.warn("value must be 0 or None", UserWarning)
|
||||
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
... warnings.warn("value must be 42", UserWarning)
|
||||
|
||||
>>> with warns(UserWarning, match=r'must be \d+$'):
|
||||
... warnings.warn("this is not here", UserWarning)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
Failed: DID NOT WARN. No warnings of type ...UserWarning... was emitted...
|
||||
|
||||
"""
|
||||
wcheck = WarningsChecker(expected_warning)
|
||||
match_expr = None
|
||||
if not args:
|
||||
return wcheck
|
||||
if "match" in kwargs:
|
||||
match_expr = kwargs.pop("match")
|
||||
return WarningsChecker(expected_warning, match_expr=match_expr)
|
||||
elif isinstance(args[0], str):
|
||||
code, = args
|
||||
assert isinstance(code, str)
|
||||
@@ -100,12 +129,12 @@ def warns(expected_warning, *args, **kwargs):
|
||||
loc = frame.f_locals.copy()
|
||||
loc.update(kwargs)
|
||||
|
||||
with wcheck:
|
||||
with WarningsChecker(expected_warning, match_expr=match_expr):
|
||||
code = _pytest._code.Source(code).compile()
|
||||
py.builtin.exec_(code, frame.f_globals, loc)
|
||||
else:
|
||||
func = args[0]
|
||||
with wcheck:
|
||||
with WarningsChecker(expected_warning, match_expr=match_expr):
|
||||
return func(*args[1:], **kwargs)
|
||||
|
||||
|
||||
@@ -165,7 +194,7 @@ class WarningsRecorder(warnings.catch_warnings):
|
||||
|
||||
|
||||
class WarningsChecker(WarningsRecorder):
|
||||
def __init__(self, expected_warning=None):
|
||||
def __init__(self, expected_warning=None, match_expr=None):
|
||||
super(WarningsChecker, self).__init__()
|
||||
|
||||
msg = ("exceptions must be old-style classes or "
|
||||
@@ -180,6 +209,7 @@ class WarningsChecker(WarningsRecorder):
|
||||
raise TypeError(msg % type(expected_warning))
|
||||
|
||||
self.expected_warning = expected_warning
|
||||
self.match_expr = match_expr
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
super(WarningsChecker, self).__exit__(*exc_info)
|
||||
@@ -190,8 +220,17 @@ class WarningsChecker(WarningsRecorder):
|
||||
if not any(issubclass(r.category, self.expected_warning)
|
||||
for r in self):
|
||||
__tracebackhide__ = True
|
||||
from _pytest.runner import fail
|
||||
fail("DID NOT WARN. No warnings of type {0} was emitted. "
|
||||
"The list of emitted warnings is: {1}.".format(
|
||||
self.expected_warning,
|
||||
[each.message for each in self]))
|
||||
self.expected_warning,
|
||||
[each.message for each in self]))
|
||||
elif self.match_expr is not None:
|
||||
for r in self:
|
||||
if issubclass(r.category, self.expected_warning):
|
||||
if re.compile(self.match_expr).search(str(r.message)):
|
||||
break
|
||||
else:
|
||||
fail("DID NOT WARN. No warnings of type {0} matching"
|
||||
" ('{1}') was emitted. The list of emitted warnings"
|
||||
" is: {2}.".format(self.expected_warning, self.match_expr,
|
||||
[each.message for each in self]))
|
||||
|
||||
@@ -6,11 +6,13 @@ from __future__ import absolute_import, division, print_function
|
||||
import py
|
||||
import os
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "resultlog plugin options")
|
||||
group.addoption('--resultlog', '--result-log', action="store",
|
||||
metavar="path", default=None,
|
||||
help="DEPRECATED path for machine-readable result log.")
|
||||
metavar="path", default=None,
|
||||
help="DEPRECATED path for machine-readable result log.")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
resultlog = config.option.resultlog
|
||||
@@ -19,13 +21,14 @@ def pytest_configure(config):
|
||||
dirname = os.path.dirname(os.path.abspath(resultlog))
|
||||
if not os.path.isdir(dirname):
|
||||
os.makedirs(dirname)
|
||||
logfile = open(resultlog, 'w', 1) # line buffered
|
||||
logfile = open(resultlog, 'w', 1) # line buffered
|
||||
config._resultlog = ResultLog(config, logfile)
|
||||
config.pluginmanager.register(config._resultlog)
|
||||
|
||||
from _pytest.deprecated import RESULT_LOG
|
||||
config.warn('C1', RESULT_LOG)
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
resultlog = getattr(config, '_resultlog', None)
|
||||
if resultlog:
|
||||
@@ -33,6 +36,7 @@ def pytest_unconfigure(config):
|
||||
del config._resultlog
|
||||
config.pluginmanager.unregister(resultlog)
|
||||
|
||||
|
||||
def generic_path(item):
|
||||
chain = item.listchain()
|
||||
gpath = [chain[0].name]
|
||||
@@ -56,10 +60,11 @@ def generic_path(item):
|
||||
fspath = newfspath
|
||||
return ''.join(gpath)
|
||||
|
||||
|
||||
class ResultLog(object):
|
||||
def __init__(self, config, logfile):
|
||||
self.config = config
|
||||
self.logfile = logfile # preferably line buffered
|
||||
self.logfile = logfile # preferably line buffered
|
||||
|
||||
def write_log_entry(self, testpath, lettercode, longrepr):
|
||||
print("%s %s" % (lettercode, testpath), file=self.logfile)
|
||||
|
||||
@@ -2,22 +2,24 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import bdb
|
||||
import os
|
||||
import sys
|
||||
from time import time
|
||||
|
||||
import py
|
||||
from _pytest._code.code import TerminalRepr, ExceptionInfo
|
||||
|
||||
|
||||
from _pytest.outcomes import skip, Skipped, TEST_OUTCOME
|
||||
|
||||
#
|
||||
# pytest plugin hooks
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
group.addoption('--durations',
|
||||
action="store", type=int, default=None, metavar="N",
|
||||
help="show N slowest setup/test durations (N=0 for all)."),
|
||||
action="store", type=int, default=None, metavar="N",
|
||||
help="show N slowest setup/test durations (N=0 for all)."),
|
||||
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
durations = terminalreporter.config.option.durations
|
||||
@@ -42,24 +44,28 @@ def pytest_terminal_summary(terminalreporter):
|
||||
for rep in dlist:
|
||||
nodeid = rep.nodeid.replace("::()::", "::")
|
||||
tr.write_line("%02.2fs %-8s %s" %
|
||||
(rep.duration, rep.when, nodeid))
|
||||
(rep.duration, rep.when, nodeid))
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
session._setupstate = SetupState()
|
||||
|
||||
|
||||
def pytest_sessionfinish(session):
|
||||
session._setupstate.teardown_all()
|
||||
|
||||
class NodeInfo:
|
||||
def __init__(self, location):
|
||||
self.location = location
|
||||
|
||||
def pytest_runtest_protocol(item, nextitem):
|
||||
item.ihook.pytest_runtest_logstart(
|
||||
nodeid=item.nodeid, location=item.location,
|
||||
)
|
||||
runtestprotocol(item, nextitem=nextitem)
|
||||
item.ihook.pytest_runtest_logfinish(
|
||||
nodeid=item.nodeid, location=item.location,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def runtestprotocol(item, log=True, nextitem=None):
|
||||
hasrequest = hasattr(item, "_request")
|
||||
if hasrequest and not item._request:
|
||||
@@ -72,7 +78,7 @@ def runtestprotocol(item, log=True, nextitem=None):
|
||||
if not item.config.option.setuponly:
|
||||
reports.append(call_and_report(item, "call", log))
|
||||
reports.append(call_and_report(item, "teardown", log,
|
||||
nextitem=nextitem))
|
||||
nextitem=nextitem))
|
||||
# after all teardown hooks have been called
|
||||
# want funcargs and request info to go away
|
||||
if hasrequest:
|
||||
@@ -80,6 +86,7 @@ def runtestprotocol(item, log=True, nextitem=None):
|
||||
item.funcargs = None
|
||||
return reports
|
||||
|
||||
|
||||
def show_test_item(item):
|
||||
"""Show test function, parameters and the fixtures of the test item."""
|
||||
tw = item.config.get_terminal_writer()
|
||||
@@ -90,10 +97,14 @@ def show_test_item(item):
|
||||
if used_fixtures:
|
||||
tw.write(' (fixtures used: {0})'.format(', '.join(used_fixtures)))
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
_update_current_test_var(item, 'setup')
|
||||
item.session._setupstate.prepare(item)
|
||||
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
_update_current_test_var(item, 'call')
|
||||
try:
|
||||
item.runtest()
|
||||
except Exception:
|
||||
@@ -106,8 +117,28 @@ def pytest_runtest_call(item):
|
||||
del tb # Get rid of it in this namespace
|
||||
raise
|
||||
|
||||
|
||||
def pytest_runtest_teardown(item, nextitem):
|
||||
_update_current_test_var(item, 'teardown')
|
||||
item.session._setupstate.teardown_exact(item, nextitem)
|
||||
_update_current_test_var(item, None)
|
||||
|
||||
|
||||
def _update_current_test_var(item, when):
|
||||
"""
|
||||
Update PYTEST_CURRENT_TEST to reflect the current item and stage.
|
||||
|
||||
If ``when`` is None, delete PYTEST_CURRENT_TEST from the environment.
|
||||
"""
|
||||
var_name = 'PYTEST_CURRENT_TEST'
|
||||
if when:
|
||||
value = '{0} ({1})'.format(item.nodeid, when)
|
||||
# don't allow null bytes on environment variables (see #2644, #2957)
|
||||
value = value.replace('\x00', '(null)')
|
||||
os.environ[var_name] = value
|
||||
else:
|
||||
os.environ.pop(var_name)
|
||||
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
if report.when in ("setup", "teardown"):
|
||||
@@ -133,21 +164,25 @@ def call_and_report(item, when, log=True, **kwds):
|
||||
hook.pytest_exception_interact(node=item, call=call, report=report)
|
||||
return report
|
||||
|
||||
|
||||
def check_interactive_exception(call, report):
|
||||
return call.excinfo and not (
|
||||
hasattr(report, "wasxfail") or
|
||||
call.excinfo.errisinstance(skip.Exception) or
|
||||
call.excinfo.errisinstance(bdb.BdbQuit))
|
||||
hasattr(report, "wasxfail") or
|
||||
call.excinfo.errisinstance(skip.Exception) or
|
||||
call.excinfo.errisinstance(bdb.BdbQuit))
|
||||
|
||||
|
||||
def call_runtest_hook(item, when, **kwds):
|
||||
hookname = "pytest_runtest_" + when
|
||||
ihook = getattr(item.ihook, hookname)
|
||||
return CallInfo(lambda: ihook(item=item, **kwds), when=when)
|
||||
|
||||
class CallInfo:
|
||||
|
||||
class CallInfo(object):
|
||||
""" Result/Exception info a function invocation. """
|
||||
#: None or ExceptionInfo object.
|
||||
excinfo = None
|
||||
|
||||
def __init__(self, func, when):
|
||||
#: context of invocation: one of "setup", "call",
|
||||
#: "teardown", "memocollect"
|
||||
@@ -158,7 +193,7 @@ class CallInfo:
|
||||
except KeyboardInterrupt:
|
||||
self.stop = time()
|
||||
raise
|
||||
except:
|
||||
except: # noqa
|
||||
self.excinfo = ExceptionInfo()
|
||||
self.stop = time()
|
||||
|
||||
@@ -169,6 +204,7 @@ class CallInfo:
|
||||
status = "result: %r" % (self.result,)
|
||||
return "<CallInfo when=%r %s>" % (self.when, status)
|
||||
|
||||
|
||||
def getslaveinfoline(node):
|
||||
try:
|
||||
return node._slaveinfocache
|
||||
@@ -179,6 +215,7 @@ def getslaveinfoline(node):
|
||||
d['id'], d['sysplatform'], ver, d['executable'])
|
||||
return s
|
||||
|
||||
|
||||
class BaseReport(object):
|
||||
|
||||
def __init__(self, **kw):
|
||||
@@ -243,10 +280,11 @@ class BaseReport(object):
|
||||
def fspath(self):
|
||||
return self.nodeid.split("::")[0]
|
||||
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
when = call.when
|
||||
duration = call.stop-call.start
|
||||
keywords = dict([(x,1) for x in item.keywords])
|
||||
duration = call.stop - call.start
|
||||
keywords = dict([(x, 1) for x in item.keywords])
|
||||
excinfo = call.excinfo
|
||||
sections = []
|
||||
if not call.excinfo:
|
||||
@@ -264,19 +302,21 @@ def pytest_runtest_makereport(item, call):
|
||||
outcome = "failed"
|
||||
if call.when == "call":
|
||||
longrepr = item.repr_failure(excinfo)
|
||||
else: # exception in setup or teardown
|
||||
else: # exception in setup or teardown
|
||||
longrepr = item._repr_failure_py(excinfo,
|
||||
style=item.config.option.tbstyle)
|
||||
style=item.config.option.tbstyle)
|
||||
for rwhen, key, content in item._report_sections:
|
||||
sections.append(("Captured %s %s" %(key, rwhen), content))
|
||||
sections.append(("Captured %s %s" % (key, rwhen), content))
|
||||
return TestReport(item.nodeid, item.location,
|
||||
keywords, outcome, longrepr, when,
|
||||
sections, duration)
|
||||
|
||||
|
||||
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, **extra):
|
||||
#: normalized collection node id
|
||||
@@ -315,14 +355,17 @@ class TestReport(BaseReport):
|
||||
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()),
|
||||
@@ -344,7 +387,7 @@ def pytest_make_collect_report(collector):
|
||||
errorinfo = CollectErrorRepr(errorinfo)
|
||||
longrepr = errorinfo
|
||||
rep = CollectReport(collector.nodeid, outcome, longrepr,
|
||||
getattr(call, 'result', None))
|
||||
getattr(call, 'result', None))
|
||||
rep.call = call # see collect_one_node
|
||||
return rep
|
||||
|
||||
@@ -365,16 +408,20 @@ class CollectReport(BaseReport):
|
||||
|
||||
def __repr__(self):
|
||||
return "<CollectReport %r lenresult=%s outcome=%r>" % (
|
||||
self.nodeid, len(self.result), self.outcome)
|
||||
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. """
|
||||
|
||||
def __init__(self):
|
||||
self.stack = []
|
||||
self._finalizers = {}
|
||||
@@ -385,8 +432,8 @@ class SetupState(object):
|
||||
is called at the end of teardown_all().
|
||||
"""
|
||||
assert colitem and not isinstance(colitem, tuple)
|
||||
assert py.builtin.callable(finalizer)
|
||||
#assert colitem in self.stack # some unit tests don't setup stack :/
|
||||
assert callable(finalizer)
|
||||
# assert colitem in self.stack # some unit tests don't setup stack :/
|
||||
self._finalizers.setdefault(colitem, []).append(finalizer)
|
||||
|
||||
def _pop_and_teardown(self):
|
||||
@@ -400,7 +447,7 @@ class SetupState(object):
|
||||
fin = finalizers.pop()
|
||||
try:
|
||||
fin()
|
||||
except Exception:
|
||||
except TEST_OUTCOME:
|
||||
# XXX Only first exception will be seen by user,
|
||||
# ideally all should be reported.
|
||||
if exc is None:
|
||||
@@ -414,7 +461,7 @@ class SetupState(object):
|
||||
colitem.teardown()
|
||||
for colitem in self._finalizers:
|
||||
assert colitem is None or colitem in self.stack \
|
||||
or isinstance(colitem, tuple)
|
||||
or isinstance(colitem, tuple)
|
||||
|
||||
def teardown_all(self):
|
||||
while self.stack:
|
||||
@@ -447,10 +494,11 @@ class SetupState(object):
|
||||
self.stack.append(col)
|
||||
try:
|
||||
col.setup()
|
||||
except Exception:
|
||||
except TEST_OUTCOME:
|
||||
col._prepare_exc = sys.exc_info()
|
||||
raise
|
||||
|
||||
|
||||
def collect_one_node(collector):
|
||||
ihook = collector.ihook
|
||||
ihook.pytest_collectstart(collector=collector)
|
||||
@@ -459,122 +507,3 @@ def collect_one_node(collector):
|
||||
if call and check_interactive_exception(call, rep):
|
||||
ihook.pytest_exception_interact(node=collector, call=call, report=rep)
|
||||
return rep
|
||||
|
||||
|
||||
# =============================================================
|
||||
# Test OutcomeExceptions and helpers for creating them.
|
||||
|
||||
|
||||
class OutcomeException(Exception):
|
||||
""" OutcomeException and its subclass instances indicate and
|
||||
contain info about test and collection outcomes.
|
||||
"""
|
||||
def __init__(self, msg=None, pytrace=True):
|
||||
Exception.__init__(self, msg)
|
||||
self.msg = msg
|
||||
self.pytrace = pytrace
|
||||
|
||||
def __repr__(self):
|
||||
if self.msg:
|
||||
val = self.msg
|
||||
if isinstance(val, bytes):
|
||||
val = py._builtin._totext(val, errors='replace')
|
||||
return val
|
||||
return "<%s instance>" %(self.__class__.__name__,)
|
||||
__str__ = __repr__
|
||||
|
||||
class Skipped(OutcomeException):
|
||||
# XXX hackish: on 3k we fake to live in the builtins
|
||||
# in order to have Skipped exception printing shorter/nicer
|
||||
__module__ = 'builtins'
|
||||
|
||||
def __init__(self, msg=None, pytrace=True, allow_module_level=False):
|
||||
OutcomeException.__init__(self, msg=msg, pytrace=pytrace)
|
||||
self.allow_module_level = allow_module_level
|
||||
|
||||
|
||||
class Failed(OutcomeException):
|
||||
""" raised from an explicit call to pytest.fail() """
|
||||
__module__ = 'builtins'
|
||||
|
||||
|
||||
class Exit(KeyboardInterrupt):
|
||||
""" raised for immediate program exits (no tracebacks/summaries)"""
|
||||
def __init__(self, msg="unknown reason"):
|
||||
self.msg = msg
|
||||
KeyboardInterrupt.__init__(self, msg)
|
||||
|
||||
# exposed helper methods
|
||||
|
||||
def exit(msg):
|
||||
""" exit testing process as if KeyboardInterrupt was triggered. """
|
||||
__tracebackhide__ = True
|
||||
raise Exit(msg)
|
||||
|
||||
|
||||
exit.Exception = Exit
|
||||
|
||||
|
||||
def skip(msg=""):
|
||||
""" 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.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise Skipped(msg=msg)
|
||||
|
||||
|
||||
skip.Exception = Skipped
|
||||
|
||||
|
||||
def fail(msg="", pytrace=True):
|
||||
""" explicitly fail an currently-executing test with the given Message.
|
||||
|
||||
:arg 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)
|
||||
|
||||
|
||||
fail.Exception = Failed
|
||||
|
||||
|
||||
def importorskip(modname, minversion=None):
|
||||
""" return imported module if it has at least "minversion" as its
|
||||
__version__ attribute. If no minversion is specified the a skip
|
||||
is only triggered if the module can not be imported.
|
||||
"""
|
||||
import warnings
|
||||
__tracebackhide__ = True
|
||||
compile(modname, '', 'eval') # to catch syntaxerrors
|
||||
should_skip = False
|
||||
|
||||
with warnings.catch_warnings():
|
||||
# make sure to ignore ImportWarnings that might happen because
|
||||
# of existing directories with the same name we're trying to
|
||||
# import but without a __init__.py file
|
||||
warnings.simplefilter('ignore')
|
||||
try:
|
||||
__import__(modname)
|
||||
except ImportError:
|
||||
# Do not raise chained exception here(#1485)
|
||||
should_skip = True
|
||||
if should_skip:
|
||||
raise Skipped("could not import %r" %(modname,), allow_module_level=True)
|
||||
mod = sys.modules[modname]
|
||||
if minversion is None:
|
||||
return mod
|
||||
verattr = getattr(mod, '__version__', None)
|
||||
if minversion is not None:
|
||||
try:
|
||||
from pkg_resources import parse_version as pv
|
||||
except ImportError:
|
||||
raise Skipped("we have a required version for %r but can not import "
|
||||
"pkg_resources to parse version strings." % (modname,),
|
||||
allow_module_level=True)
|
||||
if verattr is None or pv(verattr) < pv(minversion):
|
||||
raise Skipped("module %r has __version__ %r, required is: %r" %(
|
||||
modname, verattr, minversion), allow_module_level=True)
|
||||
return mod
|
||||
|
||||
@@ -44,7 +44,7 @@ def _show_fixture_action(fixturedef, msg):
|
||||
config = fixturedef._fixturemanager.config
|
||||
capman = config.pluginmanager.getplugin('capturemanager')
|
||||
if capman:
|
||||
out, err = capman.suspendcapture()
|
||||
out, err = capman.suspend_global_capture()
|
||||
|
||||
tw = config.get_terminal_writer()
|
||||
tw.line()
|
||||
@@ -63,7 +63,7 @@ def _show_fixture_action(fixturedef, msg):
|
||||
tw.write('[{0}]'.format(fixturedef.cached_param))
|
||||
|
||||
if capman:
|
||||
capman.resumecapture()
|
||||
capman.resume_global_capture()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import os
|
||||
import six
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import py
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
from _pytest.runner import fail, skip
|
||||
from _pytest.outcomes import fail, skip, xfail, TEST_OUTCOME
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group.addoption('--runxfail',
|
||||
action="store_true", dest="runxfail", default=False,
|
||||
help="run tests even if they are marked xfail")
|
||||
action="store_true", dest="runxfail", default=False,
|
||||
help="run tests even if they are marked xfail")
|
||||
|
||||
parser.addini("xfail_strict", "default for the strict parameter of xfail "
|
||||
"markers when not given explicitly (default: "
|
||||
@@ -33,61 +34,57 @@ def pytest_configure(config):
|
||||
def nop(*args, **kwargs):
|
||||
pass
|
||||
|
||||
nop.Exception = XFailed
|
||||
nop.Exception = xfail.Exception
|
||||
setattr(pytest, "xfail", nop)
|
||||
|
||||
config.addinivalue_line("markers",
|
||||
"skip(reason=None): skip the given test function with an optional reason. "
|
||||
"Example: skip(reason=\"no way of currently testing this\") skips the "
|
||||
"test."
|
||||
)
|
||||
"skip(reason=None): skip the given test function with an optional reason. "
|
||||
"Example: skip(reason=\"no way of currently testing this\") skips the "
|
||||
"test."
|
||||
)
|
||||
config.addinivalue_line("markers",
|
||||
"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"
|
||||
)
|
||||
"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"
|
||||
)
|
||||
config.addinivalue_line("markers",
|
||||
"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"
|
||||
)
|
||||
"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"
|
||||
)
|
||||
|
||||
|
||||
class XFailed(fail.Exception):
|
||||
""" raised from an explicit call to pytest.xfail() """
|
||||
|
||||
|
||||
def xfail(reason=""):
|
||||
""" xfail an executing test or setup functions with the given reason."""
|
||||
__tracebackhide__ = True
|
||||
raise XFailed(reason)
|
||||
|
||||
|
||||
xfail.Exception = XFailed
|
||||
|
||||
|
||||
class MarkEvaluator:
|
||||
class MarkEvaluator(object):
|
||||
def __init__(self, item, name):
|
||||
self.item = item
|
||||
self.name = name
|
||||
|
||||
@property
|
||||
def holder(self):
|
||||
return self.item.keywords.get(self.name)
|
||||
self._marks = None
|
||||
self._mark = None
|
||||
self._mark_name = name
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self.holder)
|
||||
self._marks = self._get_marks()
|
||||
return bool(self._marks)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def wasvalid(self):
|
||||
return not hasattr(self, 'exc')
|
||||
|
||||
def _get_marks(self):
|
||||
|
||||
keyword = self.item.keywords.get(self._mark_name)
|
||||
if isinstance(keyword, MarkDecorator):
|
||||
return [keyword.mark]
|
||||
elif isinstance(keyword, MarkInfo):
|
||||
return [x.combined for x in keyword]
|
||||
else:
|
||||
return []
|
||||
|
||||
def invalidraise(self, exc):
|
||||
raises = self.get('raises')
|
||||
if not raises:
|
||||
@@ -97,7 +94,7 @@ class MarkEvaluator:
|
||||
def istrue(self):
|
||||
try:
|
||||
return self._istrue()
|
||||
except Exception:
|
||||
except TEST_OUTCOME:
|
||||
self.exc = sys.exc_info()
|
||||
if isinstance(self.exc[1], SyntaxError):
|
||||
msg = [" " * (self.exc[1].offset + 4) + "^", ]
|
||||
@@ -107,7 +104,7 @@ class MarkEvaluator:
|
||||
fail("Error evaluating %r expression\n"
|
||||
" %s\n"
|
||||
"%s"
|
||||
% (self.name, self.expr, "\n".join(msg)),
|
||||
% (self._mark_name, self.expr, "\n".join(msg)),
|
||||
pytrace=False)
|
||||
|
||||
def _getglobals(self):
|
||||
@@ -119,40 +116,45 @@ class MarkEvaluator:
|
||||
def _istrue(self):
|
||||
if hasattr(self, 'result'):
|
||||
return self.result
|
||||
if self.holder:
|
||||
if self.holder.args or 'condition' in self.holder.kwargs:
|
||||
self.result = False
|
||||
# "holder" might be a MarkInfo or a MarkDecorator; only
|
||||
# MarkInfo keeps track of all parameters it received in an
|
||||
# _arglist attribute
|
||||
marks = getattr(self.holder, '_marks', None) \
|
||||
or [self.holder.mark]
|
||||
for _, args, kwargs in marks:
|
||||
if 'condition' in kwargs:
|
||||
args = (kwargs['condition'],)
|
||||
for expr in args:
|
||||
self._marks = self._get_marks()
|
||||
|
||||
if self._marks:
|
||||
self.result = False
|
||||
for mark in self._marks:
|
||||
self._mark = mark
|
||||
if 'condition' in mark.kwargs:
|
||||
args = (mark.kwargs['condition'],)
|
||||
else:
|
||||
args = mark.args
|
||||
|
||||
for expr in args:
|
||||
self.expr = expr
|
||||
if isinstance(expr, six.string_types):
|
||||
d = self._getglobals()
|
||||
result = cached_eval(self.item.config, expr, d)
|
||||
else:
|
||||
if "reason" not in mark.kwargs:
|
||||
# XXX better be checked at collection time
|
||||
msg = "you need to specify reason=STRING " \
|
||||
"when using booleans as conditions."
|
||||
fail(msg)
|
||||
result = bool(expr)
|
||||
if result:
|
||||
self.result = True
|
||||
self.reason = mark.kwargs.get('reason', None)
|
||||
self.expr = expr
|
||||
if isinstance(expr, py.builtin._basestring):
|
||||
d = self._getglobals()
|
||||
result = cached_eval(self.item.config, expr, d)
|
||||
else:
|
||||
if "reason" not in kwargs:
|
||||
# XXX better be checked at collection time
|
||||
msg = "you need to specify reason=STRING " \
|
||||
"when using booleans as conditions."
|
||||
fail(msg)
|
||||
result = bool(expr)
|
||||
if result:
|
||||
self.result = True
|
||||
self.reason = kwargs.get('reason', None)
|
||||
self.expr = expr
|
||||
return self.result
|
||||
else:
|
||||
self.result = True
|
||||
return getattr(self, 'result', False)
|
||||
return self.result
|
||||
|
||||
if not args:
|
||||
self.result = True
|
||||
self.reason = mark.kwargs.get('reason', None)
|
||||
return self.result
|
||||
return False
|
||||
|
||||
def get(self, attr, default=None):
|
||||
return self.holder.kwargs.get(attr, default)
|
||||
if self._mark is None:
|
||||
return default
|
||||
return self._mark.kwargs.get(attr, default)
|
||||
|
||||
def getexplanation(self):
|
||||
expl = getattr(self, 'reason', None) or self.get('reason', None)
|
||||
@@ -167,17 +169,17 @@ class MarkEvaluator:
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_runtest_setup(item):
|
||||
# Check if skip or skipif are specified as pytest marks
|
||||
|
||||
item._skipped_by_mark = False
|
||||
skipif_info = item.keywords.get('skipif')
|
||||
if isinstance(skipif_info, (MarkInfo, MarkDecorator)):
|
||||
eval_skipif = MarkEvaluator(item, 'skipif')
|
||||
if eval_skipif.istrue():
|
||||
item._evalskip = eval_skipif
|
||||
item._skipped_by_mark = True
|
||||
skip(eval_skipif.getexplanation())
|
||||
|
||||
skip_info = item.keywords.get('skip')
|
||||
if isinstance(skip_info, (MarkInfo, MarkDecorator)):
|
||||
item._evalskip = True
|
||||
item._skipped_by_mark = True
|
||||
if 'reason' in skip_info.kwargs:
|
||||
skip(skip_info.kwargs['reason'])
|
||||
elif skip_info.args:
|
||||
@@ -224,7 +226,6 @@ def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
rep = outcome.get_result()
|
||||
evalxfail = getattr(item, '_evalxfail', None)
|
||||
evalskip = getattr(item, '_evalskip', None)
|
||||
# unitttest special case, see setting of _unexpectedsuccess
|
||||
if hasattr(item, '_unexpectedsuccess') and rep.when == "call":
|
||||
from _pytest.compat import _is_unittest_unexpected_success_a_failure
|
||||
@@ -243,7 +244,7 @@ def pytest_runtest_makereport(item, call):
|
||||
rep.wasxfail = "reason: " + call.excinfo.value.msg
|
||||
rep.outcome = "skipped"
|
||||
elif evalxfail and not rep.skipped and evalxfail.wasvalid() and \
|
||||
evalxfail.istrue():
|
||||
evalxfail.istrue():
|
||||
if call.excinfo:
|
||||
if evalxfail.invalidraise(call.excinfo.value):
|
||||
rep.outcome = "failed"
|
||||
@@ -260,7 +261,7 @@ def pytest_runtest_makereport(item, call):
|
||||
else:
|
||||
rep.outcome = "passed"
|
||||
rep.wasxfail = explanation
|
||||
elif evalskip is not None and rep.skipped and type(rep.longrepr) is tuple:
|
||||
elif getattr(item, '_skipped_by_mark', False) and rep.skipped and type(rep.longrepr) is tuple:
|
||||
# skipped by mark.skipif; change the location of the failure
|
||||
# to point to the item definition, otherwise it will display
|
||||
# the location of where the skip exception was raised within pytest
|
||||
@@ -269,6 +270,8 @@ def pytest_runtest_makereport(item, call):
|
||||
rep.longrepr = filename, line, reason
|
||||
|
||||
# called by terminalreporter progress reporting
|
||||
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
if hasattr(report, "wasxfail"):
|
||||
if report.skipped:
|
||||
@@ -277,10 +280,12 @@ def pytest_report_teststatus(report):
|
||||
return "xpassed", "X", ("XPASS", {'yellow': True})
|
||||
|
||||
# called by the terminalreporter instance/plugin
|
||||
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
tr = terminalreporter
|
||||
if not tr.reportchars:
|
||||
#for name in "xfailed skipped failed xpassed":
|
||||
# for name in "xfailed skipped failed xpassed":
|
||||
# if not tr.stats.get(name, 0):
|
||||
# tr.write_line("HINT: use '-r' option to see extra "
|
||||
# "summary info about tests")
|
||||
@@ -353,28 +358,40 @@ def folded_skips(skipped):
|
||||
for event in skipped:
|
||||
key = event.longrepr
|
||||
assert len(key) == 3, (event, key)
|
||||
keywords = getattr(event, 'keywords', {})
|
||||
# folding reports with global pytestmark variable
|
||||
# this is workaround, because for now we cannot identify the scope of a skip marker
|
||||
# TODO: revisit after marks scope would be fixed
|
||||
when = getattr(event, 'when', None)
|
||||
if when == 'setup' and 'skip' in keywords and 'pytestmark' not in keywords:
|
||||
key = (key[0], None, key[2], )
|
||||
d.setdefault(key, []).append(event)
|
||||
l = []
|
||||
values = []
|
||||
for key, events in d.items():
|
||||
l.append((len(events),) + key)
|
||||
return l
|
||||
values.append((len(events),) + key)
|
||||
return values
|
||||
|
||||
|
||||
def show_skipped(terminalreporter, lines):
|
||||
tr = terminalreporter
|
||||
skipped = tr.stats.get('skipped', [])
|
||||
if skipped:
|
||||
#if not tr.hasopt('skipped'):
|
||||
# if not tr.hasopt('skipped'):
|
||||
# tr.write_line(
|
||||
# "%d skipped tests, specify -rs for more info" %
|
||||
# len(skipped))
|
||||
# return
|
||||
fskips = folded_skips(skipped)
|
||||
if fskips:
|
||||
#tr.write_sep("_", "skipped test summary")
|
||||
# tr.write_sep("_", "skipped test summary")
|
||||
for num, fspath, lineno, reason in fskips:
|
||||
if reason.startswith("Skipped: "):
|
||||
reason = reason[9:]
|
||||
lines.append(
|
||||
"SKIP [%d] %s:%d: %s" %
|
||||
(num, fspath, lineno, reason))
|
||||
if lineno is not None:
|
||||
lines.append(
|
||||
"SKIP [%d] %s:%d: %s" %
|
||||
(num, fspath, lineno + 1, reason))
|
||||
else:
|
||||
lines.append(
|
||||
"SKIP [%d] %s: %s" %
|
||||
(num, fspath, reason))
|
||||
|
||||
@@ -5,47 +5,55 @@ This is a good source for looking at the various reporting hooks.
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import itertools
|
||||
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
|
||||
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
|
||||
import pytest
|
||||
import py
|
||||
import platform
|
||||
import sys
|
||||
import time
|
||||
import platform
|
||||
|
||||
import _pytest._pluggy as pluggy
|
||||
import pluggy
|
||||
import py
|
||||
import six
|
||||
|
||||
import pytest
|
||||
from _pytest import nodes
|
||||
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
|
||||
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
group._addoption('-v', '--verbose', action="count",
|
||||
dest="verbose", default=0, help="increase verbosity."),
|
||||
dest="verbose", default=0, help="increase verbosity."),
|
||||
group._addoption('-q', '--quiet', action="count",
|
||||
dest="quiet", default=0, help="decrease verbosity."),
|
||||
dest="quiet", default=0, help="decrease verbosity."),
|
||||
group._addoption('-r',
|
||||
action="store", dest="reportchars", default='', metavar="chars",
|
||||
help="show extra test summary info as specified by chars (f)ailed, "
|
||||
"(E)error, (s)skipped, (x)failed, (X)passed, "
|
||||
"(p)passed, (P)passed with output, (a)all except pP. "
|
||||
"Warnings are displayed at all times except when "
|
||||
"--disable-warnings is set")
|
||||
action="store", dest="reportchars", default='', metavar="chars",
|
||||
help="show extra test summary info as specified by chars (f)ailed, "
|
||||
"(E)error, (s)skipped, (x)failed, (X)passed, "
|
||||
"(p)passed, (P)passed with output, (a)all except pP. "
|
||||
"Warnings are displayed at all times except when "
|
||||
"--disable-warnings is set")
|
||||
group._addoption('--disable-warnings', '--disable-pytest-warnings', default=False,
|
||||
dest='disable_warnings', action='store_true',
|
||||
help='disable warnings summary')
|
||||
group._addoption('-l', '--showlocals',
|
||||
action="store_true", dest="showlocals", default=False,
|
||||
help="show locals in tracebacks (disabled by default).")
|
||||
action="store_true", dest="showlocals", default=False,
|
||||
help="show locals in tracebacks (disabled by default).")
|
||||
group._addoption('--tb', metavar="style",
|
||||
action="store", dest="tbstyle", default='auto',
|
||||
choices=['auto', 'long', 'short', 'no', 'line', 'native'],
|
||||
help="traceback print mode (auto/long/short/line/native/no).")
|
||||
action="store", dest="tbstyle", default='auto',
|
||||
choices=['auto', 'long', 'short', 'no', 'line', 'native'],
|
||||
help="traceback print mode (auto/long/short/line/native/no).")
|
||||
group._addoption('--fulltrace', '--full-trace',
|
||||
action="store_true", default=False,
|
||||
help="don't cut any tracebacks (default is to cut).")
|
||||
action="store_true", default=False,
|
||||
help="don't cut any tracebacks (default is to cut).")
|
||||
group._addoption('--color', metavar="color",
|
||||
action="store", dest="color", default='auto',
|
||||
choices=['yes', 'no', 'auto'],
|
||||
help="color terminal output (yes/no/auto).")
|
||||
action="store", dest="color", default='auto',
|
||||
choices=['yes', 'no', 'auto'],
|
||||
help="color terminal output (yes/no/auto).")
|
||||
|
||||
parser.addini("console_output_style",
|
||||
help="console output: classic or with additional progress information (classic|progress).",
|
||||
default='progress')
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
config.option.verbose -= config.option.quiet
|
||||
@@ -57,6 +65,7 @@ def pytest_configure(config):
|
||||
reporter.write_line("[traceconfig] " + msg)
|
||||
config.trace.root.setprocessor("pytest:config", mywriter)
|
||||
|
||||
|
||||
def getreportopt(config):
|
||||
reportopts = ""
|
||||
reportchars = config.option.reportchars
|
||||
@@ -72,6 +81,7 @@ def getreportopt(config):
|
||||
reportopts = 'fEsxXw'
|
||||
return reportopts
|
||||
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
if report.passed:
|
||||
letter = "."
|
||||
@@ -84,10 +94,11 @@ def pytest_report_teststatus(report):
|
||||
return report.outcome, letter, report.outcome.upper()
|
||||
|
||||
|
||||
class WarningReport:
|
||||
class WarningReport(object):
|
||||
"""
|
||||
Simple structure to hold warnings information captured by ``pytest_logwarning``.
|
||||
"""
|
||||
|
||||
def __init__(self, code, message, nodeid=None, fslocation=None):
|
||||
"""
|
||||
:param code: unused
|
||||
@@ -118,7 +129,7 @@ class WarningReport:
|
||||
return None
|
||||
|
||||
|
||||
class TerminalReporter:
|
||||
class TerminalReporter(object):
|
||||
def __init__(self, config, file=None):
|
||||
import _pytest.config
|
||||
self.config = config
|
||||
@@ -127,17 +138,32 @@ class TerminalReporter:
|
||||
self.showfspath = self.verbosity >= 0
|
||||
self.showlongtestinfo = self.verbosity > 0
|
||||
self._numcollected = 0
|
||||
self._session = None
|
||||
|
||||
self.stats = {}
|
||||
self.startdir = py.path.local()
|
||||
if file is None:
|
||||
file = sys.stdout
|
||||
self._tw = self.writer = _pytest.config.create_terminal_writer(config,
|
||||
file)
|
||||
self._tw = _pytest.config.create_terminal_writer(config, file)
|
||||
# self.writer will be deprecated in pytest-3.4
|
||||
self.writer = self._tw
|
||||
self._screen_width = self._tw.fullwidth
|
||||
self.currentfspath = None
|
||||
self.reportchars = getreportopt(config)
|
||||
self.hasmarkup = self._tw.hasmarkup
|
||||
self.isatty = file.isatty()
|
||||
self._progress_nodeids_reported = set()
|
||||
self._show_progress_info = self._determine_show_progress_info()
|
||||
|
||||
def _determine_show_progress_info(self):
|
||||
"""Return True if we should display progress information based on the current config"""
|
||||
# do not show progress if we are not capturing output (#3038)
|
||||
if self.config.getoption('capture') == 'no':
|
||||
return False
|
||||
# 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'
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
|
||||
@@ -146,6 +172,8 @@ class TerminalReporter:
|
||||
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:
|
||||
self._write_progress_information_filling_space()
|
||||
self.currentfspath = fspath
|
||||
fspath = self.startdir.bestrelpath(fspath)
|
||||
self._tw.line()
|
||||
@@ -170,14 +198,28 @@ class TerminalReporter:
|
||||
self._tw.write(content, **markup)
|
||||
|
||||
def write_line(self, line, **markup):
|
||||
if not py.builtin._istext(line):
|
||||
line = py.builtin.text(line, errors="replace")
|
||||
if not isinstance(line, six.text_type):
|
||||
line = six.text_type(line, errors="replace")
|
||||
self.ensure_newline()
|
||||
self._tw.line(line, **markup)
|
||||
|
||||
def rewrite(self, line, **markup):
|
||||
"""
|
||||
Rewinds the terminal cursor to the beginning and writes the given line.
|
||||
|
||||
:kwarg erase: if True, will also add spaces until the full terminal width to ensure
|
||||
previous lines are properly erased.
|
||||
|
||||
The rest of the keyword arguments are markup instructions.
|
||||
"""
|
||||
erase = markup.pop('erase', False)
|
||||
if erase:
|
||||
fill_count = self._tw.fullwidth - len(line) - 1
|
||||
fill = ' ' * fill_count
|
||||
else:
|
||||
fill = ''
|
||||
line = str(line)
|
||||
self._tw.write("\r" + line, **markup)
|
||||
self._tw.write("\r" + line + fill, **markup)
|
||||
|
||||
def write_sep(self, sep, title=None, **markup):
|
||||
self.ensure_newline()
|
||||
@@ -190,7 +232,7 @@ class TerminalReporter:
|
||||
self._tw.line(msg, **kw)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
for line in py.builtin.text(excrepr).split("\n"):
|
||||
for line in six.text_type(excrepr).split("\n"):
|
||||
self.write_line("INTERNALERROR> " + line)
|
||||
return 1
|
||||
|
||||
@@ -225,38 +267,76 @@ class TerminalReporter:
|
||||
rep = report
|
||||
res = self.config.hook.pytest_report_teststatus(report=rep)
|
||||
cat, letter, word = res
|
||||
if isinstance(word, tuple):
|
||||
word, markup = word
|
||||
else:
|
||||
markup = None
|
||||
self.stats.setdefault(cat, []).append(rep)
|
||||
self._tests_ran = True
|
||||
if not letter and not word:
|
||||
# probably passed setup/teardown
|
||||
return
|
||||
running_xdist = hasattr(rep, 'node')
|
||||
if self.verbosity <= 0:
|
||||
if not hasattr(rep, 'node') and self.showfspath:
|
||||
if not running_xdist and self.showfspath:
|
||||
self.write_fspath_result(rep.nodeid, letter)
|
||||
else:
|
||||
self._tw.write(letter)
|
||||
else:
|
||||
if isinstance(word, tuple):
|
||||
word, markup = word
|
||||
else:
|
||||
self._progress_nodeids_reported.add(rep.nodeid)
|
||||
if markup is None:
|
||||
if rep.passed:
|
||||
markup = {'green':True}
|
||||
markup = {'green': True}
|
||||
elif rep.failed:
|
||||
markup = {'red':True}
|
||||
markup = {'red': True}
|
||||
elif rep.skipped:
|
||||
markup = {'yellow':True}
|
||||
markup = {'yellow': True}
|
||||
else:
|
||||
markup = {}
|
||||
line = self._locationline(rep.nodeid, *rep.location)
|
||||
if not hasattr(rep, 'node'):
|
||||
if not running_xdist:
|
||||
self.write_ensure_prefix(line, word, **markup)
|
||||
#self._tw.write(word, **markup)
|
||||
if self._show_progress_info:
|
||||
self._write_progress_information_filling_space()
|
||||
else:
|
||||
self.ensure_newline()
|
||||
if hasattr(rep, 'node'):
|
||||
self._tw.write("[%s] " % rep.node.gateway.id)
|
||||
self._tw.write("[%s]" % rep.node.gateway.id)
|
||||
if self._show_progress_info:
|
||||
self._tw.write(self._get_progress_information_message() + " ", cyan=True)
|
||||
else:
|
||||
self._tw.write(' ')
|
||||
self._tw.write(word, **markup)
|
||||
self._tw.write(" " + line)
|
||||
self.currentfspath = -2
|
||||
|
||||
def pytest_runtest_logfinish(self, nodeid):
|
||||
if self.verbosity <= 0 and self._show_progress_info:
|
||||
self._progress_nodeids_reported.add(nodeid)
|
||||
last_item = len(self._progress_nodeids_reported) == self._session.testscollected
|
||||
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
|
||||
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%]'
|
||||
|
||||
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)
|
||||
|
||||
def pytest_collection(self):
|
||||
if not self.isatty and self.config.option.verbose >= 1:
|
||||
self.write("collecting ... ", bold=True)
|
||||
@@ -269,7 +349,7 @@ class TerminalReporter:
|
||||
items = [x for x in report.result if isinstance(x, pytest.Item)]
|
||||
self._numcollected += len(items)
|
||||
if self.isatty:
|
||||
#self.write_fspath_result(report.nodeid, 'E')
|
||||
# self.write_fspath_result(report.nodeid, 'E')
|
||||
self.report_collect()
|
||||
|
||||
def report_collect(self, final=False):
|
||||
@@ -282,15 +362,15 @@ class TerminalReporter:
|
||||
line = "collected "
|
||||
else:
|
||||
line = "collecting "
|
||||
line += str(self._numcollected) + " items"
|
||||
line += str(self._numcollected) + " item" + ('' if self._numcollected == 1 else 's')
|
||||
if errors:
|
||||
line += " / %d errors" % errors
|
||||
if skipped:
|
||||
line += " / %d skipped" % skipped
|
||||
if self.isatty:
|
||||
self.rewrite(line, bold=True, erase=True)
|
||||
if final:
|
||||
line += " \n"
|
||||
self.rewrite(line, bold=True)
|
||||
self.write('\n')
|
||||
else:
|
||||
self.write_line(line)
|
||||
|
||||
@@ -299,6 +379,7 @@ class TerminalReporter:
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_sessionstart(self, session):
|
||||
self._session = session
|
||||
self._sessionstarttime = time.time()
|
||||
if not self.showheader:
|
||||
return
|
||||
@@ -316,6 +397,9 @@ class TerminalReporter:
|
||||
self.write_line(msg)
|
||||
lines = self.config.hook.pytest_report_header(
|
||||
config=self.config, startdir=self.startdir)
|
||||
self._write_report_lines_from_hooks(lines)
|
||||
|
||||
def _write_report_lines_from_hooks(self, lines):
|
||||
lines.reverse()
|
||||
for line in flatten(lines):
|
||||
self.write_line(line)
|
||||
@@ -342,10 +426,9 @@ class TerminalReporter:
|
||||
rep.toterminal(self._tw)
|
||||
return 1
|
||||
return 0
|
||||
if not self.showheader:
|
||||
return
|
||||
#for i, testarg in enumerate(self.config.args):
|
||||
# self.write_line("test path %d: %s" %(i+1, testarg))
|
||||
lines = self.config.hook.pytest_report_collectionfinish(
|
||||
config=self.config, startdir=self.startdir, items=session.items)
|
||||
self._write_report_lines_from_hooks(lines)
|
||||
|
||||
def _printcollecteditems(self, items):
|
||||
# to print out items and their parent collectors
|
||||
@@ -368,14 +451,14 @@ class TerminalReporter:
|
||||
stack = []
|
||||
indent = ""
|
||||
for item in items:
|
||||
needed_collectors = item.listchain()[1:] # strip root node
|
||||
needed_collectors = item.listchain()[1:] # strip root node
|
||||
while stack:
|
||||
if stack == needed_collectors[:len(stack)]:
|
||||
break
|
||||
stack.pop()
|
||||
for col in needed_collectors[len(stack):]:
|
||||
stack.append(col)
|
||||
#if col.name == "()":
|
||||
# if col.name == "()":
|
||||
# continue
|
||||
indent = (len(stack) - 1) * " "
|
||||
self._tw.line("%s%s" % (indent, col))
|
||||
@@ -424,15 +507,15 @@ class TerminalReporter:
|
||||
line = self.config.cwd_relative_nodeid(nodeid)
|
||||
if domain and line.endswith(domain):
|
||||
line = line[:-len(domain)]
|
||||
l = domain.split("[")
|
||||
l[0] = l[0].replace('.', '::') # don't replace '.' in params
|
||||
line += "[".join(l)
|
||||
values = domain.split("[")
|
||||
values[0] = values[0].replace('.', '::') # don't replace '.' in params
|
||||
line += "[".join(values)
|
||||
return line
|
||||
# collect_fspath comes from testid which has a "/"-normalized path
|
||||
|
||||
if fspath:
|
||||
res = mkrel(nodeid).replace("::()", "") # parens-normalization
|
||||
if nodeid.split("::")[0] != fspath.replace("\\", "/"):
|
||||
if nodeid.split("::")[0] != fspath.replace("\\", nodes.SEP):
|
||||
res += " <- " + self.startdir.bestrelpath(fspath)
|
||||
else:
|
||||
res = "[location]"
|
||||
@@ -443,7 +526,7 @@ class TerminalReporter:
|
||||
fspath, lineno, domain = rep.location
|
||||
return domain
|
||||
else:
|
||||
return "test session" # XXX?
|
||||
return "test session" # XXX?
|
||||
|
||||
def _getcrashline(self, rep):
|
||||
try:
|
||||
@@ -458,11 +541,11 @@ class TerminalReporter:
|
||||
# summaries for sessionfinish
|
||||
#
|
||||
def getreports(self, name):
|
||||
l = []
|
||||
values = []
|
||||
for x in self.stats.get(name, []):
|
||||
if not hasattr(x, '_pdbshown'):
|
||||
l.append(x)
|
||||
return l
|
||||
values.append(x)
|
||||
return values
|
||||
|
||||
def summary_warnings(self):
|
||||
if self.hasopt("w"):
|
||||
@@ -473,9 +556,9 @@ class TerminalReporter:
|
||||
grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config))
|
||||
|
||||
self.write_sep("=", "warnings summary", yellow=True, bold=False)
|
||||
for location, warnings in grouped:
|
||||
for location, warning_records in grouped:
|
||||
self._tw.line(str(location) or '<undetermined location>')
|
||||
for w in warnings:
|
||||
for w in warning_records:
|
||||
lines = w.message.splitlines()
|
||||
indented = '\n'.join(' ' + x for x in lines)
|
||||
self._tw.line(indented)
|
||||
@@ -502,7 +585,6 @@ class TerminalReporter:
|
||||
content = content[:-1]
|
||||
self._tw.line(content)
|
||||
|
||||
|
||||
def summary_failures(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
reports = self.getreports('failed')
|
||||
@@ -564,6 +646,7 @@ class TerminalReporter:
|
||||
self.write_sep("=", "%d tests deselected" % (
|
||||
len(self.stats['deselected'])), bold=True)
|
||||
|
||||
|
||||
def repr_pythonversion(v=None):
|
||||
if v is None:
|
||||
v = sys.version_info
|
||||
@@ -572,21 +655,23 @@ def repr_pythonversion(v=None):
|
||||
except (TypeError, ValueError):
|
||||
return str(v)
|
||||
|
||||
def flatten(l):
|
||||
for x in l:
|
||||
|
||||
def flatten(values):
|
||||
for x in values:
|
||||
if isinstance(x, (list, tuple)):
|
||||
for y in flatten(x):
|
||||
yield y
|
||||
else:
|
||||
yield x
|
||||
|
||||
|
||||
def build_summary_stats_line(stats):
|
||||
keys = ("failed passed skipped deselected "
|
||||
"xfailed xpassed warnings error").split()
|
||||
unknown_key_seen = False
|
||||
for key in stats.keys():
|
||||
if key not in keys:
|
||||
if key: # setup/teardown reports have an empty key, ignore them
|
||||
if key: # setup/teardown reports have an empty key, ignore them
|
||||
keys.append(key)
|
||||
unknown_key_seen = True
|
||||
parts = []
|
||||
@@ -613,7 +698,7 @@ def build_summary_stats_line(stats):
|
||||
|
||||
|
||||
def _plugin_nameversions(plugininfo):
|
||||
l = []
|
||||
values = []
|
||||
for plugin, dist in plugininfo:
|
||||
# gets us name and version!
|
||||
name = '{dist.project_name}-{dist.version}'.format(dist=dist)
|
||||
@@ -622,6 +707,6 @@ def _plugin_nameversions(plugininfo):
|
||||
name = name[7:]
|
||||
# we decided to print python package names
|
||||
# they can have more than one plugin
|
||||
if name not in l:
|
||||
l.append(name)
|
||||
return l
|
||||
if name not in values:
|
||||
values.append(name)
|
||||
return values
|
||||
|
||||
@@ -8,7 +8,7 @@ import py
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
|
||||
class TempdirFactory:
|
||||
class TempdirFactory(object):
|
||||
"""Factory for temporary directories under the common base temp directory.
|
||||
|
||||
The base directory can be configured using the ``--basetemp`` option.
|
||||
@@ -25,7 +25,7 @@ class TempdirFactory:
|
||||
provides an empty unique-per-test-invocation directory
|
||||
and is guaranteed to be empty.
|
||||
"""
|
||||
#py.log._apiwarn(">1.1", "use tmpdir function argument")
|
||||
# py.log._apiwarn(">1.1", "use tmpdir function argument")
|
||||
return self.getbasetemp().ensure(string, dir=dir)
|
||||
|
||||
def mktemp(self, basename, numbered=True):
|
||||
@@ -38,7 +38,7 @@ class TempdirFactory:
|
||||
p = basetemp.mkdir(basename)
|
||||
else:
|
||||
p = py.path.local.make_numbered_dir(prefix=basename,
|
||||
keep=0, rootdir=basetemp, lock_timeout=None)
|
||||
keep=0, rootdir=basetemp, lock_timeout=None)
|
||||
self.trace("mktemp", p)
|
||||
return p
|
||||
|
||||
|
||||
@@ -7,9 +7,8 @@ import traceback
|
||||
# for transferring markers
|
||||
import _pytest._code
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.runner import fail, skip
|
||||
from _pytest.outcomes import fail, skip, xfail
|
||||
from _pytest.python import transfer_markers, Class, Module, Function
|
||||
from _pytest.skipping import MarkEvaluator, xfail
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
@@ -109,13 +108,13 @@ class TestCaseFunction(Function):
|
||||
except TypeError:
|
||||
try:
|
||||
try:
|
||||
l = traceback.format_exception(*rawexcinfo)
|
||||
l.insert(0, "NOTE: Incompatible Exception Representation, "
|
||||
"displaying natively:\n\n")
|
||||
fail("".join(l), pytrace=False)
|
||||
values = traceback.format_exception(*rawexcinfo)
|
||||
values.insert(0, "NOTE: Incompatible Exception Representation, "
|
||||
"displaying natively:\n\n")
|
||||
fail("".join(values), pytrace=False)
|
||||
except (fail.Exception, KeyboardInterrupt):
|
||||
raise
|
||||
except:
|
||||
except: # noqa
|
||||
fail("ERROR: Unknown Incompatible Exception "
|
||||
"representation:\n%r" % (rawexcinfo,), pytrace=False)
|
||||
except KeyboardInterrupt:
|
||||
@@ -134,8 +133,7 @@ class TestCaseFunction(Function):
|
||||
try:
|
||||
skip(reason)
|
||||
except skip.Exception:
|
||||
self._evalskip = MarkEvaluator(self, 'SkipTest')
|
||||
self._evalskip.result = True
|
||||
self._skipped_by_mark = True
|
||||
self._addexcinfo(sys.exc_info())
|
||||
|
||||
def addExpectedFailure(self, testcase, rawexcinfo, reason=""):
|
||||
@@ -158,7 +156,7 @@ class TestCaseFunction(Function):
|
||||
# analog to pythons Lib/unittest/case.py:run
|
||||
testMethod = getattr(self._testcase, self._testcase._testMethodName)
|
||||
if (getattr(self._testcase.__class__, "__unittest_skip__", False) or
|
||||
getattr(testMethod, "__unittest_skip__", False)):
|
||||
getattr(testMethod, "__unittest_skip__", False)):
|
||||
# If the class or method was skipped.
|
||||
skip_why = (getattr(self._testcase.__class__, '__unittest_skip_why__', '') or
|
||||
getattr(testMethod, '__unittest_skip_why__', ''))
|
||||
@@ -210,7 +208,7 @@ def pytest_runtest_protocol(item):
|
||||
check_testcase_implements_trial_reporter()
|
||||
|
||||
def excstore(self, exc_value=None, exc_type=None, exc_tb=None,
|
||||
captureVars=None):
|
||||
captureVars=None):
|
||||
if exc_value is None:
|
||||
self._rawexcinfo = sys.exc_info()
|
||||
else:
|
||||
@@ -219,7 +217,7 @@ def pytest_runtest_protocol(item):
|
||||
self._rawexcinfo = (exc_type, exc_value, exc_tb)
|
||||
try:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb,
|
||||
captureVars=captureVars)
|
||||
captureVars=captureVars)
|
||||
except TypeError:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb)
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
This directory vendors the `pluggy` module.
|
||||
|
||||
For a more detailed discussion for the reasons to vendoring this
|
||||
package, please see [this issue](https://github.com/pytest-dev/pytest/issues/944).
|
||||
|
||||
To update the current version, execute:
|
||||
|
||||
```
|
||||
$ pip install -U pluggy==<version> --no-compile --target=_pytest/vendored_packages
|
||||
```
|
||||
|
||||
And commit the modified files. The `pluggy-<version>.dist-info` directory
|
||||
created by `pip` should be added as well.
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
Plugin registration and hook calling for Python
|
||||
===============================================
|
||||
|
||||
This is the plugin manager as used by pytest but stripped
|
||||
of pytest specific details.
|
||||
|
||||
During the 0.x series this plugin does not have much documentation
|
||||
except extensive docstrings in the pluggy.py module.
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pip
|
||||
@@ -1,22 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: pluggy
|
||||
Version: 0.4.0
|
||||
Summary: plugin and hook calling mechanisms for python
|
||||
Home-page: https://github.com/pytest-dev/pluggy
|
||||
Author: Holger Krekel
|
||||
Author-email: holger at merlinux.eu
|
||||
License: MIT license
|
||||
Platform: unix
|
||||
Platform: linux
|
||||
Platform: osx
|
||||
Platform: win32
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: Intended Audience :: Developers
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Operating System :: POSIX
|
||||
Classifier: Operating System :: Microsoft :: Windows
|
||||
Classifier: Operating System :: MacOS :: MacOS X
|
||||
Classifier: Topic :: Software Development :: Testing
|
||||
Classifier: Topic :: Software Development :: Libraries
|
||||
Classifier: Topic :: Utilities
|
||||
Classifier: Programming Language :: Python :: 2
|
||||
Classifier: Programming Language :: Python :: 2.6
|
||||
Classifier: Programming Language :: Python :: 2.7
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.3
|
||||
Classifier: Programming Language :: Python :: 3.4
|
||||
Classifier: Programming Language :: Python :: 3.5
|
||||
|
||||
|
||||
Plugin registration and hook calling for Python
|
||||
===============================================
|
||||
|
||||
This is the plugin manager as used by pytest but stripped
|
||||
of pytest specific details.
|
||||
|
||||
During the 0.x series this plugin does not have much documentation
|
||||
except extensive docstrings in the pluggy.py module.
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
pluggy.py,sha256=u0oG9cv-oLOkNvEBlwnnu8pp1AyxpoERgUO00S3rvpQ,31543
|
||||
pluggy-0.4.0.dist-info/DESCRIPTION.rst,sha256=ltvjkFd40LW_xShthp6RRVM6OB_uACYDFR3kTpKw7o4,307
|
||||
pluggy-0.4.0.dist-info/LICENSE.txt,sha256=ruwhUOyV1HgE9F35JVL9BCZ9vMSALx369I4xq9rhpkM,1134
|
||||
pluggy-0.4.0.dist-info/METADATA,sha256=pe2hbsqKFaLHC6wAQPpFPn0KlpcPfLBe_BnS4O70bfk,1364
|
||||
pluggy-0.4.0.dist-info/RECORD,,
|
||||
pluggy-0.4.0.dist-info/WHEEL,sha256=9Z5Xm-eel1bTS7e6ogYiKz0zmPEqDwIypurdHN1hR40,116
|
||||
pluggy-0.4.0.dist-info/metadata.json,sha256=T3go5L2qOa_-H-HpCZi3EoVKb8sZ3R-fOssbkWo2nvM,1119
|
||||
pluggy-0.4.0.dist-info/top_level.txt,sha256=xKSCRhai-v9MckvMuWqNz16c1tbsmOggoMSwTgcpYHE,7
|
||||
pluggy-0.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
||||
@@ -1,6 +0,0 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.29.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Operating System :: MacOS :: MacOS X", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Libraries", "Topic :: Utilities", "Programming Language :: Python :: 2", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5"], "extensions": {"python.details": {"contacts": [{"email": "holger at merlinux.eu", "name": "Holger Krekel", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst", "license": "LICENSE.txt"}, "project_urls": {"Home": "https://github.com/pytest-dev/pluggy"}}}, "generator": "bdist_wheel (0.29.0)", "license": "MIT license", "metadata_version": "2.0", "name": "pluggy", "platform": "unix", "summary": "plugin and hook calling mechanisms for python", "version": "0.4.0"}
|
||||
@@ -1 +0,0 @@
|
||||
pluggy
|
||||
@@ -1,802 +0,0 @@
|
||||
"""
|
||||
PluginManager, basic initialization and tracing.
|
||||
|
||||
pluggy is the cristallized core of plugin management as used
|
||||
by some 150 plugins for pytest.
|
||||
|
||||
Pluggy uses semantic versioning. Breaking changes are only foreseen for
|
||||
Major releases (incremented X in "X.Y.Z"). If you want to use pluggy in
|
||||
your project you should thus use a dependency restriction like
|
||||
"pluggy>=0.1.0,<1.0" to avoid surprises.
|
||||
|
||||
pluggy is concerned with hook specification, hook implementations and hook
|
||||
calling. For any given hook specification a hook call invokes up to N implementations.
|
||||
A hook implementation can influence its position and type of execution:
|
||||
if attributed "tryfirst" or "trylast" it will be tried to execute
|
||||
first or last. However, if attributed "hookwrapper" an implementation
|
||||
can wrap all calls to non-hookwrapper implementations. A hookwrapper
|
||||
can thus execute some code ahead and after the execution of other hooks.
|
||||
|
||||
Hook specification is done by way of a regular python function where
|
||||
both the function name and the names of all its arguments are significant.
|
||||
Each hook implementation function is verified against the original specification
|
||||
function, including the names of all its arguments. To allow for hook specifications
|
||||
to evolve over the livetime of a project, hook implementations can
|
||||
accept less arguments. One can thus add new arguments and semantics to
|
||||
a hook specification by adding another argument typically without breaking
|
||||
existing hook implementations.
|
||||
|
||||
The chosen approach is meant to let a hook designer think carefuly about
|
||||
which objects are needed by an extension writer. By contrast, subclass-based
|
||||
extension mechanisms often expose a lot more state and behaviour than needed,
|
||||
thus restricting future developments.
|
||||
|
||||
Pluggy currently consists of functionality for:
|
||||
|
||||
- a way to register new hook specifications. Without a hook
|
||||
specification no hook calling can be performed.
|
||||
|
||||
- a registry of plugins which contain hook implementation functions. It
|
||||
is possible to register plugins for which a hook specification is not yet
|
||||
known and validate all hooks when the system is in a more referentially
|
||||
consistent state. Setting an "optionalhook" attribution to a hook
|
||||
implementation will avoid PluginValidationError's if a specification
|
||||
is missing. This allows to have optional integration between plugins.
|
||||
|
||||
- a "hook" relay object from which you can launch 1:N calls to
|
||||
registered hook implementation functions
|
||||
|
||||
- a mechanism for ordering hook implementation functions
|
||||
|
||||
- mechanisms for two different type of 1:N calls: "firstresult" for when
|
||||
the call should stop when the first implementation returns a non-None result.
|
||||
And the other (default) way of guaranteeing that all hook implementations
|
||||
will be called and their non-None result collected.
|
||||
|
||||
- mechanisms for "historic" extension points such that all newly
|
||||
registered functions will receive all hook calls that happened
|
||||
before their registration.
|
||||
|
||||
- a mechanism for discovering plugin objects which are based on
|
||||
setuptools based entry points.
|
||||
|
||||
- a simple tracing mechanism, including tracing of plugin calls and
|
||||
their arguments.
|
||||
|
||||
"""
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
__version__ = '0.4.0'
|
||||
|
||||
__all__ = ["PluginManager", "PluginValidationError", "HookCallError",
|
||||
"HookspecMarker", "HookimplMarker"]
|
||||
|
||||
_py3 = sys.version_info > (3, 0)
|
||||
|
||||
|
||||
class HookspecMarker:
|
||||
""" Decorator helper class for marking functions as hook specifications.
|
||||
|
||||
You can instantiate it with a project_name to get a decorator.
|
||||
Calling PluginManager.add_hookspecs later will discover all marked functions
|
||||
if the PluginManager uses the same project_name.
|
||||
"""
|
||||
|
||||
def __init__(self, project_name):
|
||||
self.project_name = project_name
|
||||
|
||||
def __call__(self, function=None, firstresult=False, historic=False):
|
||||
""" if passed a function, directly sets attributes on the function
|
||||
which will make it discoverable to add_hookspecs(). If passed no
|
||||
function, returns a decorator which can be applied to a function
|
||||
later using the attributes supplied.
|
||||
|
||||
If firstresult is True the 1:N hook call (N being the number of registered
|
||||
hook implementation functions) will stop at I<=N when the I'th function
|
||||
returns a non-None result.
|
||||
|
||||
If historic is True calls to a hook will be memorized and replayed
|
||||
on later registered plugins.
|
||||
|
||||
"""
|
||||
def setattr_hookspec_opts(func):
|
||||
if historic and firstresult:
|
||||
raise ValueError("cannot have a historic firstresult hook")
|
||||
setattr(func, self.project_name + "_spec",
|
||||
dict(firstresult=firstresult, historic=historic))
|
||||
return func
|
||||
|
||||
if function is not None:
|
||||
return setattr_hookspec_opts(function)
|
||||
else:
|
||||
return setattr_hookspec_opts
|
||||
|
||||
|
||||
class HookimplMarker:
|
||||
""" Decorator helper class for marking functions as hook implementations.
|
||||
|
||||
You can instantiate with a project_name to get a decorator.
|
||||
Calling PluginManager.register later will discover all marked functions
|
||||
if the PluginManager uses the same project_name.
|
||||
"""
|
||||
def __init__(self, project_name):
|
||||
self.project_name = project_name
|
||||
|
||||
def __call__(self, function=None, hookwrapper=False, optionalhook=False,
|
||||
tryfirst=False, trylast=False):
|
||||
|
||||
""" if passed a function, directly sets attributes on the function
|
||||
which will make it discoverable to register(). If passed no function,
|
||||
returns a decorator which can be applied to a function later using
|
||||
the attributes supplied.
|
||||
|
||||
If optionalhook is True a missing matching hook specification will not result
|
||||
in an error (by default it is an error if no matching spec is found).
|
||||
|
||||
If tryfirst is True this hook implementation will run as early as possible
|
||||
in the chain of N hook implementations for a specfication.
|
||||
|
||||
If trylast is True this hook implementation will run as late as possible
|
||||
in the chain of N hook implementations.
|
||||
|
||||
If hookwrapper is True the hook implementations needs to execute exactly
|
||||
one "yield". The code before the yield is run early before any non-hookwrapper
|
||||
function is run. The code after the yield is run after all non-hookwrapper
|
||||
function have run. The yield receives an ``_CallOutcome`` object representing
|
||||
the exception or result outcome of the inner calls (including other hookwrapper
|
||||
calls).
|
||||
|
||||
"""
|
||||
def setattr_hookimpl_opts(func):
|
||||
setattr(func, self.project_name + "_impl",
|
||||
dict(hookwrapper=hookwrapper, optionalhook=optionalhook,
|
||||
tryfirst=tryfirst, trylast=trylast))
|
||||
return func
|
||||
|
||||
if function is None:
|
||||
return setattr_hookimpl_opts
|
||||
else:
|
||||
return setattr_hookimpl_opts(function)
|
||||
|
||||
|
||||
def normalize_hookimpl_opts(opts):
|
||||
opts.setdefault("tryfirst", False)
|
||||
opts.setdefault("trylast", False)
|
||||
opts.setdefault("hookwrapper", False)
|
||||
opts.setdefault("optionalhook", False)
|
||||
|
||||
|
||||
class _TagTracer:
|
||||
def __init__(self):
|
||||
self._tag2proc = {}
|
||||
self.writer = None
|
||||
self.indent = 0
|
||||
|
||||
def get(self, name):
|
||||
return _TagTracerSub(self, (name,))
|
||||
|
||||
def format_message(self, tags, args):
|
||||
if isinstance(args[-1], dict):
|
||||
extra = args[-1]
|
||||
args = args[:-1]
|
||||
else:
|
||||
extra = {}
|
||||
|
||||
content = " ".join(map(str, args))
|
||||
indent = " " * self.indent
|
||||
|
||||
lines = [
|
||||
"%s%s [%s]\n" % (indent, content, ":".join(tags))
|
||||
]
|
||||
|
||||
for name, value in extra.items():
|
||||
lines.append("%s %s: %s\n" % (indent, name, value))
|
||||
return lines
|
||||
|
||||
def processmessage(self, tags, args):
|
||||
if self.writer is not None and args:
|
||||
lines = self.format_message(tags, args)
|
||||
self.writer(''.join(lines))
|
||||
try:
|
||||
self._tag2proc[tags](tags, args)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def setwriter(self, writer):
|
||||
self.writer = writer
|
||||
|
||||
def setprocessor(self, tags, processor):
|
||||
if isinstance(tags, str):
|
||||
tags = tuple(tags.split(":"))
|
||||
else:
|
||||
assert isinstance(tags, tuple)
|
||||
self._tag2proc[tags] = processor
|
||||
|
||||
|
||||
class _TagTracerSub:
|
||||
def __init__(self, root, tags):
|
||||
self.root = root
|
||||
self.tags = tags
|
||||
|
||||
def __call__(self, *args):
|
||||
self.root.processmessage(self.tags, args)
|
||||
|
||||
def setmyprocessor(self, processor):
|
||||
self.root.setprocessor(self.tags, processor)
|
||||
|
||||
def get(self, name):
|
||||
return self.__class__(self.root, self.tags + (name,))
|
||||
|
||||
|
||||
def _raise_wrapfail(wrap_controller, msg):
|
||||
co = wrap_controller.gi_code
|
||||
raise RuntimeError("wrap_controller at %r %s:%d %s" %
|
||||
(co.co_name, co.co_filename, co.co_firstlineno, msg))
|
||||
|
||||
|
||||
def _wrapped_call(wrap_controller, func):
|
||||
""" Wrap calling to a function with a generator which needs to yield
|
||||
exactly once. The yield point will trigger calling the wrapped function
|
||||
and return its _CallOutcome to the yield point. The generator then needs
|
||||
to finish (raise StopIteration) in order for the wrapped call to complete.
|
||||
"""
|
||||
try:
|
||||
next(wrap_controller) # first yield
|
||||
except StopIteration:
|
||||
_raise_wrapfail(wrap_controller, "did not yield")
|
||||
call_outcome = _CallOutcome(func)
|
||||
try:
|
||||
wrap_controller.send(call_outcome)
|
||||
_raise_wrapfail(wrap_controller, "has second yield")
|
||||
except StopIteration:
|
||||
pass
|
||||
return call_outcome.get_result()
|
||||
|
||||
|
||||
class _CallOutcome:
|
||||
""" Outcome of a function call, either an exception or a proper result.
|
||||
Calling the ``get_result`` method will return the result or reraise
|
||||
the exception raised when the function was called. """
|
||||
excinfo = None
|
||||
|
||||
def __init__(self, func):
|
||||
try:
|
||||
self.result = func()
|
||||
except BaseException:
|
||||
self.excinfo = sys.exc_info()
|
||||
|
||||
def force_result(self, result):
|
||||
self.result = result
|
||||
self.excinfo = None
|
||||
|
||||
def get_result(self):
|
||||
if self.excinfo is None:
|
||||
return self.result
|
||||
else:
|
||||
ex = self.excinfo
|
||||
if _py3:
|
||||
raise ex[1].with_traceback(ex[2])
|
||||
_reraise(*ex) # noqa
|
||||
|
||||
if not _py3:
|
||||
exec("""
|
||||
def _reraise(cls, val, tb):
|
||||
raise cls, val, tb
|
||||
""")
|
||||
|
||||
|
||||
class _TracedHookExecution:
|
||||
def __init__(self, pluginmanager, before, after):
|
||||
self.pluginmanager = pluginmanager
|
||||
self.before = before
|
||||
self.after = after
|
||||
self.oldcall = pluginmanager._inner_hookexec
|
||||
assert not isinstance(self.oldcall, _TracedHookExecution)
|
||||
self.pluginmanager._inner_hookexec = self
|
||||
|
||||
def __call__(self, hook, hook_impls, kwargs):
|
||||
self.before(hook.name, hook_impls, kwargs)
|
||||
outcome = _CallOutcome(lambda: self.oldcall(hook, hook_impls, kwargs))
|
||||
self.after(outcome, hook.name, hook_impls, kwargs)
|
||||
return outcome.get_result()
|
||||
|
||||
def undo(self):
|
||||
self.pluginmanager._inner_hookexec = self.oldcall
|
||||
|
||||
|
||||
class PluginManager(object):
|
||||
""" Core Pluginmanager class which manages registration
|
||||
of plugin objects and 1:N hook calling.
|
||||
|
||||
You can register new hooks by calling ``add_hookspec(module_or_class)``.
|
||||
You can register plugin objects (which contain hooks) by calling
|
||||
``register(plugin)``. The Pluginmanager is initialized with a
|
||||
prefix that is searched for in the names of the dict of registered
|
||||
plugin objects. An optional excludefunc allows to blacklist names which
|
||||
are not considered as hooks despite a matching prefix.
|
||||
|
||||
For debugging purposes you can call ``enable_tracing()``
|
||||
which will subsequently send debug information to the trace helper.
|
||||
"""
|
||||
|
||||
def __init__(self, project_name, implprefix=None):
|
||||
""" if implprefix is given implementation functions
|
||||
will be recognized if their name matches the implprefix. """
|
||||
self.project_name = project_name
|
||||
self._name2plugin = {}
|
||||
self._plugin2hookcallers = {}
|
||||
self._plugin_distinfo = []
|
||||
self.trace = _TagTracer().get("pluginmanage")
|
||||
self.hook = _HookRelay(self.trace.root.get("hook"))
|
||||
self._implprefix = implprefix
|
||||
self._inner_hookexec = lambda hook, methods, kwargs: \
|
||||
_MultiCall(methods, kwargs, hook.spec_opts).execute()
|
||||
|
||||
def _hookexec(self, hook, methods, kwargs):
|
||||
# called from all hookcaller instances.
|
||||
# enable_tracing will set its own wrapping function at self._inner_hookexec
|
||||
return self._inner_hookexec(hook, methods, kwargs)
|
||||
|
||||
def register(self, plugin, name=None):
|
||||
""" Register a plugin and return its canonical name or None if the name
|
||||
is blocked from registering. Raise a ValueError if the plugin is already
|
||||
registered. """
|
||||
plugin_name = name or self.get_canonical_name(plugin)
|
||||
|
||||
if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
|
||||
if self._name2plugin.get(plugin_name, -1) is None:
|
||||
return # blocked plugin, return None to indicate no registration
|
||||
raise ValueError("Plugin already registered: %s=%s\n%s" %
|
||||
(plugin_name, plugin, self._name2plugin))
|
||||
|
||||
# XXX if an error happens we should make sure no state has been
|
||||
# changed at point of return
|
||||
self._name2plugin[plugin_name] = plugin
|
||||
|
||||
# register matching hook implementations of the plugin
|
||||
self._plugin2hookcallers[plugin] = hookcallers = []
|
||||
for name in dir(plugin):
|
||||
hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
|
||||
if hookimpl_opts is not None:
|
||||
normalize_hookimpl_opts(hookimpl_opts)
|
||||
method = getattr(plugin, name)
|
||||
hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
|
||||
hook = getattr(self.hook, name, None)
|
||||
if hook is None:
|
||||
hook = _HookCaller(name, self._hookexec)
|
||||
setattr(self.hook, name, hook)
|
||||
elif hook.has_spec():
|
||||
self._verify_hook(hook, hookimpl)
|
||||
hook._maybe_apply_history(hookimpl)
|
||||
hook._add_hookimpl(hookimpl)
|
||||
hookcallers.append(hook)
|
||||
return plugin_name
|
||||
|
||||
def parse_hookimpl_opts(self, plugin, name):
|
||||
method = getattr(plugin, name)
|
||||
try:
|
||||
res = getattr(method, self.project_name + "_impl", None)
|
||||
except Exception:
|
||||
res = {}
|
||||
if res is not None and not isinstance(res, dict):
|
||||
# false positive
|
||||
res = None
|
||||
elif res is None and self._implprefix and name.startswith(self._implprefix):
|
||||
res = {}
|
||||
return res
|
||||
|
||||
def unregister(self, plugin=None, name=None):
|
||||
""" unregister a plugin object and all its contained hook implementations
|
||||
from internal data structures. """
|
||||
if name is None:
|
||||
assert plugin is not None, "one of name or plugin needs to be specified"
|
||||
name = self.get_name(plugin)
|
||||
|
||||
if plugin is None:
|
||||
plugin = self.get_plugin(name)
|
||||
|
||||
# if self._name2plugin[name] == None registration was blocked: ignore
|
||||
if self._name2plugin.get(name):
|
||||
del self._name2plugin[name]
|
||||
|
||||
for hookcaller in self._plugin2hookcallers.pop(plugin, []):
|
||||
hookcaller._remove_plugin(plugin)
|
||||
|
||||
return plugin
|
||||
|
||||
def set_blocked(self, name):
|
||||
""" block registrations of the given name, unregister if already registered. """
|
||||
self.unregister(name=name)
|
||||
self._name2plugin[name] = None
|
||||
|
||||
def is_blocked(self, name):
|
||||
""" return True if the name blogs registering plugins of that name. """
|
||||
return name in self._name2plugin and self._name2plugin[name] is None
|
||||
|
||||
def add_hookspecs(self, module_or_class):
|
||||
""" add new hook specifications defined in the given module_or_class.
|
||||
Functions are recognized if they have been decorated accordingly. """
|
||||
names = []
|
||||
for name in dir(module_or_class):
|
||||
spec_opts = self.parse_hookspec_opts(module_or_class, name)
|
||||
if spec_opts is not None:
|
||||
hc = getattr(self.hook, name, None)
|
||||
if hc is None:
|
||||
hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
|
||||
setattr(self.hook, name, hc)
|
||||
else:
|
||||
# plugins registered this hook without knowing the spec
|
||||
hc.set_specification(module_or_class, spec_opts)
|
||||
for hookfunction in (hc._wrappers + hc._nonwrappers):
|
||||
self._verify_hook(hc, hookfunction)
|
||||
names.append(name)
|
||||
|
||||
if not names:
|
||||
raise ValueError("did not find any %r hooks in %r" %
|
||||
(self.project_name, module_or_class))
|
||||
|
||||
def parse_hookspec_opts(self, module_or_class, name):
|
||||
method = getattr(module_or_class, name)
|
||||
return getattr(method, self.project_name + "_spec", None)
|
||||
|
||||
def get_plugins(self):
|
||||
""" return the set of registered plugins. """
|
||||
return set(self._plugin2hookcallers)
|
||||
|
||||
def is_registered(self, plugin):
|
||||
""" Return True if the plugin is already registered. """
|
||||
return plugin in self._plugin2hookcallers
|
||||
|
||||
def get_canonical_name(self, plugin):
|
||||
""" Return canonical name for a plugin object. Note that a plugin
|
||||
may be registered under a different name which was specified
|
||||
by the caller of register(plugin, name). To obtain the name
|
||||
of an registered plugin use ``get_name(plugin)`` instead."""
|
||||
return getattr(plugin, "__name__", None) or str(id(plugin))
|
||||
|
||||
def get_plugin(self, name):
|
||||
""" Return a plugin or None for the given name. """
|
||||
return self._name2plugin.get(name)
|
||||
|
||||
def has_plugin(self, name):
|
||||
""" Return True if a plugin with the given name is registered. """
|
||||
return self.get_plugin(name) is not None
|
||||
|
||||
def get_name(self, plugin):
|
||||
""" Return name for registered plugin or None if not registered. """
|
||||
for name, val in self._name2plugin.items():
|
||||
if plugin == val:
|
||||
return name
|
||||
|
||||
def _verify_hook(self, hook, hookimpl):
|
||||
if hook.is_historic() and hookimpl.hookwrapper:
|
||||
raise PluginValidationError(
|
||||
"Plugin %r\nhook %r\nhistoric incompatible to hookwrapper" %
|
||||
(hookimpl.plugin_name, hook.name))
|
||||
|
||||
for arg in hookimpl.argnames:
|
||||
if arg not in hook.argnames:
|
||||
raise PluginValidationError(
|
||||
"Plugin %r\nhook %r\nargument %r not available\n"
|
||||
"plugin definition: %s\n"
|
||||
"available hookargs: %s" %
|
||||
(hookimpl.plugin_name, hook.name, arg,
|
||||
_formatdef(hookimpl.function), ", ".join(hook.argnames)))
|
||||
|
||||
def check_pending(self):
|
||||
""" Verify that all hooks which have not been verified against
|
||||
a hook specification are optional, otherwise raise PluginValidationError"""
|
||||
for name in self.hook.__dict__:
|
||||
if name[0] != "_":
|
||||
hook = getattr(self.hook, name)
|
||||
if not hook.has_spec():
|
||||
for hookimpl in (hook._wrappers + hook._nonwrappers):
|
||||
if not hookimpl.optionalhook:
|
||||
raise PluginValidationError(
|
||||
"unknown hook %r in plugin %r" %
|
||||
(name, hookimpl.plugin))
|
||||
|
||||
def load_setuptools_entrypoints(self, entrypoint_name):
|
||||
""" Load modules from querying the specified setuptools entrypoint name.
|
||||
Return the number of loaded plugins. """
|
||||
from pkg_resources import (iter_entry_points, DistributionNotFound,
|
||||
VersionConflict)
|
||||
for ep in iter_entry_points(entrypoint_name):
|
||||
# is the plugin registered or blocked?
|
||||
if self.get_plugin(ep.name) or self.is_blocked(ep.name):
|
||||
continue
|
||||
try:
|
||||
plugin = ep.load()
|
||||
except DistributionNotFound:
|
||||
continue
|
||||
except VersionConflict as e:
|
||||
raise PluginValidationError(
|
||||
"Plugin %r could not be loaded: %s!" % (ep.name, e))
|
||||
self.register(plugin, name=ep.name)
|
||||
self._plugin_distinfo.append((plugin, ep.dist))
|
||||
return len(self._plugin_distinfo)
|
||||
|
||||
def list_plugin_distinfo(self):
|
||||
""" return list of distinfo/plugin tuples for all setuptools registered
|
||||
plugins. """
|
||||
return list(self._plugin_distinfo)
|
||||
|
||||
def list_name_plugin(self):
|
||||
""" return list of name/plugin pairs. """
|
||||
return list(self._name2plugin.items())
|
||||
|
||||
def get_hookcallers(self, plugin):
|
||||
""" get all hook callers for the specified plugin. """
|
||||
return self._plugin2hookcallers.get(plugin)
|
||||
|
||||
def add_hookcall_monitoring(self, before, after):
|
||||
""" add before/after tracing functions for all hooks
|
||||
and return an undo function which, when called,
|
||||
will remove the added tracers.
|
||||
|
||||
``before(hook_name, hook_impls, kwargs)`` will be called ahead
|
||||
of all hook calls and receive a hookcaller instance, a list
|
||||
of HookImpl instances and the keyword arguments for the hook call.
|
||||
|
||||
``after(outcome, hook_name, hook_impls, kwargs)`` receives the
|
||||
same arguments as ``before`` but also a :py:class:`_CallOutcome`` object
|
||||
which represents the result of the overall hook call.
|
||||
"""
|
||||
return _TracedHookExecution(self, before, after).undo
|
||||
|
||||
def enable_tracing(self):
|
||||
""" enable tracing of hook calls and return an undo function. """
|
||||
hooktrace = self.hook._trace
|
||||
|
||||
def before(hook_name, methods, kwargs):
|
||||
hooktrace.root.indent += 1
|
||||
hooktrace(hook_name, kwargs)
|
||||
|
||||
def after(outcome, hook_name, methods, kwargs):
|
||||
if outcome.excinfo is None:
|
||||
hooktrace("finish", hook_name, "-->", outcome.result)
|
||||
hooktrace.root.indent -= 1
|
||||
|
||||
return self.add_hookcall_monitoring(before, after)
|
||||
|
||||
def subset_hook_caller(self, name, remove_plugins):
|
||||
""" Return a new _HookCaller instance for the named method
|
||||
which manages calls to all registered plugins except the
|
||||
ones from remove_plugins. """
|
||||
orig = getattr(self.hook, name)
|
||||
plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)]
|
||||
if plugins_to_remove:
|
||||
hc = _HookCaller(orig.name, orig._hookexec, orig._specmodule_or_class,
|
||||
orig.spec_opts)
|
||||
for hookimpl in (orig._wrappers + orig._nonwrappers):
|
||||
plugin = hookimpl.plugin
|
||||
if plugin not in plugins_to_remove:
|
||||
hc._add_hookimpl(hookimpl)
|
||||
# we also keep track of this hook caller so it
|
||||
# gets properly removed on plugin unregistration
|
||||
self._plugin2hookcallers.setdefault(plugin, []).append(hc)
|
||||
return hc
|
||||
return orig
|
||||
|
||||
|
||||
class _MultiCall:
|
||||
""" execute a call into multiple python functions/methods. """
|
||||
|
||||
# XXX note that the __multicall__ argument is supported only
|
||||
# for pytest compatibility reasons. It was never officially
|
||||
# supported there and is explicitely deprecated since 2.8
|
||||
# so we can remove it soon, allowing to avoid the below recursion
|
||||
# in execute() and simplify/speed up the execute loop.
|
||||
|
||||
def __init__(self, hook_impls, kwargs, specopts={}):
|
||||
self.hook_impls = hook_impls
|
||||
self.kwargs = kwargs
|
||||
self.kwargs["__multicall__"] = self
|
||||
self.specopts = specopts
|
||||
|
||||
def execute(self):
|
||||
all_kwargs = self.kwargs
|
||||
self.results = results = []
|
||||
firstresult = self.specopts.get("firstresult")
|
||||
|
||||
while self.hook_impls:
|
||||
hook_impl = self.hook_impls.pop()
|
||||
try:
|
||||
args = [all_kwargs[argname] for argname in hook_impl.argnames]
|
||||
except KeyError:
|
||||
for argname in hook_impl.argnames:
|
||||
if argname not in all_kwargs:
|
||||
raise HookCallError(
|
||||
"hook call must provide argument %r" % (argname,))
|
||||
if hook_impl.hookwrapper:
|
||||
return _wrapped_call(hook_impl.function(*args), self.execute)
|
||||
res = hook_impl.function(*args)
|
||||
if res is not None:
|
||||
if firstresult:
|
||||
return res
|
||||
results.append(res)
|
||||
|
||||
if not firstresult:
|
||||
return results
|
||||
|
||||
def __repr__(self):
|
||||
status = "%d meths" % (len(self.hook_impls),)
|
||||
if hasattr(self, "results"):
|
||||
status = ("%d results, " % len(self.results)) + status
|
||||
return "<_MultiCall %s, kwargs=%r>" % (status, self.kwargs)
|
||||
|
||||
|
||||
def varnames(func, startindex=None):
|
||||
""" return argument name tuple for a function, method, class or callable.
|
||||
|
||||
In case of a class, its "__init__" method is considered.
|
||||
For methods the "self" parameter is not included unless you are passing
|
||||
an unbound method with Python3 (which has no supports for unbound methods)
|
||||
"""
|
||||
cache = getattr(func, "__dict__", {})
|
||||
try:
|
||||
return cache["_varnames"]
|
||||
except KeyError:
|
||||
pass
|
||||
if inspect.isclass(func):
|
||||
try:
|
||||
func = func.__init__
|
||||
except AttributeError:
|
||||
return ()
|
||||
startindex = 1
|
||||
else:
|
||||
if not inspect.isfunction(func) and not inspect.ismethod(func):
|
||||
try:
|
||||
func = getattr(func, '__call__', func)
|
||||
except Exception:
|
||||
return ()
|
||||
if startindex is None:
|
||||
startindex = int(inspect.ismethod(func))
|
||||
|
||||
try:
|
||||
rawcode = func.__code__
|
||||
except AttributeError:
|
||||
return ()
|
||||
try:
|
||||
x = rawcode.co_varnames[startindex:rawcode.co_argcount]
|
||||
except AttributeError:
|
||||
x = ()
|
||||
else:
|
||||
defaults = func.__defaults__
|
||||
if defaults:
|
||||
x = x[:-len(defaults)]
|
||||
try:
|
||||
cache["_varnames"] = x
|
||||
except TypeError:
|
||||
pass
|
||||
return x
|
||||
|
||||
|
||||
class _HookRelay:
|
||||
""" hook holder object for performing 1:N hook calls where N is the number
|
||||
of registered plugins.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, trace):
|
||||
self._trace = trace
|
||||
|
||||
|
||||
class _HookCaller(object):
|
||||
def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None):
|
||||
self.name = name
|
||||
self._wrappers = []
|
||||
self._nonwrappers = []
|
||||
self._hookexec = hook_execute
|
||||
if specmodule_or_class is not None:
|
||||
assert spec_opts is not None
|
||||
self.set_specification(specmodule_or_class, spec_opts)
|
||||
|
||||
def has_spec(self):
|
||||
return hasattr(self, "_specmodule_or_class")
|
||||
|
||||
def set_specification(self, specmodule_or_class, spec_opts):
|
||||
assert not self.has_spec()
|
||||
self._specmodule_or_class = specmodule_or_class
|
||||
specfunc = getattr(specmodule_or_class, self.name)
|
||||
argnames = varnames(specfunc, startindex=inspect.isclass(specmodule_or_class))
|
||||
assert "self" not in argnames # sanity check
|
||||
self.argnames = ["__multicall__"] + list(argnames)
|
||||
self.spec_opts = spec_opts
|
||||
if spec_opts.get("historic"):
|
||||
self._call_history = []
|
||||
|
||||
def is_historic(self):
|
||||
return hasattr(self, "_call_history")
|
||||
|
||||
def _remove_plugin(self, plugin):
|
||||
def remove(wrappers):
|
||||
for i, method in enumerate(wrappers):
|
||||
if method.plugin == plugin:
|
||||
del wrappers[i]
|
||||
return True
|
||||
if remove(self._wrappers) is None:
|
||||
if remove(self._nonwrappers) is None:
|
||||
raise ValueError("plugin %r not found" % (plugin,))
|
||||
|
||||
def _add_hookimpl(self, hookimpl):
|
||||
if hookimpl.hookwrapper:
|
||||
methods = self._wrappers
|
||||
else:
|
||||
methods = self._nonwrappers
|
||||
|
||||
if hookimpl.trylast:
|
||||
methods.insert(0, hookimpl)
|
||||
elif hookimpl.tryfirst:
|
||||
methods.append(hookimpl)
|
||||
else:
|
||||
# find last non-tryfirst method
|
||||
i = len(methods) - 1
|
||||
while i >= 0 and methods[i].tryfirst:
|
||||
i -= 1
|
||||
methods.insert(i + 1, hookimpl)
|
||||
|
||||
def __repr__(self):
|
||||
return "<_HookCaller %r>" % (self.name,)
|
||||
|
||||
def __call__(self, **kwargs):
|
||||
assert not self.is_historic()
|
||||
return self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
|
||||
|
||||
def call_historic(self, proc=None, kwargs=None):
|
||||
self._call_history.append((kwargs or {}, proc))
|
||||
# historizing hooks don't return results
|
||||
self._hookexec(self, self._nonwrappers + self._wrappers, kwargs)
|
||||
|
||||
def call_extra(self, methods, kwargs):
|
||||
""" Call the hook with some additional temporarily participating
|
||||
methods using the specified kwargs as call parameters. """
|
||||
old = list(self._nonwrappers), list(self._wrappers)
|
||||
for method in methods:
|
||||
opts = dict(hookwrapper=False, trylast=False, tryfirst=False)
|
||||
hookimpl = HookImpl(None, "<temp>", method, opts)
|
||||
self._add_hookimpl(hookimpl)
|
||||
try:
|
||||
return self(**kwargs)
|
||||
finally:
|
||||
self._nonwrappers, self._wrappers = old
|
||||
|
||||
def _maybe_apply_history(self, method):
|
||||
if self.is_historic():
|
||||
for kwargs, proc in self._call_history:
|
||||
res = self._hookexec(self, [method], kwargs)
|
||||
if res and proc is not None:
|
||||
proc(res[0])
|
||||
|
||||
|
||||
class HookImpl:
|
||||
def __init__(self, plugin, plugin_name, function, hook_impl_opts):
|
||||
self.function = function
|
||||
self.argnames = varnames(self.function)
|
||||
self.plugin = plugin
|
||||
self.opts = hook_impl_opts
|
||||
self.plugin_name = plugin_name
|
||||
self.__dict__.update(hook_impl_opts)
|
||||
|
||||
|
||||
class PluginValidationError(Exception):
|
||||
""" plugin failed validation. """
|
||||
|
||||
|
||||
class HookCallError(Exception):
|
||||
""" Hook was called wrongly. """
|
||||
|
||||
|
||||
if hasattr(inspect, 'signature'):
|
||||
def _formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
str(inspect.signature(func))
|
||||
)
|
||||
else:
|
||||
def _formatdef(func):
|
||||
return "%s%s" % (
|
||||
func.__name__,
|
||||
inspect.formatargspec(*inspect.getargspec(func))
|
||||
)
|
||||
@@ -39,8 +39,9 @@ def pytest_addoption(parser):
|
||||
'-W', '--pythonwarnings', action='append',
|
||||
help="set which warnings to report, see -W option of python itself.")
|
||||
parser.addini("filterwarnings", type="linelist",
|
||||
help="Each line specifies warning filter pattern which would be passed"
|
||||
"to warnings.filterwarnings. Process after -W and --pythonwarnings.")
|
||||
help="Each line specifies a pattern for "
|
||||
"warnings.filterwarnings. "
|
||||
"Processed after -W and --pythonwarnings.")
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -59,6 +60,11 @@ def catch_warnings_for_item(item):
|
||||
for arg in inifilters:
|
||||
_setoption(warnings, arg)
|
||||
|
||||
mark = item.get_marker('filterwarnings')
|
||||
if mark:
|
||||
for arg in mark.args:
|
||||
warnings._setoption(arg)
|
||||
|
||||
yield
|
||||
|
||||
for warning in log:
|
||||
@@ -66,8 +72,11 @@ def catch_warnings_for_item(item):
|
||||
unicode_warning = False
|
||||
|
||||
if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args):
|
||||
warn_msg.args = [compat.safe_str(m) for m in warn_msg.args]
|
||||
unicode_warning = True
|
||||
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,
|
||||
@@ -76,8 +85,8 @@ def catch_warnings_for_item(item):
|
||||
|
||||
if unicode_warning:
|
||||
warnings.warn(
|
||||
"This warning %s is broken as it's message is not a str instance"
|
||||
"(after all this is a stdlib problem workaround)" % msg,
|
||||
"Warning is using unicode non convertible to ascii, "
|
||||
"converting to a safe representation:\n %s" % msg,
|
||||
UnicodeWarning)
|
||||
|
||||
|
||||
|
||||
16
appveyor.yml
16
appveyor.yml
@@ -10,9 +10,7 @@ environment:
|
||||
- TOXENV: "coveralls"
|
||||
# note: please use "tox --listenvs" to populate the build matrix below
|
||||
- TOXENV: "linting"
|
||||
- TOXENV: "py26"
|
||||
- TOXENV: "py27"
|
||||
- TOXENV: "py33"
|
||||
- TOXENV: "py34"
|
||||
- TOXENV: "py35"
|
||||
- TOXENV: "py36"
|
||||
@@ -20,12 +18,16 @@ environment:
|
||||
- TOXENV: "py27-pexpect"
|
||||
- TOXENV: "py27-xdist"
|
||||
- TOXENV: "py27-trial"
|
||||
- TOXENV: "py35-pexpect"
|
||||
- TOXENV: "py35-xdist"
|
||||
- TOXENV: "py35-trial"
|
||||
- TOXENV: "py27-numpy"
|
||||
- TOXENV: "py27-pluggymaster"
|
||||
- TOXENV: "py36-pexpect"
|
||||
- TOXENV: "py36-xdist"
|
||||
- TOXENV: "py36-trial"
|
||||
- TOXENV: "py36-numpy"
|
||||
- TOXENV: "py36-pluggymaster"
|
||||
- TOXENV: "py27-nobyte"
|
||||
- TOXENV: "doctesting"
|
||||
- TOXENV: "freeze"
|
||||
- TOXENV: "py35-freeze"
|
||||
- TOXENV: "docs"
|
||||
|
||||
install:
|
||||
@@ -34,7 +36,7 @@ install:
|
||||
|
||||
- if "%TOXENV%" == "pypy" call scripts\install-pypy.bat
|
||||
|
||||
- C:\Python35\python -m pip install tox
|
||||
- C:\Python36\python -m pip install --upgrade --pre tox
|
||||
|
||||
build: false # Not a C# project, build stuff at the test step instead.
|
||||
|
||||
|
||||
32
changelog/README.rst
Normal file
32
changelog/README.rst
Normal file
@@ -0,0 +1,32 @@
|
||||
This directory contains "newsfragments" which are short files that contain a small **ReST**-formatted
|
||||
text that will be added to the next ``CHANGELOG``.
|
||||
|
||||
The ``CHANGELOG`` will be read by users, so this description should be aimed to pytest users
|
||||
instead of describing internal changes which are only relevant to the developers.
|
||||
|
||||
Make sure to use full sentences with correct case and punctuation, for example::
|
||||
|
||||
Fix issue with non-ascii messages from the ``warnings`` module.
|
||||
|
||||
Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
|
||||
``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
|
||||
|
||||
* ``feature``: new user facing features, like new command-line options and new behavior.
|
||||
* ``bugfix``: fixes a reported bug.
|
||||
* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
|
||||
* ``removal``: feature deprecation or removal.
|
||||
* ``vendor``: changes in packages vendored in pytest.
|
||||
* ``trivial``: fixing a small typo or internal change that might be noteworthy.
|
||||
|
||||
So for example: ``123.feature.rst``, ``456.bugfix.rst``.
|
||||
|
||||
If your PR fixes an issue, use that number here. If there is no issue,
|
||||
then after you submit the PR and get the PR number you can add a
|
||||
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``
|
||||
if you want to get a preview of how your change will look in the final release notes.
|
||||
@@ -6,14 +6,15 @@
|
||||
|
||||
{% endif %}
|
||||
{% if sections[section] %}
|
||||
{% for category, val in definitions.items() if category in sections[section] and category != 'trivial' %}
|
||||
{% for category, val in definitions.items() if category in sections[section] %}
|
||||
|
||||
{{ definitions[category]['name'] }}
|
||||
{{ underline * definitions[category]['name']|length }}
|
||||
|
||||
{% if definitions[category]['showcontent'] %}
|
||||
{% for text, values in sections[section][category]|dictsort(by='value') %}
|
||||
- {{ text }}{% if category != 'vendor' %} ({{ values|sort|join(', ') }}){% endif %}
|
||||
{% 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 %}
|
||||
|
||||
|
||||
{% endfor %}
|
||||
|
||||
@@ -13,8 +13,6 @@ PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||
|
||||
REGENDOC_ARGS := \
|
||||
--normalize "/={8,} (.*) ={8,}/======= \1 ========/" \
|
||||
--normalize "/_{8,} (.*) _{8,}/_______ \1 ________/" \
|
||||
--normalize "/in \d+.\d+ seconds/in 0.12 seconds/" \
|
||||
--normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \
|
||||
--normalize "@pytest-(\d+)\\.[^ ,]+@pytest-\1.x.y@" \
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
<li><a href="{{ pathto('contact') }}">Contact</a></li>
|
||||
<li><a href="{{ pathto('talks') }}">Talks/Posts</a></li>
|
||||
<li><a href="{{ pathto('changelog') }}">Changelog</a></li>
|
||||
<li><a href="{{ pathto('backwards-compatibility') }}">Backwards Compatibility</a></li>
|
||||
<li><a href="{{ pathto('license') }}">License</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -6,6 +6,20 @@ Release announcements
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-3.4.2
|
||||
release-3.4.1
|
||||
release-3.4.0
|
||||
release-3.3.2
|
||||
release-3.3.1
|
||||
release-3.3.0
|
||||
release-3.2.5
|
||||
release-3.2.4
|
||||
release-3.2.3
|
||||
release-3.2.2
|
||||
release-3.2.1
|
||||
release-3.2.0
|
||||
release-3.1.3
|
||||
release-3.1.2
|
||||
release-3.1.1
|
||||
release-3.1.0
|
||||
release-3.0.7
|
||||
|
||||
@@ -62,7 +62,7 @@ holger krekel
|
||||
- fix issue655: work around different ways that cause python2/3
|
||||
to leak sys.exc_info into fixtures/tests causing failures in 3rd party code
|
||||
|
||||
- fix issue615: assertion re-writing did not correctly escape % signs
|
||||
- fix issue615: assertion rewriting did not correctly escape % signs
|
||||
when formatting boolean operations, which tripped over mixing
|
||||
booleans with modulo operators. Thanks to Tom Viner for the report,
|
||||
triaging and fix.
|
||||
|
||||
23
doc/en/announce/release-3.1.2.rst
Normal file
23
doc/en/announce/release-3.1.2.rst
Normal file
@@ -0,0 +1,23 @@
|
||||
pytest-3.1.2
|
||||
=======================================
|
||||
|
||||
pytest 3.1.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Andreas Pelme
|
||||
* ApaDoctor
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Ronny Pfannschmidt
|
||||
* Segev Finer
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
23
doc/en/announce/release-3.1.3.rst
Normal file
23
doc/en/announce/release-3.1.3.rst
Normal file
@@ -0,0 +1,23 @@
|
||||
pytest-3.1.3
|
||||
=======================================
|
||||
|
||||
pytest 3.1.3 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Antoine Legrand
|
||||
* Bruno Oliveira
|
||||
* Max Moroz
|
||||
* Raphael Pierzina
|
||||
* Ronny Pfannschmidt
|
||||
* Ryan Fitzpatrick
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
48
doc/en/announce/release-3.2.0.rst
Normal file
48
doc/en/announce/release-3.2.0.rst
Normal file
@@ -0,0 +1,48 @@
|
||||
pytest-3.2.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.2.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1600 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:
|
||||
|
||||
* Alex Hartoto
|
||||
* Andras Tim
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Florian Bruhin
|
||||
* Floris Bruynooghe
|
||||
* John Still
|
||||
* Jordan Moldow
|
||||
* Kale Kundert
|
||||
* Lawrence Mitchell
|
||||
* Llandy Riveron Del Risco
|
||||
* Maik Figura
|
||||
* Martin Altmayer
|
||||
* Mihai Capotă
|
||||
* Nathaniel Waisbrot
|
||||
* Nguyễn Hồng Quân
|
||||
* Pauli Virtanen
|
||||
* Raphael Pierzina
|
||||
* Ronny Pfannschmidt
|
||||
* Segev Finer
|
||||
* V.Kuznetsov
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
22
doc/en/announce/release-3.2.1.rst
Normal file
22
doc/en/announce/release-3.2.1.rst
Normal file
@@ -0,0 +1,22 @@
|
||||
pytest-3.2.1
|
||||
=======================================
|
||||
|
||||
pytest 3.2.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Alex Gaynor
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Ronny Pfannschmidt
|
||||
* Srinivas Reddy Thatiparthy
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
28
doc/en/announce/release-3.2.2.rst
Normal file
28
doc/en/announce/release-3.2.2.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pytest-3.2.2
|
||||
=======================================
|
||||
|
||||
pytest 3.2.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Andreas Pelme
|
||||
* Antonio Hidalgo
|
||||
* Bruno Oliveira
|
||||
* Felipe Dau
|
||||
* Fernando Macedo
|
||||
* Jesús Espino
|
||||
* Joan Massich
|
||||
* Joe Talbott
|
||||
* Kirill Pinchuk
|
||||
* Ronny Pfannschmidt
|
||||
* Xuan Luong
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
23
doc/en/announce/release-3.2.3.rst
Normal file
23
doc/en/announce/release-3.2.3.rst
Normal file
@@ -0,0 +1,23 @@
|
||||
pytest-3.2.3
|
||||
=======================================
|
||||
|
||||
pytest 3.2.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:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Evan
|
||||
* Joe Hamman
|
||||
* Oliver Bestwalter
|
||||
* Ronny Pfannschmidt
|
||||
* Xuan Luong
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
36
doc/en/announce/release-3.2.4.rst
Normal file
36
doc/en/announce/release-3.2.4.rst
Normal file
@@ -0,0 +1,36 @@
|
||||
pytest-3.2.4
|
||||
=======================================
|
||||
|
||||
pytest 3.2.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 http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Christian Boelsen
|
||||
* Christoph Buchner
|
||||
* Daw-Ran Liou
|
||||
* Florian Bruhin
|
||||
* Franck Michea
|
||||
* Leonard Lausen
|
||||
* Matty G
|
||||
* Owen Tuz
|
||||
* Pavel Karateev
|
||||
* Pierre GIRAUD
|
||||
* Ronny Pfannschmidt
|
||||
* Stephen Finucane
|
||||
* Sviatoslav Abakumov
|
||||
* Thomas Hisch
|
||||
* Tom Dalton
|
||||
* Xuan Luong
|
||||
* Yorgos Pagles
|
||||
* Семён Марьясин
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
18
doc/en/announce/release-3.2.5.rst
Normal file
18
doc/en/announce/release-3.2.5.rst
Normal file
@@ -0,0 +1,18 @@
|
||||
pytest-3.2.5
|
||||
=======================================
|
||||
|
||||
pytest 3.2.5 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:
|
||||
|
||||
* Bruno Oliveira
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
50
doc/en/announce/release-3.3.0.rst
Normal file
50
doc/en/announce/release-3.3.0.rst
Normal file
@@ -0,0 +1,50 @@
|
||||
pytest-3.3.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.3.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1600 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:
|
||||
|
||||
* Anthony Sottile
|
||||
* Bruno Oliveira
|
||||
* Ceridwen
|
||||
* Daniel Hahler
|
||||
* Dirk Thomas
|
||||
* Dmitry Malinovsky
|
||||
* Florian Bruhin
|
||||
* George Y. Kussumoto
|
||||
* Hugo
|
||||
* Jesús Espino
|
||||
* Joan Massich
|
||||
* Ofir
|
||||
* OfirOshir
|
||||
* Ronny Pfannschmidt
|
||||
* Samuel Dion-Girardeau
|
||||
* Srinivas Reddy Thatiparthy
|
||||
* Sviatoslav Abakumov
|
||||
* Tarcisio Fischer
|
||||
* Thomas Hisch
|
||||
* Tyler Goodlet
|
||||
* hugovk
|
||||
* je
|
||||
* prokaktus
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
25
doc/en/announce/release-3.3.1.rst
Normal file
25
doc/en/announce/release-3.3.1.rst
Normal file
@@ -0,0 +1,25 @@
|
||||
pytest-3.3.1
|
||||
=======================================
|
||||
|
||||
pytest 3.3.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Daniel Hahler
|
||||
* Eugene Prikazchikov
|
||||
* Florian Bruhin
|
||||
* Roland Puntaier
|
||||
* Ronny Pfannschmidt
|
||||
* Sebastian Rahlf
|
||||
* Tom Viner
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
28
doc/en/announce/release-3.3.2.rst
Normal file
28
doc/en/announce/release-3.3.2.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pytest-3.3.2
|
||||
=======================================
|
||||
|
||||
pytest 3.3.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Anthony Sottile
|
||||
* Antony Lee
|
||||
* Austin
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Floris Bruynooghe
|
||||
* Henk-Jaap Wagenaar
|
||||
* Jurko Gospodnetić
|
||||
* Ronny Pfannschmidt
|
||||
* Srinivas Reddy Thatiparthy
|
||||
* Thomas Hisch
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
52
doc/en/announce/release-3.4.0.rst
Normal file
52
doc/en/announce/release-3.4.0.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
pytest-3.4.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.4.0 release!
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1600 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:
|
||||
|
||||
* Aaron
|
||||
* Alan Velasco
|
||||
* Anders Hovmöller
|
||||
* Andrew Toolan
|
||||
* Anthony Sottile
|
||||
* Aron Coyle
|
||||
* Brian Maissy
|
||||
* Bruno Oliveira
|
||||
* Cyrus Maden
|
||||
* Florian Bruhin
|
||||
* Henk-Jaap Wagenaar
|
||||
* Ian Lesperance
|
||||
* Jon Dufresne
|
||||
* Jurko Gospodnetić
|
||||
* Kate
|
||||
* Kimberly
|
||||
* Per A. Brodtkorb
|
||||
* Pierre-Alexandre Fonta
|
||||
* Raphael Castaneda
|
||||
* Ronny Pfannschmidt
|
||||
* ST John
|
||||
* Segev Finer
|
||||
* Thomas Hisch
|
||||
* Tzu-ping Chung
|
||||
* feuillemorte
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
27
doc/en/announce/release-3.4.1.rst
Normal file
27
doc/en/announce/release-3.4.1.rst
Normal file
@@ -0,0 +1,27 @@
|
||||
pytest-3.4.1
|
||||
=======================================
|
||||
|
||||
pytest 3.4.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Aaron
|
||||
* Alan Velasco
|
||||
* Andy Freeland
|
||||
* Brian Maissy
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Jason R. Coombs
|
||||
* Marcin Bachry
|
||||
* Pedro Algarvio
|
||||
* Ronny Pfannschmidt
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
28
doc/en/announce/release-3.4.2.rst
Normal file
28
doc/en/announce/release-3.4.2.rst
Normal file
@@ -0,0 +1,28 @@
|
||||
pytest-3.4.2
|
||||
=======================================
|
||||
|
||||
pytest 3.4.2 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Allan Feldman
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Jason R. Coombs
|
||||
* Kyle Altendorf
|
||||
* Maik Figura
|
||||
* Ronny Pfannschmidt
|
||||
* codetriage-readme-bot
|
||||
* feuillemorte
|
||||
* joshm91
|
||||
* mike
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
@@ -25,15 +25,15 @@ to assert that your function returns a certain value. If this assertion fails
|
||||
you will see the return value of the function call::
|
||||
|
||||
$ pytest test_assert1.py
|
||||
======= test session starts ========
|
||||
=========================== 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 1 items
|
||||
collected 1 item
|
||||
|
||||
test_assert1.py F
|
||||
test_assert1.py F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_function ________
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_function _______________________________
|
||||
|
||||
def test_function():
|
||||
> assert f() == 4
|
||||
@@ -41,7 +41,7 @@ you will see the return value of the function call::
|
||||
E + where 3 = f()
|
||||
|
||||
test_assert1.py:5: AssertionError
|
||||
======= 1 failed in 0.12 seconds ========
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
``pytest`` has support for showing the values of the most common subexpressions
|
||||
including calls, attributes, comparisons, and binary and unary
|
||||
@@ -119,9 +119,9 @@ exceptions your own code is deliberately raising, whereas using
|
||||
like documenting unfixed bugs (where the test describes what "should" happen)
|
||||
or bugs in dependencies.
|
||||
|
||||
If you want to test that a regular expression matches on the string
|
||||
representation of an exception (like the ``TestCase.assertRaisesRegexp`` method
|
||||
from ``unittest``) you can use the ``ExceptionInfo.match`` method::
|
||||
Also, the context manager form accepts a ``match`` keyword parameter to test
|
||||
that a regular expression matches on the string representation of an exception
|
||||
(like the ``TestCase.assertRaisesRegexp`` method from ``unittest``)::
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -129,12 +129,11 @@ from ``unittest``) you can use the ``ExceptionInfo.match`` method::
|
||||
raise ValueError("Exception 123 raised")
|
||||
|
||||
def test_match():
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
with pytest.raises(ValueError, match=r'.* 123 .*'):
|
||||
myfunc()
|
||||
excinfo.match(r'.* 123 .*')
|
||||
|
||||
The regexp parameter of the ``match`` method is matched with the ``re.search``
|
||||
function. So in the above example ``excinfo.match('123')`` would have worked as
|
||||
function. So in the above example ``match='123'`` would have worked as
|
||||
well.
|
||||
|
||||
|
||||
@@ -169,15 +168,15 @@ when it encounters comparisons. For example::
|
||||
if you run this module::
|
||||
|
||||
$ pytest test_assert2.py
|
||||
======= test session starts ========
|
||||
=========================== 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 1 items
|
||||
collected 1 item
|
||||
|
||||
test_assert2.py F
|
||||
test_assert2.py F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_set_comparison ________
|
||||
================================= FAILURES =================================
|
||||
___________________________ test_set_comparison ____________________________
|
||||
|
||||
def test_set_comparison():
|
||||
set1 = set("1308")
|
||||
@@ -191,7 +190,7 @@ if you run this module::
|
||||
E Use -v to get the full diff
|
||||
|
||||
test_assert2.py:5: AssertionError
|
||||
======= 1 failed in 0.12 seconds ========
|
||||
========================= 1 failed in 0.12 seconds =========================
|
||||
|
||||
Special comparisons are done for a number of cases:
|
||||
|
||||
@@ -210,8 +209,8 @@ the ``pytest_assertrepr_compare`` hook.
|
||||
.. autofunction:: _pytest.hookspec.pytest_assertrepr_compare
|
||||
:noindex:
|
||||
|
||||
As an example consider adding the following hook in a conftest.py which
|
||||
provides an alternative explanation for ``Foo`` objects::
|
||||
As an example consider adding the following hook in a :ref:`conftest.py <conftest.py>`
|
||||
file which provides an alternative explanation for ``Foo`` objects::
|
||||
|
||||
# content of conftest.py
|
||||
from test_foocompare import Foo
|
||||
@@ -239,9 +238,9 @@ you can run the test module and get the custom output defined in
|
||||
the conftest file::
|
||||
|
||||
$ pytest -q test_foocompare.py
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_compare ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_compare _______________________________
|
||||
|
||||
def test_compare():
|
||||
f1 = Foo(1)
|
||||
|
||||
@@ -10,3 +10,11 @@ With the pytest 3.0 release we introduced a clear communication scheme for when
|
||||
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``).
|
||||
|
||||
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).
|
||||
|
||||
|
||||
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.
|
||||
|
||||
Following our deprecation policy, after starting issuing deprecation warnings we keep features for *at least* two minor versions before considering removal.
|
||||
|
||||
@@ -38,7 +38,7 @@ Examples at :ref:`assertraises`.
|
||||
Comparing floating point numbers
|
||||
--------------------------------
|
||||
|
||||
.. autoclass:: approx
|
||||
.. autofunction:: approx
|
||||
|
||||
Raising a specific test outcome
|
||||
--------------------------------------
|
||||
@@ -47,11 +47,11 @@ You can use the following functions in your test, fixture or setup
|
||||
functions to force a certain test outcome. Note that most often
|
||||
you can rather use declarative marks, see :ref:`skipping`.
|
||||
|
||||
.. autofunction:: _pytest.runner.fail
|
||||
.. autofunction:: _pytest.runner.skip
|
||||
.. autofunction:: _pytest.runner.importorskip
|
||||
.. autofunction:: _pytest.skipping.xfail
|
||||
.. autofunction:: _pytest.runner.exit
|
||||
.. autofunction:: _pytest.outcomes.fail
|
||||
.. autofunction:: _pytest.outcomes.skip
|
||||
.. autofunction:: _pytest.outcomes.importorskip
|
||||
.. autofunction:: _pytest.outcomes.xfail
|
||||
.. autofunction:: _pytest.outcomes.exit
|
||||
|
||||
Fixtures and requests
|
||||
-----------------------------------------------------
|
||||
@@ -91,11 +91,23 @@ You can ask for available builtin or project-custom
|
||||
capsys
|
||||
Enable capturing of writes to sys.stdout/sys.stderr and make
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
|
||||
objects.
|
||||
capsysbinary
|
||||
Enable capturing of writes to sys.stdout/sys.stderr and make
|
||||
captured output available via ``capsys.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``bytes``
|
||||
objects.
|
||||
capfd
|
||||
Enable capturing of writes to file descriptors 1 and 2 and make
|
||||
captured output available via ``capfd.readouterr()`` method calls
|
||||
which return a ``(out, err)`` tuple.
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be ``text``
|
||||
objects.
|
||||
capfdbinary
|
||||
Enable capturing of write to file descriptors 1 and 2 and make
|
||||
captured output available via ``capfdbinary.readouterr`` method calls
|
||||
which return a ``(out, err)`` tuple. ``out`` and ``err`` will be
|
||||
``bytes`` objects.
|
||||
doctest_namespace
|
||||
Inject names into the doctest namespace.
|
||||
pytestconfig
|
||||
@@ -104,18 +116,30 @@ You can ask for available builtin or project-custom
|
||||
Add extra xml properties to the tag for the calling test.
|
||||
The fixture is callable with ``(name, value)``, with value being automatically
|
||||
xml-encoded.
|
||||
record_xml_attribute
|
||||
Add extra xml attributes to the tag for the calling test.
|
||||
The fixture is callable with ``(name, value)``, with value being automatically
|
||||
xml-encoded
|
||||
caplog
|
||||
Access and control log capturing.
|
||||
|
||||
Captured logs are available through the following methods::
|
||||
|
||||
* caplog.text() -> string containing formatted log output
|
||||
* caplog.records() -> list of logging.LogRecord instances
|
||||
* caplog.record_tuples() -> list of (logger_name, level, message) tuples
|
||||
monkeypatch
|
||||
The returned ``monkeypatch`` fixture provides these
|
||||
helper methods to modify objects, dictionaries or os.environ::
|
||||
|
||||
monkeypatch.setattr(obj, name, value, raising=True)
|
||||
monkeypatch.delattr(obj, name, raising=True)
|
||||
monkeypatch.setitem(mapping, name, value)
|
||||
monkeypatch.delitem(obj, name, raising=True)
|
||||
monkeypatch.setenv(name, value, prepend=False)
|
||||
monkeypatch.delenv(name, value, raising=True)
|
||||
monkeypatch.syspath_prepend(path)
|
||||
monkeypatch.chdir(path)
|
||||
monkeypatch.setattr(obj, name, value, raising=True)
|
||||
monkeypatch.delattr(obj, name, raising=True)
|
||||
monkeypatch.setitem(mapping, name, value)
|
||||
monkeypatch.delitem(obj, name, raising=True)
|
||||
monkeypatch.setenv(name, value, prepend=False)
|
||||
monkeypatch.delenv(name, value, raising=True)
|
||||
monkeypatch.syspath_prepend(path)
|
||||
monkeypatch.chdir(path)
|
||||
|
||||
All modifications will be undone after the requesting
|
||||
test function or fixture has finished. The ``raising``
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
.. _`cache_provider`:
|
||||
.. _cache:
|
||||
|
||||
|
||||
Cache: working with cross-testrun state
|
||||
=======================================
|
||||
|
||||
.. versionadded:: 2.8
|
||||
|
||||
.. warning::
|
||||
|
||||
The functionality of this core plugin was previously distributed
|
||||
as a third party plugin named ``pytest-cache``. The core plugin
|
||||
is compatible regarding command line options and API usage except that you
|
||||
can only store/receive data between test runs that is json-serializable.
|
||||
|
||||
|
||||
Usage
|
||||
---------
|
||||
|
||||
@@ -50,9 +46,9 @@ First, let's create 50 test invocation of which only 2 fail::
|
||||
If you run this for the first time you will see two failures::
|
||||
|
||||
$ pytest -q
|
||||
.................F.......F........................
|
||||
======= FAILURES ========
|
||||
_______ test_num[17] ________
|
||||
.................F.......F........................ [100%]
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_num[17] _______________________________
|
||||
|
||||
i = 17
|
||||
|
||||
@@ -63,7 +59,7 @@ If you run this for the first time you will see two failures::
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:6: Failed
|
||||
_______ test_num[25] ________
|
||||
_______________________________ test_num[25] _______________________________
|
||||
|
||||
i = 25
|
||||
|
||||
@@ -79,16 +75,16 @@ If you run this for the first time you will see two failures::
|
||||
If you then run it with ``--lf``::
|
||||
|
||||
$ pytest --lf
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
run-last-failure: rerun last 2 failures
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 50 items
|
||||
run-last-failure: rerun previous 2 failures
|
||||
|
||||
test_50.py FF
|
||||
test_50.py FF [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_num[17] ________
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_num[17] _______________________________
|
||||
|
||||
i = 17
|
||||
|
||||
@@ -99,7 +95,7 @@ If you then run it with ``--lf``::
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:6: Failed
|
||||
_______ test_num[25] ________
|
||||
_______________________________ test_num[25] _______________________________
|
||||
|
||||
i = 25
|
||||
|
||||
@@ -110,8 +106,8 @@ If you then run it with ``--lf``::
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:6: Failed
|
||||
======= 48 tests deselected ========
|
||||
======= 2 failed, 48 deselected in 0.12 seconds ========
|
||||
=========================== 48 tests deselected ============================
|
||||
================= 2 failed, 48 deselected in 0.12 seconds ==================
|
||||
|
||||
You have run only the two failing test from the last run, while 48 tests have
|
||||
not been run ("deselected").
|
||||
@@ -121,16 +117,16 @@ previous failures will be executed first (as can be seen from the series
|
||||
of ``FF`` and dots)::
|
||||
|
||||
$ pytest --ff
|
||||
======= test session starts ========
|
||||
=========================== test session starts ============================
|
||||
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
|
||||
run-last-failure: rerun last 2 failures first
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collected 50 items
|
||||
run-last-failure: rerun previous 2 failures first
|
||||
|
||||
test_50.py FF................................................
|
||||
test_50.py FF................................................ [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_num[17] ________
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_num[17] _______________________________
|
||||
|
||||
i = 17
|
||||
|
||||
@@ -141,7 +137,7 @@ of ``FF`` and dots)::
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:6: Failed
|
||||
_______ test_num[25] ________
|
||||
_______________________________ test_num[25] _______________________________
|
||||
|
||||
i = 25
|
||||
|
||||
@@ -152,7 +148,7 @@ of ``FF`` and dots)::
|
||||
E Failed: bad luck
|
||||
|
||||
test_50.py:6: Failed
|
||||
======= 2 failed, 48 passed in 0.12 seconds ========
|
||||
=================== 2 failed, 48 passed in 0.12 seconds ====================
|
||||
|
||||
.. _`config.cache`:
|
||||
|
||||
@@ -186,9 +182,9 @@ If you run this command once, it will take a while because
|
||||
of the sleep::
|
||||
|
||||
$ pytest -q
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_function ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_function _______________________________
|
||||
|
||||
mydata = 42
|
||||
|
||||
@@ -203,9 +199,9 @@ If you run it a second time the value will be retrieved from
|
||||
the cache and this will be quick::
|
||||
|
||||
$ pytest -q
|
||||
F
|
||||
======= FAILURES ========
|
||||
_______ test_function ________
|
||||
F [100%]
|
||||
================================= FAILURES =================================
|
||||
______________________________ test_function _______________________________
|
||||
|
||||
mydata = 42
|
||||
|
||||
@@ -226,17 +222,17 @@ You can always peek at the content of the cache using the
|
||||
``--cache-show`` command line option::
|
||||
|
||||
$ py.test --cache-show
|
||||
======= test session starts ========
|
||||
=========================== 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:
|
||||
cachedir: $REGENDOC_TMPDIR/.cache
|
||||
cachedir: $REGENDOC_TMPDIR/.pytest_cache
|
||||
------------------------------- cache values -------------------------------
|
||||
cache/lastfailed contains:
|
||||
{'test_caching.py::test_function': True}
|
||||
example/value contains:
|
||||
42
|
||||
|
||||
======= no tests ran in 0.12 seconds ========
|
||||
======================= no tests ran in 0.12 seconds =======================
|
||||
|
||||
Clearing Cache content
|
||||
-------------------------------
|
||||
|
||||
@@ -63,15 +63,15 @@ and running this module will show you precisely the output
|
||||
of the failing function and hide the other one::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== 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 2 items
|
||||
|
||||
test_module.py .F
|
||||
test_module.py .F [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_func2 ________
|
||||
================================= FAILURES =================================
|
||||
________________________________ test_func2 ________________________________
|
||||
|
||||
def test_func2():
|
||||
> assert False
|
||||
@@ -80,26 +80,26 @@ of the failing function and hide the other one::
|
||||
test_module.py:9: AssertionError
|
||||
-------------------------- Captured stdout setup ---------------------------
|
||||
setting up <function test_func2 at 0xdeadbeef>
|
||||
======= 1 failed, 1 passed in 0.12 seconds ========
|
||||
==================== 1 failed, 1 passed in 0.12 seconds ====================
|
||||
|
||||
Accessing captured output from a test function
|
||||
---------------------------------------------------
|
||||
|
||||
The ``capsys`` and ``capfd`` fixtures allow to access stdout/stderr
|
||||
output created during test execution. Here is an example test function
|
||||
that performs some output related checks:
|
||||
The ``capsys``, ``capsysbinary``, ``capfd``, and ``capfdbinary`` fixtures
|
||||
allow access to stdout/stderr output created during test execution. Here is
|
||||
an example test function that performs some output related checks:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def test_myoutput(capsys): # or use "capfd" for fd-level
|
||||
print ("hello")
|
||||
print("hello")
|
||||
sys.stderr.write("world\n")
|
||||
out, err = capsys.readouterr()
|
||||
assert out == "hello\n"
|
||||
assert err == "world\n"
|
||||
print ("next")
|
||||
out, err = capsys.readouterr()
|
||||
assert out == "next\n"
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "hello\n"
|
||||
assert captured.err == "world\n"
|
||||
print("next")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "next\n"
|
||||
|
||||
The ``readouterr()`` call snapshots the output so far -
|
||||
and capturing will be continued. After the test
|
||||
@@ -110,11 +110,30 @@ output streams and also interacts well with pytest's
|
||||
own per-test capturing.
|
||||
|
||||
If you want to capture on filedescriptor level you can use
|
||||
the ``capfd`` function argument which offers the exact
|
||||
the ``capfd`` fixture which offers the exact
|
||||
same interface but allows to also capture output from
|
||||
libraries or subprocesses that directly write to operating
|
||||
system level output streams (FD1 and FD2).
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
The return value from ``readouterr`` changed to a ``namedtuple`` with two attributes, ``out`` and ``err``.
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
If the code under test writes non-textual data, you can capture this using
|
||||
the ``capsysbinary`` fixture which instead returns ``bytes`` from
|
||||
the ``readouterr`` method. The ``capfsysbinary`` fixture is currently only
|
||||
available in python 3.
|
||||
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
If the code under test writes non-textual data, you can capture this using
|
||||
the ``capfdbinary`` fixture which instead returns ``bytes`` from
|
||||
the ``readouterr`` method. The ``capfdbinary`` fixture operates on the
|
||||
filedescriptor level.
|
||||
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
|
||||
@@ -19,9 +19,9 @@ Contact channels
|
||||
- `pytest-commit at python.org (mailing list)`_: for commits and new issues
|
||||
|
||||
- :doc:`contribution guide <contributing>` for help on submitting pull
|
||||
requests to bitbucket (including using git via gitifyhg).
|
||||
requests to GitHub.
|
||||
|
||||
- #pylib on irc.freenode.net IRC channel for random questions.
|
||||
- ``#pylib`` on irc.freenode.net IRC channel for random questions.
|
||||
|
||||
- private mail to Holger.Krekel at gmail com if you want to communicate sensitive issues
|
||||
|
||||
@@ -46,6 +46,5 @@ Contact channels
|
||||
.. _`py-dev`:
|
||||
.. _`development mailing list`:
|
||||
.. _`pytest-dev at python.org (mailing list)`: http://mail.python.org/mailman/listinfo/pytest-dev
|
||||
.. _`py-svn`:
|
||||
.. _`pytest-commit at python.org (mailing list)`: http://mail.python.org/mailman/listinfo/pytest-commit
|
||||
|
||||
|
||||
@@ -30,15 +30,19 @@ Full pytest documentation
|
||||
xunit_setup
|
||||
plugins
|
||||
writing_plugins
|
||||
logging
|
||||
|
||||
example/index
|
||||
goodpractices
|
||||
pythonpath
|
||||
customize
|
||||
example/index
|
||||
bash-completion
|
||||
|
||||
backwards-compatibility
|
||||
historical-notes
|
||||
license
|
||||
contributing
|
||||
development_guide
|
||||
talks
|
||||
projects
|
||||
faq
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Basic test configuration
|
||||
===================================
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Command line options and configuration file settings
|
||||
-----------------------------------------------------------------
|
||||
@@ -15,17 +15,31 @@ which were registered by installed plugins.
|
||||
.. _rootdir:
|
||||
.. _inifiles:
|
||||
|
||||
initialization: determining rootdir and inifile
|
||||
Initialization: determining rootdir and inifile
|
||||
-----------------------------------------------
|
||||
|
||||
.. versionadded:: 2.7
|
||||
|
||||
pytest determines a "rootdir" for each test run which depends on
|
||||
pytest determines a ``rootdir`` for each test run which depends on
|
||||
the command line arguments (specified test files, paths) and on
|
||||
the existence of inifiles. The determined rootdir and ini-file are
|
||||
printed as part of the pytest header. The rootdir is used for constructing
|
||||
"nodeids" during collection and may also be used by plugins to store
|
||||
project/testrun-specific information.
|
||||
the existence of *ini-files*. The determined ``rootdir`` and *ini-file* are
|
||||
printed as part of the pytest header during startup.
|
||||
|
||||
Here's a summary what ``pytest`` uses ``rootdir`` for:
|
||||
|
||||
* Construct *nodeids* during collection; each test is assigned
|
||||
a unique *nodeid* which is rooted at the ``rootdir`` and takes in account full path,
|
||||
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
|
||||
in ``rootdir`` to store its cross-test run state.
|
||||
|
||||
Important to emphasize that ``rootdir`` is **NOT** used to modify ``sys.path``/``PYTHONPATH`` or
|
||||
influence how modules are imported. See :ref:`pythonpath` for more details.
|
||||
|
||||
Finding the ``rootdir``
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Here is the algorithm which finds the rootdir from ``args``:
|
||||
|
||||
@@ -112,25 +126,51 @@ progress output, you can write it into a configuration file:
|
||||
# content of pytest.ini
|
||||
# (or tox.ini or setup.cfg)
|
||||
[pytest]
|
||||
addopts = -rsxX -q
|
||||
addopts = -ra -q
|
||||
|
||||
Alternatively, you can set a PYTEST_ADDOPTS environment variable to add command
|
||||
Alternatively, you can set a ``PYTEST_ADDOPTS`` environment variable to add command
|
||||
line options while the environment is in use::
|
||||
|
||||
export PYTEST_ADDOPTS="-rsxX -q"
|
||||
export PYTEST_ADDOPTS="-v"
|
||||
|
||||
From now on, running ``pytest`` will add the specified options.
|
||||
Here's how the command-line is built in the presence of ``addopts`` or the environment variable::
|
||||
|
||||
<pytest.ini:addopts> $PYTEST_ADDOTPS <extra command-line arguments>
|
||||
|
||||
So if the user executes in the command-line::
|
||||
|
||||
pytest -m slow
|
||||
|
||||
The actual command line executed is::
|
||||
|
||||
pytest -ra -q -v -m slow
|
||||
|
||||
Note that as usual for other command-line applications, in case of conflicting options the last one wins, so the example
|
||||
above will show verbose output because ``-v`` overwrites ``-q``.
|
||||
|
||||
|
||||
Builtin configuration file options
|
||||
----------------------------------------------
|
||||
|
||||
Here is a list of builtin configuration options that may be written in a ``pytest.ini``, ``tox.ini`` or ``setup.cfg``
|
||||
file, usually located at the root of your repository. All options must be under a ``[pytest]`` section
|
||||
(``[tool:pytest]`` for ``setup.cfg`` files).
|
||||
|
||||
Configuration file options may be overwritten in the command-line by using ``-o/--override``, which can also be
|
||||
passed multiple times. The expected format is ``name=value``. For example::
|
||||
|
||||
pytest -o console_output_style=classic -o cache_dir=/tmp/mycache
|
||||
|
||||
|
||||
.. confval:: minversion
|
||||
|
||||
Specifies a minimal pytest version required for running tests.
|
||||
|
||||
minversion = 2.1 # will fail if we run with pytest-2.0
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
minversion = 3.0 # will fail if we run with pytest-2.8
|
||||
|
||||
.. confval:: addopts
|
||||
|
||||
@@ -139,6 +179,7 @@ Builtin configuration file options
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
addopts = --maxfail=2 -rf # exit after 2 failures, report fail info
|
||||
|
||||
@@ -171,7 +212,16 @@ Builtin configuration file options
|
||||
norecursedirs = .svn _build tmp*
|
||||
|
||||
This would tell ``pytest`` to not look into typical subversion or
|
||||
sphinx-build directories or into any ``tmp`` prefixed directory.
|
||||
sphinx-build directories or into any ``tmp`` prefixed directory.
|
||||
|
||||
Additionally, ``pytest`` will attempt to intelligently identify and ignore a
|
||||
virtualenv by the presence of an activation script. Any directory deemed to
|
||||
be the root of a virtual environment will not be considered during test
|
||||
collection unless ``‑‑collect‑in‑virtualenv`` is given. Note also that
|
||||
``norecursedirs`` takes precedence over ``‑‑collect‑in‑virtualenv``; e.g. if
|
||||
you intend to run tests in a virtualenv with a base directory that matches
|
||||
``'.*'`` you *must* override ``norecursedirs`` in addition to using the
|
||||
``‑‑collect‑in‑virtualenv`` flag.
|
||||
|
||||
.. confval:: testpaths
|
||||
|
||||
@@ -195,13 +245,16 @@ Builtin configuration file options
|
||||
.. confval:: python_files
|
||||
|
||||
One or more Glob-style file patterns determining which python files
|
||||
are considered as test modules.
|
||||
are considered as test modules. By default, pytest will consider
|
||||
any file matching with ``test_*.py`` and ``*_test.py`` globs as a test
|
||||
module.
|
||||
|
||||
.. confval:: python_classes
|
||||
|
||||
One or more name prefixes or glob-style patterns determining which classes
|
||||
are considered for test collection. Here is an example of how to collect
|
||||
tests from classes that end in ``Suite``:
|
||||
are considered for test collection. 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``:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
@@ -216,7 +269,8 @@ Builtin configuration file options
|
||||
.. confval:: python_functions
|
||||
|
||||
One or more name prefixes or glob-patterns determining which test functions
|
||||
and methods are considered tests. Here is an example of how
|
||||
and methods are considered tests. 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``:
|
||||
|
||||
.. code-block:: ini
|
||||
@@ -262,3 +316,58 @@ Builtin configuration file options
|
||||
|
||||
This tells pytest to ignore deprecation warnings and turn all other warnings
|
||||
into errors. For more information please refer to :ref:`warnings`.
|
||||
|
||||
.. confval:: cache_dir
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
Sets a directory where stores content of cache plugin. Default directory is
|
||||
``.cache`` which is created in :ref:`rootdir <rootdir>`. Directory may be
|
||||
relative or absolute path. If setting relative path, then directory is created
|
||||
relative to :ref:`rootdir <rootdir>`. Additionally path may contain environment
|
||||
variables, that will be expanded. For more information about cache plugin
|
||||
please refer to :ref:`cache_provider`.
|
||||
|
||||
|
||||
.. confval:: console_output_style
|
||||
|
||||
.. versionadded:: 3.3
|
||||
|
||||
Sets the console output style while running tests:
|
||||
|
||||
* ``classic``: classic pytest output.
|
||||
* ``progress``: like classic pytest output, but with a progress indicator.
|
||||
|
||||
The default is ``progress``, but you can fallback to ``classic`` if you prefer or
|
||||
the new mode is causing unexpected problems:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
console_output_style = classic
|
||||
|
||||
|
||||
.. confval:: empty_parameter_set_mark
|
||||
|
||||
.. versionadded:: 3.4
|
||||
|
||||
Allows to pick the action for empty parametersets in parameterization
|
||||
|
||||
* ``skip`` skips tests with a empty parameterset (default)
|
||||
* ``xfail`` marks tests with a empty parameterset as xfail(run=False)
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
empty_parameter_set_mark = xfail
|
||||
|
||||
.. note::
|
||||
|
||||
The default value of this option is planned to change to ``xfail`` in future releases
|
||||
as this is considered less error prone, see `#3155`_ for more details.
|
||||
|
||||
|
||||
|
||||
.. _`#3155`: https://github.com/pytest-dev/pytest/issues/3155
|
||||
|
||||
108
doc/en/development_guide.rst
Normal file
108
doc/en/development_guide.rst
Normal file
@@ -0,0 +1,108 @@
|
||||
=================
|
||||
Development Guide
|
||||
=================
|
||||
|
||||
Some general guidelines regarding development in pytest for core maintainers and general contributors. Nothing here
|
||||
is set in stone and can't be changed, feel free to suggest improvements or changes in the workflow.
|
||||
|
||||
|
||||
Code Style
|
||||
----------
|
||||
|
||||
* `PEP-8 <https://www.python.org/dev/peps/pep-0008>`_
|
||||
* `flake8 <https://pypi.python.org/pypi/flake8>`_ for quality checks
|
||||
* `invoke <http://www.pyinvoke.org/>`_ to automate development tasks
|
||||
|
||||
|
||||
Branches
|
||||
--------
|
||||
|
||||
We have two long term branches:
|
||||
|
||||
* ``master``: contains the code for the next bugfix release.
|
||||
* ``features``: contains the code with new features for the next minor release.
|
||||
|
||||
The official repository usually does not contain topic branches, developers and contributors should create topic
|
||||
branches in their own forks.
|
||||
|
||||
Exceptions can be made for cases where more than one contributor is working on the same
|
||||
topic or where it makes sense to use some automatic capability of the main repository, such as automatic docs from
|
||||
`readthedocs <readthedocs.org>`_ for a branch dealing with documentation refactoring.
|
||||
|
||||
Issues
|
||||
------
|
||||
|
||||
Any question, feature, bug or proposal is welcome as an issue. Users are encouraged to use them whenever they need.
|
||||
|
||||
GitHub issues should use labels to categorize them. Labels should be created sporadically, to fill a niche; we should
|
||||
avoid creating labels just for the sake of creating them.
|
||||
|
||||
Here is a list of labels and a brief description mentioning their intent.
|
||||
|
||||
|
||||
**Type**
|
||||
|
||||
* ``type: backward compatibility``: issue that will cause problems with old pytest versions.
|
||||
* ``type: bug``: problem that needs to be addressed.
|
||||
* ``type: deprecation``: feature that will be deprecated in the future.
|
||||
* ``type: docs``: documentation missing or needing clarification.
|
||||
* ``type: enhancement``: new feature or API change, should be merged into ``features``.
|
||||
* ``type: feature-branch``: new feature or API change, should be merged into ``features``.
|
||||
* ``type: infrastructure``: improvement to development/releases/CI structure.
|
||||
* ``type: performance``: performance or memory problem/improvement.
|
||||
* ``type: proposal``: proposal for a new feature, often to gather opinions or design the API around the new feature.
|
||||
* ``type: question``: question regarding usage, installation, internals or how to test something.
|
||||
* ``type: refactoring``: internal improvements to the code.
|
||||
* ``type: regression``: indicates a problem that was introduced in a release which was working previously.
|
||||
|
||||
**Status**
|
||||
|
||||
* ``status: critical``: grave problem or usability issue that affects lots of users.
|
||||
* ``status: easy``: easy issue that is friendly to new contributors.
|
||||
* ``status: help wanted``: core developers need help from experts on this topic.
|
||||
* ``status: needs information``: reporter needs to provide more information; can be closed after 2 or more weeks of inactivity.
|
||||
|
||||
**Topic**
|
||||
|
||||
* ``topic: collection``
|
||||
* ``topic: fixtures``
|
||||
* ``topic: parametrize``
|
||||
* ``topic: reporting``
|
||||
* ``topic: selection``
|
||||
* ``topic: tracebacks``
|
||||
|
||||
**Plugin (internal or external)**
|
||||
|
||||
* ``plugin: cache``
|
||||
* ``plugin: capture``
|
||||
* ``plugin: doctests``
|
||||
* ``plugin: junitxml``
|
||||
* ``plugin: monkeypatch``
|
||||
* ``plugin: nose``
|
||||
* ``plugin: pastebin``
|
||||
* ``plugin: pytester``
|
||||
* ``plugin: tmpdir``
|
||||
* ``plugin: unittest``
|
||||
* ``plugin: warnings``
|
||||
* ``plugin: xdist``
|
||||
|
||||
|
||||
**OS**
|
||||
|
||||
Issues specific to a single operating system. Do not use as a means to indicate where an issue originated from, only
|
||||
for problems that happen **only** in that system.
|
||||
|
||||
* ``os: linux``
|
||||
* ``os: mac``
|
||||
* ``os: windows``
|
||||
|
||||
**Temporary**
|
||||
|
||||
Used to classify issues for limited time, to help find issues related in events for example.
|
||||
They should be removed after they are no longer relevant.
|
||||
|
||||
* ``temporary: EP2017 sprint``:
|
||||
* ``temporary: sprint-candidate``:
|
||||
|
||||
|
||||
.. include:: ../../HOWTORELEASE.rst
|
||||
@@ -61,14 +61,14 @@ and another like this::
|
||||
then you can just invoke ``pytest`` without command line options::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
=========================== 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 1 items
|
||||
collected 1 item
|
||||
|
||||
mymodule.py .
|
||||
mymodule.py . [100%]
|
||||
|
||||
======= 1 passed in 0.12 seconds ========
|
||||
========================= 1 passed in 0.12 seconds =========================
|
||||
|
||||
It is possible to use fixtures using the ``getfixture`` helper::
|
||||
|
||||
@@ -81,9 +81,9 @@ Also, :ref:`usefixtures` and :ref:`autouse` fixtures are supported
|
||||
when executing text doctest files.
|
||||
|
||||
The standard ``doctest`` module provides some setting flags to configure the
|
||||
strictness of doctest tests. In pytest You can enable those flags those flags
|
||||
using the configuration file. To make pytest ignore trailing whitespaces and
|
||||
ignore lengthy exception stack traces you can just write:
|
||||
strictness of doctest tests. In pytest, you can enable those flags using the
|
||||
configuration file. To make pytest ignore trailing whitespaces and ignore
|
||||
lengthy exception stack traces you can just write:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
|
||||
@@ -157,12 +157,14 @@ class TestRaises(object):
|
||||
|
||||
# thanks to Matthew Scott for this test
|
||||
def test_dynamic_compile_shows_nicely():
|
||||
import imp
|
||||
import sys
|
||||
src = 'def foo():\n assert 1 == 0\n'
|
||||
name = 'abc-123'
|
||||
module = py.std.imp.new_module(name)
|
||||
module = imp.new_module(name)
|
||||
code = _pytest._code.compile(src, name, 'exec')
|
||||
py.builtin.exec_(code, module.__dict__)
|
||||
py.std.sys.modules[name] = module
|
||||
sys.modules[name] = module
|
||||
module.foo()
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ example: specifying and selecting acceptance tests
|
||||
|
||||
class AcceptFixture(object):
|
||||
def __init__(self, request):
|
||||
if not request.config.option.acceptance:
|
||||
if not request.config.getoption('acceptance'):
|
||||
pytest.skip("specify -A to run acceptance tests")
|
||||
self.tmpdir = request.config.mktemp(request.function.__name__, numbered=True)
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
.. _examples:
|
||||
|
||||
Usages and Examples
|
||||
===========================================
|
||||
Examples and customization tricks
|
||||
=================================
|
||||
|
||||
Here is a (growing) list of examples. :ref:`Contact <contact>` us if you
|
||||
need more examples or have questions. Also take a look at the
|
||||
|
||||
@@ -30,32 +30,32 @@ You can "mark" a test function with custom metadata like this::
|
||||
You can then restrict a test run to only run tests marked with ``webtest``::
|
||||
|
||||
$ pytest -v -m webtest
|
||||
======= test session starts ========
|
||||
=========================== 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
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::test_send_http PASSED
|
||||
test_server.py::test_send_http PASSED [100%]
|
||||
|
||||
======= 3 tests deselected ========
|
||||
======= 1 passed, 3 deselected in 0.12 seconds ========
|
||||
============================ 3 tests deselected ============================
|
||||
================== 1 passed, 3 deselected in 0.12 seconds ==================
|
||||
|
||||
Or the inverse, running all tests except the webtest ones::
|
||||
|
||||
$ pytest -v -m "not webtest"
|
||||
======= test session starts ========
|
||||
=========================== 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
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::test_something_quick PASSED
|
||||
test_server.py::test_another PASSED
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::test_something_quick PASSED [ 33%]
|
||||
test_server.py::test_another PASSED [ 66%]
|
||||
test_server.py::TestClass::test_method PASSED [100%]
|
||||
|
||||
======= 1 tests deselected ========
|
||||
======= 3 passed, 1 deselected in 0.12 seconds ========
|
||||
============================ 1 tests deselected ============================
|
||||
================== 3 passed, 1 deselected in 0.12 seconds ==================
|
||||
|
||||
Selecting tests based on their node ID
|
||||
--------------------------------------
|
||||
@@ -65,42 +65,42 @@ arguments to select only specified tests. This makes it easy to select
|
||||
tests based on their module, class, method, or function name::
|
||||
|
||||
$ pytest -v test_server.py::TestClass::test_method
|
||||
======= test session starts ========
|
||||
=========================== 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
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 5 items
|
||||
collecting ... collected 1 item
|
||||
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::TestClass::test_method PASSED [100%]
|
||||
|
||||
======= 1 passed in 0.12 seconds ========
|
||||
========================= 1 passed in 0.12 seconds =========================
|
||||
|
||||
You can also select on the class::
|
||||
|
||||
$ pytest -v test_server.py::TestClass
|
||||
======= test session starts ========
|
||||
=========================== 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
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
collecting ... collected 1 item
|
||||
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::TestClass::test_method PASSED [100%]
|
||||
|
||||
======= 1 passed in 0.12 seconds ========
|
||||
========================= 1 passed in 0.12 seconds =========================
|
||||
|
||||
Or select multiple nodes::
|
||||
|
||||
$ pytest -v test_server.py::TestClass test_server.py::test_send_http
|
||||
======= test session starts ========
|
||||
=========================== 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
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 8 items
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::test_send_http PASSED
|
||||
test_server.py::TestClass::test_method PASSED [ 50%]
|
||||
test_server.py::test_send_http PASSED [100%]
|
||||
|
||||
======= 2 passed in 0.12 seconds ========
|
||||
========================= 2 passed in 0.12 seconds =========================
|
||||
|
||||
.. _node-id:
|
||||
|
||||
@@ -129,58 +129,62 @@ exact match on markers that ``-m`` provides. This makes it easy to
|
||||
select tests based on their names::
|
||||
|
||||
$ pytest -v -k http # running with the above defined example module
|
||||
======= test session starts ========
|
||||
=========================== 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
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::test_send_http PASSED
|
||||
test_server.py::test_send_http PASSED [100%]
|
||||
|
||||
======= 3 tests deselected ========
|
||||
======= 1 passed, 3 deselected in 0.12 seconds ========
|
||||
============================ 3 tests deselected ============================
|
||||
================== 1 passed, 3 deselected in 0.12 seconds ==================
|
||||
|
||||
And you can also run all tests except the ones that match the keyword::
|
||||
|
||||
$ pytest -k "not send_http" -v
|
||||
======= test session starts ========
|
||||
=========================== 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
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::test_something_quick PASSED
|
||||
test_server.py::test_another PASSED
|
||||
test_server.py::TestClass::test_method PASSED
|
||||
test_server.py::test_something_quick PASSED [ 33%]
|
||||
test_server.py::test_another PASSED [ 66%]
|
||||
test_server.py::TestClass::test_method PASSED [100%]
|
||||
|
||||
======= 1 tests deselected ========
|
||||
======= 3 passed, 1 deselected in 0.12 seconds ========
|
||||
============================ 1 tests deselected ============================
|
||||
================== 3 passed, 1 deselected in 0.12 seconds ==================
|
||||
|
||||
Or to select "http" and "quick" tests::
|
||||
|
||||
$ pytest -k "http or quick" -v
|
||||
======= test session starts ========
|
||||
=========================== 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
|
||||
cachedir: .cache
|
||||
cachedir: .pytest_cache
|
||||
rootdir: $REGENDOC_TMPDIR, inifile:
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py::test_send_http PASSED
|
||||
test_server.py::test_something_quick PASSED
|
||||
test_server.py::test_send_http PASSED [ 50%]
|
||||
test_server.py::test_something_quick PASSED [100%]
|
||||
|
||||
======= 2 tests deselected ========
|
||||
======= 2 passed, 2 deselected in 0.12 seconds ========
|
||||
============================ 2 tests deselected ============================
|
||||
================== 2 passed, 2 deselected in 0.12 seconds ==================
|
||||
|
||||
.. note::
|
||||
|
||||
If you are using expressions such as "X and Y" then both X and Y
|
||||
need to be simple non-keyword names. For example, "pass" or "from"
|
||||
will result in SyntaxErrors because "-k" evaluates the expression.
|
||||
If you are using expressions such as ``"X and Y"`` then both ``X`` and ``Y``
|
||||
need to be simple non-keyword names. For example, ``"pass"`` or ``"from"``
|
||||
will result in SyntaxErrors because ``"-k"`` evaluates the expression using
|
||||
Python's `eval`_ function.
|
||||
|
||||
However, if the "-k" argument is a simple string, no such restrictions
|
||||
apply. Also "-k 'not STRING'" has no restrictions. You can also
|
||||
specify numbers like "-k 1.3" to match tests which are parametrized
|
||||
with the float "1.3".
|
||||
.. _`eval`: https://docs.python.org/3.6/library/functions.html#eval
|
||||
|
||||
|
||||
However, if the ``"-k"`` argument is a simple string, no such restrictions
|
||||
apply. Also ``"-k 'not STRING'"`` has no restrictions. You can also
|
||||
specify numbers like ``"-k 1.3"`` to match tests which are parametrized
|
||||
with the float ``"1.3"``.
|
||||
|
||||
Registering markers
|
||||
-------------------------------------
|
||||
@@ -223,13 +227,12 @@ For an example on how to add and work with markers from a plugin, see
|
||||
|
||||
It is recommended to explicitly register markers so that:
|
||||
|
||||
* there is one place in your test suite defining your markers
|
||||
* There is one place in your test suite defining your markers
|
||||
|
||||
* asking for existing markers via ``pytest --markers`` gives good output
|
||||
* Asking for existing markers via ``pytest --markers`` gives good output
|
||||
|
||||
* typos in function markers are treated as an error if you use
|
||||
the ``--strict`` option. Future versions of ``pytest`` are probably
|
||||
going to start treating non-registered markers as errors at some point.
|
||||
* Typos in function markers are treated as an error if you use
|
||||
the ``--strict`` option.
|
||||
|
||||
.. _`scoped-marking`:
|
||||
|
||||
@@ -351,26 +354,26 @@ and an example invocations specifying a different environment than what
|
||||
the test needs::
|
||||
|
||||
$ pytest -E stage2
|
||||
======= test session starts ========
|
||||
=========================== 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 1 items
|
||||
collected 1 item
|
||||
|
||||
test_someenv.py s
|
||||
test_someenv.py s [100%]
|
||||
|
||||
======= 1 skipped in 0.12 seconds ========
|
||||
======================== 1 skipped in 0.12 seconds =========================
|
||||
|
||||
and here is one that specifies exactly the environment needed::
|
||||
|
||||
$ pytest -E stage1
|
||||
======= test session starts ========
|
||||
=========================== 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 1 items
|
||||
collected 1 item
|
||||
|
||||
test_someenv.py .
|
||||
test_someenv.py . [100%]
|
||||
|
||||
======= 1 passed in 0.12 seconds ========
|
||||
========================= 1 passed in 0.12 seconds =========================
|
||||
|
||||
The ``--markers`` option always gives you a list of available markers::
|
||||
|
||||
@@ -392,6 +395,49 @@ The ``--markers`` option always gives you a list of available markers::
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
||||
|
||||
|
||||
.. _`passing callables to custom markers`:
|
||||
|
||||
Passing a callable to custom markers
|
||||
--------------------------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
Below is the config file that will be used in the next examples::
|
||||
|
||||
# content of conftest.py
|
||||
import sys
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
marker = item.get_marker('my_marker')
|
||||
if marker is not None:
|
||||
for info in marker:
|
||||
print('Marker info name={} args={} kwars={}'.format(info.name, info.args, info.kwargs))
|
||||
sys.stdout.flush()
|
||||
|
||||
A custom marker can have its argument set, i.e. ``args`` and ``kwargs`` properties, defined by either invoking it as a callable or using ``pytest.mark.MARKER_NAME.with_args``. These two methods achieve the same effect most of the time.
|
||||
|
||||
However, if there is a callable as the single positional argument with no keyword arguments, using the ``pytest.mark.MARKER_NAME(c)`` will not pass ``c`` as a positional argument but decorate ``c`` with the custom marker (see :ref:`MarkDecorator <mark>`). Fortunately, ``pytest.mark.MARKER_NAME.with_args`` comes to the rescue::
|
||||
|
||||
# content of test_custom_marker.py
|
||||
import pytest
|
||||
|
||||
def hello_world(*args, **kwargs):
|
||||
return 'Hello World'
|
||||
|
||||
@pytest.mark.my_marker.with_args(hello_world)
|
||||
def test_with_args():
|
||||
pass
|
||||
|
||||
The output is as follows::
|
||||
|
||||
$ pytest -q -s
|
||||
Marker info name=my_marker args=(<function hello_world at 0xdeadbeef>,) kwars={}
|
||||
.
|
||||
1 passed in 0.12 seconds
|
||||
|
||||
We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``.
|
||||
|
||||
|
||||
Reading markers which were set from multiple places
|
||||
----------------------------------------------------
|
||||
|
||||
@@ -484,29 +530,29 @@ Let's do a little test file to show how this looks like::
|
||||
then you will see two tests skipped and two executed tests as expected::
|
||||
|
||||
$ pytest -rs # this option reports skip reasons
|
||||
======= test session starts ========
|
||||
=========================== 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 4 items
|
||||
|
||||
test_plat.py s.s.
|
||||
======= short test summary info ========
|
||||
SKIP [2] $REGENDOC_TMPDIR/conftest.py:12: cannot run on platform linux
|
||||
test_plat.py s.s. [100%]
|
||||
========================= short test summary info ==========================
|
||||
SKIP [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux
|
||||
|
||||
======= 2 passed, 2 skipped in 0.12 seconds ========
|
||||
=================== 2 passed, 2 skipped in 0.12 seconds ====================
|
||||
|
||||
Note that if you specify a platform via the marker-command line option like this::
|
||||
|
||||
$ pytest -m linux
|
||||
======= test session starts ========
|
||||
=========================== 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 4 items
|
||||
|
||||
test_plat.py .
|
||||
test_plat.py . [100%]
|
||||
|
||||
======= 3 tests deselected ========
|
||||
======= 1 passed, 3 deselected in 0.12 seconds ========
|
||||
============================ 3 tests deselected ============================
|
||||
================== 1 passed, 3 deselected in 0.12 seconds ==================
|
||||
|
||||
then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests.
|
||||
|
||||
@@ -550,47 +596,47 @@ We want to dynamically define two markers and can do it in a
|
||||
We can now use the ``-m option`` to select one set::
|
||||
|
||||
$ pytest -m interface --tb=short
|
||||
======= test session starts ========
|
||||
=========================== 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 4 items
|
||||
|
||||
test_module.py FF
|
||||
test_module.py FF [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_interface_simple ________
|
||||
================================= FAILURES =================================
|
||||
__________________________ test_interface_simple ___________________________
|
||||
test_module.py:3: in test_interface_simple
|
||||
assert 0
|
||||
E assert 0
|
||||
_______ test_interface_complex ________
|
||||
__________________________ test_interface_complex __________________________
|
||||
test_module.py:6: in test_interface_complex
|
||||
assert 0
|
||||
E assert 0
|
||||
======= 2 tests deselected ========
|
||||
======= 2 failed, 2 deselected in 0.12 seconds ========
|
||||
============================ 2 tests deselected ============================
|
||||
================== 2 failed, 2 deselected in 0.12 seconds ==================
|
||||
|
||||
or to select both "event" and "interface" tests::
|
||||
|
||||
$ pytest -m "interface or event" --tb=short
|
||||
======= test session starts ========
|
||||
=========================== 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 4 items
|
||||
|
||||
test_module.py FFF
|
||||
test_module.py FFF [100%]
|
||||
|
||||
======= FAILURES ========
|
||||
_______ test_interface_simple ________
|
||||
================================= FAILURES =================================
|
||||
__________________________ test_interface_simple ___________________________
|
||||
test_module.py:3: in test_interface_simple
|
||||
assert 0
|
||||
E assert 0
|
||||
_______ test_interface_complex ________
|
||||
__________________________ test_interface_complex __________________________
|
||||
test_module.py:6: in test_interface_complex
|
||||
assert 0
|
||||
E assert 0
|
||||
_______ test_event_simple ________
|
||||
____________________________ test_event_simple _____________________________
|
||||
test_module.py:9: in test_event_simple
|
||||
assert 0
|
||||
E assert 0
|
||||
======= 1 tests deselected ========
|
||||
======= 3 failed, 1 deselected in 0.12 seconds ========
|
||||
============================ 1 tests deselected ============================
|
||||
================== 3 failed, 1 deselected in 0.12 seconds ==================
|
||||
|
||||
@@ -6,7 +6,7 @@ import py
|
||||
import pytest
|
||||
import _pytest._code
|
||||
|
||||
pythonlist = ['python2.6', 'python2.7', 'python3.4', 'python3.5']
|
||||
pythonlist = ['python2.7', 'python3.4', 'python3.5']
|
||||
@pytest.fixture(params=pythonlist)
|
||||
def python1(request, tmpdir):
|
||||
picklefile = tmpdir.join("data.pickle")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user