Compare commits
1050 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a111ff700 | ||
|
|
5c6758fde4 | ||
|
|
9bd8420a6b | ||
|
|
cbdab02d05 | ||
|
|
ce30896cd2 | ||
|
|
2e8b0a83fe | ||
|
|
369c711f14 | ||
|
|
cf0cac3b73 | ||
|
|
a9dd37f429 | ||
|
|
4de433e280 | ||
|
|
70f1e3b4b0 | ||
|
|
fdfc1946da | ||
|
|
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 | ||
|
|
2fd7626046 | ||
|
|
0540d72c87 | ||
|
|
1dee443c2b | ||
|
|
32e2642233 | ||
|
|
454426cba5 | ||
|
|
f96a1d89c5 | ||
|
|
ee0844dbd8 | ||
|
|
b74c626026 | ||
|
|
4e6e29dbee | ||
|
|
6117930642 | ||
|
|
7bb06b6dad | ||
|
|
7950c26a8e | ||
|
|
836dc451f4 | ||
|
|
8df3e55a31 | ||
|
|
53add4435f | ||
|
|
d7a5c5716f | ||
|
|
313a884459 | ||
|
|
c39689da41 | ||
|
|
17f64704c2 | ||
|
|
f9953fbe7c | ||
|
|
0ea80eb63c | ||
|
|
38ebf8dd10 | ||
|
|
04b1583d10 | ||
|
|
d9b93674c3 | ||
|
|
7d6bde2496 | ||
|
|
bd065a12bb | ||
|
|
f9df750025 | ||
|
|
d343f9497c | ||
|
|
5192191c38 | ||
|
|
69343310c6 | ||
|
|
c9c2c34b44 | ||
|
|
9beeef970e | ||
|
|
43aa037ebd | ||
|
|
2abf2070f2 | ||
|
|
ce0ff0040f | ||
|
|
6d2e11b7d1 | ||
|
|
9b48613baa | ||
|
|
0ff7f5d0c6 | ||
|
|
36cf89a2de | ||
|
|
3a4d37248d | ||
|
|
637550b249 | ||
|
|
598aefc686 | ||
|
|
7af0e6bda1 | ||
|
|
f7247dc99d | ||
|
|
d86c89e193 | ||
|
|
e484f4760f | ||
|
|
70bcd1fb7b | ||
|
|
6f407ef308 | ||
|
|
feab3ba70f | ||
|
|
00e7ee532e | ||
|
|
fe49c78f32 | ||
|
|
3c41349fe1 | ||
|
|
bd708068ab | ||
|
|
783670b84e | ||
|
|
03753ca201 | ||
|
|
456925b604 | ||
|
|
f39f416c5d | ||
|
|
d1e44d16e7 | ||
|
|
2ab8d12fe3 | ||
|
|
c9282f9e94 | ||
|
|
bcfa6264f1 | ||
|
|
204db4d1e2 | ||
|
|
fe7d89f033 | ||
|
|
c765fa6d04 | ||
|
|
f1c4e2c032 | ||
|
|
a92e397011 | ||
|
|
b6125d9a13 | ||
|
|
66ba3c3aa4 | ||
|
|
7ee2db23df | ||
|
|
52c67af63c | ||
|
|
8bcf88ec12 | ||
|
|
daca618012 | ||
|
|
f3b359f5b8 | ||
|
|
3fc917a261 | ||
|
|
814ea9d62c | ||
|
|
630cca2fba | ||
|
|
bfd2563b3a | ||
|
|
60b8339166 | ||
|
|
34f488757f | ||
|
|
cccb2cc92b | ||
|
|
d7d2249d99 | ||
|
|
a280e43949 | ||
|
|
f0533194ed | ||
|
|
a9b44c4529 | ||
|
|
e02cb6d7ce | ||
|
|
314d4afa57 | ||
|
|
25371ddbfd | ||
|
|
80225ce72c | ||
|
|
4242bf6262 | ||
|
|
dcefb287fc | ||
|
|
2cf422733c | ||
|
|
c0a51f5662 | ||
|
|
31e6fe8f52 | ||
|
|
c3aee4b1e6 | ||
|
|
581b463b60 | ||
|
|
90be44c812 | ||
|
|
80cabca21a | ||
|
|
cac82e71d8 | ||
|
|
6e2bbe88b1 | ||
|
|
d9a2e70155 | ||
|
|
7dfdfa5813 | ||
|
|
7d4ac14a31 | ||
|
|
731776702d | ||
|
|
0baf5e1499 | ||
|
|
83c508eea3 | ||
|
|
78ac1bf5d1 | ||
|
|
02da278894 | ||
|
|
1125786e78 | ||
|
|
08d83a5c6a | ||
|
|
0ab85e7a9c | ||
|
|
47a2a77cb4 | ||
|
|
21f1c2b03f | ||
|
|
8c69d5c939 | ||
|
|
f2300fbab2 | ||
|
|
45852386e5 | ||
|
|
5462697924 | ||
|
|
639c592f31 | ||
|
|
f7caa56a6b | ||
|
|
3aa4fb62d6 | ||
|
|
c734a2d8d5 | ||
|
|
44a3db3dc6 | ||
|
|
1b5f898dc5 | ||
|
|
83b241b449 | ||
|
|
24ac923938 | ||
|
|
333ce9849d | ||
|
|
417b54abed | ||
|
|
144d90932e | ||
|
|
a542ed48a2 | ||
|
|
58ac4faf0c | ||
|
|
afb1778294 | ||
|
|
ebeba79be3 | ||
|
|
6165939b0d | ||
|
|
efe03400d8 | ||
|
|
c9ab421398 | ||
|
|
147bb8aea5 | ||
|
|
7cdefce656 | ||
|
|
4d31ea8316 | ||
|
|
bb750a7945 | ||
|
|
92f6ab1881 | ||
|
|
809c36e1f6 | ||
|
|
23bc9815c4 | ||
|
|
ae234786ea | ||
|
|
99c8f2d403 | ||
|
|
61f418a267 | ||
|
|
9b58d6eaca | ||
|
|
839c936153 | ||
|
|
7d797b7dbf | ||
|
|
9b755f6ec6 | ||
|
|
90788defb2 | ||
|
|
6a02cdbb35 | ||
|
|
c74103f395 | ||
|
|
794fd5658c | ||
|
|
fab9b993f8 | ||
|
|
5818e65cf3 | ||
|
|
2a130daae6 | ||
|
|
0c1c2580d0 | ||
|
|
74b54ac0ec | ||
|
|
2c730743f1 | ||
|
|
916d272c44 | ||
|
|
eabe3eed6b | ||
|
|
fa56114115 | ||
|
|
d027f760c0 | ||
|
|
3373e02eae | ||
|
|
9f85584656 | ||
|
|
de8607deb2 | ||
|
|
e8a1b36c82 | ||
|
|
6cfe087261 | ||
|
|
8b57aaf944 | ||
|
|
d58bc14645 | ||
|
|
e368fb4b29 | ||
|
|
a122ae85e9 | ||
|
|
4d947077bb | ||
|
|
e5021dc9dc | ||
|
|
42a5d6bdfa | ||
|
|
7684b3af7b | ||
|
|
78194093af | ||
|
|
be5db6fa22 | ||
|
|
0baed781fe | ||
|
|
337f891d78 | ||
|
|
5482dfe0f3 | ||
|
|
75ec893d75 | ||
|
|
76df77418d | ||
|
|
55b891ddd0 | ||
|
|
aad4946fb6 | ||
|
|
9062fbb9cc | ||
|
|
dc6890709e | ||
|
|
272aba98e2 | ||
|
|
6c9011c12f | ||
|
|
fa15ae7545 | ||
|
|
5056d8cbe8 | ||
|
|
4a9348324d | ||
|
|
5e52a4dda4 | ||
|
|
92b49d246e | ||
|
|
3c07072bfd | ||
|
|
22864b75ee | ||
|
|
d1ea7c8cc8 | ||
|
|
1e0cf5ce4d | ||
|
|
581857aab6 | ||
|
|
841f731707 | ||
|
|
272afa9422 | ||
|
|
bddb922f7b | ||
|
|
de09023e45 | ||
|
|
e24081bf76 | ||
|
|
0c94f517a1 | ||
|
|
26e50f1162 | ||
|
|
0f3d7acdc4 | ||
|
|
8b598f00e9 | ||
|
|
6ba3475448 | ||
|
|
44ad369c17 | ||
|
|
82785fcd40 | ||
|
|
a7643a5fbe | ||
|
|
f1900bbea6 | ||
|
|
abd6ad3751 | ||
|
|
fb0b90646e | ||
|
|
da828aac05 | ||
|
|
91c6bef77a | ||
|
|
208fae5bf0 | ||
|
|
abbff681ba | ||
|
|
43662ce789 | ||
|
|
3b47cb45e6 | ||
|
|
3f30c22894 | ||
|
|
713bdc1f9f | ||
|
|
34e98bce0a | ||
|
|
0e58c3fa80 | ||
|
|
c848d0a771 | ||
|
|
88f7befabb | ||
|
|
250597d468 | ||
|
|
123289a88e | ||
|
|
6c011f43e9 | ||
|
|
e412ea1d5a | ||
|
|
d4afa1554b | ||
|
|
3494dd06fe | ||
|
|
9e9547a9e4 | ||
|
|
7930a8373d | ||
|
|
0bd8159b60 | ||
|
|
56d1858ea2 | ||
|
|
6fd0394c63 | ||
|
|
8f1450114f | ||
|
|
a9193a1531 | ||
|
|
964ccb93bb | ||
|
|
402fbe503a | ||
|
|
a63b34c685 | ||
|
|
522d59e844 | ||
|
|
669332b7e0 | ||
|
|
9c224c94f0 | ||
|
|
2edfc805af | ||
|
|
f5afd8cb54 | ||
|
|
f8fef07b4c | ||
|
|
b7fb9fac91 | ||
|
|
1f62e5b5a0 | ||
|
|
c043bbb854 | ||
|
|
929912de29 | ||
|
|
d254c6b0ae | ||
|
|
ed977513ec | ||
|
|
36eb5b36d1 | ||
|
|
b30a6d22c5 | ||
|
|
bd343ef757 | ||
|
|
5ce551e469 | ||
|
|
26ca5a702e | ||
|
|
1da1906483 | ||
|
|
9db32aea48 | ||
|
|
e31421a5d2 | ||
|
|
75740337d1 | ||
|
|
6876ba9ba6 | ||
|
|
0fab78ee8f | ||
|
|
efc54b2e56 | ||
|
|
d49e9e5562 | ||
|
|
b3c337db00 | ||
|
|
e9668d75b8 | ||
|
|
377e649e61 | ||
|
|
6ec0c3f369 | ||
|
|
ce138060ac | ||
|
|
f229b573fa | ||
|
|
28621b0510 | ||
|
|
bc94a51a96 | ||
|
|
9d00615bbf | ||
|
|
acee88a118 | ||
|
|
0061d9bd3d | ||
|
|
b629da424e | ||
|
|
4667b4decc | ||
|
|
b0c78c867d | ||
|
|
3d211da9bd | ||
|
|
1ab1962eb1 | ||
|
|
12ac3c7338 | ||
|
|
7e2f66adc3 | ||
|
|
654af0ba25 | ||
|
|
acac78adc0 | ||
|
|
3444796f3e | ||
|
|
ff492ca73f | ||
|
|
8985c0be3e | ||
|
|
3fce78498f | ||
|
|
5e96edd435 | ||
|
|
d75748ef6f | ||
|
|
c7b4b8cf6f | ||
|
|
0ac85218d1 | ||
|
|
887c097f8e | ||
|
|
01db0f1cd1 | ||
|
|
4df74a5cfb | ||
|
|
999e7c6541 | ||
|
|
dd64d823b9 | ||
|
|
dc16fe2bb9 | ||
|
|
5b260d80f9 | ||
|
|
34117be98b | ||
|
|
330a2f6784 | ||
|
|
f1faaea3fd | ||
|
|
d781b76627 | ||
|
|
81a733f2dc | ||
|
|
07ad71e851 | ||
|
|
b4fd74c6ff | ||
|
|
69f72c6f4b | ||
|
|
383fc02ba6 | ||
|
|
d217984129 | ||
|
|
45524241a5 | ||
|
|
1812387bf0 | ||
|
|
10094a3f09 | ||
|
|
1c9bd9278e | ||
|
|
28b1896e9a | ||
|
|
e1674f60e7 | ||
|
|
e572c16d3f | ||
|
|
98ac1dd34b | ||
|
|
f5d900d972 | ||
|
|
9d2149d9c0 | ||
|
|
1b259f70f3 | ||
|
|
c5675d3efc | ||
|
|
6359894fe4 | ||
|
|
7704f73db9 | ||
|
|
db922403cc | ||
|
|
bc5a8c761e | ||
|
|
3feee0c483 | ||
|
|
b9c4ecf5a8 | ||
|
|
6b135c83be |
@@ -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
|
||||
|
||||
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
17
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -2,13 +2,14 @@ Thanks for submitting a PR, your contribution is really appreciated!
|
||||
|
||||
Here's a quick checklist that should be present in PRs:
|
||||
|
||||
- [ ] Target: for bug or doc fixes, target `master`; for new features, target `features`;
|
||||
- [ ] Add a new news fragment into the changelog folder
|
||||
* name it `$issue_id.$type` for example (588.bugfix)
|
||||
* 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
|
||||
|
||||
Unless your change is trivial documentation fix (e.g., a typo or reword of a small section) please:
|
||||
Unless your change is a trivial or a documentation fix (e.g., a typo or reword of a small section) please:
|
||||
|
||||
- [ ] Make sure to include one or more tests for your change;
|
||||
- [ ] Add yourself to `AUTHORS`;
|
||||
- [ ] Add a new entry to `CHANGELOG.rst`
|
||||
* Choose any open position to avoid merge conflicts with other PRs.
|
||||
* Add a link to the issue you are fixing (if any) using RST syntax.
|
||||
* The pytest team likes to have people to acknowledged in the `CHANGELOG`, so please add a thank note to yourself ("Thanks @user for the PR") and a link to your GitHub profile. It may sound weird thanking yourself, but otherwise a maintainer would have to do it manually before or after merging instead of just using GitHub's merge button. This makes it easier on the maintainers to merge PRs.
|
||||
- [ ] Add yourself to `AUTHORS`, in alphabetical order;
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,6 +18,9 @@ include/
|
||||
*~
|
||||
.hypothesis/
|
||||
|
||||
# autogenerated
|
||||
_pytest/_version.py
|
||||
# setuptools
|
||||
.eggs/
|
||||
|
||||
doc/*/_build
|
||||
|
||||
29
.travis.yml
29
.travis.yml
@@ -1,9 +1,10 @@
|
||||
sudo: false
|
||||
language: python
|
||||
python:
|
||||
- '3.5'
|
||||
- '3.6'
|
||||
# command to install dependencies
|
||||
install: "pip install -U tox"
|
||||
install:
|
||||
- pip install --upgrade --pre tox
|
||||
# # command to run tests
|
||||
env:
|
||||
matrix:
|
||||
@@ -11,27 +12,31 @@ env:
|
||||
- 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:
|
||||
include:
|
||||
- env: TOXENV=py36
|
||||
python: '3.6-dev'
|
||||
- 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'
|
||||
allow_failures:
|
||||
|
||||
38
AUTHORS
38
AUTHORS
@@ -6,12 +6,15 @@ Contributors include::
|
||||
Abdeali JK
|
||||
Abhijeet Kasurde
|
||||
Ahn Ki-Wook
|
||||
Alexander Johnson
|
||||
Alexei Kozlenok
|
||||
Anatoly Bubenkoff
|
||||
Andras Tim
|
||||
Andreas Zeidler
|
||||
Andrzej Ostrowski
|
||||
Andy Freeland
|
||||
Anthon van der Neut
|
||||
Anthony Sottile
|
||||
Antony Lee
|
||||
Armin Rigo
|
||||
Aron Curzon
|
||||
@@ -27,6 +30,7 @@ Brianna Laugher
|
||||
Bruno Oliveira
|
||||
Cal Leeming
|
||||
Carl Friedrich Bolz
|
||||
Ceridwen
|
||||
Charles Cloud
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Chris Lamb
|
||||
@@ -43,9 +47,11 @@ Dave Hunt
|
||||
David Díaz-Barquero
|
||||
David Mohr
|
||||
David Vierra
|
||||
Daw-Ran Liou
|
||||
Denis Kirisov
|
||||
Diego Russo
|
||||
Dmitry Dygalo
|
||||
Dmitry Pribysh
|
||||
Duncan Betts
|
||||
Edison Gustavo Muenz
|
||||
Edoardo Batini
|
||||
@@ -60,6 +66,7 @@ Feng Ma
|
||||
Florian Bruhin
|
||||
Floris Bruynooghe
|
||||
Gabriel Reis
|
||||
George Kussumoto
|
||||
Georgy Dyuldin
|
||||
Graham Horler
|
||||
Greg Price
|
||||
@@ -67,6 +74,8 @@ Grig Gheorghiu
|
||||
Grigorii Eremeev (budulianin)
|
||||
Guido Wesdorp
|
||||
Harald Armin Massa
|
||||
Hugo van Kemenade
|
||||
Hui Wang (coldnight)
|
||||
Ian Bicking
|
||||
Jaap Broekhuizen
|
||||
Jan Balster
|
||||
@@ -75,39 +84,54 @@ 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
|
||||
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
|
||||
Marcin Bachry
|
||||
Mark Abramowitz
|
||||
Markus Unterwaditzer
|
||||
Martijn Faassen
|
||||
Martin Altmayer
|
||||
Martin K. Scherer
|
||||
Martin Prusse
|
||||
Mathieu Clabaut
|
||||
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
|
||||
@@ -116,6 +140,7 @@ Oliver Bestwalter
|
||||
Omar Kohl
|
||||
Omer Hadari
|
||||
Patrick Hayes
|
||||
Paweł Adamczak
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Punyashloka Biswal
|
||||
@@ -124,6 +149,7 @@ Ralf Schmitt
|
||||
Ran Benita
|
||||
Raphael Pierzina
|
||||
Raquel Alegre
|
||||
Ravi Chandra
|
||||
Roberto Polli
|
||||
Romain Dorgueil
|
||||
Roman Bolshakov
|
||||
@@ -131,21 +157,33 @@ 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
|
||||
Vasily Kuznetsov
|
||||
Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
Vitaly Lashmanov
|
||||
Vlad Dragos
|
||||
Wouter van Ackooy
|
||||
Xuan Luong
|
||||
Xuecong Liao
|
||||
Zoltán Máté
|
||||
Roland Puntaier
|
||||
|
||||
855
CHANGELOG.rst
855
CHANGELOG.rst
@@ -1,10 +1,854 @@
|
||||
..
|
||||
You should *NOT* be adding new change log entries to this file, this
|
||||
file is managed by towncrier. You *may* edit previous change logs to
|
||||
fix problems like typo corrections or such.
|
||||
To add a new change log entry, please see
|
||||
https://pip.pypa.io/en/latest/development/#adding-a-news-entry
|
||||
we named the news folder changelog
|
||||
|
||||
.. towncrier release notes start
|
||||
|
||||
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 python2 it will show the original traceback with a new
|
||||
message that explains in which plugin. In python3 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)
|
||||
=========================
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
- pytest warning capture no longer overrides existing warning filters. The
|
||||
previous behaviour would override all filters and caused regressions in test
|
||||
suites which configure warning filters to match their needs. Note that as a
|
||||
side-effect of this is that ``DeprecationWarning`` and
|
||||
``PendingDeprecationWarning`` are no longer shown by default. (#2430)
|
||||
|
||||
- Fix issue with non-ascii contents in doctest text files. (#2434)
|
||||
|
||||
- Fix encoding errors for unicode warnings in Python 2. (#2436)
|
||||
|
||||
- ``pytest.deprecated_call`` now captures ``PendingDeprecationWarning`` in
|
||||
context manager form. (#2441)
|
||||
|
||||
|
||||
Improved Documentation
|
||||
----------------------
|
||||
|
||||
- Addition of towncrier for changelog management. (#2390)
|
||||
|
||||
|
||||
3.1.0 (2017-05-22)
|
||||
==================
|
||||
|
||||
|
||||
New Features
|
||||
------------
|
||||
|
||||
* The ``pytest-warnings`` plugin has been integrated into the core and now ``pytest`` automatically
|
||||
captures and displays warnings at the end of the test session.
|
||||
|
||||
.. warning::
|
||||
|
||||
This feature may disrupt test suites which apply and treat warnings themselves, and can be
|
||||
disabled in your ``pytest.ini``:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[pytest]
|
||||
addopts = -p no:warnings
|
||||
|
||||
See the `warnings documentation page <https://docs.pytest.org/en/latest/warnings.html>`_ for more
|
||||
information.
|
||||
|
||||
Thanks `@nicoddemus`_ for the PR.
|
||||
|
||||
* Added ``junit_suite_name`` ini option to specify root ``<testsuite>`` name for JUnit XML reports (`#533`_).
|
||||
|
||||
* Added an ini option ``doctest_encoding`` to specify which encoding to use for doctest files.
|
||||
Thanks `@wheerd`_ for the PR (`#2101`_).
|
||||
|
||||
* ``pytest.warns`` now checks for subclass relationship rather than
|
||||
class equality. Thanks `@lesteve`_ for the PR (`#2166`_)
|
||||
|
||||
* ``pytest.raises`` now asserts that the error message matches a text or regex
|
||||
with the ``match`` keyword argument. Thanks `@Kriechi`_ for the PR.
|
||||
|
||||
* ``pytest.param`` can be used to declare test parameter sets with marks and test ids.
|
||||
Thanks `@RonnyPfannschmidt`_ for the PR.
|
||||
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
* remove all internal uses of pytest_namespace hooks,
|
||||
this is to prepare the removal of preloadconfig in pytest 4.0
|
||||
Thanks to `@RonnyPfannschmidt`_ for the PR.
|
||||
|
||||
* pytest now warns when a callable ids raises in a parametrized test. Thanks `@fogo`_ for the PR.
|
||||
|
||||
* It is now possible to skip test classes from being collected by setting a
|
||||
``__test__`` attribute to ``False`` in the class body (`#2007`_). Thanks
|
||||
to `@syre`_ for the report and `@lwm`_ for the PR.
|
||||
|
||||
* Change junitxml.py to produce reports that comply with Junitxml schema.
|
||||
If the same test fails with failure in call and then errors in teardown
|
||||
we split testcase element into two, one containing the error and the other
|
||||
the failure. (`#2228`_) Thanks to `@kkoukiou`_ for the PR.
|
||||
|
||||
* Testcase reports with a ``url`` attribute will now properly write this to junitxml.
|
||||
Thanks `@fushi`_ for the PR (`#1874`_).
|
||||
|
||||
* Remove common items from dict comparision output when verbosity=1. Also update
|
||||
the truncation message to make it clearer that pytest truncates all
|
||||
assertion messages if verbosity < 2 (`#1512`_).
|
||||
Thanks `@mattduck`_ for the PR
|
||||
|
||||
* ``--pdbcls`` no longer implies ``--pdb``. This makes it possible to use
|
||||
``addopts=--pdbcls=module.SomeClass`` on ``pytest.ini``. Thanks `@davidszotten`_ for
|
||||
the PR (`#1952`_).
|
||||
|
||||
* fix `#2013`_: turn RecordedWarning into ``namedtuple``,
|
||||
to give it a comprehensible repr while preventing unwarranted modification.
|
||||
|
||||
* fix `#2208`_: ensure a iteration limit for _pytest.compat.get_real_func.
|
||||
Thanks `@RonnyPfannschmidt`_ for the report and PR.
|
||||
|
||||
* Hooks are now verified after collection is complete, rather than right after loading installed plugins. This
|
||||
makes it easy to write hooks for plugins which will be loaded during collection, for example using the
|
||||
``pytest_plugins`` special variable (`#1821`_).
|
||||
Thanks `@nicoddemus`_ for the PR.
|
||||
|
||||
* Modify ``pytest_make_parametrize_id()`` hook to accept ``argname`` as an
|
||||
additional parameter.
|
||||
Thanks `@unsignedint`_ for the PR.
|
||||
|
||||
* Add ``venv`` to the default ``norecursedirs`` setting.
|
||||
Thanks `@The-Compiler`_ for the PR.
|
||||
|
||||
* ``PluginManager.import_plugin`` now accepts unicode plugin names in Python 2.
|
||||
Thanks `@reutsharabani`_ for the PR.
|
||||
|
||||
* fix `#2308`_: When using both ``--lf`` and ``--ff``, only the last failed tests are run.
|
||||
Thanks `@ojii`_ for the PR.
|
||||
|
||||
* Replace minor/patch level version numbers in the documentation with placeholders.
|
||||
This significantly reduces change-noise as different contributors regnerate
|
||||
the documentation on different platforms.
|
||||
Thanks `@RonnyPfannschmidt`_ for the PR.
|
||||
|
||||
* fix `#2391`_: consider pytest_plugins on all plugin modules
|
||||
Thanks `@RonnyPfannschmidt`_ for the PR.
|
||||
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
* Fix ``AttributeError`` on ``sys.stdout.buffer`` / ``sys.stderr.buffer``
|
||||
while using ``capsys`` fixture in python 3. (`#1407`_).
|
||||
Thanks to `@asottile`_.
|
||||
|
||||
* Change capture.py's ``DontReadFromInput`` class to throw ``io.UnsupportedOperation`` errors rather
|
||||
than ValueErrors in the ``fileno`` method (`#2276`_).
|
||||
Thanks `@metasyn`_ and `@vlad-dragos`_ for the PR.
|
||||
|
||||
* Fix exception formatting while importing modules when the exception message
|
||||
contains non-ascii characters (`#2336`_).
|
||||
Thanks `@fabioz`_ for the report and `@nicoddemus`_ for the PR.
|
||||
|
||||
* Added documentation related to issue (`#1937`_)
|
||||
Thanks `@skylarjhdownes`_ for the PR.
|
||||
|
||||
* Allow collecting files with any file extension as Python modules (`#2369`_).
|
||||
Thanks `@Kodiologist`_ for the PR.
|
||||
|
||||
* Show the correct error message when collect "parametrize" func with wrong args (`#2383`_).
|
||||
Thanks `@The-Compiler`_ for the report and `@robin0371`_ for the PR.
|
||||
|
||||
|
||||
.. _@davidszotten: https://github.com/davidszotten
|
||||
.. _@fabioz: https://github.com/fabioz
|
||||
.. _@fogo: https://github.com/fogo
|
||||
.. _@fushi: https://github.com/fushi
|
||||
.. _@Kodiologist: https://github.com/Kodiologist
|
||||
.. _@Kriechi: https://github.com/Kriechi
|
||||
.. _@mandeep: https://github.com/mandeep
|
||||
.. _@mattduck: https://github.com/mattduck
|
||||
.. _@metasyn: https://github.com/metasyn
|
||||
.. _@MichalTHEDUDE: https://github.com/MichalTHEDUDE
|
||||
.. _@ojii: https://github.com/ojii
|
||||
.. _@reutsharabani: https://github.com/reutsharabani
|
||||
.. _@robin0371: https://github.com/robin0371
|
||||
.. _@skylarjhdownes: https://github.com/skylarjhdownes
|
||||
.. _@unsignedint: https://github.com/unsignedint
|
||||
.. _@wheerd: https://github.com/wheerd
|
||||
|
||||
|
||||
.. _#1407: https://github.com/pytest-dev/pytest/issues/1407
|
||||
.. _#1512: https://github.com/pytest-dev/pytest/issues/1512
|
||||
.. _#1821: https://github.com/pytest-dev/pytest/issues/1821
|
||||
.. _#1874: https://github.com/pytest-dev/pytest/pull/1874
|
||||
.. _#1937: https://github.com/pytest-dev/pytest/issues/1937
|
||||
.. _#1952: https://github.com/pytest-dev/pytest/pull/1952
|
||||
.. _#2007: https://github.com/pytest-dev/pytest/issues/2007
|
||||
.. _#2013: https://github.com/pytest-dev/pytest/issues/2013
|
||||
.. _#2101: https://github.com/pytest-dev/pytest/pull/2101
|
||||
.. _#2166: https://github.com/pytest-dev/pytest/pull/2166
|
||||
.. _#2208: https://github.com/pytest-dev/pytest/issues/2208
|
||||
.. _#2228: https://github.com/pytest-dev/pytest/issues/2228
|
||||
.. _#2276: https://github.com/pytest-dev/pytest/issues/2276
|
||||
.. _#2308: https://github.com/pytest-dev/pytest/issues/2308
|
||||
.. _#2336: https://github.com/pytest-dev/pytest/issues/2336
|
||||
.. _#2369: https://github.com/pytest-dev/pytest/issues/2369
|
||||
.. _#2383: https://github.com/pytest-dev/pytest/issues/2383
|
||||
.. _#2391: https://github.com/pytest-dev/pytest/issues/2391
|
||||
.. _#533: https://github.com/pytest-dev/pytest/issues/533
|
||||
|
||||
|
||||
|
||||
3.0.7 (2017-03-14)
|
||||
==================
|
||||
|
||||
|
||||
* Fix issue in assertion rewriting breaking due to modules silently discarding
|
||||
other modules when importing fails
|
||||
Notably, importing the `anydbm` module is fixed. (`#2248`_).
|
||||
Notably, importing the ``anydbm`` module is fixed. (`#2248`_).
|
||||
Thanks `@pfhayes`_ for the PR.
|
||||
|
||||
* junitxml: Fix problematic case where system-out tag occured twice per testcase
|
||||
@@ -15,13 +859,13 @@
|
||||
|
||||
* Ignore exceptions raised from descriptors (e.g. properties) during Python test collection (`#2234`_).
|
||||
Thanks to `@bluetech`_.
|
||||
|
||||
|
||||
* ``--override-ini`` now correctly overrides some fundamental options like ``python_files`` (`#2238`_).
|
||||
Thanks `@sirex`_ for the report and `@nicoddemus`_ for the PR.
|
||||
|
||||
* Replace ``raise StopIteration`` usages in the code by simple ``returns`` to finish generators, in accordance to `PEP-479`_ (`#2160`_).
|
||||
Thanks `@tgoodlet`_ for the report and `@nicoddemus`_ for the PR.
|
||||
|
||||
|
||||
* Fix internal errors when an unprintable ``AssertionError`` is raised inside a test.
|
||||
Thanks `@omerhadari`_ for the PR.
|
||||
|
||||
@@ -149,6 +993,7 @@
|
||||
* Cope gracefully with a .pyc file with no matching .py file (`#2038`_). Thanks
|
||||
`@nedbat`_.
|
||||
|
||||
.. _@syre: https://github.com/syre
|
||||
.. _@adler-j: https://github.com/adler-j
|
||||
.. _@d-b-w: https://bitbucket.org/d-b-w/
|
||||
.. _@DuncanBetts: https://github.com/DuncanBetts
|
||||
@@ -256,6 +1101,7 @@
|
||||
.. _@raquel-ucl: https://github.com/raquel-ucl
|
||||
.. _@axil: https://github.com/axil
|
||||
.. _@tgoodlet: https://github.com/tgoodlet
|
||||
.. _@vlad-dragos: https://github.com/vlad-dragos
|
||||
|
||||
.. _#1853: https://github.com/pytest-dev/pytest/issues/1853
|
||||
.. _#1905: https://github.com/pytest-dev/pytest/issues/1905
|
||||
@@ -265,6 +1111,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
3.0.2 (2016-09-01)
|
||||
==================
|
||||
|
||||
@@ -1451,7 +2298,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
|
||||
@@ -206,35 +228,43 @@ but here is a simple overview:
|
||||
|
||||
#. Run all the tests
|
||||
|
||||
You need to have Python 2.7 and 3.5 available in your system. Now
|
||||
You need to have Python 2.7 and 3.6 available in your system. Now
|
||||
running tests is as simple as issuing this command::
|
||||
|
||||
$ tox -e linting,py27,py35
|
||||
$ tox -e linting,py27,py36
|
||||
|
||||
This command will run tests via the "tox" tool against Python 2.7 and 3.5
|
||||
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
|
||||
|
||||
Or to only run tests in a particular test module on Python 3.5::
|
||||
Or to only run tests in a particular test module on Python 3.6::
|
||||
|
||||
$ tox -e py35 -- testing/test_config.py
|
||||
$ tox -e py36 -- testing/test_config.py
|
||||
|
||||
#. Commit and push once your tests pass and you are happy with your change(s)::
|
||||
|
||||
$ 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::
|
||||
|
||||
|
||||
104
HOWTORELEASE.rst
104
HOWTORELEASE.rst
@@ -1,85 +1,65 @@
|
||||
How to release pytest
|
||||
--------------------------------------------
|
||||
Release Procedure
|
||||
-----------------
|
||||
|
||||
Note: this assumes you have already registered on pypi.
|
||||
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.
|
||||
|
||||
1. Bump version numbers in ``_pytest/__init__.py`` (``setup.py`` reads it).
|
||||
.. important::
|
||||
|
||||
2. Check and finalize ``CHANGELOG.rst``.
|
||||
pytest releases must be prepared on **Linux** because the docs and examples expect
|
||||
to be executed in that platform.
|
||||
|
||||
3. Write ``doc/en/announce/release-VERSION.txt`` and include
|
||||
it in ``doc/en/announce/index.txt``. Run this command to list names of authors involved::
|
||||
#. Install development dependencies in a virtual environment with::
|
||||
|
||||
git log $(git describe --abbrev=0 --tags)..HEAD --format='%aN' | sort -u
|
||||
pip3 install -r tasks/requirements.txt
|
||||
|
||||
4. Regenerate the docs examples using tox::
|
||||
#. Create a branch ``release-X.Y.Z`` with the version for the release.
|
||||
|
||||
tox -e regen
|
||||
* **patch releases**: from the latest ``master``;
|
||||
|
||||
5. At this point, open a PR named ``release-X`` so others can help find regressions or provide suggestions.
|
||||
* **minor releases**: from the latest ``features``; then merge with the latest ``master``;
|
||||
|
||||
6. Use devpi for uploading a release tarball to a staging area::
|
||||
Ensure your are in a clean work tree.
|
||||
|
||||
devpi use https://devpi.net/USER/dev
|
||||
devpi upload --formats sdist,bdist_wheel
|
||||
#. Generate docs, changelog, announcements and upload a package to
|
||||
your ``devpi`` staging server::
|
||||
|
||||
7. Run from multiple machines::
|
||||
invoke generate.pre-release <VERSION> <DEVPI USER> --password <DEVPI PASSWORD>
|
||||
|
||||
devpi use https://devpi.net/USER/dev
|
||||
devpi test pytest==VERSION
|
||||
If ``--password`` is not given, it is assumed the user is already logged in ``devpi``.
|
||||
If you don't have an account, please ask for one.
|
||||
|
||||
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``).
|
||||
#. Open a PR for this branch targeting ``master``.
|
||||
|
||||
8. Check that tests pass for relevant combinations with::
|
||||
#. Test the package
|
||||
|
||||
* **Manual method**
|
||||
|
||||
Run from multiple machines::
|
||||
|
||||
devpi use https://devpi.net/USER/dev
|
||||
devpi test pytest==VERSION
|
||||
|
||||
Check that tests pass for relevant combinations with::
|
||||
|
||||
devpi list pytest
|
||||
|
||||
or look at failures with "devpi list -f pytest".
|
||||
* **CI servers**
|
||||
|
||||
9. Feeling confident? Publish to pypi::
|
||||
Configure a repository as per-instructions on
|
||||
devpi-cloud-test_ to test the package on Travis_ and AppVeyor_.
|
||||
All test environments should pass.
|
||||
|
||||
devpi push pytest==VERSION pypi:NAME
|
||||
#. Publish to PyPI::
|
||||
|
||||
where NAME is the name of pypi.python.org as configured in your ``~/.pypirc``
|
||||
invoke generate.publish-release <VERSION> <DEVPI USER> <PYPI_NAME>
|
||||
|
||||
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>`_.
|
||||
|
||||
10. Tag the release::
|
||||
|
||||
git tag VERSION <hash>
|
||||
git push origin VERSION
|
||||
|
||||
Make sure ``<hash>`` is **exactly** the git hash at the time the package was created.
|
||||
|
||||
11. Send release announcement to mailing lists:
|
||||
|
||||
- pytest-dev@python.org
|
||||
- python-announce-list@python.org
|
||||
- testing-in-python@lists.idyll.org (only for minor/major releases)
|
||||
|
||||
And announce the release on Twitter, making sure to add the hashtag ``#pytest``.
|
||||
|
||||
12. **After the release**
|
||||
|
||||
a. **patch release (2.8.3)**:
|
||||
|
||||
1. Checkout ``master``.
|
||||
2. Update version number in ``_pytest/__init__.py`` to ``"2.8.4.dev0"``.
|
||||
3. Create a new section in ``CHANGELOG.rst`` titled ``2.8.4.dev0`` and add a few bullet points as placeholders for new entries.
|
||||
4. Commit and push.
|
||||
|
||||
b. **minor release (2.9.0)**:
|
||||
|
||||
1. Merge ``features`` into ``master``.
|
||||
2. Checkout ``master``.
|
||||
3. Follow the same steps for a **patch release** above, using the next patch release: ``2.9.1.dev0``.
|
||||
4. Commit ``master``.
|
||||
5. Checkout ``features`` and merge with ``master`` (should be a fast-forward at this point).
|
||||
6. Update version number in ``_pytest/__init__.py`` to the next minor release: ``"2.10.0.dev0"``.
|
||||
7. Create a new section in ``CHANGELOG.rst`` titled ``2.10.0.dev0``, above ``2.9.1.dev0``, and add a few bullet points as placeholders for new entries.
|
||||
8. Commit ``features``.
|
||||
9. Push ``master`` and ``features``.
|
||||
|
||||
c. **major release (3.0.0)**: same steps as that of a **minor release**
|
||||
|
||||
#. After a minor/major release, merge ``release-X.Y.Z`` into ``master`` and push (or open a PR).
|
||||
|
||||
.. _devpi-cloud-test: https://github.com/obestwalter/devpi-cloud-test
|
||||
.. _AppVeyor: https://www.appveyor.com/
|
||||
.. _Travis: https://travis-ci.org
|
||||
|
||||
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
|
||||
|
||||
36
MANIFEST.in
36
MANIFEST.in
@@ -1,36 +0,0 @@
|
||||
include CHANGELOG.rst
|
||||
include LICENSE
|
||||
include AUTHORS
|
||||
|
||||
include README.rst
|
||||
include CONTRIBUTING.rst
|
||||
include HOWTORELEASE.rst
|
||||
|
||||
include tox.ini
|
||||
include setup.py
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
21
README.rst
21
README.rst
@@ -6,13 +6,20 @@
|
||||
------
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/pytest.svg
|
||||
:target: https://pypi.python.org/pypi/pytest
|
||||
:target: https://pypi.python.org/pypi/pytest
|
||||
|
||||
.. image:: https://anaconda.org/conda-forge/pytest/badges/version.svg
|
||||
:target: https://anaconda.org/conda-forge/pytest
|
||||
|
||||
.. image:: https://img.shields.io/pypi/pyversions/pytest.svg
|
||||
:target: https://pypi.python.org/pypi/pytest
|
||||
:target: https://pypi.python.org/pypi/pytest
|
||||
|
||||
.. image:: https://img.shields.io/coveralls/pytest-dev/pytest/master.svg
|
||||
:target: https://coveralls.io/r/pytest-dev/pytest
|
||||
:target: https://coveralls.io/r/pytest-dev/pytest
|
||||
|
||||
.. image:: https://travis-ci.org/pytest-dev/pytest.svg?branch=master
|
||||
:target: https://travis-ci.org/pytest-dev/pytest
|
||||
|
||||
.. image:: https://ci.appveyor.com/api/projects/status/mrgbjaua7t33pg6b?svg=true
|
||||
:target: https://ci.appveyor.com/project/pytestbot/pytest
|
||||
|
||||
@@ -34,7 +41,7 @@ An example of a simple test:
|
||||
To execute it::
|
||||
|
||||
$ pytest
|
||||
============================= test session starts =============================
|
||||
============================= test session starts =============================
|
||||
collected 1 items
|
||||
|
||||
test_sample.py F
|
||||
@@ -69,9 +76,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
|
||||
@@ -95,7 +102,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.
|
||||
|
||||
|
||||
@@ -1,2 +1,8 @@
|
||||
#
|
||||
__version__ = '3.0.7'
|
||||
__all__ = ['__version__']
|
||||
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
except ImportError:
|
||||
# broken installation, we don't even try
|
||||
# unknown only works because we do poor mans version compare
|
||||
__version__ = 'unknown'
|
||||
|
||||
@@ -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().
|
||||
|
||||
@@ -57,26 +54,29 @@ If things do not work right away:
|
||||
which should throw a KeyError: 'COMPLINE' (which is properly set by the
|
||||
global argcomplete script).
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import sys
|
||||
import os
|
||||
from glob import glob
|
||||
|
||||
|
||||
class FastFilesCompleter:
|
||||
'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
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
""" python inspection/code generation API """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
from .code import Code # noqa
|
||||
from .code import ExceptionInfo # noqa
|
||||
from .code import Frame # noqa
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
# CHANGES:
|
||||
# - some_str is replaced, trying to create unicode strings
|
||||
#
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import types
|
||||
|
||||
|
||||
def format_exception_only(etype, value):
|
||||
"""Format the exception part of a traceback.
|
||||
|
||||
@@ -29,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__
|
||||
@@ -61,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)
|
||||
@@ -70,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,14 +1,14 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import sys
|
||||
from inspect import CO_VARARGS, CO_VARKEYWORDS
|
||||
import re
|
||||
from weakref import ref
|
||||
from _pytest.compat import _PY2, _PY3, PY35, safe_str
|
||||
|
||||
import py
|
||||
builtin_repr = repr
|
||||
|
||||
reprlib = py.builtin._tryimport('repr', 'reprlib')
|
||||
|
||||
if sys.version_info[0] >= 3:
|
||||
if _PY3:
|
||||
from traceback import format_exception_only
|
||||
else:
|
||||
from ._py2traceback import format_exception_only
|
||||
@@ -16,6 +16,7 @@ else:
|
||||
|
||||
class Code(object):
|
||||
""" wrapper around Python code objects """
|
||||
|
||||
def __init__(self, rawcode):
|
||||
if not hasattr(rawcode, "co_filename"):
|
||||
rawcode = getrawcode(rawcode)
|
||||
@@ -24,7 +25,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):
|
||||
@@ -80,6 +81,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."""
|
||||
@@ -117,7 +119,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'
|
||||
@@ -141,6 +143,7 @@ class Frame(object):
|
||||
pass # this can occur when using Psyco
|
||||
return retval
|
||||
|
||||
|
||||
class TracebackEntry(object):
|
||||
""" a single entry in a traceback """
|
||||
|
||||
@@ -166,7 +169,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):
|
||||
@@ -230,7 +233,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
|
||||
@@ -245,19 +248,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
|
||||
@@ -287,7 +292,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
|
||||
|
||||
@@ -313,7 +318,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
|
||||
@@ -328,31 +333,32 @@ 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.
|
||||
"""
|
||||
_striptext = ''
|
||||
_assert_start_repr = "AssertionError(u\'assert " if sys.version_info[0] < 3 else "AssertionError(\'assert "
|
||||
_assert_start_repr = "AssertionError(u\'assert " if _PY2 else "AssertionError(\'assert "
|
||||
|
||||
def __init__(self, tup=None, exprinfo=None):
|
||||
import _pytest._code
|
||||
@@ -403,10 +409,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
|
||||
@@ -423,7 +429,7 @@ class ExceptionInfo(object):
|
||||
)), 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):
|
||||
@@ -467,15 +473,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()))
|
||||
|
||||
@@ -511,7 +517,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)
|
||||
@@ -544,10 +550,10 @@ 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)
|
||||
@@ -573,14 +579,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))
|
||||
@@ -600,24 +606,54 @@ 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 sys.version_info[0] < 3:
|
||||
if _PY2:
|
||||
reprtraceback = self.repr_traceback(excinfo)
|
||||
reprcrash = excinfo._getreprcrash()
|
||||
|
||||
@@ -641,7 +677,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:'
|
||||
@@ -654,7 +690,7 @@ class FormattedExcinfo(object):
|
||||
class TerminalRepr(object):
|
||||
def __str__(self):
|
||||
s = self.__unicode__()
|
||||
if sys.version_info[0] < 3:
|
||||
if _PY2:
|
||||
s = s.encode('utf-8')
|
||||
return s
|
||||
|
||||
@@ -667,7 +703,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):
|
||||
@@ -711,6 +747,7 @@ class ReprExceptionInfo(ExceptionRepr):
|
||||
self.reprtraceback.toterminal(tw)
|
||||
super(ReprExceptionInfo, self).toterminal(tw)
|
||||
|
||||
|
||||
class ReprTraceback(TerminalRepr):
|
||||
entrysep = "_ "
|
||||
|
||||
@@ -726,7 +763,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)
|
||||
@@ -734,12 +771,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"
|
||||
|
||||
@@ -749,6 +788,7 @@ class ReprEntryNative(TerminalRepr):
|
||||
def toterminal(self, tw):
|
||||
tw.write("".join(self.lines))
|
||||
|
||||
|
||||
class ReprEntry(TerminalRepr):
|
||||
localssep = "_ "
|
||||
|
||||
@@ -765,7 +805,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)
|
||||
@@ -773,7 +813,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:
|
||||
@@ -786,6 +826,7 @@ class ReprEntry(TerminalRepr):
|
||||
self.reprlocals,
|
||||
self.reprfileloc)
|
||||
|
||||
|
||||
class ReprFileLocation(TerminalRepr):
|
||||
def __init__(self, path, lineno, message):
|
||||
self.path = str(path)
|
||||
@@ -802,6 +843,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
|
||||
@@ -810,6 +852,7 @@ class ReprLocals(TerminalRepr):
|
||||
for line in self.lines:
|
||||
tw.line(line)
|
||||
|
||||
|
||||
class ReprFuncArgs(TerminalRepr):
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
@@ -818,11 +861,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
|
||||
@@ -850,7 +893,7 @@ def getrawcode(obj, trycall=True):
|
||||
return obj
|
||||
|
||||
|
||||
if sys.version_info[:2] >= (3, 5): # RecursionError introduced in 3.5
|
||||
if PY35: # RecursionError introduced in 3.5
|
||||
def is_recursion_error(excinfo):
|
||||
return excinfo.errisinstance(RecursionError) # noqa
|
||||
else:
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from __future__ import generators
|
||||
from __future__ import absolute_import, division, generators, print_function
|
||||
|
||||
from bisect import bisect_right
|
||||
import sys
|
||||
import inspect, tokenize
|
||||
import six
|
||||
import inspect
|
||||
import tokenize
|
||||
import py
|
||||
cpy_compile = compile
|
||||
|
||||
@@ -19,6 +21,7 @@ class Source(object):
|
||||
possibly deindenting it.
|
||||
"""
|
||||
_compilecounter = 0
|
||||
|
||||
def __init__(self, *parts, **kwargs):
|
||||
self.lines = lines = []
|
||||
de = kwargs.get('deindent', True)
|
||||
@@ -30,7 +33,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 +76,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 +89,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 +98,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 +137,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 +147,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 +168,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 +184,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
|
||||
@@ -198,8 +202,8 @@ class Source(object):
|
||||
# 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
|
||||
@@ -208,7 +212,7 @@ def compile_(source, filename=None, mode='exec', flags=
|
||||
if _ast is not None and 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
|
||||
@@ -245,12 +249,13 @@ def getfslineno(obj):
|
||||
# helper functions
|
||||
#
|
||||
|
||||
|
||||
def findsource(obj):
|
||||
try:
|
||||
sourcelines, lineno = py.std.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 +279,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 +298,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 +320,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)
|
||||
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 +396,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 +408,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.
|
||||
"""
|
||||
|
||||
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
|
||||
@@ -1,12 +1,13 @@
|
||||
"""
|
||||
support for presenting detailed information in failing assertions.
|
||||
"""
|
||||
import py
|
||||
import os
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import sys
|
||||
import six
|
||||
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion import rewrite
|
||||
from _pytest.assertion import truncate
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -24,10 +25,6 @@ def pytest_addoption(parser):
|
||||
expression information.""")
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {'register_assert_rewrite': register_assert_rewrite}
|
||||
|
||||
|
||||
def register_assert_rewrite(*names):
|
||||
"""Register one or more module names to be rewritten on import.
|
||||
|
||||
@@ -70,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')
|
||||
@@ -100,12 +95,6 @@ def pytest_collection(session):
|
||||
assertstate.hook.set_session(session)
|
||||
|
||||
|
||||
def _running_on_ci():
|
||||
"""Check if we're currently running on a CI system."""
|
||||
env_vars = ['CI', 'BUILD_NUMBER']
|
||||
return any(var in os.environ for var in env_vars)
|
||||
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
"""Setup the pytest_assertrepr_compare hook
|
||||
|
||||
@@ -119,8 +108,8 @@ def pytest_runtest_setup(item):
|
||||
|
||||
This uses the first result from the hook and then ensures the
|
||||
following:
|
||||
* Overly verbose explanations are dropped unless -vv was used or
|
||||
running on a CI.
|
||||
* Overly verbose explanations are truncated unless configured otherwise
|
||||
(eg. if running in verbose mode).
|
||||
* Embedded newlines are escaped to help util.format_explanation()
|
||||
later.
|
||||
* If the rewrite mode is used embedded %-characters are replaced
|
||||
@@ -133,16 +122,9 @@ def pytest_runtest_setup(item):
|
||||
config=item.config, op=op, left=left, right=right)
|
||||
for new_expl in hook_result:
|
||||
if new_expl:
|
||||
if (sum(len(p) for p in new_expl[1:]) > 80*8 and
|
||||
item.config.option.verbose < 2 and
|
||||
not _running_on_ci()):
|
||||
show_max = 10
|
||||
truncated_lines = len(new_expl) - show_max
|
||||
new_expl[show_max:] = [py.builtin._totext(
|
||||
'Detailed information truncated (%d more lines)'
|
||||
', use "-vv" to show' % truncated_lines)]
|
||||
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,5 +1,5 @@
|
||||
"""Rewrite assertion AST to produce nice error messages"""
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import ast
|
||||
import _ast
|
||||
import errno
|
||||
@@ -8,10 +8,10 @@ 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 +34,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,26 +163,22 @@ 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:
|
||||
@@ -194,7 +190,7 @@ class AssertionRewritingHook(object):
|
||||
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 +210,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 +260,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 +282,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 +307,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 +320,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:
|
||||
@@ -337,7 +328,7 @@ def _rewrite_test(config, fn):
|
||||
return None, None
|
||||
rewrite_asserts(tree, fn, config)
|
||||
try:
|
||||
co = compile(tree, fn.strpath, "exec")
|
||||
co = compile(tree, fn.strpath, "exec", dont_inherit=True)
|
||||
except SyntaxError:
|
||||
# It's possible that this error is from some bug in the
|
||||
# assertion rewriting, but I don't know of a fast way to tell.
|
||||
@@ -345,6 +336,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 +350,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 +401,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 +423,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 +485,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 +529,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 +538,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 +591,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 +636,9 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
not isinstance(field, ast.expr)):
|
||||
nodes.append(field)
|
||||
|
||||
def is_rewrite_disabled(self, 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 +722,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 +731,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 +795,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 +847,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 +901,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)
|
||||
|
||||
102
_pytest/assertion/truncate.py
Normal file
102
_pytest/assertion/truncate.py
Normal file
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Utilities for truncating assertion output.
|
||||
|
||||
Current default behaviour is to truncate assertion explanations at
|
||||
~8 terminal lines, unless running in "-vv" mode or running on CI.
|
||||
"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
|
||||
DEFAULT_MAX_LINES = 8
|
||||
DEFAULT_MAX_CHARS = 8 * 80
|
||||
USAGE_MSG = "use '-vv' to show"
|
||||
|
||||
|
||||
def truncate_if_required(explanation, item, max_length=None):
|
||||
"""
|
||||
Truncate this assertion explanation if the given test item is eligible.
|
||||
"""
|
||||
if _should_truncate_item(item):
|
||||
return _truncate_explanation(explanation)
|
||||
return explanation
|
||||
|
||||
|
||||
def _should_truncate_item(item):
|
||||
"""
|
||||
Whether or not this test item is eligible for truncation.
|
||||
"""
|
||||
verbose = item.config.option.verbose
|
||||
return verbose < 2 and not _running_on_ci()
|
||||
|
||||
|
||||
def _running_on_ci():
|
||||
"""Check if we're currently running on a CI system."""
|
||||
env_vars = ['CI', 'BUILD_NUMBER']
|
||||
return any(var in os.environ for var in env_vars)
|
||||
|
||||
|
||||
def _truncate_explanation(input_lines, max_lines=None, max_chars=None):
|
||||
"""
|
||||
Truncate given list of strings that makes up the assertion explanation.
|
||||
|
||||
Truncates to either 8 lines, or 640 characters - whichever the input reaches
|
||||
first. The remaining lines will be replaced by a usage message.
|
||||
"""
|
||||
|
||||
if max_lines is None:
|
||||
max_lines = DEFAULT_MAX_LINES
|
||||
if max_chars is None:
|
||||
max_chars = DEFAULT_MAX_CHARS
|
||||
|
||||
# Check if truncation required
|
||||
input_char_count = len("".join(input_lines))
|
||||
if len(input_lines) <= max_lines and input_char_count <= max_chars:
|
||||
return input_lines
|
||||
|
||||
# Truncate first to max_lines, and then truncate to max_chars if max_chars
|
||||
# is exceeded.
|
||||
truncated_explanation = input_lines[:max_lines]
|
||||
truncated_explanation = _truncate_by_char_count(truncated_explanation, max_chars)
|
||||
|
||||
# Add ellipsis to final line
|
||||
truncated_explanation[-1] = truncated_explanation[-1] + "..."
|
||||
|
||||
# Append useful message to explanation
|
||||
truncated_line_count = len(input_lines) - len(truncated_explanation)
|
||||
truncated_line_count += 1 # Account for the part-truncated final line
|
||||
msg = '...Full output truncated'
|
||||
if truncated_line_count == 1:
|
||||
msg += ' ({0} line hidden)'.format(truncated_line_count)
|
||||
else:
|
||||
msg += ' ({0} lines hidden)'.format(truncated_line_count)
|
||||
msg += ", {0}" .format(USAGE_MSG)
|
||||
truncated_explanation.extend([
|
||||
six.text_type(""),
|
||||
six.text_type(msg),
|
||||
])
|
||||
return truncated_explanation
|
||||
|
||||
|
||||
def _truncate_by_char_count(input_lines, max_chars):
|
||||
# Check if truncation required
|
||||
if len("".join(input_lines)) <= max_chars:
|
||||
return input_lines
|
||||
|
||||
# Find point at which input length exceeds total allowed length
|
||||
iterated_char_count = 0
|
||||
for iterated_index, input_line in enumerate(input_lines):
|
||||
if iterated_char_count + len(input_line) > max_chars:
|
||||
break
|
||||
iterated_char_count += len(input_line)
|
||||
|
||||
# Create truncated explanation with modified final line
|
||||
truncated_result = input_lines[:iterated_index]
|
||||
final_line = input_lines[iterated_index]
|
||||
if final_line:
|
||||
final_line_truncate_point = max_chars - iterated_char_count
|
||||
final_line = final_line[:final_line_truncate_point]
|
||||
truncated_result.append(final_line)
|
||||
return truncated_result
|
||||
@@ -1,15 +1,17 @@
|
||||
"""Utilities for assertion debugging"""
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import pprint
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
import six
|
||||
try:
|
||||
from collections import Sequence
|
||||
except ImportError:
|
||||
Sequence = list
|
||||
|
||||
BuiltinAssertionError = py.builtin.builtins.AssertionError
|
||||
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
|
||||
@@ -52,11 +54,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
|
||||
|
||||
|
||||
@@ -81,7 +83,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()
|
||||
@@ -90,7 +92,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
|
||||
|
||||
@@ -105,16 +107,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, (list, tuple, 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:
|
||||
@@ -167,9 +175,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
|
||||
@@ -256,8 +264,8 @@ def _compare_eq_dict(left, right, verbose=False):
|
||||
explanation = []
|
||||
common = set(left).intersection(set(right))
|
||||
same = dict((k, left[k]) for k in common if left[k] == right[k])
|
||||
if same and not verbose:
|
||||
explanation += [u('Omitting %s identical items, use -v to show') %
|
||||
if same and verbose < 2:
|
||||
explanation += [u('Omitting %s identical items, use -vv to show') %
|
||||
len(same)]
|
||||
elif same:
|
||||
explanation += [u('Common items:')]
|
||||
@@ -284,7 +292,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)]
|
||||
|
||||
@@ -4,17 +4,18 @@ merged implementation of the cache provider
|
||||
the name cache was not chosen to ensure pluggy automatically
|
||||
ignores the external pytest-cache
|
||||
"""
|
||||
|
||||
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"):
|
||||
self.trace("clearing cachedir")
|
||||
@@ -22,6 +23,16 @@ class Cache(object):
|
||||
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
|
||||
@@ -89,31 +100,31 @@ class Cache(object):
|
||||
|
||||
class LFPlugin:
|
||||
""" 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.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.getvalue("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,22 +146,24 @@ 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("failedfirst"):
|
||||
items[:] = previously_failed + previously_passed
|
||||
else:
|
||||
return
|
||||
if self.config.getvalue("lf"):
|
||||
items[:] = previously_failed
|
||||
config.hook.pytest_deselected(items=previously_passed)
|
||||
else:
|
||||
items[:] = previously_failed + previously_passed
|
||||
|
||||
def pytest_sessionfinish(self, session):
|
||||
config = self.config
|
||||
if config.getvalue("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 +184,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='.cache',
|
||||
help="cache directory path.")
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
@@ -179,7 +195,6 @@ def pytest_cmdline_main(config):
|
||||
return wrap_session(config, cacheshow)
|
||||
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_configure(config):
|
||||
config.cache = Cache(config)
|
||||
@@ -219,12 +234,12 @@ def cacheshow(config, session):
|
||||
basedir = config.cache._cachedir
|
||||
vdir = basedir.join("v")
|
||||
tw.sep("-", "cache values")
|
||||
for valpath in vdir.visit(lambda x: x.isfile()):
|
||||
for valpath in sorted(vdir.visit(lambda x: x.isfile())):
|
||||
key = valpath.relto(vdir).replace(valpath.sep, "/")
|
||||
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()
|
||||
@@ -235,8 +250,8 @@ def cacheshow(config, session):
|
||||
ddir = basedir.join("d")
|
||||
if ddir.isdir() and ddir.listdir():
|
||||
tw.sep("-", "cache directories")
|
||||
for p in basedir.join("d").visit():
|
||||
#if p.check(dir=1):
|
||||
for p in sorted(basedir.join("d").visit()):
|
||||
# if p.check(dir=1):
|
||||
# print("%s/" % p.relto(basedir))
|
||||
if p.isfile():
|
||||
key = p.relto(basedir)
|
||||
|
||||
@@ -2,18 +2,20 @@
|
||||
per-test stdout/stderr capturing mechanism.
|
||||
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
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
|
||||
|
||||
from py.io import TextIO
|
||||
unicode = py.builtin.text
|
||||
|
||||
patchsysdict = {0: 'stdin', 1: 'stdout', 2: 'stderr'}
|
||||
|
||||
@@ -32,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():
|
||||
@@ -48,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:
|
||||
"""
|
||||
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":
|
||||
@@ -70,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()
|
||||
@@ -94,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))
|
||||
@@ -121,64 +143,132 @@ 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
|
||||
|
||||
|
||||
@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:
|
||||
@@ -205,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"):
|
||||
@@ -234,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)
|
||||
|
||||
@@ -247,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
|
||||
|
||||
@@ -311,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:
|
||||
__init__ = start = done = suspend = resume = lambda *args: None
|
||||
|
||||
class FDCapture:
|
||||
""" Capture IO to/from a given os-level filedescriptor. """
|
||||
|
||||
class FDCaptureBinary:
|
||||
"""Capture IO to/from a given os-level filedescriptor.
|
||||
|
||||
snap() produces `bytes`
|
||||
"""
|
||||
|
||||
def __init__(self, targetfd, tmpfile=None):
|
||||
self.targetfd = targetfd
|
||||
@@ -357,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,
|
||||
@@ -388,11 +488,24 @@ 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 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:
|
||||
def __init__(self, fd, tmpfile=None):
|
||||
name = patchsysdict[fd]
|
||||
@@ -402,17 +515,16 @@ class SysCapture:
|
||||
if name == "stdin":
|
||||
tmpfile = DontReadFromInput()
|
||||
else:
|
||||
tmpfile = TextIO()
|
||||
tmpfile = CaptureIO()
|
||||
self.tmpfile = tmpfile
|
||||
|
||||
def start(self):
|
||||
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):
|
||||
@@ -431,6 +543,14 @@ class SysCapture:
|
||||
self._old.flush()
|
||||
|
||||
|
||||
class SysCaptureBinary(SysCapture):
|
||||
def snap(self):
|
||||
res = self.tmpfile.buffer.getvalue()
|
||||
self.tmpfile.seek(0)
|
||||
self.tmpfile.truncate()
|
||||
return res
|
||||
|
||||
|
||||
class DontReadFromInput:
|
||||
"""Temporary stub class. Ideally when stdin is accessed, the
|
||||
capturing should be turned off, with possibly all data captured
|
||||
@@ -448,7 +568,8 @@ class DontReadFromInput:
|
||||
__iter__ = read
|
||||
|
||||
def fileno(self):
|
||||
raise ValueError("redirected Stdin is pseudofile, has no fileno()")
|
||||
raise UnsupportedOperation("redirected stdin is pseudofile, "
|
||||
"has no fileno()")
|
||||
|
||||
def isatty(self):
|
||||
return False
|
||||
@@ -458,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
|
||||
@@ -489,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')
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
"""
|
||||
python version compatibility code
|
||||
"""
|
||||
import sys
|
||||
import inspect
|
||||
import types
|
||||
import re
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
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
|
||||
@@ -24,18 +25,23 @@ _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()
|
||||
|
||||
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
|
||||
@@ -57,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):
|
||||
@@ -77,55 +82,68 @@ def num_mock_patch_args(function):
|
||||
mock = sys.modules.get("mock", sys.modules.get("unittest.mock", None))
|
||||
if mock 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 is mock.DEFAULT])
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
@@ -144,19 +162,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,
|
||||
|
||||
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.
|
||||
@@ -178,8 +191,18 @@ def get_real_func(obj):
|
||||
""" gets the real function object of the (possibly) wrapped object by
|
||||
functools.wraps or functools.partial.
|
||||
"""
|
||||
while hasattr(obj, "__wrapped__"):
|
||||
obj = obj.__wrapped__
|
||||
start_obj = obj
|
||||
for i in range(100):
|
||||
new_obj = getattr(obj, '__wrapped__', None)
|
||||
if new_obj is None:
|
||||
break
|
||||
obj = new_obj
|
||||
else:
|
||||
raise ValueError(
|
||||
("could not find real function of {start}"
|
||||
"\nstopped at {current}").format(
|
||||
start=py.io.saferepr(start_obj),
|
||||
current=py.io.saferepr(obj)))
|
||||
if isinstance(obj, functools.partial):
|
||||
obj = obj.func
|
||||
return obj
|
||||
@@ -199,21 +222,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 issue214
|
||||
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
|
||||
|
||||
|
||||
@@ -237,5 +259,64 @@ else:
|
||||
try:
|
||||
return str(v)
|
||||
except UnicodeError:
|
||||
if not isinstance(v, unicode):
|
||||
v = unicode(v)
|
||||
errors = 'replace'
|
||||
return v.encode('ascii', errors)
|
||||
return v.encode('utf-8', errors)
|
||||
|
||||
|
||||
COLLECT_FAKEMODULE_ATTRIBUTES = (
|
||||
'Collector',
|
||||
'Module',
|
||||
'Generator',
|
||||
'Function',
|
||||
'Instance',
|
||||
'Session',
|
||||
'Item',
|
||||
'Class',
|
||||
'File',
|
||||
'_fillfuncargs',
|
||||
)
|
||||
|
||||
|
||||
def _setup_collect_fakemodule():
|
||||
from types import ModuleType
|
||||
import pytest
|
||||
pytest.collect = ModuleType('pytest.collect')
|
||||
pytest.collect.__all__ = [] # used for setns
|
||||
for attr in COLLECT_FAKEMODULE_ATTRIBUTES:
|
||||
setattr(pytest.collect, attr, getattr(pytest, attr))
|
||||
|
||||
|
||||
if _PY2:
|
||||
# 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
|
||||
|
||||
class CaptureIO(io.TextIOWrapper):
|
||||
def __init__(self):
|
||||
super(CaptureIO, self).__init__(
|
||||
io.BytesIO(),
|
||||
encoding='UTF-8', newline='', write_through=True,
|
||||
)
|
||||
|
||||
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.
|
||||
"""
|
||||
@property
|
||||
def funcargnames(self):
|
||||
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
|
||||
return self.fixturenames
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
""" command line options, ini-file and conftest.py processing. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import argparse
|
||||
import shlex
|
||||
import traceback
|
||||
import types
|
||||
import warnings
|
||||
|
||||
import six
|
||||
import py
|
||||
# DON't import pytest here because it causes import cycle troubles
|
||||
import sys, os
|
||||
import sys
|
||||
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")
|
||||
@@ -53,15 +56,15 @@ def main(args=None, plugins=None):
|
||||
return 4
|
||||
else:
|
||||
try:
|
||||
config.pluginmanager.check_pending()
|
||||
return config.hook.pytest_cmdline_main(config=config)
|
||||
finally:
|
||||
config._ensure_unconfigure()
|
||||
except UsageError as e:
|
||||
for msg in e.args:
|
||||
sys.stderr.write("ERROR: %s\n" %(msg,))
|
||||
sys.stderr.write("ERROR: %s\n" % (msg,))
|
||||
return 4
|
||||
|
||||
|
||||
class cmdline: # compatibility namespace
|
||||
main = staticmethod(main)
|
||||
|
||||
@@ -70,6 +73,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.
|
||||
|
||||
@@ -92,25 +101,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").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)
|
||||
@@ -118,6 +120,7 @@ def get_config():
|
||||
pluginmanager.import_plugin(spec)
|
||||
return config
|
||||
|
||||
|
||||
def get_plugin_manager():
|
||||
"""
|
||||
Obtain a new instance of the
|
||||
@@ -129,6 +132,7 @@ def get_plugin_manager():
|
||||
"""
|
||||
return get_config().pluginmanager
|
||||
|
||||
|
||||
def _prepareconfig(args=None, plugins=None):
|
||||
warning = None
|
||||
if args is None:
|
||||
@@ -146,14 +150,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
|
||||
@@ -161,13 +165,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()
|
||||
@@ -198,7 +203,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)),
|
||||
@@ -227,7 +233,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_"):
|
||||
@@ -235,22 +241,19 @@ 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)
|
||||
return ret
|
||||
|
||||
def getplugin(self, name):
|
||||
@@ -265,11 +268,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 {
|
||||
@@ -293,7 +296,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
|
||||
@@ -304,7 +307,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:
|
||||
@@ -371,7 +374,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
|
||||
|
||||
@@ -381,7 +384,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)
|
||||
|
||||
@@ -395,8 +398,7 @@ class PytestPluginManager(PluginManager):
|
||||
self.import_plugin(arg)
|
||||
|
||||
def consider_conftest(self, conftestmodule):
|
||||
if self.register(conftestmodule, name=conftestmodule.__file__):
|
||||
self.consider_module(conftestmodule)
|
||||
self.register(conftestmodule, name=conftestmodule.__file__)
|
||||
|
||||
def consider_env(self):
|
||||
self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
|
||||
@@ -414,8 +416,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, str), "module name as string required, got %r" % modname
|
||||
if self.get_plugin(modname) is not None:
|
||||
assert isinstance(modname, (six.text_type, str)), "module name as text required, got %r" % modname
|
||||
modname = str(modname)
|
||||
if self.is_blocked(modname) or self.get_plugin(modname) is not None:
|
||||
return
|
||||
if modname in builtin_plugins:
|
||||
importspec = "_pytest." + modname
|
||||
@@ -425,21 +428,20 @@ 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)
|
||||
self.consider_module(mod)
|
||||
|
||||
|
||||
def _get_plugin_specs_as_list(specs):
|
||||
@@ -501,7 +503,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):
|
||||
@@ -539,7 +541,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):
|
||||
@@ -627,7 +629,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.'
|
||||
@@ -683,7 +685,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
|
||||
|
||||
@@ -767,7 +769,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
|
||||
@@ -795,9 +797,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:
|
||||
@@ -808,7 +811,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 = {}
|
||||
@@ -826,7 +829,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('-', '')):
|
||||
@@ -835,22 +838,26 @@ 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:
|
||||
def __repr__(self):
|
||||
return "<NOTSET>"
|
||||
@@ -860,6 +867,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. """
|
||||
|
||||
@@ -910,11 +929,11 @@ class Config(object):
|
||||
fin = self._cleanup.pop()
|
||||
fin()
|
||||
|
||||
def warn(self, code, message, fslocation=None):
|
||||
def warn(self, code, message, fslocation=None, nodeid=None):
|
||||
""" generate a warning for this test session. """
|
||||
self.hook.pytest_logwarning.call_historic(kwargs=dict(
|
||||
code=code, message=message,
|
||||
fslocation=fslocation, nodeid=None))
|
||||
fslocation=fslocation, nodeid=nodeid))
|
||||
|
||||
def get_terminal_writer(self):
|
||||
return self.pluginmanager.get_plugin("terminalreporter")._tw
|
||||
@@ -930,14 +949,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):
|
||||
@@ -980,11 +999,11 @@ class Config(object):
|
||||
self._parser.addini('minversion', 'minimally required pytest version')
|
||||
self._override_ini = ns.override_ini or ()
|
||||
|
||||
def _consider_importhook(self, args, entrypoint_name):
|
||||
"""Install the PEP 302 import hook if using assertion re-writing.
|
||||
def _consider_importhook(self, args):
|
||||
"""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)
|
||||
@@ -995,26 +1014,34 @@ class Config(object):
|
||||
except SystemError:
|
||||
mode = 'plain'
|
||||
else:
|
||||
import pkg_resources
|
||||
self.pluginmanager.rewrite_hook = hook
|
||||
for entrypoint in pkg_resources.iter_entry_points('pytest11'):
|
||||
# 'RECORD' available for plugins installed normally (pip install)
|
||||
# 'SOURCES.txt' available for plugins installed in dev mode (pip install -e)
|
||||
# for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa
|
||||
# so it shouldn't be an issue
|
||||
for metadata in ('RECORD', 'SOURCES.txt'):
|
||||
for entry in entrypoint.dist._get_metadata(metadata):
|
||||
fn = entry.split(',')[0]
|
||||
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)
|
||||
self._mark_plugins_for_rewrite(hook)
|
||||
self._warn_about_missing_assertion(mode)
|
||||
|
||||
def _mark_plugins_for_rewrite(self, hook):
|
||||
"""
|
||||
Given an importhook, mark for rewrite any top-level
|
||||
modules or packages in the distribution package for
|
||||
all pytest plugins.
|
||||
"""
|
||||
import pkg_resources
|
||||
self.pluginmanager.rewrite_hook = hook
|
||||
|
||||
# 'RECORD' available for plugins installed normally (pip install)
|
||||
# 'SOURCES.txt' available for plugins installed in dev mode (pip install -e)
|
||||
# for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa
|
||||
# so it shouldn't be an issue
|
||||
metadata_files = 'RECORD', 'SOURCES.txt'
|
||||
|
||||
package_files = (
|
||||
entry.split(',')[0]
|
||||
for entrypoint in pkg_resources.iter_entry_points('pytest11')
|
||||
for metadata in metadata_files
|
||||
for entry in entrypoint.dist._get_metadata(metadata)
|
||||
)
|
||||
|
||||
for name in _iter_rewritable_modules(package_files):
|
||||
hook.mark_rewrite(name)
|
||||
|
||||
def _warn_about_missing_assertion(self, mode):
|
||||
try:
|
||||
assert False
|
||||
@@ -1033,24 +1060,23 @@ 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()
|
||||
entrypoint_name = 'pytest11'
|
||||
self._consider_importhook(args, entrypoint_name)
|
||||
self._consider_importhook(args)
|
||||
self.pluginmanager.consider_preparse(args)
|
||||
self.pluginmanager.load_setuptools_entrypoints(entrypoint_name)
|
||||
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:
|
||||
@@ -1068,28 +1094,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
|
||||
@@ -1097,12 +1127,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]
|
||||
@@ -1114,7 +1144,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:
|
||||
@@ -1127,10 +1157,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":
|
||||
@@ -1147,13 +1177,13 @@ 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
|
||||
@@ -1191,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):
|
||||
@@ -1202,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,
|
||||
@@ -1311,7 +1343,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 {}
|
||||
@@ -1333,7 +1365,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)
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
""" interactive debugging with PDB, the Python Debugger. """
|
||||
from __future__ import absolute_import
|
||||
from __future__ import absolute_import, division, print_function
|
||||
import pdb
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
@@ -16,19 +14,17 @@ def pytest_addoption(parser):
|
||||
help="start a custom interactive Python debugger on errors. "
|
||||
"For example: --pdbcls=IPython.terminal.debugger:TerminalPdb")
|
||||
|
||||
def pytest_namespace():
|
||||
return {'set_trace': pytestPDB().set_trace}
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.getvalue("usepdb") or config.getvalue("usepdb_cls"):
|
||||
if config.getvalue("usepdb_cls"):
|
||||
modname, classname = config.getvalue("usepdb_cls").split(":")
|
||||
__import__(modname)
|
||||
pdb_cls = getattr(sys.modules[modname], classname)
|
||||
else:
|
||||
pdb_cls = pdb.Pdb
|
||||
|
||||
if config.getvalue("usepdb"):
|
||||
config.pluginmanager.register(PdbInvoke(), 'pdbinvoke')
|
||||
if config.getvalue("usepdb_cls"):
|
||||
modname, classname = config.getvalue("usepdb_cls").split(":")
|
||||
__import__(modname)
|
||||
pdb_cls = getattr(sys.modules[modname], classname)
|
||||
else:
|
||||
pdb_cls = pdb.Pdb
|
||||
pytestPDB._pdb_cls = pdb_cls
|
||||
|
||||
old = (pdb.set_trace, pytestPDB._pluginmanager)
|
||||
|
||||
@@ -37,44 +33,47 @@ def pytest_configure(config):
|
||||
pytestPDB._config = None
|
||||
pytestPDB._pdb_cls = pdb.Pdb
|
||||
|
||||
pdb.set_trace = pytest.set_trace
|
||||
pdb.set_trace = pytestPDB.set_trace
|
||||
pytestPDB._pluginmanager = config.pluginmanager
|
||||
pytestPDB._config = config
|
||||
pytestPDB._pdb_cls = pdb_cls
|
||||
config._cleanup.append(fin)
|
||||
|
||||
|
||||
class pytestPDB:
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
_pluginmanager = None
|
||||
_config = None
|
||||
_pdb_cls = pdb.Pdb
|
||||
|
||||
def set_trace(self):
|
||||
@classmethod
|
||||
def set_trace(cls):
|
||||
""" invoke PDB set_trace debugging, dropping any IO capturing. """
|
||||
import _pytest.config
|
||||
frame = sys._getframe().f_back
|
||||
if self._pluginmanager is not None:
|
||||
capman = self._pluginmanager.getplugin("capturemanager")
|
||||
if cls._pluginmanager is not None:
|
||||
capman = cls._pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
capman.suspendcapture(in_=True)
|
||||
tw = _pytest.config.create_terminal_writer(self._config)
|
||||
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)")
|
||||
self._pluginmanager.hook.pytest_enter_pdb(config=self._config)
|
||||
self._pdb_cls().set_trace(frame)
|
||||
cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config)
|
||||
cls._pdb_cls().set_trace(frame)
|
||||
|
||||
|
||||
class PdbInvoke:
|
||||
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)
|
||||
|
||||
def pytest_internalerror(self, excrepr, excinfo):
|
||||
for line in str(excrepr).split("\n"):
|
||||
sys.stderr.write("INTERNALERROR> %s\n" %line)
|
||||
sys.stderr.write("INTERNALERROR> %s\n" % line)
|
||||
sys.stderr.flush()
|
||||
tb = _postmortem_traceback(excinfo)
|
||||
post_mortem(tb)
|
||||
|
||||
@@ -5,10 +5,15 @@ that is planned to be removed in the next pytest release.
|
||||
Keeping it in a central location makes it easy to track what is deprecated and should
|
||||
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."
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
""" discover and run doctests in modules and test files."""
|
||||
from __future__ import absolute_import
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import traceback
|
||||
|
||||
@@ -22,38 +22,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
|
||||
@@ -118,7 +127,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 = '>>>'
|
||||
@@ -127,18 +136,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():
|
||||
@@ -171,15 +180,16 @@ class DoctestTextfile(pytest.Module):
|
||||
|
||||
# inspired by doctest.testfile; ideally we would use it directly,
|
||||
# but it doesn't support passing a custom checker
|
||||
text = self.fspath.read()
|
||||
encoding = self.config.getini("doctest_encoding")
|
||||
text = self.fspath.read_text(encoding)
|
||||
filename = str(self.fspath)
|
||||
name = self.fspath.basename
|
||||
globs = {'__name__': '__main__'}
|
||||
|
||||
|
||||
optionflags = get_optionflags(self)
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
|
||||
checker=_get_checker())
|
||||
_fix_spoof_python2(runner, encoding)
|
||||
|
||||
parser = doctest.DocTestParser()
|
||||
test = parser.get_doctest(text, globs, name, filename, 0)
|
||||
@@ -215,6 +225,7 @@ class DoctestModule(pytest.Module):
|
||||
optionflags = get_optionflags(self)
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
|
||||
checker=_get_checker())
|
||||
|
||||
for test in finder.find(module, module.__name__):
|
||||
if test.examples: # skip empty doctests
|
||||
yield DoctestItem(test.name, self, runner, test)
|
||||
@@ -323,6 +334,33 @@ def _get_report_choice(key):
|
||||
DOCTEST_REPORT_CHOICE_NONE: 0,
|
||||
}[key]
|
||||
|
||||
|
||||
def _fix_spoof_python2(runner, encoding):
|
||||
"""
|
||||
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
|
||||
if not _PY2:
|
||||
return
|
||||
|
||||
from doctest import _SpoofOut
|
||||
|
||||
class UnicodeSpoof(_SpoofOut):
|
||||
|
||||
def getvalue(self):
|
||||
result = _SpoofOut.getvalue(self)
|
||||
if encoding:
|
||||
result = result.decode(encoding)
|
||||
return result
|
||||
|
||||
runner._fakeout = UnicodeSpoof()
|
||||
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def doctest_namespace():
|
||||
"""
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import sys
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import sys
|
||||
import warnings
|
||||
from collections import OrderedDict
|
||||
|
||||
import attr
|
||||
import py
|
||||
from py._code.code import FormattedExcinfo
|
||||
|
||||
import py
|
||||
import pytest
|
||||
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,9 +19,20 @@ from _pytest.compat import (
|
||||
is_generator, isclass, getimfunc,
|
||||
getlocation, getfuncargnames,
|
||||
safe_getattr,
|
||||
FuncargnamesCompatAttr,
|
||||
)
|
||||
from _pytest.outcomes import fail, TEST_OUTCOME
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
import _pytest.python
|
||||
|
||||
scopename2class.update({
|
||||
'class': _pytest.python.Class,
|
||||
'module': _pytest.python.Module,
|
||||
'function': _pytest.main.Item,
|
||||
'session': _pytest.main.Session,
|
||||
})
|
||||
session._fixturemanager = FixtureManager(session)
|
||||
|
||||
|
||||
@@ -30,6 +45,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__
|
||||
@@ -44,24 +60,9 @@ def scopeproperty(name=None, doc=None):
|
||||
return decoratescope
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
scopename2class.update({
|
||||
'class': pytest.Class,
|
||||
'module': pytest.Module,
|
||||
'function': pytest.Item,
|
||||
})
|
||||
return {
|
||||
'fixture': fixture,
|
||||
'yield_fixture': yield_fixture,
|
||||
'collect': {'_fillfuncargs': fillfixtures}
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@@ -74,7 +75,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 = {}
|
||||
@@ -104,34 +105,32 @@ def add_funcarg_pseudo_fixture_def(collector, metafunc, fixturemanager):
|
||||
if scope != "function":
|
||||
node = get_scope_node(collector, scope)
|
||||
if node is None:
|
||||
assert scope == "class" and isinstance(collector, pytest.Module)
|
||||
assert scope == "class" and isinstance(collector, _pytest.python.Module)
|
||||
# use module-level collector for class-scope (for now)
|
||||
node = collector
|
||||
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. """
|
||||
@@ -141,10 +140,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
|
||||
@@ -166,20 +165,21 @@ def reorder_items(items):
|
||||
for scopenum in range(0, scopenum_function):
|
||||
argkeys_cache[scopenum] = d = {}
|
||||
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)
|
||||
|
||||
|
||||
def reorder_items_atscope(items, ignore, argkeys_cache, 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])
|
||||
slice_items(items, ignore, argkeys_cache[scopenum])
|
||||
items_before = reorder_items_atscope(
|
||||
items_before, ignore, argkeys_cache,scopenum+1)
|
||||
items_before, ignore, argkeys_cache, scopenum + 1)
|
||||
if items_same is None:
|
||||
# nothing to reorder in this scope
|
||||
assert items_other is None
|
||||
@@ -200,9 +200,9 @@ def slice_items(items, ignore, scoped_argkeys_cache):
|
||||
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()
|
||||
newargkeys = OrderedDict.fromkeys(k for k in argkeys if k not in ignore)
|
||||
if newargkeys: # found a slicing key
|
||||
slicing_argkey, _ = newargkeys.popitem()
|
||||
items_before = items[:i]
|
||||
items_same = [item]
|
||||
items_other = []
|
||||
@@ -210,7 +210,7 @@ def slice_items(items, ignore, scoped_argkeys_cache):
|
||||
for item in it:
|
||||
argkeys = scoped_argkeys_cache.get(item)
|
||||
if argkeys and slicing_argkey in argkeys and \
|
||||
slicing_argkey not in ignore:
|
||||
slicing_argkey not in ignore:
|
||||
items_same.append(item)
|
||||
else:
|
||||
items_other.append(item)
|
||||
@@ -220,17 +220,6 @@ def slice_items(items, ignore, scoped_argkeys_cache):
|
||||
return items, None, None, None
|
||||
|
||||
|
||||
|
||||
class FuncargnamesCompatAttr:
|
||||
""" helper class so that Metafunc, Function and FixtureRequest
|
||||
don't need to each define the "funcargnames" compatibility attribute.
|
||||
"""
|
||||
@property
|
||||
def funcargnames(self):
|
||||
""" alias attribute for ``fixturenames`` for pre-2.3 compatibility"""
|
||||
return self.fixturenames
|
||||
|
||||
|
||||
def fillfixtures(function):
|
||||
""" fill missing funcargs for a test function. """
|
||||
try:
|
||||
@@ -253,10 +242,10 @@ def fillfixtures(function):
|
||||
request._fillfixtures()
|
||||
|
||||
|
||||
|
||||
def get_direct_param_fixture_func(request):
|
||||
return request.param
|
||||
|
||||
|
||||
class FuncFixtureInfo:
|
||||
def __init__(self, argnames, names_closure, name2fixturedefs):
|
||||
self.argnames = argnames
|
||||
@@ -295,7 +284,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:
|
||||
@@ -317,7 +305,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. """
|
||||
@@ -326,7 +313,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
@scopeproperty("class")
|
||||
def cls(self):
|
||||
""" class (can be None) where the test function was collected. """
|
||||
clscol = self._pyfuncitem.getparent(pytest.Class)
|
||||
clscol = self._pyfuncitem.getparent(_pytest.python.Class)
|
||||
if clscol:
|
||||
return clscol.obj
|
||||
|
||||
@@ -344,7 +331,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
@scopeproperty()
|
||||
def module(self):
|
||||
""" python module object where the test function was collected. """
|
||||
return self._pyfuncitem.getparent(pytest.Module).obj
|
||||
return self._pyfuncitem.getparent(_pytest.python.Module).obj
|
||||
|
||||
@scopeproperty()
|
||||
def fspath(self):
|
||||
@@ -413,7 +400,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:
|
||||
@@ -444,7 +431,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):
|
||||
@@ -469,13 +457,13 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
|
||||
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):
|
||||
@@ -507,7 +495,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
source_lineno,
|
||||
)
|
||||
)
|
||||
pytest.fail(msg)
|
||||
fail(msg)
|
||||
else:
|
||||
# indices might not be set if old-style metafunc.addcall() was used
|
||||
param_index = funcitem.callspec.indices.get(argname, 0)
|
||||
@@ -530,7 +518,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
val = 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
|
||||
|
||||
@@ -540,11 +528,11 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
if scopemismatch(invoking_scope, requested_scope):
|
||||
# try to report something helpful
|
||||
lines = self._factorytraceback()
|
||||
pytest.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)
|
||||
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)
|
||||
|
||||
def _factorytraceback(self):
|
||||
lines = []
|
||||
@@ -553,7 +541,7 @@ class FixtureRequest(FuncargnamesCompatAttr):
|
||||
fs, lineno = getfslineno(factory)
|
||||
p = self._pyfuncitem.session.fspath.bestrelpath(fs)
|
||||
args = _format_args(factory)
|
||||
lines.append("%s:%d: def %s%s" %(
|
||||
lines.append("%s:%d: def %s%s" % (
|
||||
p, lineno, factory.__name__, args))
|
||||
return lines
|
||||
|
||||
@@ -565,16 +553,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
|
||||
@@ -583,9 +572,8 @@ 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_values = request._fixture_values
|
||||
self._fixture_defs = request._fixture_defs
|
||||
self._arg2fixturedefs = request._arg2fixturedefs
|
||||
self._arg2index = request._arg2index
|
||||
@@ -594,6 +582,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
|
||||
@@ -625,6 +616,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
|
||||
@@ -647,9 +639,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)
|
||||
@@ -665,7 +657,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)
|
||||
@@ -691,15 +683,16 @@ 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)
|
||||
pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location,
|
||||
pytrace=False)
|
||||
fail(msg + ":\n\n" + str(source.indent()) + "\n" + location,
|
||||
pytrace=False)
|
||||
|
||||
|
||||
def call_fixture_func(fixturefunc, request, kwargs):
|
||||
yieldctx = is_generator(fixturefunc)
|
||||
@@ -714,7 +707,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:
|
||||
@@ -724,6 +717,7 @@ def call_fixture_func(fixturefunc, request, kwargs):
|
||||
|
||||
class FixtureDef:
|
||||
""" A container for a factory definition. """
|
||||
|
||||
def __init__(self, fixturemanager, baseid, argname, func, scope, params,
|
||||
unittest=False, ids=None):
|
||||
self._fixturemanager = fixturemanager
|
||||
@@ -738,27 +732,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()
|
||||
@@ -766,7 +771,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)
|
||||
@@ -779,16 +784,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 = {}
|
||||
@@ -814,35 +820,42 @@ 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.
|
||||
|
||||
This decorator can be used (with or or without parameters) to define
|
||||
a fixture function. The name of the fixture function can later be
|
||||
This decorator can be used (with or without parameters) to define a
|
||||
fixture function. The name of the fixture function can later be
|
||||
referenced to cause its invocation ahead of running tests: test
|
||||
modules or classes can use the pytest.mark.usefixtures(fixturename)
|
||||
marker. Test functions can directly use fixture names as input
|
||||
@@ -861,25 +874,25 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
|
||||
reference is needed to activate the fixture.
|
||||
|
||||
:arg ids: list of string ids each corresponding to the params
|
||||
so that they are part of the test id. If no ids are provided
|
||||
they will be generated automatically from the params.
|
||||
so that they are part of the test id. If no ids are provided
|
||||
they will be generated automatically from the params.
|
||||
|
||||
:arg name: the name of the fixture. This defaults to the name of the
|
||||
decorated function. If a fixture is used in the same module in
|
||||
which it is defined, the function name of the fixture will be
|
||||
shadowed by the function arg that requests the fixture; one way
|
||||
to resolve this is to name the decorated function
|
||||
``fixture_<fixturename>`` and then use
|
||||
``@pytest.fixture(name='<fixturename>')``.
|
||||
decorated function. If a fixture is used in the same module in
|
||||
which it is defined, the function name of the fixture will be
|
||||
shadowed by the function arg that requests the fixture; one way
|
||||
to resolve this is to name the decorated function
|
||||
``fixture_<fixturename>`` and then use
|
||||
``@pytest.fixture(name='<fixturename>')``.
|
||||
|
||||
Fixtures can optionally provide their values to test functions using a ``yield`` statement,
|
||||
instead of ``return``. In this case, the code block after the ``yield`` statement is executed
|
||||
as teardown code regardless of the test outcome. A fixture function must yield exactly once.
|
||||
"""
|
||||
if callable(scope) and params is None and autouse == 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)
|
||||
@@ -894,7 +907,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)
|
||||
|
||||
@@ -953,14 +966,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)
|
||||
@@ -984,8 +992,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):
|
||||
@@ -995,7 +1003,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)
|
||||
@@ -1040,9 +1048,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:
|
||||
@@ -1080,7 +1093,7 @@ class FixtureManager:
|
||||
continue
|
||||
marker = defaultfuncargprefixmarker
|
||||
from _pytest import deprecated
|
||||
self.config.warn('C1', deprecated.FUNCARG_PREFIX.format(name=name))
|
||||
self.config.warn('C1', deprecated.FUNCARG_PREFIX.format(name=name), nodeid=nodeid)
|
||||
name = name[len(self._argprefix):]
|
||||
elif not isinstance(marker, FixtureFunctionMarker):
|
||||
# magic globals with __getattr__ might have got us a wrong
|
||||
@@ -1130,6 +1143,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
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
Provides a function to report all internal modules for using freezing tools
|
||||
pytest
|
||||
"""
|
||||
|
||||
def pytest_namespace():
|
||||
return {'freeze_includes': freeze_includes}
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
|
||||
def freeze_includes():
|
||||
@@ -42,4 +40,4 @@ def _iter_all_modules(package, prefix=''):
|
||||
for m in _iter_all_modules(os.path.join(path, name), prefix=name + '.'):
|
||||
yield prefix + m
|
||||
else:
|
||||
yield prefix + name
|
||||
yield prefix + name
|
||||
|
||||
@@ -1,25 +1,61 @@
|
||||
""" version info, help messages, tracing configuration. """
|
||||
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",
|
||||
action="append",
|
||||
@@ -34,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)
|
||||
@@ -51,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:
|
||||
@@ -67,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
|
||||
@@ -82,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()
|
||||
@@ -111,6 +149,7 @@ conftest_options = [
|
||||
('pytest_plugins', 'list of plugin names to load'),
|
||||
]
|
||||
|
||||
|
||||
def getpluginversioninfo(config):
|
||||
lines = []
|
||||
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
||||
@@ -122,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:
|
||||
@@ -140,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,6 +8,7 @@ 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
|
||||
@@ -16,11 +17,14 @@ def pytest_addhooks(pluginmanager):
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_namespace():
|
||||
"""return dict of name->object to be made globally available in
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_plugin_registered(plugin, manager):
|
||||
""" a new pytest plugin got registered. """
|
||||
@@ -56,11 +60,20 @@ def pytest_addoption(parser):
|
||||
via (deprecated) ``pytest.config``.
|
||||
"""
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
:arg config: pytest config object
|
||||
:type config: _pytest.config.Config
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -69,17 +82,25 @@ def pytest_configure(config):
|
||||
# discoverable conftest.py local 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` """
|
||||
|
||||
|
||||
def pytest_cmdline_preparse(config, args):
|
||||
"""(deprecated) modify command line arguments before option parsing. """
|
||||
|
||||
|
||||
@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.
|
||||
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
|
||||
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
""" implements the loading of initial conftest files ahead
|
||||
@@ -92,88 +113,124 @@ 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` """
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(session, config, items):
|
||||
""" called after collection has been performed, may filter or re-order
|
||||
the items in-place."""
|
||||
|
||||
|
||||
def pytest_collection_finish(session):
|
||||
""" called after collection has been performed and modified. """
|
||||
|
||||
|
||||
@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`
|
||||
"""
|
||||
|
||||
|
||||
@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` """
|
||||
|
||||
|
||||
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."""
|
||||
|
||||
# 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):
|
||||
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` """
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# 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` """
|
||||
|
||||
|
||||
def pytest_itemstart(item, node):
|
||||
""" (deprecated, use pytest_runtest_logstart). """
|
||||
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_runtest_protocol(item, nextitem):
|
||||
""" implements the runtest_setup/call/teardown protocol for
|
||||
@@ -187,17 +244,23 @@ 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. """
|
||||
|
||||
|
||||
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``.
|
||||
|
||||
@@ -207,12 +270,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
|
||||
@@ -222,11 +288,15 @@ 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):
|
||||
Stops at first non-None result, see :ref:`firstresult` """
|
||||
|
||||
|
||||
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."""
|
||||
@@ -235,12 +305,15 @@ def pytest_fixture_post_finalizer(fixturedef):
|
||||
# test session related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
""" before session.main() is called. """
|
||||
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
""" whole test run finishes. """
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
""" called before test process is exited. """
|
||||
|
||||
@@ -262,8 +335,12 @@ def pytest_assertrepr_compare(config, op, left, right):
|
||||
# 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 config: the pytest config object.
|
||||
:param startdir: py.path object with the starting dir
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -272,9 +349,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 config: the 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. """
|
||||
@@ -290,20 +385,26 @@ def pytest_logwarning(message, code, nodeid, fslocation):
|
||||
# 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.
|
||||
@@ -312,6 +413,7 @@ 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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
|
||||
Based on initial code from Ross Lawley.
|
||||
|
||||
Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/
|
||||
src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
||||
"""
|
||||
# Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/
|
||||
# src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import functools
|
||||
import py
|
||||
@@ -15,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
|
||||
@@ -105,6 +108,8 @@ class _NodeReporter(object):
|
||||
}
|
||||
if testreport.location[1] is not None:
|
||||
attrs["line"] = testreport.location[1]
|
||||
if hasattr(testreport, "url"):
|
||||
attrs["url"] = testreport.url
|
||||
self.attrs = attrs
|
||||
|
||||
def to_xml(self):
|
||||
@@ -222,13 +227,14 @@ def pytest_addoption(parser):
|
||||
metavar="str",
|
||||
default=None,
|
||||
help="prepend prefix to classnames in junit-xml output")
|
||||
parser.addini("junit_suite_name", "Test suite name for JUnit report", default="pytest")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
xmlpath = config.option.xmlpath
|
||||
# prevent opening xmllog on slave nodes (xdist)
|
||||
if xmlpath and not hasattr(config, 'slaveinput'):
|
||||
config._xml = LogXML(xmlpath, config.option.junitprefix)
|
||||
config._xml = LogXML(xmlpath, config.option.junitprefix, config.getini("junit_suite_name"))
|
||||
config.pluginmanager.register(config._xml)
|
||||
|
||||
|
||||
@@ -247,7 +253,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
|
||||
@@ -255,10 +261,11 @@ def mangle_test_address(address):
|
||||
|
||||
|
||||
class LogXML(object):
|
||||
def __init__(self, logfile, prefix):
|
||||
def __init__(self, logfile, prefix, suite_name="pytest"):
|
||||
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
||||
self.logfile = os.path.normpath(os.path.abspath(logfile))
|
||||
self.prefix = prefix
|
||||
self.suite_name = suite_name
|
||||
self.stats = dict.fromkeys([
|
||||
'error',
|
||||
'passed',
|
||||
@@ -268,6 +275,9 @@ class LogXML(object):
|
||||
self.node_reporters = {} # nodeid -> _NodeReporter
|
||||
self.node_reporters_ordered = []
|
||||
self.global_properties = []
|
||||
# List of reports that failed on call but teardown is pending.
|
||||
self.open_reports = []
|
||||
self.cnt_double_fail_tests = 0
|
||||
|
||||
def finalize(self, report):
|
||||
nodeid = getattr(report, 'nodeid', report)
|
||||
@@ -327,14 +337,33 @@ class LogXML(object):
|
||||
-> teardown node2
|
||||
-> teardown node1
|
||||
"""
|
||||
close_report = None
|
||||
if report.passed:
|
||||
if report.when == "call": # ignore setup/teardown
|
||||
reporter = self._opentestcase(report)
|
||||
reporter.append_pass(report)
|
||||
elif report.failed:
|
||||
if report.when == "teardown":
|
||||
# The following vars are needed when xdist plugin is used
|
||||
report_wid = getattr(report, "worker_id", None)
|
||||
report_ii = getattr(report, "item_index", None)
|
||||
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
|
||||
)
|
||||
), None)
|
||||
if close_report:
|
||||
# We need to open new testcase in case we have failure in
|
||||
# call and error in teardown in order to follow junit
|
||||
# schema
|
||||
self.finalize(close_report)
|
||||
self.cnt_double_fail_tests += 1
|
||||
reporter = self._opentestcase(report)
|
||||
if report.when == "call":
|
||||
reporter.append_failure(report)
|
||||
self.open_reports.append(report)
|
||||
else:
|
||||
reporter.append_error(report)
|
||||
elif report.skipped:
|
||||
@@ -345,6 +374,17 @@ class LogXML(object):
|
||||
reporter = self._opentestcase(report)
|
||||
reporter.write_captured_output(report)
|
||||
self.finalize(report)
|
||||
report_wid = getattr(report, "worker_id", None)
|
||||
report_ii = getattr(report, "item_index", None)
|
||||
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
|
||||
)
|
||||
), None)
|
||||
if close_report:
|
||||
self.open_reports.remove(close_report)
|
||||
|
||||
def update_testcase_duration(self, report):
|
||||
"""accumulates total duration for nodeid from given report and updates
|
||||
@@ -377,14 +417,15 @@ class LogXML(object):
|
||||
suite_stop_time = time.time()
|
||||
suite_time_delta = suite_stop_time - self.suite_start_time
|
||||
|
||||
numtests = self.stats['passed'] + self.stats['failure'] + self.stats['skipped'] + self.stats['error']
|
||||
|
||||
numtests = (self.stats['passed'] + self.stats['failure'] +
|
||||
self.stats['skipped'] + self.stats['error'] -
|
||||
self.cnt_double_fail_tests)
|
||||
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
|
||||
|
||||
logfile.write(Junit.testsuite(
|
||||
self._get_global_properties_node(),
|
||||
[x.to_xml() for x in self.node_reporters_ordered],
|
||||
name="pytest",
|
||||
name=self.suite_name,
|
||||
errors=self.stats['error'],
|
||||
failures=self.stats['failure'],
|
||||
skips=self.stats['skipped'],
|
||||
@@ -404,9 +445,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 ''
|
||||
|
||||
337
_pytest/logging.py
Normal file
337
_pytest/logging.py
Normal file
@@ -0,0 +1,337 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import logging
|
||||
from contextlib import closing, contextmanager
|
||||
import sys
|
||||
import six
|
||||
|
||||
import pytest
|
||||
import py
|
||||
|
||||
|
||||
DEFAULT_LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
|
||||
DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S'
|
||||
|
||||
|
||||
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.')
|
||||
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 logging_using_handler(handler, logger=None):
|
||||
"""Context manager that safely registers a given handler."""
|
||||
logger = logger or logging.getLogger(logger)
|
||||
|
||||
if handler in logger.handlers: # reentrancy
|
||||
# Adding the same handler twice would confuse logging system.
|
||||
# Just don't do that.
|
||||
yield
|
||||
else:
|
||||
logger.addHandler(handler)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
logger.removeHandler(handler)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def catching_logs(handler, formatter=None,
|
||||
level=logging.NOTSET, logger=None):
|
||||
"""Context manager that prepares the whole logging machinery properly."""
|
||||
logger = logger or logging.getLogger(logger)
|
||||
|
||||
if formatter is not None:
|
||||
handler.setFormatter(formatter)
|
||||
handler.setLevel(level)
|
||||
|
||||
with logging_using_handler(handler, logger):
|
||||
orig_level = logger.level
|
||||
logger.setLevel(min(orig_level, level))
|
||||
try:
|
||||
yield handler
|
||||
finally:
|
||||
logger.setLevel(orig_level)
|
||||
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
def handler(self):
|
||||
return self._item.catch_log_handler
|
||||
|
||||
@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.
|
||||
|
||||
By default, the level is set on the handler used to capture
|
||||
logs. Specify a logger name to instead set the level of any
|
||||
logger.
|
||||
"""
|
||||
if logger is None:
|
||||
logger = self.handler
|
||||
else:
|
||||
logger = logging.getLogger(logger)
|
||||
logger.setLevel(level)
|
||||
|
||||
@contextmanager
|
||||
def at_level(self, level, logger=None):
|
||||
"""Context manager that sets the level for capturing of logs.
|
||||
|
||||
By default, the level is set on the handler used to capture
|
||||
logs. Specify a logger name to instead set the level of any
|
||||
logger.
|
||||
"""
|
||||
if logger is None:
|
||||
logger = self.handler
|
||||
else:
|
||||
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
|
||||
"""
|
||||
return LogCaptureFixture(request.node)
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
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.log_cli_level = get_actual_log_level(
|
||||
config, 'log_cli_level', 'log_level') or logging.WARNING
|
||||
|
||||
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'))
|
||||
|
||||
log_cli_handler = logging.StreamHandler(sys.stderr)
|
||||
log_cli_format = get_option_ini(
|
||||
config, 'log_cli_format', 'log_format')
|
||||
log_cli_date_format = get_option_ini(
|
||||
config, 'log_cli_date_format', 'log_date_format')
|
||||
log_cli_formatter = logging.Formatter(
|
||||
log_cli_format,
|
||||
datefmt=log_cli_date_format)
|
||||
self.log_cli_handler = log_cli_handler # needed for a single unittest
|
||||
self.live_logs = catching_logs(log_cli_handler,
|
||||
formatter=log_cli_formatter,
|
||||
level=self.log_cli_level)
|
||||
|
||||
log_file = get_option_ini(config, 'log_file')
|
||||
if log_file:
|
||||
self.log_file_level = get_actual_log_level(
|
||||
config, 'log_file_level') or logging.WARNING
|
||||
|
||||
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')
|
||||
self.log_file_handler = logging.FileHandler(
|
||||
log_file,
|
||||
# Each pytest runtests session will write to a clean logfile
|
||||
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
|
||||
|
||||
@contextmanager
|
||||
def _runtest_for(self, item, when):
|
||||
"""Implements the internals of pytest_runtest_xxx() hook."""
|
||||
with catching_logs(LogCaptureHandler(),
|
||||
formatter=self.formatter) as log_handler:
|
||||
item.catch_log_handler = log_handler
|
||||
try:
|
||||
yield # run test
|
||||
finally:
|
||||
del item.catch_log_handler
|
||||
|
||||
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
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtestloop(self, session):
|
||||
"""Runs all collected test items."""
|
||||
with self.live_logs:
|
||||
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
|
||||
233
_pytest/main.py
233
_pytest/main.py
@@ -1,18 +1,22 @@
|
||||
""" core implementation of testing process: init, session, runtest loop. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import functools
|
||||
import os
|
||||
import six
|
||||
import sys
|
||||
|
||||
import _pytest
|
||||
from _pytest import nodes
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest
|
||||
try:
|
||||
from collections import MutableMapping as MappingMixin
|
||||
except ImportError:
|
||||
from UserDict import DictMixin as MappingMixin
|
||||
|
||||
from _pytest.config import directory_arg
|
||||
from _pytest.config import directory_arg, UsageError, hookimpl
|
||||
from _pytest.outcomes import exit
|
||||
from _pytest.runner import collect_one_node
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
@@ -25,63 +29,64 @@ EXIT_INTERNALERROR = 3
|
||||
EXIT_USAGEERROR = 4
|
||||
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'])
|
||||
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():
|
||||
collect = dict(Item=Item, Collector=Collector, File=File, Session=Session)
|
||||
return dict(collect=collect)
|
||||
help="base temporary directory for this test run.")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
pytest.config = config # compatibility
|
||||
__import__('pytest').config = config # compatibiltiy
|
||||
|
||||
|
||||
def wrap_session(config, doit):
|
||||
@@ -96,17 +101,18 @@ def wrap_session(config, doit):
|
||||
config.hook.pytest_sessionstart(session=session)
|
||||
initstate = 2
|
||||
session.exitstatus = doit(config, session) or 0
|
||||
except pytest.UsageError:
|
||||
except UsageError:
|
||||
raise
|
||||
except Failed:
|
||||
session.exitstatus = EXIT_TESTSFAILED
|
||||
except KeyboardInterrupt:
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
if initstate < 2 and isinstance(
|
||||
excinfo.value, pytest.exit.Exception):
|
||||
if initstate < 2 and isinstance(excinfo.value, exit.Exception):
|
||||
sys.stderr.write('{0}: {1}\n'.format(
|
||||
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
|
||||
@@ -123,9 +129,11 @@ def wrap_session(config, doit):
|
||||
config._ensure_unconfigure()
|
||||
return session.exitstatus
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
return wrap_session(config, _main)
|
||||
|
||||
|
||||
def _main(config, session):
|
||||
""" default command line protocol for initialization, session,
|
||||
running tests and reporting. """
|
||||
@@ -137,9 +145,11 @@ def _main(config, session):
|
||||
elif session.testscollected == 0:
|
||||
return EXIT_NOTESTSCOLLECTED
|
||||
|
||||
|
||||
def pytest_collection(session):
|
||||
return session.perform_collect()
|
||||
|
||||
|
||||
def pytest_runtestloop(session):
|
||||
if (session.testsfailed and
|
||||
not session.config.option.continue_on_collection_errors):
|
||||
@@ -150,21 +160,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.exists():
|
||||
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,6 +217,7 @@ class FSHookProxy:
|
||||
self.__dict__[name] = x
|
||||
return x
|
||||
|
||||
|
||||
class _CompatProperty(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
@@ -203,8 +231,7 @@ class _CompatProperty(object):
|
||||
# "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(pytest, self.name)
|
||||
|
||||
return getattr(__import__('pytest'), self.name)
|
||||
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
@@ -287,7 +314,7 @@ class Node(object):
|
||||
def _getcustomclass(self, name):
|
||||
maybe_compatprop = getattr(type(self), name)
|
||||
if isinstance(maybe_compatprop, _CompatProperty):
|
||||
return getattr(pytest, name)
|
||||
return getattr(__import__('pytest'), name)
|
||||
else:
|
||||
cls = getattr(self, name)
|
||||
# TODO: reenable in the features branch
|
||||
@@ -297,8 +324,8 @@ class Node(object):
|
||||
return cls
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %r>" %(self.__class__.__name__,
|
||||
getattr(self, 'name', None))
|
||||
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
|
||||
@@ -307,9 +334,6 @@ class Node(object):
|
||||
fslocation = getattr(self, "location", None)
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
else:
|
||||
fslocation = "%s:%s" % (fslocation[0], fslocation[1] + 1)
|
||||
|
||||
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
|
||||
code=code, message=message,
|
||||
nodeid=self.nodeid, fslocation=fslocation))
|
||||
@@ -336,24 +360,6 @@ class Node(object):
|
||||
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. """
|
||||
@@ -370,9 +376,9 @@ class Node(object):
|
||||
|
||||
``marker`` can be a string or pytest.mark.* instance.
|
||||
"""
|
||||
from _pytest.mark import MarkDecorator
|
||||
if isinstance(marker, py.builtin._basestring):
|
||||
marker = MarkDecorator(marker)
|
||||
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
|
||||
@@ -422,7 +428,7 @@ class Node(object):
|
||||
return excinfo.value.formatrepr()
|
||||
tbfilter = True
|
||||
if self.config.option.fulltrace:
|
||||
style="long"
|
||||
style = "long"
|
||||
else:
|
||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
||||
self._prunetraceback(excinfo)
|
||||
@@ -450,6 +456,7 @@ class Node(object):
|
||||
|
||||
repr_failure = _repr_failure_py
|
||||
|
||||
|
||||
class Collector(Node):
|
||||
""" Collector instances create children through collect()
|
||||
and thus iteratively build a tree.
|
||||
@@ -471,10 +478,6 @@ class Collector(Node):
|
||||
return str(exc.args[0])
|
||||
return self._repr_failure_py(excinfo, style="short")
|
||||
|
||||
def _memocollect(self):
|
||||
""" internal helper method to cache results of calling collect(). """
|
||||
return self._memoizedcall('_collected', lambda: list(self.collect()))
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
if hasattr(self, 'fspath'):
|
||||
traceback = excinfo.traceback
|
||||
@@ -483,27 +486,38 @@ class Collector(Node):
|
||||
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?
|
||||
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, "/")
|
||||
name = name.replace(os.sep, nodes.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 os.sep != "/":
|
||||
relpath = relpath.replace(os.sep, "/")
|
||||
|
||||
if not relpath:
|
||||
relpath = self._check_initialpaths_for_relpath()
|
||||
if os.sep != nodes.SEP:
|
||||
relpath = relpath.replace(os.sep, nodes.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.
|
||||
@@ -515,6 +529,21 @@ class Item(Node):
|
||||
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))
|
||||
|
||||
@@ -538,15 +567,23 @@ class Item(Node):
|
||||
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 Failed(Exception):
|
||||
""" signals an stop as failed test run. """
|
||||
|
||||
|
||||
class Session(FSCollector):
|
||||
Interrupted = Interrupted
|
||||
Failed = Failed
|
||||
|
||||
def __init__(self, config):
|
||||
FSCollector.__init__(self, config.rootdir, parent=None,
|
||||
@@ -554,6 +591,7 @@ class Session(FSCollector):
|
||||
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()
|
||||
@@ -562,18 +600,20 @@ class Session(FSCollector):
|
||||
def _makeid(self):
|
||||
return ""
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_collectstart(self):
|
||||
if self.shouldfail:
|
||||
raise self.Failed(self.shouldfail)
|
||||
if self.shouldstop:
|
||||
raise self.Interrupted(self.shouldstop)
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.failed and not hasattr(report, 'wasxfail'):
|
||||
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
|
||||
|
||||
@@ -598,8 +638,9 @@ class Session(FSCollector):
|
||||
hook = self.config.hook
|
||||
try:
|
||||
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)
|
||||
@@ -626,8 +667,8 @@ class Session(FSCollector):
|
||||
for arg, exc in self._notfound:
|
||||
line = "(no name %r in any of %r)" % (arg, exc.args[0])
|
||||
errors.append("not found: %s\n%s" % (arg, line))
|
||||
#XXX: test this
|
||||
raise pytest.UsageError(*errors)
|
||||
# XXX: test this
|
||||
raise UsageError(*errors)
|
||||
if not genitems:
|
||||
return rep.result
|
||||
else:
|
||||
@@ -655,7 +696,7 @@ class Session(FSCollector):
|
||||
names = self._parsearg(arg)
|
||||
path = names.pop(0)
|
||||
if path.check(dir=1):
|
||||
assert not names, "invalid arg %r" %(arg,)
|
||||
assert not names, "invalid arg %r" % (arg,)
|
||||
for path in path.visit(fil=lambda x: x.check(file=1),
|
||||
rec=self._recurse, bf=True, sort=True):
|
||||
for x in self._collectfile(path):
|
||||
@@ -714,9 +755,11 @@ class Session(FSCollector):
|
||||
path = self.config.invocation_dir.join(relpath, abs=True)
|
||||
if not path.check():
|
||||
if self.config.option.pyargs:
|
||||
raise pytest.UsageError("file or package not found: " + arg + " (missing __init__.py?)")
|
||||
raise UsageError(
|
||||
"file or package not found: " + arg +
|
||||
" (missing __init__.py?)")
|
||||
else:
|
||||
raise pytest.UsageError("file not found: " + arg)
|
||||
raise UsageError("file not found: " + arg)
|
||||
parts[0] = path
|
||||
return parts
|
||||
|
||||
@@ -739,11 +782,11 @@ class Session(FSCollector):
|
||||
nextnames = names[1:]
|
||||
resultnodes = []
|
||||
for node in matching:
|
||||
if isinstance(node, pytest.Item):
|
||||
if isinstance(node, Item):
|
||||
if not names:
|
||||
resultnodes.append(node)
|
||||
continue
|
||||
assert isinstance(node, pytest.Collector)
|
||||
assert isinstance(node, Collector)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
has_matched = False
|
||||
@@ -756,16 +799,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, pytest.Item):
|
||||
if isinstance(node, Item):
|
||||
node.ihook.pytest_itemcollected(item=node)
|
||||
yield node
|
||||
else:
|
||||
assert isinstance(node, pytest.Collector)
|
||||
assert isinstance(node, Collector)
|
||||
rep = collect_one_node(node)
|
||||
if rep.passed:
|
||||
for subnode in rep.result:
|
||||
|
||||
332
_pytest/mark.py
332
_pytest/mark.py
@@ -1,5 +1,97 @@
|
||||
""" generic mechanism for marking and selecting python functions. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import inspect
|
||||
import warnings
|
||||
import attr
|
||||
from collections import namedtuple
|
||||
from operator import attrgetter
|
||||
from six.moves import map
|
||||
from .deprecated import MARK_PARAMETERSET_UNPACKING
|
||||
from .compat import NOTSET, getfslineno
|
||||
|
||||
|
||||
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')):
|
||||
@classmethod
|
||||
def param(cls, *values, **kw):
|
||||
marks = kw.pop('marks', ())
|
||||
if isinstance(marks, MarkDecorator):
|
||||
marks = marks,
|
||||
else:
|
||||
assert isinstance(marks, (tuple, list, set))
|
||||
|
||||
def param_extract_id(id=None):
|
||||
return id
|
||||
|
||||
id = param_extract_id(**kw)
|
||||
return cls(values, marks, id)
|
||||
|
||||
@classmethod
|
||||
def extract_from(cls, parameterset, legacy_force_tuple=False):
|
||||
"""
|
||||
:param parameterset:
|
||||
a legacy style parameterset that may or may not be a tuple,
|
||||
and may or may not be wrapped into a mess of mark objects
|
||||
|
||||
:param legacy_force_tuple:
|
||||
enforce tuple wrapping so single argument tuple values
|
||||
don't get decomposed and break tests
|
||||
|
||||
"""
|
||||
|
||||
if isinstance(parameterset, cls):
|
||||
return parameterset
|
||||
if not isinstance(parameterset, MarkDecorator) and legacy_force_tuple:
|
||||
return cls.param(parameterset)
|
||||
|
||||
newmarks = []
|
||||
argval = parameterset
|
||||
while isinstance(argval, MarkDecorator):
|
||||
newmarks.append(MarkDecorator(Mark(
|
||||
argval.markname, argval.args[:-1], argval.kwargs)))
|
||||
argval = argval.args[-1]
|
||||
assert not isinstance(argval, ParameterSet)
|
||||
if legacy_force_tuple:
|
||||
argval = argval,
|
||||
|
||||
if newmarks:
|
||||
warnings.warn(MARK_PARAMETERSET_UNPACKING)
|
||||
|
||||
return cls(argval, marks=newmarks, id=None)
|
||||
|
||||
@classmethod
|
||||
def _for_parameterize(cls, argnames, argvalues, function):
|
||||
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:
|
||||
fs, lineno = getfslineno(function)
|
||||
reason = "got empty parameter set %r, function %s at %s:%d" % (
|
||||
argnames, function.__name__, fs, lineno)
|
||||
mark = MARK_GEN.skip(reason=reason)
|
||||
parameters.append(ParameterSet(
|
||||
values=(NOTSET,) * len(argnames),
|
||||
marks=[mark],
|
||||
id=None,
|
||||
))
|
||||
return argnames, parameters
|
||||
|
||||
|
||||
class MarkerError(Exception):
|
||||
@@ -7,8 +99,8 @@ class MarkerError(Exception):
|
||||
"""Error in use of a pytest marker/attribute."""
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {'mark': MarkGenerator()}
|
||||
def param(*values, **kw):
|
||||
return ParameterSet.param(*values, **kw)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -21,7 +113,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."
|
||||
@@ -48,7 +141,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()
|
||||
@@ -93,24 +188,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
|
||||
|
||||
@@ -123,7 +224,7 @@ class KeywordMapping:
|
||||
|
||||
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):
|
||||
@@ -162,9 +263,13 @@ def matchkeyword(colitem, keywordexpr):
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
import pytest
|
||||
config._old_mark_config = MARK_GEN._config
|
||||
if config.option.strict:
|
||||
pytest.mark._config = config
|
||||
MARK_GEN._config = config
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
MARK_GEN._config = getattr(config, '_old_mark_config', None)
|
||||
|
||||
|
||||
class MarkGenerator:
|
||||
@@ -178,13 +283,14 @@ class MarkGenerator:
|
||||
|
||||
will set a 'slowtest' :class:`MarkInfo` object
|
||||
on the ``test_function`` object. """
|
||||
_config = None
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name[0] == "_":
|
||||
raise AttributeError("Marker name must NOT start with underscore")
|
||||
if hasattr(self, '_config'):
|
||||
if self._config is not None:
|
||||
self._check(name)
|
||||
return MarkDecorator(name)
|
||||
return MarkDecorator(Mark(name, (), {}))
|
||||
|
||||
def _check(self, name):
|
||||
try:
|
||||
@@ -192,19 +298,36 @@ 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,))
|
||||
|
||||
|
||||
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>`.
|
||||
@@ -237,19 +360,33 @@ class MarkDecorator:
|
||||
additional keyword or positional arguments.
|
||||
|
||||
"""
|
||||
def __init__(self, name, args=None, kwargs=None):
|
||||
self.name = name
|
||||
self.args = args or ()
|
||||
self.kwargs = kwargs or {}
|
||||
|
||||
mark = attr.ib(validator=attr.validators.instance_of(Mark))
|
||||
|
||||
name = alias('mark.name')
|
||||
args = alias('mark.args')
|
||||
kwargs = alias('mark.kwargs')
|
||||
|
||||
@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 if isinstance(other, MarkDecorator) else False
|
||||
|
||||
def __repr__(self):
|
||||
d = self.__dict__.copy()
|
||||
name = d.pop('name')
|
||||
return "<MarkDecorator %r %r>" % (name, d)
|
||||
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.
|
||||
@@ -259,70 +396,101 @@ 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.name, self.args, self.kwargs
|
||||
)
|
||||
setattr(func, self.name, holder)
|
||||
else:
|
||||
holder.add(self.args, self.kwargs)
|
||||
store_legacy_markinfo(func, self.mark)
|
||||
store_mark(func, self.mark)
|
||||
return func
|
||||
kw = self.kwargs.copy()
|
||||
kw.update(kwargs)
|
||||
args = self.args + args
|
||||
return self.__class__(self.name, args=args, kwargs=kw)
|
||||
return self.with_args(*args, **kwargs)
|
||||
|
||||
|
||||
def extract_argvalue(maybe_marked_args):
|
||||
# TODO: incorrect mark data, the old code wanst able to collect lists
|
||||
# individual parametrized argument sets can be wrapped in a series
|
||||
# of markers in which case we unwrap the values and apply the mark
|
||||
# at Function init
|
||||
newmarks = {}
|
||||
argval = maybe_marked_args
|
||||
while isinstance(argval, MarkDecorator):
|
||||
newmark = MarkDecorator(argval.markname,
|
||||
argval.args[:-1], argval.kwargs)
|
||||
newmarks[newmark.markname] = newmark
|
||||
argval = argval.args[-1]
|
||||
return argval, newmarks
|
||||
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
|
||||
]
|
||||
|
||||
|
||||
class MarkInfo:
|
||||
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]
|
||||
|
||||
|
||||
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, name, args, kwargs):
|
||||
#: name of attribute
|
||||
self.name = name
|
||||
#: positional argument list, empty if none specified
|
||||
self.args = args
|
||||
#: keyword argument dictionary, empty if nothing specified
|
||||
self.kwargs = kwargs.copy()
|
||||
self._arglist = [(args, kwargs.copy())]
|
||||
|
||||
def __init__(self, mark):
|
||||
assert isinstance(mark, Mark), repr(mark)
|
||||
self.combined = mark
|
||||
self._marks = [mark]
|
||||
|
||||
name = alias('combined.name')
|
||||
args = alias('combined.args')
|
||||
kwargs = alias('combined.kwargs')
|
||||
|
||||
def __repr__(self):
|
||||
return "<MarkInfo %r args=%r kwargs=%r>" % (
|
||||
self.name, self.args, self.kwargs
|
||||
)
|
||||
return "<MarkInfo {0!r}>".format(self.combined)
|
||||
|
||||
def add(self, args, kwargs):
|
||||
def add_mark(self, mark):
|
||||
""" add a MarkInfo with the given args and kwargs. """
|
||||
self._arglist.append((args, kwargs))
|
||||
self.args += args
|
||||
self.kwargs.update(kwargs)
|
||||
self._marks.append(mark)
|
||||
self.combined = self.combined.combined_with(mark)
|
||||
|
||||
def __iter__(self):
|
||||
""" yield MarkInfo objects each relating to a marking-call. """
|
||||
for args, kwargs in self._arglist:
|
||||
yield MarkInfo(self.name, args, kwargs)
|
||||
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)
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
""" monkeypatching and mocking functionality. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import os, sys
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
|
||||
from py.builtin import _basestring
|
||||
|
||||
import pytest
|
||||
import six
|
||||
from _pytest.fixtures import fixture
|
||||
|
||||
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@fixture
|
||||
def monkeypatch():
|
||||
"""The returned ``monkeypatch`` fixture provides these
|
||||
helper methods to modify objects, dictionaries or os.environ::
|
||||
@@ -70,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)
|
||||
@@ -124,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")
|
||||
@@ -154,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")
|
||||
|
||||
37
_pytest/nodes.py
Normal file
37
_pytest/nodes.py
Normal file
@@ -0,0 +1,37 @@
|
||||
SEP = "/"
|
||||
|
||||
|
||||
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
|
||||
@@ -1,10 +1,10 @@
|
||||
""" run test suites written for nose. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import sys
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from _pytest import unittest
|
||||
from _pytest import unittest, runner, python
|
||||
from _pytest.config import hookimpl
|
||||
|
||||
|
||||
def get_skip_exceptions():
|
||||
@@ -19,52 +19,53 @@ def get_skip_exceptions():
|
||||
def pytest_runtest_makereport(item, call):
|
||||
if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()):
|
||||
# let's substitute the excinfo with a pytest.skip one
|
||||
call2 = call.__class__(lambda:
|
||||
pytest.skip(str(call.excinfo.value)), call.when)
|
||||
call2 = call.__class__(
|
||||
lambda: runner.skip(str(call.excinfo.value)), call.when)
|
||||
call.excinfo = call2.excinfo
|
||||
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
@hookimpl(trylast=True)
|
||||
def pytest_runtest_setup(item):
|
||||
if is_potential_nosetest(item):
|
||||
if isinstance(item.parent, pytest.Generator):
|
||||
if isinstance(item.parent, python.Generator):
|
||||
gen = item.parent
|
||||
if not hasattr(gen, '_nosegensetup'):
|
||||
call_optional(gen.obj, 'setup')
|
||||
if isinstance(gen.parent, pytest.Instance):
|
||||
if isinstance(gen.parent, python.Instance):
|
||||
call_optional(gen.parent.obj, 'setup')
|
||||
gen._nosegensetup = True
|
||||
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
|
||||
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
if isinstance(collector, pytest.Generator):
|
||||
if isinstance(collector, python.Generator):
|
||||
call_optional(collector.obj, 'setup')
|
||||
|
||||
|
||||
def is_potential_nosetest(item):
|
||||
# extra check needed since we do not do nose style setup/teardown
|
||||
# on direct unittest style classes
|
||||
return isinstance(item, pytest.Function) and \
|
||||
return isinstance(item, python.Function) and \
|
||||
not isinstance(item, unittest.TestCaseFunction)
|
||||
|
||||
|
||||
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
|
||||
@@ -1,5 +1,8 @@
|
||||
""" submit failure or test session information to a pastebin service. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import pytest
|
||||
import six
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
@@ -7,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;
|
||||
@@ -27,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)
|
||||
|
||||
@@ -95,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))
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
""" (disabled by default) support for testing pytest and pytest plugins. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import codecs
|
||||
import gc
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import six
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from fnmatch import fnmatch
|
||||
|
||||
from py.builtin import print_
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
from _pytest.capture import MultiCapture, SysCapture
|
||||
from _pytest._code import Source
|
||||
import py
|
||||
import pytest
|
||||
@@ -19,27 +23,22 @@ from _pytest.main import Session, EXIT_OK
|
||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||
|
||||
|
||||
PYTEST_FULLPATH = os.path.abspath(pytest.__file__.rstrip("oc")).replace("$py.class", ".py")
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
# group = parser.getgroup("pytester", "pytester (self-tests) options")
|
||||
parser.addoption('--lsof',
|
||||
action="store_true", dest="lsof", default=False,
|
||||
help=("run FD checks if lsof is available"))
|
||||
action="store_true", dest="lsof", default=False,
|
||||
help=("run FD checks if lsof is available"))
|
||||
|
||||
parser.addoption('--runpytest', default="inprocess", dest="runpytest",
|
||||
choices=("inprocess", "subprocess", ),
|
||||
help=("run pytest sub runs in tests using an 'inprocess' "
|
||||
"or 'subprocess' (python -m main) method"))
|
||||
choices=("inprocess", "subprocess", ),
|
||||
help=("run pytest sub runs in tests using an 'inprocess' "
|
||||
"or 'subprocess' (python -m main) method"))
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
# This might be called multiple times. Only take the first.
|
||||
global _pytest_fullpath
|
||||
try:
|
||||
_pytest_fullpath
|
||||
except NameError:
|
||||
_pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc"))
|
||||
_pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py")
|
||||
|
||||
if config.getvalue("lsof"):
|
||||
checker = LsofFdLeakChecker()
|
||||
if checker.matching_platform():
|
||||
@@ -59,7 +58,7 @@ class LsofFdLeakChecker(object):
|
||||
def _parse_lsof_output(self, out):
|
||||
def isopen(line):
|
||||
return line.startswith('f') and ("deleted" not in line and
|
||||
'mem' not in line and "txt" not in line and 'cwd' not in line)
|
||||
'mem' not in line and "txt" not in line and 'cwd' not in line)
|
||||
|
||||
open_files = []
|
||||
|
||||
@@ -85,7 +84,7 @@ class LsofFdLeakChecker(object):
|
||||
return True
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
|
||||
def pytest_runtest_item(self, item):
|
||||
def pytest_runtest_protocol(self, item):
|
||||
lines1 = self.get_open_files()
|
||||
yield
|
||||
if hasattr(sys, "pypy_version_info"):
|
||||
@@ -104,20 +103,19 @@ class LsofFdLeakChecker(object):
|
||||
error.extend([str(f) for f in lines2])
|
||||
error.append(error[0])
|
||||
error.append("*** function %s:%s: %s " % item.location)
|
||||
pytest.fail("\n".join(error), pytrace=False)
|
||||
error.append("See issue #2366")
|
||||
item.warn('', "\n".join(error))
|
||||
|
||||
|
||||
# XXX copied from execnet's conftest.py - needs to be merged
|
||||
winpymap = {
|
||||
'python2.7': r'C:\Python27\python.exe',
|
||||
'python2.6': r'C:\Python26\python.exe',
|
||||
'python3.1': r'C:\Python31\python.exe',
|
||||
'python3.2': r'C:\Python32\python.exe',
|
||||
'python3.3': r'C:\Python33\python.exe',
|
||||
'python3.4': r'C:\Python34\python.exe',
|
||||
'python3.5': r'C:\Python35\python.exe',
|
||||
'python3.6': r'C:\Python36\python.exe',
|
||||
}
|
||||
|
||||
|
||||
def getexecutable(name, cache={}):
|
||||
try:
|
||||
return cache[name]
|
||||
@@ -126,21 +124,21 @@ def getexecutable(name, cache={}):
|
||||
if executable:
|
||||
import subprocess
|
||||
popen = subprocess.Popen([str(executable), "--version"],
|
||||
universal_newlines=True, stderr=subprocess.PIPE)
|
||||
universal_newlines=True, stderr=subprocess.PIPE)
|
||||
out, err = popen.communicate()
|
||||
if name == "jython":
|
||||
if not err or "2.5" not in err:
|
||||
executable = None
|
||||
if "2.5.2" in err:
|
||||
executable = None # http://bugs.jython.org/issue1790
|
||||
executable = None # http://bugs.jython.org/issue1790
|
||||
elif popen.returncode != 0:
|
||||
# Handle pyenv's 127.
|
||||
executable = None
|
||||
cache[name] = executable
|
||||
return executable
|
||||
|
||||
@pytest.fixture(params=['python2.6', 'python2.7', 'python3.3', "python3.4",
|
||||
'pypy', 'pypy3'])
|
||||
|
||||
@pytest.fixture(params=['python2.7', 'python3.4', 'pypy', 'pypy3'])
|
||||
def anypython(request):
|
||||
name = request.param
|
||||
executable = getexecutable(name)
|
||||
@@ -155,6 +153,8 @@ def anypython(request):
|
||||
return executable
|
||||
|
||||
# used at least by pytest-xdist plugin
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def _pytest(request):
|
||||
""" Return a helper which offers a gethookrecorder(hook)
|
||||
@@ -163,6 +163,7 @@ def _pytest(request):
|
||||
"""
|
||||
return PytestArg(request)
|
||||
|
||||
|
||||
class PytestArg:
|
||||
def __init__(self, request):
|
||||
self.request = request
|
||||
@@ -173,9 +174,9 @@ class PytestArg:
|
||||
return hookrecorder
|
||||
|
||||
|
||||
def get_public_names(l):
|
||||
"""Only return names from iterator l without a leading underscore."""
|
||||
return [x for x in l if x[0] != "_"]
|
||||
def get_public_names(values):
|
||||
"""Only return names from iterator values without a leading underscore."""
|
||||
return [x for x in values if x[0] != "_"]
|
||||
|
||||
|
||||
class ParsedCall:
|
||||
@@ -186,7 +187,7 @@ class ParsedCall:
|
||||
def __repr__(self):
|
||||
d = self.__dict__.copy()
|
||||
del d['_name']
|
||||
return "<ParsedCall %r(**%r)>" %(self._name, d)
|
||||
return "<ParsedCall %r(**%r)>" % (self._name, d)
|
||||
|
||||
|
||||
class HookRecorder:
|
||||
@@ -226,15 +227,15 @@ class HookRecorder:
|
||||
name, check = entries.pop(0)
|
||||
for ind, call in enumerate(self.calls[i:]):
|
||||
if call._name == name:
|
||||
print_("NAMEMATCH", name, call)
|
||||
print("NAMEMATCH", name, call)
|
||||
if eval(check, backlocals, call.__dict__):
|
||||
print_("CHECKERMATCH", repr(check), "->", call)
|
||||
print("CHECKERMATCH", repr(check), "->", call)
|
||||
else:
|
||||
print_("NOCHECKERMATCH", repr(check), "-", call)
|
||||
print("NOCHECKERMATCH", repr(check), "-", call)
|
||||
continue
|
||||
i += ind + 1
|
||||
break
|
||||
print_("NONAMEMATCH", name, "with", call)
|
||||
print("NONAMEMATCH", name, "with", call)
|
||||
else:
|
||||
pytest.fail("could not find %r check %r" % (name, check))
|
||||
|
||||
@@ -249,9 +250,9 @@ class HookRecorder:
|
||||
pytest.fail("\n".join(lines))
|
||||
|
||||
def getcall(self, name):
|
||||
l = self.getcalls(name)
|
||||
assert len(l) == 1, (name, l)
|
||||
return l[0]
|
||||
values = self.getcalls(name)
|
||||
assert len(values) == 1, (name, values)
|
||||
return values[0]
|
||||
|
||||
# functionality for test reports
|
||||
|
||||
@@ -260,9 +261,9 @@ class HookRecorder:
|
||||
return [x.report for x in self.getcalls(names)]
|
||||
|
||||
def matchreport(self, inamepart="",
|
||||
names="pytest_runtest_logreport pytest_collectreport", when=None):
|
||||
names="pytest_runtest_logreport pytest_collectreport", when=None):
|
||||
""" return a testreport whose dotted import path matches """
|
||||
l = []
|
||||
values = []
|
||||
for rep in self.getreports(names=names):
|
||||
try:
|
||||
if not when and rep.when != "call" and rep.passed:
|
||||
@@ -273,14 +274,14 @@ class HookRecorder:
|
||||
if when and getattr(rep, 'when', None) != when:
|
||||
continue
|
||||
if not inamepart or inamepart in rep.nodeid.split("::"):
|
||||
l.append(rep)
|
||||
if not l:
|
||||
values.append(rep)
|
||||
if not values:
|
||||
raise ValueError("could not find test report matching %r: "
|
||||
"no test reports at all!" % (inamepart,))
|
||||
if len(l) > 1:
|
||||
if len(values) > 1:
|
||||
raise ValueError(
|
||||
"found 2 or more testreports matching %r: %s" %(inamepart, l))
|
||||
return l[0]
|
||||
"found 2 or more testreports matching %r: %s" % (inamepart, values))
|
||||
return values[0]
|
||||
|
||||
def getfailures(self,
|
||||
names='pytest_runtest_logreport pytest_collectreport'):
|
||||
@@ -294,7 +295,7 @@ class HookRecorder:
|
||||
skipped = []
|
||||
failed = []
|
||||
for rep in self.getreports(
|
||||
"pytest_collectreport pytest_runtest_logreport"):
|
||||
"pytest_collectreport pytest_runtest_logreport"):
|
||||
if rep.passed:
|
||||
if getattr(rep, "when", None) == "call":
|
||||
passed.append(rep)
|
||||
@@ -333,6 +334,8 @@ def testdir(request, tmpdir_factory):
|
||||
|
||||
|
||||
rex_outcome = re.compile(r"(\d+) ([\w-]+)")
|
||||
|
||||
|
||||
class RunResult:
|
||||
"""The result of running a command.
|
||||
|
||||
@@ -348,6 +351,7 @@ class RunResult:
|
||||
:duration: Duration in seconds.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, ret, outlines, errlines, duration):
|
||||
self.ret = ret
|
||||
self.outlines = outlines
|
||||
@@ -369,14 +373,17 @@ class RunResult:
|
||||
return d
|
||||
raise ValueError("Pytest terminal report not found")
|
||||
|
||||
def assert_outcomes(self, passed=0, skipped=0, failed=0):
|
||||
def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0):
|
||||
""" assert that the specified outcomes appear with the respective
|
||||
numbers (0 means it didn't occur) in the text output from a test run."""
|
||||
d = self.parseoutcomes()
|
||||
assert passed == d.get("passed", 0)
|
||||
assert skipped == d.get("skipped", 0)
|
||||
assert failed == d.get("failed", 0)
|
||||
|
||||
obtained = {
|
||||
'passed': d.get('passed', 0),
|
||||
'skipped': d.get('skipped', 0),
|
||||
'failed': d.get('failed', 0),
|
||||
'error': d.get('error', 0),
|
||||
}
|
||||
assert obtained == dict(passed=passed, skipped=skipped, failed=failed, error=error)
|
||||
|
||||
|
||||
class Testdir:
|
||||
@@ -402,20 +409,13 @@ class Testdir:
|
||||
|
||||
def __init__(self, request, tmpdir_factory):
|
||||
self.request = request
|
||||
# XXX remove duplication with tmpdir plugin
|
||||
basetmp = tmpdir_factory.ensuretemp("testdir")
|
||||
self._mod_collections = WeakKeyDictionary()
|
||||
name = request.function.__name__
|
||||
for i in range(100):
|
||||
try:
|
||||
tmpdir = basetmp.mkdir(name + str(i))
|
||||
except py.error.EEXIST:
|
||||
continue
|
||||
break
|
||||
self.tmpdir = tmpdir
|
||||
self.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
|
||||
self.plugins = []
|
||||
self._savesyspath = (list(sys.path), list(sys.meta_path))
|
||||
self._savemodulekeys = set(sys.modules)
|
||||
self.chdir() # always chdir
|
||||
self.chdir() # always chdir
|
||||
self.request.addfinalizer(self.finalize)
|
||||
method = self.request.config.getoption("--runpytest")
|
||||
if method == "inprocess":
|
||||
@@ -447,9 +447,10 @@ class Testdir:
|
||||
the module is re-imported.
|
||||
"""
|
||||
for name in set(sys.modules).difference(self._savemodulekeys):
|
||||
# zope.interface (used by twisted-related tests) keeps internal
|
||||
# state and can't be deleted
|
||||
if not name.startswith("zope.interface"):
|
||||
# some zope modules used by twisted-related tests keeps internal
|
||||
# state and can't be deleted; we had some trouble in the past
|
||||
# with zope.interface for example
|
||||
if not name.startswith("zope"):
|
||||
del sys.modules[name]
|
||||
|
||||
def make_hook_recorder(self, pluginmanager):
|
||||
@@ -469,29 +470,24 @@ class Testdir:
|
||||
if not hasattr(self, '_olddir'):
|
||||
self._olddir = old
|
||||
|
||||
def _makefile(self, ext, args, kwargs):
|
||||
def _makefile(self, ext, args, kwargs, encoding='utf-8'):
|
||||
items = list(kwargs.items())
|
||||
|
||||
def to_text(s):
|
||||
return s.decode(encoding) if isinstance(s, bytes) else six.text_type(s)
|
||||
|
||||
if args:
|
||||
source = py.builtin._totext("\n").join(
|
||||
map(py.builtin._totext, args)) + py.builtin._totext("\n")
|
||||
source = u"\n".join(to_text(x) for x in args)
|
||||
basename = self.request.function.__name__
|
||||
items.insert(0, (basename, source))
|
||||
|
||||
ret = None
|
||||
for name, value in items:
|
||||
p = self.tmpdir.join(name).new(ext=ext)
|
||||
for basename, value in items:
|
||||
p = self.tmpdir.join(basename).new(ext=ext)
|
||||
p.dirpath().ensure_dir()
|
||||
source = Source(value)
|
||||
|
||||
def my_totext(s, encoding="utf-8"):
|
||||
if py.builtin._isbytes(s):
|
||||
s = py.builtin._totext(s, encoding=encoding)
|
||||
return s
|
||||
|
||||
source_unicode = "\n".join([my_totext(line) for line in source.lines])
|
||||
source = py.builtin._totext(source_unicode)
|
||||
content = source.strip().encode("utf-8") # + "\n"
|
||||
#content = content.rstrip() + "\n"
|
||||
p.write(content, "wb")
|
||||
source = u"\n".join(to_text(line) for line in source.lines)
|
||||
p.write(source.strip().encode(encoding), "wb")
|
||||
if ret is None:
|
||||
ret = p
|
||||
return ret
|
||||
@@ -575,6 +571,7 @@ class Testdir:
|
||||
return p
|
||||
|
||||
Session = Session
|
||||
|
||||
def getnode(self, config, arg):
|
||||
"""Return the collection node of a file.
|
||||
|
||||
@@ -655,8 +652,8 @@ class Testdir:
|
||||
|
||||
"""
|
||||
p = self.makepyfile(source)
|
||||
l = list(cmdlineargs) + [p]
|
||||
return self.inline_run(*l)
|
||||
values = list(cmdlineargs) + [p]
|
||||
return self.inline_run(*values)
|
||||
|
||||
def inline_genitems(self, *args):
|
||||
"""Run ``pytest.main(['--collectonly'])`` in-process.
|
||||
@@ -694,7 +691,7 @@ class Testdir:
|
||||
# When running py.test inline any plugins active in the main
|
||||
# test process are already imported. So this disables the
|
||||
# warning which will trigger to say they can no longer be
|
||||
# re-written, which is fine as they are already re-written.
|
||||
# rewritten, which is fine as they are already rewritten.
|
||||
orig_warn = AssertionRewritingHook._warn_already_imported
|
||||
|
||||
def revert():
|
||||
@@ -734,7 +731,8 @@ class Testdir:
|
||||
if kwargs.get("syspathinsert"):
|
||||
self.syspathinsert()
|
||||
now = time.time()
|
||||
capture = py.io.StdCapture()
|
||||
capture = MultiCapture(Capture=SysCapture)
|
||||
capture.start_capturing()
|
||||
try:
|
||||
try:
|
||||
reprec = self.inline_run(*args, **kwargs)
|
||||
@@ -749,13 +747,14 @@ class Testdir:
|
||||
class reprec:
|
||||
ret = 3
|
||||
finally:
|
||||
out, err = capture.reset()
|
||||
out, err = capture.readouterr()
|
||||
capture.stop_capturing()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
res = RunResult(reprec.ret,
|
||||
out.split("\n"), err.split("\n"),
|
||||
time.time()-now)
|
||||
time.time() - now)
|
||||
res.reprec = reprec
|
||||
return res
|
||||
|
||||
@@ -771,11 +770,11 @@ class Testdir:
|
||||
args = [str(x) for x in args]
|
||||
for x in args:
|
||||
if str(x).startswith('--basetemp'):
|
||||
#print ("basedtemp exists: %s" %(args,))
|
||||
# print("basedtemp exists: %s" %(args,))
|
||||
break
|
||||
else:
|
||||
args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp'))
|
||||
#print ("added basetemp: %s" %(args,))
|
||||
# print("added basetemp: %s" %(args,))
|
||||
return args
|
||||
|
||||
def parseconfig(self, *args):
|
||||
@@ -813,7 +812,7 @@ class Testdir:
|
||||
self.request.addfinalizer(config._ensure_unconfigure)
|
||||
return config
|
||||
|
||||
def getitem(self, source, funcname="test_func"):
|
||||
def getitem(self, source, funcname="test_func"):
|
||||
"""Return the test item for a test function.
|
||||
|
||||
This writes the source to a python file and runs pytest's
|
||||
@@ -830,10 +829,10 @@ class Testdir:
|
||||
for item in items:
|
||||
if item.name == funcname:
|
||||
return item
|
||||
assert 0, "%r item not found in module:\n%s\nitems: %s" %(
|
||||
assert 0, "%r item not found in module:\n%s\nitems: %s" % (
|
||||
funcname, source, items)
|
||||
|
||||
def getitems(self, source):
|
||||
def getitems(self, source):
|
||||
"""Return all test items collected from the module.
|
||||
|
||||
This writes the source to a python file and runs pytest's
|
||||
@@ -844,7 +843,7 @@ class Testdir:
|
||||
modcol = self.getmodulecol(source)
|
||||
return self.genitems([modcol])
|
||||
|
||||
def getmodulecol(self, source, configargs=(), withinit=False):
|
||||
def getmodulecol(self, source, configargs=(), withinit=False):
|
||||
"""Return the module collection node for ``source``.
|
||||
|
||||
This writes ``source`` to a file using :py:meth:`makepyfile`
|
||||
@@ -863,9 +862,10 @@ class Testdir:
|
||||
kw = {self.request.function.__name__: Source(source).strip()}
|
||||
path = self.makepyfile(**kw)
|
||||
if withinit:
|
||||
self.makepyfile(__init__ = "#")
|
||||
self.makepyfile(__init__="#")
|
||||
self.config = config = self.parseconfigure(path, *configargs)
|
||||
node = self.getnode(config, path)
|
||||
|
||||
return node
|
||||
|
||||
def collect_by_name(self, modcol, name):
|
||||
@@ -880,7 +880,9 @@ class Testdir:
|
||||
:param name: The name of the node to return.
|
||||
|
||||
"""
|
||||
for colitem in modcol._memocollect():
|
||||
if modcol not in self._mod_collections:
|
||||
self._mod_collections[modcol] = list(modcol.collect())
|
||||
for colitem in self._mod_collections[modcol]:
|
||||
if colitem.name == name:
|
||||
return colitem
|
||||
|
||||
@@ -897,8 +899,11 @@ class Testdir:
|
||||
env['PYTHONPATH'] = os.pathsep.join(filter(None, [
|
||||
str(os.getcwd()), env.get('PYTHONPATH', '')]))
|
||||
kw['env'] = env
|
||||
return subprocess.Popen(cmdargs,
|
||||
stdout=stdout, stderr=stderr, **kw)
|
||||
|
||||
popen = subprocess.Popen(cmdargs, stdin=subprocess.PIPE, stdout=stdout, stderr=stderr, **kw)
|
||||
popen.stdin.close()
|
||||
|
||||
return popen
|
||||
|
||||
def run(self, *cmdargs):
|
||||
"""Run a command with arguments.
|
||||
@@ -915,14 +920,14 @@ class Testdir:
|
||||
cmdargs = [str(x) for x in cmdargs]
|
||||
p1 = self.tmpdir.join("stdout")
|
||||
p2 = self.tmpdir.join("stderr")
|
||||
print_("running:", ' '.join(cmdargs))
|
||||
print_(" in:", str(py.path.local()))
|
||||
print("running:", ' '.join(cmdargs))
|
||||
print(" in:", str(py.path.local()))
|
||||
f1 = codecs.open(str(p1), "w", encoding="utf8")
|
||||
f2 = codecs.open(str(p2), "w", encoding="utf8")
|
||||
try:
|
||||
now = time.time()
|
||||
popen = self.popen(cmdargs, stdout=f1, stderr=f2,
|
||||
close_fds=(sys.platform != "win32"))
|
||||
close_fds=(sys.platform != "win32"))
|
||||
ret = popen.wait()
|
||||
finally:
|
||||
f1.close()
|
||||
@@ -937,19 +942,19 @@ class Testdir:
|
||||
f2.close()
|
||||
self._dump_lines(out, sys.stdout)
|
||||
self._dump_lines(err, sys.stderr)
|
||||
return RunResult(ret, out, err, time.time()-now)
|
||||
return RunResult(ret, out, err, time.time() - now)
|
||||
|
||||
def _dump_lines(self, lines, fp):
|
||||
try:
|
||||
for line in lines:
|
||||
py.builtin.print_(line, file=fp)
|
||||
print(line, file=fp)
|
||||
except UnicodeEncodeError:
|
||||
print("couldn't print to %s because of encoding" % (fp,))
|
||||
|
||||
def _getpytestargs(self):
|
||||
# we cannot use "(sys.executable,script)"
|
||||
# because on windows the script is e.g. a pytest.exe
|
||||
return (sys.executable, _pytest_fullpath,) # noqa
|
||||
return (sys.executable, PYTEST_FULLPATH) # noqa
|
||||
|
||||
def runpython(self, script):
|
||||
"""Run a python script using sys.executable as interpreter.
|
||||
@@ -976,12 +981,12 @@ class Testdir:
|
||||
|
||||
"""
|
||||
p = py.path.local.make_numbered_dir(prefix="runpytest-",
|
||||
keep=None, rootdir=self.tmpdir)
|
||||
keep=None, rootdir=self.tmpdir)
|
||||
args = ('--basetemp=%s' % p, ) + args
|
||||
#for x in args:
|
||||
# for x in args:
|
||||
# if '--confcutdir' in str(x):
|
||||
# break
|
||||
#else:
|
||||
# else:
|
||||
# pass
|
||||
# args = ('--confcutdir=.',) + args
|
||||
plugins = [x for x in self.plugins if isinstance(x, str)]
|
||||
@@ -999,7 +1004,7 @@ class Testdir:
|
||||
The pexpect child is returned.
|
||||
|
||||
"""
|
||||
basetemp = self.tmpdir.mkdir("pexpect")
|
||||
basetemp = self.tmpdir.mkdir("temp-pexpect")
|
||||
invoke = " ".join(map(str, self._getpytestargs()))
|
||||
cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string)
|
||||
return self.spawn(cmd, expect_timeout=expect_timeout)
|
||||
@@ -1020,12 +1025,13 @@ class Testdir:
|
||||
child.timeout = expect_timeout
|
||||
return child
|
||||
|
||||
|
||||
def getdecoded(out):
|
||||
try:
|
||||
return out.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (
|
||||
py.io.saferepr(out),)
|
||||
try:
|
||||
return out.decode("utf-8")
|
||||
except UnicodeDecodeError:
|
||||
return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (
|
||||
py.io.saferepr(out),)
|
||||
|
||||
|
||||
class LineComp:
|
||||
@@ -1055,7 +1061,7 @@ class LineMatcher:
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, lines):
|
||||
def __init__(self, lines):
|
||||
self.lines = lines
|
||||
self._log_output = []
|
||||
|
||||
@@ -1071,6 +1077,23 @@ class LineMatcher:
|
||||
return lines2
|
||||
|
||||
def fnmatch_lines_random(self, lines2):
|
||||
"""Check lines exist in the output using ``fnmatch.fnmatch``, in any order.
|
||||
|
||||
The argument is a list of lines which have to occur in the
|
||||
output, in any order.
|
||||
"""
|
||||
self._match_lines_random(lines2, fnmatch)
|
||||
|
||||
def re_match_lines_random(self, lines2):
|
||||
"""Check lines exist in the output using ``re.match``, in any order.
|
||||
|
||||
The argument is a list of lines which have to occur in the
|
||||
output, in any order.
|
||||
|
||||
"""
|
||||
self._match_lines_random(lines2, lambda name, pat: re.match(pat, name))
|
||||
|
||||
def _match_lines_random(self, lines2, match_func):
|
||||
"""Check lines exist in the output.
|
||||
|
||||
The argument is a list of lines which have to occur in the
|
||||
@@ -1080,7 +1103,7 @@ class LineMatcher:
|
||||
lines2 = self._getlines(lines2)
|
||||
for line in lines2:
|
||||
for x in self.lines:
|
||||
if line == x or fnmatch(x, line):
|
||||
if line == x or match_func(x, line):
|
||||
self._log("matched: ", repr(line))
|
||||
break
|
||||
else:
|
||||
@@ -1094,7 +1117,7 @@ class LineMatcher:
|
||||
"""
|
||||
for i, line in enumerate(self.lines):
|
||||
if fnline == line or fnmatch(line, fnline):
|
||||
return self.lines[i+1:]
|
||||
return self.lines[i + 1:]
|
||||
raise ValueError("line %r not found in output" % fnline)
|
||||
|
||||
def _log(self, *args):
|
||||
@@ -1105,13 +1128,37 @@ class LineMatcher:
|
||||
return '\n'.join(self._log_output)
|
||||
|
||||
def fnmatch_lines(self, lines2):
|
||||
"""Search the text for matching lines.
|
||||
"""Search captured text for matching lines using ``fnmatch.fnmatch``.
|
||||
|
||||
The argument is a list of lines which have to match and can
|
||||
use glob wildcards. If they do not match an pytest.fail() is
|
||||
use glob wildcards. If they do not match a pytest.fail() is
|
||||
called. The matches and non-matches are also printed on
|
||||
stdout.
|
||||
|
||||
"""
|
||||
self._match_lines(lines2, fnmatch, 'fnmatch')
|
||||
|
||||
def re_match_lines(self, lines2):
|
||||
"""Search captured text for matching lines using ``re.match``.
|
||||
|
||||
The argument is a list of lines which have to match using ``re.match``.
|
||||
If they do not match a pytest.fail() is called.
|
||||
|
||||
The matches and non-matches are also printed on
|
||||
stdout.
|
||||
"""
|
||||
self._match_lines(lines2, lambda name, pat: re.match(pat, name), 're.match')
|
||||
|
||||
def _match_lines(self, lines2, match_func, match_nickname):
|
||||
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
|
||||
|
||||
:param list[str] lines2: list of string patterns to match. The actual format depends on
|
||||
``match_func``.
|
||||
:param match_func: a callable ``match_func(line, pattern)`` where line is the captured
|
||||
line from stdout/stderr and pattern is the matching pattern.
|
||||
|
||||
:param str match_nickname: the nickname for the match function that will be logged
|
||||
to stdout when a match occurs.
|
||||
"""
|
||||
lines2 = self._getlines(lines2)
|
||||
lines1 = self.lines[:]
|
||||
@@ -1125,8 +1172,8 @@ class LineMatcher:
|
||||
if line == nextline:
|
||||
self._log("exact match:", repr(line))
|
||||
break
|
||||
elif fnmatch(nextline, line):
|
||||
self._log("fnmatch:", repr(line))
|
||||
elif match_func(nextline, line):
|
||||
self._log("%s:" % match_nickname, repr(line))
|
||||
self._log(" with:", repr(nextline))
|
||||
break
|
||||
else:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
619
_pytest/python_api.py
Normal file
619
_pytest/python_api.py
Normal file
@@ -0,0 +1,619 @@
|
||||
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.
|
||||
"""
|
||||
|
||||
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, 1e-12)
|
||||
|
||||
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, 1e-6) * 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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
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.
|
||||
|
||||
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")
|
||||
message += " matching '{0}'".format(match_expr)
|
||||
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:
|
||||
self.excinfo.match(self.match_expr)
|
||||
return suppress_exception
|
||||
@@ -1,4 +1,5 @@
|
||||
""" recording warnings during test function execution. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import inspect
|
||||
|
||||
@@ -6,11 +7,15 @@ import _pytest._code
|
||||
import py
|
||||
import sys
|
||||
import warnings
|
||||
import pytest
|
||||
|
||||
import re
|
||||
|
||||
from _pytest.fixtures import yield_fixture
|
||||
from _pytest.outcomes import fail
|
||||
|
||||
|
||||
@pytest.yield_fixture
|
||||
def recwarn(request):
|
||||
@yield_fixture
|
||||
def recwarn():
|
||||
"""Return a WarningsRecorder instance that provides these methods:
|
||||
|
||||
* ``pop(category=None)``: return last warning matching the category.
|
||||
@@ -25,16 +30,9 @@ def recwarn(request):
|
||||
yield wrec
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {'deprecated_call': deprecated_call,
|
||||
'warns': warns}
|
||||
|
||||
|
||||
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():
|
||||
@@ -44,40 +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)
|
||||
|
||||
categories = []
|
||||
|
||||
def warn_explicit(message, category, *args, **kwargs):
|
||||
categories.append(category)
|
||||
old_warn_explicit(message, category, *args, **kwargs)
|
||||
|
||||
def warn(message, category=None, *args, **kwargs):
|
||||
if isinstance(message, Warning):
|
||||
categories.append(message.__class__)
|
||||
else:
|
||||
categories.append(category)
|
||||
old_warn(message, category, *args, **kwargs)
|
||||
|
||||
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):
|
||||
@@ -95,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)
|
||||
@@ -106,33 +129,23 @@ 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)
|
||||
|
||||
|
||||
class RecordedWarning(object):
|
||||
def __init__(self, message, category, filename, lineno, file, line):
|
||||
self.message = message
|
||||
self.category = category
|
||||
self.filename = filename
|
||||
self.lineno = lineno
|
||||
self.file = file
|
||||
self.line = line
|
||||
|
||||
|
||||
class WarningsRecorder(object):
|
||||
class WarningsRecorder(warnings.catch_warnings):
|
||||
"""A context manager to record raised warnings.
|
||||
|
||||
Adapted from `warnings.catch_warnings`.
|
||||
"""
|
||||
|
||||
def __init__(self, module=None):
|
||||
self._module = sys.modules['warnings'] if module is None else module
|
||||
def __init__(self):
|
||||
super(WarningsRecorder, self).__init__(record=True)
|
||||
self._entered = False
|
||||
self._list = []
|
||||
|
||||
@@ -169,38 +182,20 @@ class WarningsRecorder(object):
|
||||
if self._entered:
|
||||
__tracebackhide__ = True
|
||||
raise RuntimeError("Cannot enter %r twice" % self)
|
||||
self._entered = True
|
||||
self._filters = self._module.filters
|
||||
self._module.filters = self._filters[:]
|
||||
self._showwarning = self._module.showwarning
|
||||
|
||||
def showwarning(message, category, filename, lineno,
|
||||
file=None, line=None):
|
||||
self._list.append(RecordedWarning(
|
||||
message, category, filename, lineno, file, line))
|
||||
|
||||
# still perform old showwarning functionality
|
||||
self._showwarning(
|
||||
message, category, filename, lineno, file=file, line=line)
|
||||
|
||||
self._module.showwarning = showwarning
|
||||
|
||||
# allow the same warning to be raised more than once
|
||||
|
||||
self._module.simplefilter('always')
|
||||
self._list = super(WarningsRecorder, self).__enter__()
|
||||
warnings.simplefilter('always')
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
if not self._entered:
|
||||
__tracebackhide__ = True
|
||||
raise RuntimeError("Cannot exit %r without entering first" % self)
|
||||
self._module.filters = self._filters
|
||||
self._module.showwarning = self._showwarning
|
||||
super(WarningsRecorder, self).__exit__(*exc_info)
|
||||
|
||||
|
||||
class WarningsChecker(WarningsRecorder):
|
||||
def __init__(self, expected_warning=None, module=None):
|
||||
super(WarningsChecker, self).__init__(module=module)
|
||||
def __init__(self, expected_warning=None, match_expr=None):
|
||||
super(WarningsChecker, self).__init__()
|
||||
|
||||
msg = ("exceptions must be old-style classes or "
|
||||
"derived from Warning, not %s")
|
||||
@@ -214,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)
|
||||
@@ -221,9 +217,20 @@ class WarningsChecker(WarningsRecorder):
|
||||
# only check if we're not currently handling an exception
|
||||
if all(a is None for a in exc_info):
|
||||
if self.expected_warning is not None:
|
||||
if not any(r.category in self.expected_warning for r in self):
|
||||
if not any(issubclass(r.category, self.expected_warning)
|
||||
for r in self):
|
||||
__tracebackhide__ = True
|
||||
pytest.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]))
|
||||
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]))
|
||||
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]))
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
""" log machine-parseable test session result information in a plain
|
||||
text file.
|
||||
"""
|
||||
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
|
||||
@@ -18,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:
|
||||
@@ -32,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]
|
||||
@@ -55,15 +60,16 @@ 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):
|
||||
py.builtin.print_("%s %s" % (lettercode, testpath), file=self.logfile)
|
||||
print("%s %s" % (lettercode, testpath), file=self.logfile)
|
||||
for line in longrepr.splitlines():
|
||||
py.builtin.print_(" %s" % line, file=self.logfile)
|
||||
print(" %s" % line, file=self.logfile)
|
||||
|
||||
def log_outcome(self, report, lettercode, longrepr):
|
||||
testpath = getattr(report, 'nodeid', None)
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
""" basic collect and runtest protocol implementations """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import bdb
|
||||
import os
|
||||
import sys
|
||||
from time import time
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from _pytest._code.code import TerminalRepr, ExceptionInfo
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {
|
||||
'fail' : fail,
|
||||
'skip' : skip,
|
||||
'importorskip' : importorskip,
|
||||
'exit' : exit,
|
||||
}
|
||||
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
|
||||
@@ -48,16 +44,16 @@ 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(
|
||||
@@ -66,6 +62,7 @@ def pytest_runtest_protocol(item, nextitem):
|
||||
runtestprotocol(item, nextitem=nextitem)
|
||||
return True
|
||||
|
||||
|
||||
def runtestprotocol(item, log=True, nextitem=None):
|
||||
hasrequest = hasattr(item, "_request")
|
||||
if hasrequest and not item._request:
|
||||
@@ -78,7 +75,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:
|
||||
@@ -86,6 +83,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()
|
||||
@@ -96,10 +94,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:
|
||||
@@ -112,8 +114,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"):
|
||||
@@ -139,21 +161,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:
|
||||
""" 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"
|
||||
@@ -164,7 +190,7 @@ class CallInfo:
|
||||
except KeyboardInterrupt:
|
||||
self.stop = time()
|
||||
raise
|
||||
except:
|
||||
except: # noqa
|
||||
self.excinfo = ExceptionInfo()
|
||||
self.stop = time()
|
||||
|
||||
@@ -175,6 +201,7 @@ class CallInfo:
|
||||
status = "result: %r" % (self.result,)
|
||||
return "<CallInfo when=%r %s>" % (self.when, status)
|
||||
|
||||
|
||||
def getslaveinfoline(node):
|
||||
try:
|
||||
return node._slaveinfocache
|
||||
@@ -185,6 +212,7 @@ def getslaveinfoline(node):
|
||||
d['id'], d['sysplatform'], ver, d['executable'])
|
||||
return s
|
||||
|
||||
|
||||
class BaseReport(object):
|
||||
|
||||
def __init__(self, **kw):
|
||||
@@ -249,10 +277,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:
|
||||
@@ -262,7 +291,7 @@ def pytest_runtest_makereport(item, call):
|
||||
if not isinstance(excinfo, ExceptionInfo):
|
||||
outcome = "failed"
|
||||
longrepr = excinfo
|
||||
elif excinfo.errisinstance(pytest.skip.Exception):
|
||||
elif excinfo.errisinstance(skip.Exception):
|
||||
outcome = "skipped"
|
||||
r = excinfo._getreprcrash()
|
||||
longrepr = (str(r.path), r.lineno, r.message)
|
||||
@@ -270,19 +299,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
|
||||
@@ -321,16 +352,21 @@ 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(collector._memocollect, "memocollect")
|
||||
call = CallInfo(
|
||||
lambda: list(collector.collect()),
|
||||
'collect')
|
||||
longrepr = None
|
||||
if not call.excinfo:
|
||||
outcome = "passed"
|
||||
@@ -348,7 +384,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
|
||||
|
||||
@@ -369,16 +405,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 = {}
|
||||
@@ -389,8 +429,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):
|
||||
@@ -404,7 +444,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:
|
||||
@@ -418,7 +458,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:
|
||||
@@ -451,10 +491,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)
|
||||
@@ -463,116 +504,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.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
compile(modname, '', 'eval') # to catch syntaxerrors
|
||||
should_skip = False
|
||||
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
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
|
||||
@@ -42,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()
|
||||
@@ -61,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)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
""" support for skip/xfail functions and markers. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import os
|
||||
import six
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
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: "
|
||||
@@ -23,71 +26,65 @@ def pytest_addoption(parser):
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.option.runxfail:
|
||||
# yay a hack
|
||||
import pytest
|
||||
old = pytest.xfail
|
||||
config._cleanup.append(lambda: setattr(pytest, "xfail", old))
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return dict(xfail=xfail)
|
||||
|
||||
|
||||
class XFailed(pytest.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,18 +94,18 @@ 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) + "^",]
|
||||
msg = [" " * (self.exc[1].offset + 4) + "^", ]
|
||||
msg.append("SyntaxError: invalid syntax")
|
||||
else:
|
||||
msg = traceback.format_exception_only(*self.exc[:2])
|
||||
pytest.fail("Error evaluating %r expression\n"
|
||||
" %s\n"
|
||||
"%s"
|
||||
%(self.name, self.expr, "\n".join(msg)),
|
||||
pytrace=False)
|
||||
fail("Error evaluating %r expression\n"
|
||||
" %s\n"
|
||||
"%s"
|
||||
% (self._mark_name, self.expr, "\n".join(msg)),
|
||||
pytrace=False)
|
||||
|
||||
def _getglobals(self):
|
||||
d = {'os': os, 'sys': sys, 'config': self.item.config}
|
||||
@@ -119,42 +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
|
||||
if hasattr(self.holder, '_arglist'):
|
||||
arglist = self.holder._arglist
|
||||
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:
|
||||
arglist = [(self.holder.args, self.holder.kwargs)]
|
||||
for args, kwargs in arglist:
|
||||
if 'condition' in kwargs:
|
||||
args = (kwargs['condition'],)
|
||||
for expr in args:
|
||||
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."
|
||||
pytest.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)
|
||||
@@ -166,32 +166,32 @@ class MarkEvaluator:
|
||||
return expl
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
@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
|
||||
pytest.skip(eval_skipif.getexplanation())
|
||||
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:
|
||||
pytest.skip(skip_info.kwargs['reason'])
|
||||
skip(skip_info.kwargs['reason'])
|
||||
elif skip_info.args:
|
||||
pytest.skip(skip_info.args[0])
|
||||
skip(skip_info.args[0])
|
||||
else:
|
||||
pytest.skip("unconditional skip")
|
||||
skip("unconditional skip")
|
||||
|
||||
item._evalxfail = MarkEvaluator(item, 'xfail')
|
||||
check_xfail_no_run(item)
|
||||
|
||||
|
||||
@pytest.mark.hookwrapper
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
check_xfail_no_run(pyfuncitem)
|
||||
outcome = yield
|
||||
@@ -206,7 +206,7 @@ def check_xfail_no_run(item):
|
||||
evalxfail = item._evalxfail
|
||||
if evalxfail.istrue():
|
||||
if not evalxfail.get('run', True):
|
||||
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||
xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||
|
||||
|
||||
def check_strict_xfail(pyfuncitem):
|
||||
@@ -218,15 +218,14 @@ def check_strict_xfail(pyfuncitem):
|
||||
if is_strict_xfail:
|
||||
del pyfuncitem._evalxfail
|
||||
explanation = evalxfail.getexplanation()
|
||||
pytest.fail('[XPASS(strict)] ' + explanation, pytrace=False)
|
||||
fail('[XPASS(strict)] ' + explanation, pytrace=False)
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@hookimpl(hookwrapper=True)
|
||||
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
|
||||
@@ -241,11 +240,11 @@ def pytest_runtest_makereport(item, call):
|
||||
rep.wasxfail = rep.longrepr
|
||||
elif item.config.option.runxfail:
|
||||
pass # don't interefere
|
||||
elif call.excinfo and call.excinfo.errisinstance(pytest.xfail.Exception):
|
||||
elif call.excinfo and call.excinfo.errisinstance(xfail.Exception):
|
||||
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"
|
||||
@@ -262,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 item._skipped_by_mark 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
|
||||
@@ -271,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:
|
||||
@@ -279,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")
|
||||
@@ -309,12 +312,14 @@ def pytest_terminal_summary(terminalreporter):
|
||||
for line in lines:
|
||||
tr._tw.line(line)
|
||||
|
||||
|
||||
def show_simple(terminalreporter, lines, stat, format):
|
||||
failed = terminalreporter.stats.get(stat)
|
||||
if failed:
|
||||
for rep in failed:
|
||||
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
|
||||
lines.append(format %(pos,))
|
||||
lines.append(format % (pos,))
|
||||
|
||||
|
||||
def show_xfailed(terminalreporter, lines):
|
||||
xfailed = terminalreporter.stats.get("xfailed")
|
||||
@@ -326,13 +331,15 @@ def show_xfailed(terminalreporter, lines):
|
||||
if reason:
|
||||
lines.append(" " + str(reason))
|
||||
|
||||
|
||||
def show_xpassed(terminalreporter, lines):
|
||||
xpassed = terminalreporter.stats.get("xpassed")
|
||||
if xpassed:
|
||||
for rep in xpassed:
|
||||
pos = terminalreporter.config.cwd_relative_nodeid(rep.nodeid)
|
||||
reason = rep.wasxfail
|
||||
lines.append("XPASS %s %s" %(pos, reason))
|
||||
lines.append("XPASS %s %s" % (pos, reason))
|
||||
|
||||
|
||||
def cached_eval(config, expr, d):
|
||||
if not hasattr(config, '_evalcache'):
|
||||
@@ -351,26 +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))
|
||||
|
||||
@@ -2,47 +2,58 @@
|
||||
|
||||
This is a good source for looking at the various reporting hooks.
|
||||
"""
|
||||
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, \
|
||||
EXIT_USAGEERROR, EXIT_NOTESTSCOLLECTED
|
||||
import pytest
|
||||
import py
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import itertools
|
||||
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. "
|
||||
"The pytest warnings are displayed at all times except when "
|
||||
"--disable-pytest-warnings is set")
|
||||
group._addoption('--disable-pytest-warnings', default=False,
|
||||
dest='disablepytestwarnings', action='store_true',
|
||||
help='disable warnings summary, overrides -r w flag')
|
||||
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
|
||||
@@ -54,12 +65,13 @@ def pytest_configure(config):
|
||||
reporter.write_line("[traceconfig] " + msg)
|
||||
config.trace.root.setprocessor("pytest:config", mywriter)
|
||||
|
||||
|
||||
def getreportopt(config):
|
||||
reportopts = ""
|
||||
reportchars = config.option.reportchars
|
||||
if not config.option.disablepytestwarnings and 'w' not in reportchars:
|
||||
if not config.option.disable_warnings and 'w' not in reportchars:
|
||||
reportchars += 'w'
|
||||
elif config.option.disablepytestwarnings and 'w' in reportchars:
|
||||
elif config.option.disable_warnings and 'w' in reportchars:
|
||||
reportchars = reportchars.replace('w', '')
|
||||
if reportchars:
|
||||
for char in reportchars:
|
||||
@@ -69,6 +81,7 @@ def getreportopt(config):
|
||||
reportopts = 'fEsxXw'
|
||||
return reportopts
|
||||
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
if report.passed:
|
||||
letter = "."
|
||||
@@ -80,13 +93,41 @@ def pytest_report_teststatus(report):
|
||||
letter = "f"
|
||||
return report.outcome, letter, report.outcome.upper()
|
||||
|
||||
|
||||
class WarningReport:
|
||||
"""
|
||||
Simple structure to hold warnings information captured by ``pytest_logwarning``.
|
||||
"""
|
||||
|
||||
def __init__(self, code, message, nodeid=None, fslocation=None):
|
||||
"""
|
||||
:param code: unused
|
||||
:param str message: user friendly message about the warning
|
||||
:param str|None nodeid: node id that generated the warning (see ``get_location``).
|
||||
:param tuple|py.path.local fslocation:
|
||||
file system location of the source of the warning (see ``get_location``).
|
||||
"""
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.nodeid = nodeid
|
||||
self.fslocation = fslocation
|
||||
|
||||
def get_location(self, config):
|
||||
"""
|
||||
Returns the more user-friendly information about the location
|
||||
of a warning, or None.
|
||||
"""
|
||||
if self.nodeid:
|
||||
return self.nodeid
|
||||
if self.fslocation:
|
||||
if isinstance(self.fslocation, tuple) and len(self.fslocation) >= 2:
|
||||
filename, linenum = self.fslocation[:2]
|
||||
relpath = py.path.local(filename).relto(config.invocation_dir)
|
||||
return '%s:%s' % (relpath, linenum)
|
||||
else:
|
||||
return str(self.fslocation)
|
||||
return None
|
||||
|
||||
|
||||
class TerminalReporter:
|
||||
def __init__(self, config, file=None):
|
||||
@@ -97,17 +138,22 @@ 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_items_reported = 0
|
||||
self._show_progress_info = self.config.getini('console_output_style') == 'progress'
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
|
||||
@@ -116,6 +162,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()
|
||||
@@ -130,6 +178,7 @@ class TerminalReporter:
|
||||
if extra:
|
||||
self._tw.write(extra, **kwargs)
|
||||
self.currentfspath = -2
|
||||
self._write_progress_information_filling_space()
|
||||
|
||||
def ensure_newline(self):
|
||||
if self.currentfspath:
|
||||
@@ -140,14 +189,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()
|
||||
@@ -160,14 +223,12 @@ 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
|
||||
|
||||
def pytest_logwarning(self, code, fslocation, message, nodeid):
|
||||
warnings = self.stats.setdefault("warnings", [])
|
||||
if isinstance(fslocation, tuple):
|
||||
fslocation = "%s:%d" % fslocation
|
||||
warning = WarningReport(code=code, fslocation=fslocation,
|
||||
message=message, nodeid=nodeid)
|
||||
warnings.append(warning)
|
||||
@@ -197,38 +258,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')
|
||||
self._progress_items_reported += 1
|
||||
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)
|
||||
self._write_progress_if_past_edge()
|
||||
else:
|
||||
if isinstance(word, tuple):
|
||||
word, markup = word
|
||||
else:
|
||||
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)
|
||||
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 _write_progress_if_past_edge(self):
|
||||
if not self._show_progress_info:
|
||||
return
|
||||
last_item = self._progress_items_reported == self._session.testscollected
|
||||
if last_item:
|
||||
self._write_progress_information_filling_space()
|
||||
return
|
||||
|
||||
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):
|
||||
collected = self._session.testscollected
|
||||
if collected:
|
||||
progress = self._progress_items_reported * 100 // collected
|
||||
return ' [{:3d}%]'.format(progress)
|
||||
return ' [100%]'
|
||||
|
||||
def _write_progress_information_filling_space(self):
|
||||
if not self._show_progress_info:
|
||||
return
|
||||
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)
|
||||
@@ -241,7 +340,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):
|
||||
@@ -254,15 +353,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)
|
||||
|
||||
@@ -271,6 +370,7 @@ class TerminalReporter:
|
||||
|
||||
@pytest.hookimpl(trylast=True)
|
||||
def pytest_sessionstart(self, session):
|
||||
self._session = session
|
||||
self._sessionstarttime = time.time()
|
||||
if not self.showheader:
|
||||
return
|
||||
@@ -288,6 +388,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)
|
||||
@@ -314,10 +417,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
|
||||
@@ -340,14 +442,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))
|
||||
@@ -396,15 +498,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]"
|
||||
@@ -415,7 +517,7 @@ class TerminalReporter:
|
||||
fspath, lineno, domain = rep.location
|
||||
return domain
|
||||
else:
|
||||
return "test session" # XXX?
|
||||
return "test session" # XXX?
|
||||
|
||||
def _getcrashline(self, rep):
|
||||
try:
|
||||
@@ -430,21 +532,29 @@ 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"):
|
||||
warnings = self.stats.get("warnings")
|
||||
if not warnings:
|
||||
all_warnings = self.stats.get("warnings")
|
||||
if not all_warnings:
|
||||
return
|
||||
self.write_sep("=", "pytest-warning summary")
|
||||
for w in warnings:
|
||||
self._tw.line("W%s %s %s" % (w.code,
|
||||
w.fslocation, w.message))
|
||||
|
||||
grouped = itertools.groupby(all_warnings, key=lambda wr: wr.get_location(self.config))
|
||||
|
||||
self.write_sep("=", "warnings summary", yellow=True, bold=False)
|
||||
for location, warning_records in grouped:
|
||||
self._tw.line(str(location) or '<undetermined location>')
|
||||
for w in warning_records:
|
||||
lines = w.message.splitlines()
|
||||
indented = '\n'.join(' ' + x for x in lines)
|
||||
self._tw.line(indented)
|
||||
self._tw.line()
|
||||
self._tw.line('-- Docs: http://doc.pytest.org/en/latest/warnings.html')
|
||||
|
||||
def summary_passes(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
@@ -466,7 +576,6 @@ class TerminalReporter:
|
||||
content = content[:-1]
|
||||
self._tw.line(content)
|
||||
|
||||
|
||||
def summary_failures(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
reports = self.getreports('failed')
|
||||
@@ -528,6 +637,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
|
||||
@@ -536,30 +646,30 @@ 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()
|
||||
key_translation = {'warnings': 'pytest-warnings'}
|
||||
"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 = []
|
||||
for key in keys:
|
||||
val = stats.get(key, None)
|
||||
if val:
|
||||
key_name = key_translation.get(key, key)
|
||||
parts.append("%d %s" % (len(val), key_name))
|
||||
parts.append("%d %s" % (len(val), key))
|
||||
|
||||
if parts:
|
||||
line = ", ".join(parts)
|
||||
@@ -579,7 +689,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)
|
||||
@@ -588,6 +698,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
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
""" support for providing temporary directories to test functions. """
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import re
|
||||
|
||||
import pytest
|
||||
@@ -23,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):
|
||||
@@ -36,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
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
""" discovery and running of std-library "unittest" style tests. """
|
||||
from __future__ import absolute_import
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import pytest
|
||||
# for transferring markers
|
||||
import _pytest._code
|
||||
from _pytest.python import transfer_markers
|
||||
from _pytest.skipping import MarkEvaluator
|
||||
from _pytest.config import hookimpl
|
||||
from _pytest.outcomes import fail, skip, xfail
|
||||
from _pytest.python import transfer_markers, Class, Module, Function
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
@@ -22,11 +22,11 @@ def pytest_pycollect_makeitem(collector, name, obj):
|
||||
return UnitTestCase(name, parent=collector)
|
||||
|
||||
|
||||
class UnitTestCase(pytest.Class):
|
||||
class UnitTestCase(Class):
|
||||
# marker for fixturemanger.getfixtureinfo()
|
||||
# to declare that our children do not support funcargs
|
||||
nofuncargs = True
|
||||
|
||||
|
||||
def setup(self):
|
||||
cls = self.obj
|
||||
if getattr(cls, '__unittest_skip__', False):
|
||||
@@ -46,7 +46,7 @@ class UnitTestCase(pytest.Class):
|
||||
return
|
||||
self.session._fixturemanager.parsefactories(self, unittest=True)
|
||||
loader = TestLoader()
|
||||
module = self.getparent(pytest.Module).obj
|
||||
module = self.getparent(Module).obj
|
||||
foundsomething = False
|
||||
for name in loader.getTestCaseNames(self.obj):
|
||||
x = getattr(self.obj, name)
|
||||
@@ -65,7 +65,7 @@ class UnitTestCase(pytest.Class):
|
||||
yield TestCaseFunction('runTest', parent=self)
|
||||
|
||||
|
||||
class TestCaseFunction(pytest.Function):
|
||||
class TestCaseFunction(Function):
|
||||
_excinfo = None
|
||||
|
||||
def setup(self):
|
||||
@@ -108,38 +108,38 @@ class TestCaseFunction(pytest.Function):
|
||||
except TypeError:
|
||||
try:
|
||||
try:
|
||||
l = traceback.format_exception(*rawexcinfo)
|
||||
l.insert(0, "NOTE: Incompatible Exception Representation, "
|
||||
"displaying natively:\n\n")
|
||||
pytest.fail("".join(l), pytrace=False)
|
||||
except (pytest.fail.Exception, KeyboardInterrupt):
|
||||
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:
|
||||
pytest.fail("ERROR: Unknown Incompatible Exception "
|
||||
"representation:\n%r" %(rawexcinfo,), pytrace=False)
|
||||
except: # noqa
|
||||
fail("ERROR: Unknown Incompatible Exception "
|
||||
"representation:\n%r" % (rawexcinfo,), pytrace=False)
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except pytest.fail.Exception:
|
||||
except fail.Exception:
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
self.__dict__.setdefault('_excinfo', []).append(excinfo)
|
||||
|
||||
def addError(self, testcase, rawexcinfo):
|
||||
self._addexcinfo(rawexcinfo)
|
||||
|
||||
def addFailure(self, testcase, rawexcinfo):
|
||||
self._addexcinfo(rawexcinfo)
|
||||
|
||||
def addSkip(self, testcase, reason):
|
||||
try:
|
||||
pytest.skip(reason)
|
||||
except pytest.skip.Exception:
|
||||
self._evalskip = MarkEvaluator(self, 'SkipTest')
|
||||
self._evalskip.result = True
|
||||
skip(reason)
|
||||
except skip.Exception:
|
||||
self._skipped_by_mark = True
|
||||
self._addexcinfo(sys.exc_info())
|
||||
|
||||
def addExpectedFailure(self, testcase, rawexcinfo, reason=""):
|
||||
try:
|
||||
pytest.xfail(str(reason))
|
||||
except pytest.xfail.Exception:
|
||||
xfail(str(reason))
|
||||
except xfail.Exception:
|
||||
self._addexcinfo(sys.exc_info())
|
||||
|
||||
def addUnexpectedSuccess(self, testcase, reason=""):
|
||||
@@ -156,7 +156,7 @@ class TestCaseFunction(pytest.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__', ''))
|
||||
@@ -179,13 +179,14 @@ class TestCaseFunction(pytest.Function):
|
||||
self._testcase.debug()
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
pytest.Function._prunetraceback(self, excinfo)
|
||||
Function._prunetraceback(self, excinfo)
|
||||
traceback = excinfo.traceback.filter(
|
||||
lambda x:not x.frame.f_globals.get('__unittest'))
|
||||
lambda x: not x.frame.f_globals.get('__unittest'))
|
||||
if traceback:
|
||||
excinfo.traceback = traceback
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
|
||||
@hookimpl(tryfirst=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
if isinstance(item, TestCaseFunction):
|
||||
if item._excinfo:
|
||||
@@ -197,7 +198,8 @@ def pytest_runtest_makereport(item, call):
|
||||
|
||||
# twisted trial support
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
|
||||
@hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_protocol(item):
|
||||
if isinstance(item, TestCaseFunction) and \
|
||||
'twisted.trial.unittest' in sys.modules:
|
||||
@@ -206,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:
|
||||
@@ -215,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))
|
||||
)
|
||||
96
_pytest/warnings.py
Normal file
96
_pytest/warnings.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
|
||||
import pytest
|
||||
|
||||
from _pytest import compat
|
||||
|
||||
|
||||
def _setoption(wmod, arg):
|
||||
"""
|
||||
Copy of the warning._setoption function but does not escape arguments.
|
||||
"""
|
||||
parts = arg.split(':')
|
||||
if len(parts) > 5:
|
||||
raise wmod._OptionError("too many fields (max 5): %r" % (arg,))
|
||||
while len(parts) < 5:
|
||||
parts.append('')
|
||||
action, message, category, module, lineno = [s.strip()
|
||||
for s in parts]
|
||||
action = wmod._getaction(action)
|
||||
category = wmod._getcategory(category)
|
||||
if lineno:
|
||||
try:
|
||||
lineno = int(lineno)
|
||||
if lineno < 0:
|
||||
raise ValueError
|
||||
except (ValueError, OverflowError):
|
||||
raise wmod._OptionError("invalid lineno %r" % (lineno,))
|
||||
else:
|
||||
lineno = 0
|
||||
wmod.filterwarnings(action, message, category, module, lineno)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("pytest-warnings")
|
||||
group.addoption(
|
||||
'-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 a pattern for "
|
||||
"warnings.filterwarnings. "
|
||||
"Processed after -W and --pythonwarnings.")
|
||||
|
||||
|
||||
@contextmanager
|
||||
def catch_warnings_for_item(item):
|
||||
"""
|
||||
catches the warnings generated during setup/call/teardown execution
|
||||
of the given item and after it is done posts them as warnings to this
|
||||
item.
|
||||
"""
|
||||
args = item.config.getoption('pythonwarnings') or []
|
||||
inifilters = item.config.getini("filterwarnings")
|
||||
with warnings.catch_warnings(record=True) as log:
|
||||
for arg in args:
|
||||
warnings._setoption(arg)
|
||||
|
||||
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:
|
||||
warn_msg = warning.message
|
||||
unicode_warning = False
|
||||
|
||||
if compat._PY2 and any(isinstance(m, compat.UNICODE_TYPES) for m in warn_msg.args):
|
||||
new_args = []
|
||||
for m in warn_msg.args:
|
||||
new_args.append(compat.ascii_escaped(m) if isinstance(m, compat.UNICODE_TYPES) else m)
|
||||
unicode_warning = list(warn_msg.args) != new_args
|
||||
warn_msg.args = new_args
|
||||
|
||||
msg = warnings.formatwarning(
|
||||
warn_msg, warning.category,
|
||||
warning.filename, warning.lineno, warning.line)
|
||||
item.warn("unused", msg)
|
||||
|
||||
if unicode_warning:
|
||||
warnings.warn(
|
||||
"Warning is using unicode non convertible to ascii, "
|
||||
"converting to a safe representation:\n %s" % msg,
|
||||
UnicodeWarning)
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_protocol(item):
|
||||
with catch_warnings_for_item(item):
|
||||
yield
|
||||
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.
|
||||
|
||||
|
||||
40
changelog/_template.rst
Normal file
40
changelog/_template.rst
Normal file
@@ -0,0 +1,40 @@
|
||||
{% for section in sections %}
|
||||
{% set underline = "-" %}
|
||||
{% if section %}
|
||||
{{section}}
|
||||
{{ underline * section|length }}{% set underline = "~" %}
|
||||
|
||||
{% endif %}
|
||||
{% if sections[section] %}
|
||||
{% 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') %}
|
||||
{% 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 %}
|
||||
{% else %}
|
||||
- {{ sections[section][category]['']|sort|join(', ') }}
|
||||
|
||||
|
||||
{% endif %}
|
||||
{% if sections[section][category]|length == 0 %}
|
||||
|
||||
No significant changes.
|
||||
|
||||
|
||||
{% else %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
|
||||
No significant changes.
|
||||
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@@ -13,11 +13,14 @@ 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@" \
|
||||
--normalize "@(This is pytest version )(\d+)\\.[^ ,]+@\1\2.x.y@" \
|
||||
--normalize "@py-(\d+)\\.[^ ,]+@py-\1.x.y@" \
|
||||
--normalize "@pluggy-(\d+)\\.[.\d,]+@pluggy-\1.x.y@" \
|
||||
--normalize "@hypothesis-(\d+)\\.[.\d,]+@hypothesis-\1.x.y@" \
|
||||
--normalize "@Python (\d+)\\.[^ ,]+@Python \1.x.y@"
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
|
||||
|
||||
@@ -36,7 +39,7 @@ clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
regen:
|
||||
PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.rst */*.rst ${REGENDOC_ARGS}
|
||||
PYTHONDONTWRITEBYTECODE=1 PYTEST_ADDOPT=-pno:hypothesis COLUMNS=76 regendoc --update *.rst */*.rst ${REGENDOC_ARGS}
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
|
||||
@@ -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,18 @@ Release announcements
|
||||
:maxdepth: 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
|
||||
release-3.0.6
|
||||
release-3.0.5
|
||||
|
||||
@@ -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.
|
||||
|
||||
61
doc/en/announce/release-3.1.0.rst
Normal file
61
doc/en/announce/release-3.1.0.rst
Normal file
@@ -0,0 +1,61 @@
|
||||
pytest-3.1.0
|
||||
=======================================
|
||||
|
||||
The pytest team is proud to announce the 3.1.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 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
|
||||
* Ben Lloyd
|
||||
* Bruno Oliveira
|
||||
* David Giese
|
||||
* David Szotten
|
||||
* Dmitri Pribysh
|
||||
* Florian Bruhin
|
||||
* Florian Schulze
|
||||
* Floris Bruynooghe
|
||||
* John Towler
|
||||
* Jonas Obrist
|
||||
* Katerina Koukiou
|
||||
* Kodi Arfer
|
||||
* Krzysztof Szularz
|
||||
* Lev Maximov
|
||||
* Loïc Estève
|
||||
* Luke Murphy
|
||||
* Manuel Krebber
|
||||
* Matthew Duck
|
||||
* Matthias Bussonnier
|
||||
* Michael Howitz
|
||||
* Michal Wajszczuk
|
||||
* Paweł Adamczak
|
||||
* Rafael Bertoldi
|
||||
* Ravi Chandra
|
||||
* Ronny Pfannschmidt
|
||||
* Skylar Downes
|
||||
* Thomas Kriechbaumer
|
||||
* Vitaly Lashmanov
|
||||
* Vlad Dragos
|
||||
* Wheerd
|
||||
* Xander Johnson
|
||||
* mandeep
|
||||
* reut
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
23
doc/en/announce/release-3.1.1.rst
Normal file
23
doc/en/announce/release-3.1.1.rst
Normal file
@@ -0,0 +1,23 @@
|
||||
pytest-3.1.1
|
||||
=======================================
|
||||
|
||||
pytest 3.1.1 has just been released to PyPI.
|
||||
|
||||
This is a bug-fix release, being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Floris Bruynooghe
|
||||
* Jason R. Coombs
|
||||
* Ronny Pfannschmidt
|
||||
* wanghui
|
||||
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
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
|
||||
@@ -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 ========
|
||||
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0
|
||||
=========================== 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 ========
|
||||
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0
|
||||
=========================== 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
|
||||
@@ -223,7 +222,7 @@ provides an alternative explanation for ``Foo`` objects::
|
||||
now, given this test module::
|
||||
|
||||
# content of test_foocompare.py
|
||||
class Foo:
|
||||
class Foo(object):
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
@@ -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)
|
||||
@@ -270,12 +269,21 @@ supporting modules which are not themselves test modules will not be rewritten.
|
||||
|
||||
.. note::
|
||||
|
||||
``pytest`` rewrites test modules on import. It does this by using an import
|
||||
hook to write new pyc files. Most of the time this works transparently.
|
||||
``pytest`` rewrites test modules on import by using an import
|
||||
hook to write new ``pyc`` files. Most of the time this works transparently.
|
||||
However, if you are messing with import yourself, the import hook may
|
||||
interfere. If this is the case, use ``--assert=plain``. Additionally,
|
||||
rewriting will fail silently if it cannot write new pycs, i.e. in a read-only
|
||||
filesystem or a zipfile.
|
||||
interfere.
|
||||
|
||||
If this is the case you have two options:
|
||||
|
||||
* Disable rewriting for a specific module by adding the string
|
||||
``PYTEST_DONT_REWRITE`` to its docstring.
|
||||
|
||||
* Disable rewriting for all modules by using ``--assert=plain``.
|
||||
|
||||
Additionally, rewriting will fail silently if it cannot write new ``.pyc`` files,
|
||||
i.e. in a read-only filesystem or a zipfile.
|
||||
|
||||
|
||||
For further information, Benjamin Peterson wrote up `Behind the scenes of pytest's new assertion rewriting <http://pybites.blogspot.com/2011/07/behind-scenes-of-pytests-new-assertion.html>`_.
|
||||
|
||||
|
||||
@@ -10,3 +10,96 @@ 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
|
||||
-------------------
|
||||
|
||||
This page lists deprecated features and when we plan to remove them. It is important to list the feature, the version where it got deprecated and the version we plan to remove it.
|
||||
|
||||
Following our deprecation policy, we should aim to keep features for *at least* two minor versions after it was considered deprecated.
|
||||
|
||||
|
||||
Future Releases
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
3.4
|
||||
^^^
|
||||
|
||||
**Old style classes**
|
||||
|
||||
Issue: `#2147 <https://github.com/pytest-dev/pytest/issues/2147>`_.
|
||||
|
||||
Deprecated in ``3.2``.
|
||||
|
||||
4.0
|
||||
^^^
|
||||
|
||||
**Yield tests**
|
||||
|
||||
Deprecated in ``3.0``.
|
||||
|
||||
**pytest-namespace hook**
|
||||
|
||||
deprecated in ``3.2``.
|
||||
|
||||
**Marks in parameter sets**
|
||||
|
||||
Deprecated in ``3.2``.
|
||||
|
||||
**--result-log**
|
||||
|
||||
Deprecated in ``3.0``.
|
||||
|
||||
See `#830 <https://github.com/pytest-dev/pytest/issues/830>`_ for more information. Suggested alternative: `pytest-tap <https://pypi.python.org/pypi/pytest-tap>`_.
|
||||
|
||||
**metafunc.addcall**
|
||||
|
||||
Issue: `#2876 <https://github.com/pytest-dev/pytest/issues/2876>`_.
|
||||
|
||||
Deprecated in ``3.3``.
|
||||
|
||||
**pytest_plugins in non-toplevel conftests**
|
||||
|
||||
There is a deep conceptual confusion as ``conftest.py`` files themselves are activated/deactivated based on path, but the plugins they depend on aren't.
|
||||
|
||||
Issue: `#2639 <https://github.com/pytest-dev/pytest/issues/2639>`_.
|
||||
|
||||
Not yet officially deprecated.
|
||||
|
||||
**passing a single string to pytest.main()**
|
||||
|
||||
Pass a list of strings to ``pytest.main()`` instead.
|
||||
|
||||
Deprecated in ``3.1``.
|
||||
|
||||
**[pytest] section in setup.cfg**
|
||||
|
||||
Use ``[tool:pytest]`` instead for compatibility with other tools.
|
||||
|
||||
Deprecated in ``3.0``.
|
||||
|
||||
Past Releases
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
3.0
|
||||
^^^
|
||||
|
||||
* The following deprecated commandline options were removed:
|
||||
|
||||
* ``--genscript``: no longer supported;
|
||||
* ``--no-assert``: use ``--assert=plain`` instead;
|
||||
* ``--nomagic``: use ``--assert=plain`` instead;
|
||||
* ``--report``: use ``-r`` instead;
|
||||
|
||||
* Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points
|
||||
were never documented and a leftover from a pre-virtualenv era. These entry
|
||||
points also created broken entry points in wheels, so removing them also
|
||||
removes a source of confusion for users.
|
||||
|
||||
|
||||
|
||||
3.3
|
||||
^^^
|
||||
|
||||
* Dropped support for EOL Python 2.6 and 3.3.
|
||||
@@ -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,26 @@ 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.
|
||||
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 ========
|
||||
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0
|
||||
run-last-failure: rerun last 2 failures
|
||||
=========================== 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 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 ========
|
||||
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0
|
||||
run-last-failure: rerun last 2 failures first
|
||||
=========================== 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 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,8 +222,8 @@ You can always peek at the content of the cache using the
|
||||
``--cache-show`` command line option::
|
||||
|
||||
$ py.test --cache-show
|
||||
======= test session starts ========
|
||||
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0
|
||||
=========================== 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
|
||||
------------------------------- cache values -------------------------------
|
||||
@@ -236,7 +232,7 @@ You can always peek at the content of the cache using the
|
||||
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 ========
|
||||
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0
|
||||
=========================== 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,14 +80,14 @@ 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
|
||||
|
||||
@@ -110,11 +110,26 @@ 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -12,13 +12,14 @@ Full pytest documentation
|
||||
|
||||
getting-started
|
||||
usage
|
||||
existingtestsuite
|
||||
assert
|
||||
builtin
|
||||
fixture
|
||||
monkeypatch
|
||||
tmpdir
|
||||
capture
|
||||
recwarn
|
||||
warnings
|
||||
doctest
|
||||
mark
|
||||
skipping
|
||||
@@ -29,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``:
|
||||
|
||||
@@ -45,11 +59,11 @@ Here is the algorithm which finds the rootdir from ``args``:
|
||||
matched, it becomes the ini-file and its directory becomes the rootdir.
|
||||
|
||||
- if no ini-file was found, use the already determined common ancestor as root
|
||||
directory. This allows to work with pytest in structures that are not part of
|
||||
directory. This allows the use of pytest in structures that are not part of
|
||||
a package and don't have any particular ini-file configuration.
|
||||
|
||||
If no ``args`` are given, pytest collects test below the current working
|
||||
directory and also starts determining the rootdir from there.
|
||||
directory and also starts determining the rootdir from there.
|
||||
|
||||
:warning: custom pytest plugin commandline arguments may include a path, as in
|
||||
``pytest --log-output ../../test.log args``. Then ``args`` is mandatory,
|
||||
@@ -97,6 +111,8 @@ check for ini-files as follows::
|
||||
.. _`how to change command line options defaults`:
|
||||
.. _`adding default options`:
|
||||
|
||||
|
||||
|
||||
How to change command line options defaults
|
||||
------------------------------------------------
|
||||
|
||||
@@ -110,15 +126,27 @@ 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
|
||||
@@ -158,7 +186,7 @@ Builtin configuration file options
|
||||
[seq] matches any character in seq
|
||||
[!seq] matches any char not in seq
|
||||
|
||||
Default patterns are ``'.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg'``.
|
||||
Default patterns are ``'.*', 'build', 'dist', 'CVS', '_darcs', '{arch}', '*.egg', 'venv'``.
|
||||
Setting a ``norecursedirs`` replaces the default. Here is an example of
|
||||
how to avoid certain directories:
|
||||
|
||||
@@ -169,7 +197,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
|
||||
|
||||
@@ -193,13 +230,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
|
||||
|
||||
@@ -214,7 +254,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
|
||||
@@ -240,3 +281,53 @@ Builtin configuration file options
|
||||
By default, pytest will stop searching for ``conftest.py`` files upwards
|
||||
from ``pytest.ini``/``tox.ini``/``setup.cfg`` of the project if any,
|
||||
or up to the file-system root.
|
||||
|
||||
|
||||
.. confval:: filterwarnings
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
Sets a list of filters and actions that should be taken for matched
|
||||
warnings. By default all warnings emitted during the test session
|
||||
will be displayed in a summary at the end of the test session.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
filterwarnings =
|
||||
error
|
||||
ignore::DeprecationWarning
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
@@ -11,6 +11,19 @@ can change the pattern by issuing::
|
||||
on the command line. Since version ``2.9``, ``--doctest-glob``
|
||||
can be given multiple times in the command-line.
|
||||
|
||||
.. versionadded:: 3.1
|
||||
|
||||
You can specify the encoding that will be used for those doctest files
|
||||
using the ``doctest_encoding`` ini option:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
doctest_encoding = latin1
|
||||
|
||||
The default encoding is UTF-8.
|
||||
|
||||
You can also trigger running of doctests
|
||||
from docstrings in all python modules (including regular
|
||||
python test modules)::
|
||||
@@ -48,14 +61,14 @@ and another like this::
|
||||
then you can just invoke ``pytest`` without command line options::
|
||||
|
||||
$ pytest
|
||||
======= test session starts ========
|
||||
platform linux -- Python 3.5.2, pytest-3.0.7, py-1.4.32, pluggy-0.4.0
|
||||
=========================== 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::
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ def test_attribute_multiple():
|
||||
def globf(x):
|
||||
return x+1
|
||||
|
||||
class TestRaises:
|
||||
class TestRaises(object):
|
||||
def test_raises(self):
|
||||
s = 'qwe'
|
||||
raises(TypeError, "int(s)")
|
||||
@@ -167,7 +167,7 @@ def test_dynamic_compile_shows_nicely():
|
||||
|
||||
|
||||
|
||||
class TestMoreErrors:
|
||||
class TestMoreErrors(object):
|
||||
def test_complex_error(self):
|
||||
def f():
|
||||
return 44
|
||||
@@ -213,23 +213,23 @@ class TestMoreErrors:
|
||||
x = 0
|
||||
|
||||
|
||||
class TestCustomAssertMsg:
|
||||
class TestCustomAssertMsg(object):
|
||||
|
||||
def test_single_line(self):
|
||||
class A:
|
||||
class A(object):
|
||||
a = 1
|
||||
b = 2
|
||||
assert A.a == b, "A.a appears not to be b"
|
||||
|
||||
def test_multiline(self):
|
||||
class A:
|
||||
class A(object):
|
||||
a = 1
|
||||
b = 2
|
||||
assert A.a == b, "A.a appears not to be b\n" \
|
||||
"or does not appear to be b\none of those"
|
||||
|
||||
def test_custom_repr(self):
|
||||
class JSON:
|
||||
class JSON(object):
|
||||
a = 1
|
||||
def __repr__(self):
|
||||
return "This is JSON\n{\n 'foo': 'bar'\n}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
def setup_module(module):
|
||||
module.TestStateFullThing.classcount = 0
|
||||
|
||||
class TestStateFullThing:
|
||||
class TestStateFullThing(object):
|
||||
def setup_class(cls):
|
||||
cls.classcount += 1
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ example: specifying and selecting acceptance tests
|
||||
def pytest_funcarg__accept(request):
|
||||
return AcceptFixture(request)
|
||||
|
||||
class AcceptFixture:
|
||||
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)
|
||||
|
||||
@@ -61,7 +61,7 @@ extend the `accept example`_ by putting this in our test module:
|
||||
arg.tmpdir.mkdir("special")
|
||||
return arg
|
||||
|
||||
class TestSpecialAcceptance:
|
||||
class TestSpecialAcceptance(object):
|
||||
def test_sometest(self, accept):
|
||||
assert accept.tmpdir.join("special").check()
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ def setup(request):
|
||||
yield setup
|
||||
setup.finalize()
|
||||
|
||||
class CostlySetup:
|
||||
class CostlySetup(object):
|
||||
def __init__(self):
|
||||
import time
|
||||
print ("performing costly setup")
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user