Compare commits
1379 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90c934e25e | ||
|
|
d58780f9a6 | ||
|
|
b1ab2ca963 | ||
|
|
906b40fbb2 | ||
|
|
cee578e327 | ||
|
|
29383d477d | ||
|
|
e05ff0338a | ||
|
|
b28749eb92 | ||
|
|
07623e78ce | ||
|
|
dd25ae7f33 | ||
|
|
02dc545311 | ||
|
|
b61dcded37 | ||
|
|
f71467f5b1 | ||
|
|
6aaf7ae18b | ||
|
|
6a52fe1650 | ||
|
|
5721d8aed1 | ||
|
|
3aac3d0a00 | ||
|
|
3e3f20380e | ||
|
|
bb5f200ed7 | ||
|
|
0a89db2739 | ||
|
|
d3a6be4130 | ||
|
|
6680cb9100 | ||
|
|
5fd010c4c3 | ||
|
|
21a09f0895 | ||
|
|
a88017cf26 | ||
|
|
58d7f4e048 | ||
|
|
9c809f5ad0 | ||
|
|
27f12ed0c3 | ||
|
|
0a26132232 | ||
|
|
8f98ac5ae8 | ||
|
|
ede4e9171f | ||
|
|
eeb6603d71 | ||
|
|
231e2f9a90 | ||
|
|
c4d974460c | ||
|
|
6b5566db66 | ||
|
|
49289fed52 | ||
|
|
00ec30353b | ||
|
|
58ce3a9e8c | ||
|
|
427bf42a52 | ||
|
|
b536fb7ace | ||
|
|
9eb1d73951 | ||
|
|
3d9c5cf19f | ||
|
|
6a097aa0f1 | ||
|
|
a4fb971c1f | ||
|
|
3a0a0c2df9 | ||
|
|
87fb689ab1 | ||
|
|
ccf9877447 | ||
|
|
a4d2a5785b | ||
|
|
832c89dd5f | ||
|
|
1a88a91c7a | ||
|
|
bad261279c | ||
|
|
ad56cd8027 | ||
|
|
176c680e19 | ||
|
|
da5a3dba87 | ||
|
|
e1c5314d80 | ||
|
|
36b6f17727 | ||
|
|
d1c725078a | ||
|
|
0931fe2c89 | ||
|
|
c8032a9bbb | ||
|
|
d98d122e81 | ||
|
|
beb77c1a38 | ||
|
|
d076e4158f | ||
|
|
902fd2ff6a | ||
|
|
839aa963a1 | ||
|
|
400b0779f9 | ||
|
|
c9f327dc87 | ||
|
|
0e64cd045c | ||
|
|
22da561ae5 | ||
|
|
449b88c640 | ||
|
|
34b898b47e | ||
|
|
01eaf9db51 | ||
|
|
4d0c635252 | ||
|
|
55f21bd2b9 | ||
|
|
c39d846c1b | ||
|
|
403122281a | ||
|
|
15a3b57ec7 | ||
|
|
6a96b464ab | ||
|
|
7b4afd8946 | ||
|
|
1a2d6388ac | ||
|
|
3766060893 | ||
|
|
4082f4024a | ||
|
|
e0c48b4fe7 | ||
|
|
7b4368f3f4 | ||
|
|
c477f09177 | ||
|
|
2574da8d32 | ||
|
|
d15724f74f | ||
|
|
61fa91f3d0 | ||
|
|
125e89b7f8 | ||
|
|
46a9861d29 | ||
|
|
3dfdbaf490 | ||
|
|
7cd7c283dd | ||
|
|
043aadeaf2 | ||
|
|
e18b2a427a | ||
|
|
ff309b3584 | ||
|
|
aa82db9fe2 | ||
|
|
64cb67b703 | ||
|
|
7559400183 | ||
|
|
9477f598d8 | ||
|
|
6d81c684cc | ||
|
|
b769e41d8f | ||
|
|
ef903460b1 | ||
|
|
df409a0c0e | ||
|
|
8db9915374 | ||
|
|
3d18c9c1c6 | ||
|
|
78f03888f4 | ||
|
|
03a7a2cd3e | ||
|
|
7592c5b491 | ||
|
|
091148f843 | ||
|
|
718f0b0255 | ||
|
|
b4295aa19e | ||
|
|
7d259401cd | ||
|
|
515fb09995 | ||
|
|
088b742d40 | ||
|
|
6b24ce2a9d | ||
|
|
1680eeb3a3 | ||
|
|
0bb8a4a36d | ||
|
|
f7a1d369c3 | ||
|
|
316406291d | ||
|
|
a27c824fd0 | ||
|
|
fc74eb332b | ||
|
|
bfada968d3 | ||
|
|
c5f0b751f4 | ||
|
|
f94189b48b | ||
|
|
3f5edc705a | ||
|
|
caee5ce489 | ||
|
|
1312b83866 | ||
|
|
45eb9b566c | ||
|
|
3a59acf69f | ||
|
|
81c9bdcd0b | ||
|
|
da40bcf97f | ||
|
|
a4a30ae4a2 | ||
|
|
f42a954cb3 | ||
|
|
9c285dfc1d | ||
|
|
8afca5d0fa | ||
|
|
3a0a1d2de3 | ||
|
|
6a52afc8c9 | ||
|
|
f592c7746a | ||
|
|
31f114e51f | ||
|
|
833acb9d3c | ||
|
|
0febd855e1 | ||
|
|
3c81f83602 | ||
|
|
57c4489916 | ||
|
|
5365f7c9ca | ||
|
|
1f0401ab62 | ||
|
|
7480342710 | ||
|
|
db62f160e1 | ||
|
|
81528ea81f | ||
|
|
64193add91 | ||
|
|
bc0f7e6243 | ||
|
|
9ed3d76b51 | ||
|
|
c856537e71 | ||
|
|
e612619aea | ||
|
|
30f0152ae6 | ||
|
|
f8d195253e | ||
|
|
8208a77a3e | ||
|
|
8b4da9d955 | ||
|
|
454d288138 | ||
|
|
40cffacadc | ||
|
|
6473c3d87e | ||
|
|
4e1609b12e | ||
|
|
0735d4549d | ||
|
|
f25ba4dd0b | ||
|
|
788e394c93 | ||
|
|
2d7197926a | ||
|
|
0a30f072e6 | ||
|
|
0e6ad8e59f | ||
|
|
b38fad4b82 | ||
|
|
483754216f | ||
|
|
1e97ea60f7 | ||
|
|
58f28bf049 | ||
|
|
5566b3ccb6 | ||
|
|
8138d88da2 | ||
|
|
0aa891543d | ||
|
|
6c5475660a | ||
|
|
1aa5bfea11 | ||
|
|
a6084ed797 | ||
|
|
d05d19da78 | ||
|
|
fa4d5da4ca | ||
|
|
6120570198 | ||
|
|
2e6a58ab69 | ||
|
|
c1b83cdeea | ||
|
|
33796c8a13 | ||
|
|
8763590eef | ||
|
|
b3efd9aa59 | ||
|
|
33c0b06fdf | ||
|
|
38f7562c7c | ||
|
|
629d8e9fd6 | ||
|
|
a5b5090c72 | ||
|
|
a3319ffe80 | ||
|
|
1e2b2af296 | ||
|
|
632c4d5daf | ||
|
|
984d4ce5ec | ||
|
|
1eb5a690d4 | ||
|
|
06bb61bbe3 | ||
|
|
cbf261c74e | ||
|
|
0ba930a11d | ||
|
|
73d481552d | ||
|
|
50328f47db | ||
|
|
f0e0250cd5 | ||
|
|
ec69514eb2 | ||
|
|
c169c883d3 | ||
|
|
5185f2a6ae | ||
|
|
351395b7ea | ||
|
|
71b68334e2 | ||
|
|
98caeedd9e | ||
|
|
1519b38af0 | ||
|
|
3e01e83390 | ||
|
|
ad4ef4f583 | ||
|
|
5717c71179 | ||
|
|
6c8c1da428 | ||
|
|
b8c6f13b37 | ||
|
|
8e0f7d3793 | ||
|
|
aaa547e763 | ||
|
|
26b1519534 | ||
|
|
84d7068723 | ||
|
|
ab274299fe | ||
|
|
ff72db2f1a | ||
|
|
fc304b8b44 | ||
|
|
1130b9f742 | ||
|
|
552c7d4286 | ||
|
|
1e5b21cd61 | ||
|
|
0b94c43bac | ||
|
|
e46e653794 | ||
|
|
07af307e4a | ||
|
|
a190ad27f2 | ||
|
|
f331e8f576 | ||
|
|
006a901b86 | ||
|
|
45b21fa9b0 | ||
|
|
e2bb4f893b | ||
|
|
e3544553b7 | ||
|
|
1e6ed2a25a | ||
|
|
382fa231a1 | ||
|
|
6f93ffb5d4 | ||
|
|
35d154f580 | ||
|
|
4e9c633185 | ||
|
|
7f95ea31d5 | ||
|
|
f2c01c5407 | ||
|
|
60a347aeb5 | ||
|
|
11ec96a927 | ||
|
|
37dcdfbc58 | ||
|
|
2a2b8cee09 | ||
|
|
82fb63ca2d | ||
|
|
620b384b69 | ||
|
|
5cbfefbba0 | ||
|
|
95007ddeca | ||
|
|
995e60efbf | ||
|
|
918af99a2a | ||
|
|
c0719a5b4c | ||
|
|
afc1e2b0e1 | ||
|
|
de1614923f | ||
|
|
bc1f8666aa | ||
|
|
78eec0d7f8 | ||
|
|
3301a1c173 | ||
|
|
65ebc75ee8 | ||
|
|
cf13355d3f | ||
|
|
1289cbb9a5 | ||
|
|
10433db225 | ||
|
|
50b960c1f0 | ||
|
|
d47ae799a7 | ||
|
|
c93a9e3361 | ||
|
|
a1d446b8e8 | ||
|
|
7d66e4eae1 | ||
|
|
fc02003220 | ||
|
|
336d7900c5 | ||
|
|
57bb3c6922 | ||
|
|
a87b1c79c1 | ||
|
|
30f3d95aeb | ||
|
|
ba6ecc14c8 | ||
|
|
41d3b3f4f9 | ||
|
|
dda17994ec | ||
|
|
427cee109a | ||
|
|
30d459e2e3 | ||
|
|
f31447b82b | ||
|
|
b071fdc633 | ||
|
|
19766ef0bc | ||
|
|
94155ee62a | ||
|
|
835328d862 | ||
|
|
8dc497b54b | ||
|
|
da201c7d29 | ||
|
|
01068e5571 | ||
|
|
73cab77249 | ||
|
|
09bcf7f170 | ||
|
|
f1c4cfea2c | ||
|
|
e5deb8a927 | ||
|
|
24db3c123d | ||
|
|
940ed7e943 | ||
|
|
f279bd2bd5 | ||
|
|
6db2f315fa | ||
|
|
7660a19d0a | ||
|
|
8cba033bbb | ||
|
|
6b56c36ae7 | ||
|
|
65be1231b1 | ||
|
|
8639bf7554 | ||
|
|
f484e7c9ca | ||
|
|
2bafa74765 | ||
|
|
77d842ceb2 | ||
|
|
863b7d0c50 | ||
|
|
40ec35767f | ||
|
|
04cf5e1df4 | ||
|
|
138e255631 | ||
|
|
51378aba01 | ||
|
|
8e67dd13e7 | ||
|
|
99efc281ee | ||
|
|
a2b8981b50 | ||
|
|
e21ae3991d | ||
|
|
8a41b26f56 | ||
|
|
b6f766ae87 | ||
|
|
ceeb5149f3 | ||
|
|
b38cf77562 | ||
|
|
c60854474a | ||
|
|
c69fbd79a9 | ||
|
|
183f3737d4 | ||
|
|
1e10de574d | ||
|
|
722f9eadcd | ||
|
|
7a4fe7582c | ||
|
|
96a1d2941b | ||
|
|
ee284ec587 | ||
|
|
927f411ee2 | ||
|
|
919f50a3bd | ||
|
|
4e58c9a7d0 | ||
|
|
a9f3053f72 | ||
|
|
d512e7f26b | ||
|
|
f985f47a02 | ||
|
|
4c45b93007 | ||
|
|
a094fb3aa6 | ||
|
|
e43d1174f7 | ||
|
|
696a9112be | ||
|
|
be08223d5a | ||
|
|
10b3274924 | ||
|
|
67ba8aaaa2 | ||
|
|
edf8283bd8 | ||
|
|
c8a366e551 | ||
|
|
4eeb475138 | ||
|
|
82218e4ee1 | ||
|
|
8593bb12ee | ||
|
|
e75078eae2 | ||
|
|
519f02b014 | ||
|
|
cb7c472e34 | ||
|
|
a947e83be9 | ||
|
|
e92d373460 | ||
|
|
86b8801470 | ||
|
|
856ad719d3 | ||
|
|
9c45d6cd83 | ||
|
|
a152ea2dbb | ||
|
|
8f516d27fa | ||
|
|
3345ac9216 | ||
|
|
972a5fb5d5 | ||
|
|
ea0febad28 | ||
|
|
ecb20b96fc | ||
|
|
49fc4e5e4c | ||
|
|
3c866e7080 | ||
|
|
df200297e2 | ||
|
|
7538680c98 | ||
|
|
d3f4b3d14a | ||
|
|
a1597aca89 | ||
|
|
7d498fdc82 | ||
|
|
2f11a85698 | ||
|
|
847067fb02 | ||
|
|
1673667232 | ||
|
|
53a0e2b118 | ||
|
|
d99ceb1218 | ||
|
|
b54ea74d4d | ||
|
|
6a8160b318 | ||
|
|
c3148d1d6b | ||
|
|
b0ede044ac | ||
|
|
3fbf2e7a80 | ||
|
|
af0ec120fe | ||
|
|
678750c8f8 | ||
|
|
4e9c1fbe96 | ||
|
|
406777d104 | ||
|
|
abe8f5e23f | ||
|
|
6a0e849067 | ||
|
|
a20c3f9c44 | ||
|
|
783aff17dd | ||
|
|
86ec3f37af | ||
|
|
e306a53999 | ||
|
|
90fb8cb08b | ||
|
|
5129c2f867 | ||
|
|
87d2d1d838 | ||
|
|
9aec8d9a47 | ||
|
|
c8f53d6690 | ||
|
|
875bcd4224 | ||
|
|
63dc71c57e | ||
|
|
3a200b75c9 | ||
|
|
745c8c17f1 | ||
|
|
5ecf3d78f1 | ||
|
|
b85a3b0d71 | ||
|
|
8777f61e7b | ||
|
|
31ede2432c | ||
|
|
c8fbf3ae34 | ||
|
|
3455dfc804 | ||
|
|
cd39cc1eec | ||
|
|
ccfa8d15bf | ||
|
|
799dab9dba | ||
|
|
c74ce371ab | ||
|
|
7f18367582 | ||
|
|
a3e6c14da3 | ||
|
|
be9356aeba | ||
|
|
9ce30e0085 | ||
|
|
3748112a94 | ||
|
|
0334e75c30 | ||
|
|
56df9fcc72 | ||
|
|
66673c0dd3 | ||
|
|
030c42203d | ||
|
|
3ba475c0f2 | ||
|
|
463e6572c5 | ||
|
|
3e685d6a8d | ||
|
|
68ebf552a1 | ||
|
|
a01cbce662 | ||
|
|
0fb34cd2a1 | ||
|
|
224ef67374 | ||
|
|
4ed412eb59 | ||
|
|
92498109e4 | ||
|
|
dfc659f781 | ||
|
|
0173952961 | ||
|
|
767c28d422 | ||
|
|
d1f2f779ee | ||
|
|
bb3d6d87b6 | ||
|
|
018197d72a | ||
|
|
ea379e0e4f | ||
|
|
789e4670e7 | ||
|
|
c8ab79402c | ||
|
|
ab86dea529 | ||
|
|
707b6b5e3f | ||
|
|
09e647c7d9 | ||
|
|
f25771a101 | ||
|
|
55ec1d7f56 | ||
|
|
497152505e | ||
|
|
ca5957932b | ||
|
|
d58a8e36d7 | ||
|
|
d3b855104c | ||
|
|
c163cc7937 | ||
|
|
16cb5d01b1 | ||
|
|
5b95ee3c19 | ||
|
|
225341cf2c | ||
|
|
14a4dd0697 | ||
|
|
296f42a2c9 | ||
|
|
10a6ed1707 | ||
|
|
34925a31a9 | ||
|
|
c4d9c7ea55 | ||
|
|
e4028b4505 | ||
|
|
99a4a1a784 | ||
|
|
4ab2e57ebd | ||
|
|
802755ceed | ||
|
|
ac5c39e534 | ||
|
|
21230aa017 | ||
|
|
eb08135280 | ||
|
|
a2891420de | ||
|
|
4fc20d09fe | ||
|
|
1a79137d04 | ||
|
|
530b0050b4 | ||
|
|
ff296fd541 | ||
|
|
08002ab75a | ||
|
|
6759b042b5 | ||
|
|
8b8c698f1a | ||
|
|
d28801d794 | ||
|
|
72df32f1fd | ||
|
|
701d5fc727 | ||
|
|
6e3105dc8f | ||
|
|
6711b1d6ab | ||
|
|
d5be6cba13 | ||
|
|
277b6d3974 | ||
|
|
ea6191a0cd | ||
|
|
9540106fe7 | ||
|
|
1c8fe962f3 | ||
|
|
48f4e18280 | ||
|
|
21a90c8c50 | ||
|
|
eed21e06db | ||
|
|
a6b2732507 | ||
|
|
946466abf4 | ||
|
|
3cd2e37c55 | ||
|
|
553dc2600f | ||
|
|
226f2795ba | ||
|
|
b380eb5b34 | ||
|
|
44ecf2ab2f | ||
|
|
31c5194562 | ||
|
|
3c8222f1db | ||
|
|
ac215e9cff | ||
|
|
aa145fa83e | ||
|
|
76fbc6379f | ||
|
|
80508203b0 | ||
|
|
eaf8d9ce19 | ||
|
|
510a6083ba | ||
|
|
ffb583ae91 | ||
|
|
ae9d3bf886 | ||
|
|
7b1520687e | ||
|
|
fd765f854c | ||
|
|
ed36d627e4 | ||
|
|
9a68681719 | ||
|
|
0b8a91b858 | ||
|
|
5565c1f4ae | ||
|
|
c40dcb3c18 | ||
|
|
05728d1317 | ||
|
|
b4ad4cc46e | ||
|
|
c2864aba3d | ||
|
|
9cf09cda7b | ||
|
|
eb5b163698 | ||
|
|
d911bfcb8a | ||
|
|
8f29ce26e9 | ||
|
|
6c8b0a28e1 | ||
|
|
d72afe7e08 | ||
|
|
ab6aef1d1f | ||
|
|
a2b04d02c2 | ||
|
|
f7ad173fee | ||
|
|
d37af20527 | ||
|
|
a309a571d9 | ||
|
|
e9d729bd46 | ||
|
|
c9a2e611ce | ||
|
|
a24146dd3c | ||
|
|
7862517c28 | ||
|
|
42adaf5a61 | ||
|
|
4aede6faa6 | ||
|
|
d000f2536a | ||
|
|
0ae77be9f0 | ||
|
|
5c5d7e05f7 | ||
|
|
f450e0a1db | ||
|
|
fabe8cda2f | ||
|
|
3c4158ac35 | ||
|
|
e00199212c | ||
|
|
e9a67e6702 | ||
|
|
6799a47c78 | ||
|
|
655df7f839 | ||
|
|
9891593413 | ||
|
|
bbc7f3a631 | ||
|
|
1704b7d265 | ||
|
|
3631719050 | ||
|
|
f26fa5a441 | ||
|
|
94731fc2a1 | ||
|
|
51fa244650 | ||
|
|
d5a70acd48 | ||
|
|
018acc6bae | ||
|
|
f8f690de64 | ||
|
|
e229a27e8b | ||
|
|
ec7695e15d | ||
|
|
1a33025a76 | ||
|
|
922a295729 | ||
|
|
014ebc9202 | ||
|
|
87ca4b95fb | ||
|
|
fd8e019cc1 | ||
|
|
625b603f1f | ||
|
|
38e0e08074 | ||
|
|
1aab6e3bc2 | ||
|
|
7e37497d5a | ||
|
|
ae0798522f | ||
|
|
832ada1b44 | ||
|
|
4c112401c5 | ||
|
|
05f3422d7c | ||
|
|
4f2bf965cb | ||
|
|
eaa4ee3fdf | ||
|
|
6aea164b6d | ||
|
|
20f97c3041 | ||
|
|
e0f08a73ab | ||
|
|
93aae987a2 | ||
|
|
1204cbade4 | ||
|
|
9dadaa8a41 | ||
|
|
0403266bf0 | ||
|
|
3fd8257c17 | ||
|
|
2a05c311e9 | ||
|
|
bcc58ec916 | ||
|
|
9af872a230 | ||
|
|
61cc5c4d4e | ||
|
|
317b3f257d | ||
|
|
2b9973846e | ||
|
|
58a8150bc5 | ||
|
|
0ac3eaa1db | ||
|
|
0a53797fa3 | ||
|
|
8a73a2ad60 | ||
|
|
fb21493856 | ||
|
|
ff8fb4950e | ||
|
|
d1852a48b7 | ||
|
|
ee374e3b80 | ||
|
|
3328cd2620 | ||
|
|
350ebc9167 | ||
|
|
24fbbbef1f | ||
|
|
22bb43413f | ||
|
|
691dc8bc68 | ||
|
|
14af12cb7b | ||
|
|
51ee7f8734 | ||
|
|
9007e16cdf | ||
|
|
02dd7d612a | ||
|
|
ab0b6faa5f | ||
|
|
1fb09d9dd5 | ||
|
|
1266ebec83 | ||
|
|
6e9ee2b766 | ||
|
|
3cfebdd7c5 | ||
|
|
743f59afb2 | ||
|
|
944da5b98a | ||
|
|
a98e3cefc5 | ||
|
|
dd5ce96cd7 | ||
|
|
aeccd6b4a2 | ||
|
|
44c3055e79 | ||
|
|
4a763accc5 | ||
|
|
dfe1209d2c | ||
|
|
54ea27c283 | ||
|
|
f827810fa8 | ||
|
|
15e97a7c78 | ||
|
|
c4f20a1834 | ||
|
|
4c56c95eb8 | ||
|
|
7ee3dd1cb5 | ||
|
|
458ecae1df | ||
|
|
ad4125dc0d | ||
|
|
5506dc700c | ||
|
|
0dd1c8bf14 | ||
|
|
0ca06962e9 | ||
|
|
2ffe354f21 | ||
|
|
fb4da00a32 | ||
|
|
6f68dfcc47 | ||
|
|
6383b53ad9 | ||
|
|
8ed055efd8 | ||
|
|
775100881a | ||
|
|
29289b472f | ||
|
|
8c49561470 | ||
|
|
7a2058e3db | ||
|
|
668ebb102c | ||
|
|
293351cfd0 | ||
|
|
dad6aa8a16 | ||
|
|
b9a91dc112 | ||
|
|
f31c31a73c | ||
|
|
94e4a2dd67 | ||
|
|
0171cfa30f | ||
|
|
61e605f60b | ||
|
|
cc0920ceb1 | ||
|
|
067e044f97 | ||
|
|
10c5e6fd9c | ||
|
|
8d39ce17da | ||
|
|
6438895a23 | ||
|
|
b650c3c118 | ||
|
|
f7b5bb2f97 | ||
|
|
f74dd8550f | ||
|
|
e3c43a1462 | ||
|
|
7927dff8a1 | ||
|
|
1451a1ab00 | ||
|
|
75ecd94294 | ||
|
|
771c4539fa | ||
|
|
2a43237527 | ||
|
|
7dc8d1ab60 | ||
|
|
1e60294188 | ||
|
|
e877e25587 | ||
|
|
21d27784eb | ||
|
|
ccd395ffe0 | ||
|
|
76756c0c0b | ||
|
|
44695ae46c | ||
|
|
7e78965c79 | ||
|
|
58e558141d | ||
|
|
22c2d87633 | ||
|
|
5891061ac1 | ||
|
|
48ac1a0986 | ||
|
|
db9b3e9522 | ||
|
|
b9536608c5 | ||
|
|
f7a2048e96 | ||
|
|
7239f36466 | ||
|
|
1b0dbd8c40 | ||
|
|
0e2ebc96ff | ||
|
|
b4e0fabf93 | ||
|
|
1ce4670062 | ||
|
|
6d4cee2159 | ||
|
|
0db4ae15a9 | ||
|
|
c17027e576 | ||
|
|
e04d9ff80b | ||
|
|
e2f550156e | ||
|
|
68bed00d5b | ||
|
|
856e6cab75 | ||
|
|
13a188fe37 | ||
|
|
3a9e9fdf82 | ||
|
|
72450408ed | ||
|
|
95b83958f4 | ||
|
|
2af3a7a9d7 | ||
|
|
35cd12e4de | ||
|
|
1b6bc4d606 | ||
|
|
c519b9517a | ||
|
|
eb98a8eefb | ||
|
|
acfdd85dff | ||
|
|
e0242146ec | ||
|
|
df17f862fa | ||
|
|
7eb1318db2 | ||
|
|
ce603dc0ea | ||
|
|
70ea3ce7f7 | ||
|
|
9a5224e2f8 | ||
|
|
32ca5cdb09 | ||
|
|
891e1677b6 | ||
|
|
9877bf47e3 | ||
|
|
7a3daac85b | ||
|
|
da5c579d82 | ||
|
|
032ce8baf6 | ||
|
|
05b5554cac | ||
|
|
5860c609ae | ||
|
|
526c564576 | ||
|
|
693859210a | ||
|
|
4f8b8c8d31 | ||
|
|
c6a711c2fc | ||
|
|
84f0dcecf8 | ||
|
|
757f37f445 | ||
|
|
939407ef63 | ||
|
|
b3615ac29d | ||
|
|
d81f23009b | ||
|
|
0c63762d9c | ||
|
|
7612b650a0 | ||
|
|
62255d8000 | ||
|
|
ea5bda0898 | ||
|
|
77689eb486 | ||
|
|
3d263c64c3 | ||
|
|
dc55551213 | ||
|
|
df9918eda3 | ||
|
|
c6af737d4e | ||
|
|
1a5e530b98 | ||
|
|
f2bb3df310 | ||
|
|
6359e75ff8 | ||
|
|
69bab4ab04 | ||
|
|
ecc97aa3b9 | ||
|
|
dd97a2e7c8 | ||
|
|
0dbf77e08e | ||
|
|
f7d50dfa91 | ||
|
|
7d60fcc098 | ||
|
|
c8c32fd9c0 | ||
|
|
61992b4e22 | ||
|
|
f59d8f7720 | ||
|
|
18ef7de96b | ||
|
|
e96cd51a2a | ||
|
|
f7585c7549 | ||
|
|
9cb851716d | ||
|
|
a2420ce051 | ||
|
|
be1dabd6a9 | ||
|
|
5e0d78f4f1 | ||
|
|
083f64100d | ||
|
|
db79ed5c4d | ||
|
|
fb79fa711b | ||
|
|
1a75139f72 | ||
|
|
ee311e1eae | ||
|
|
2c5c4f3f78 | ||
|
|
2c6cfa42fa | ||
|
|
de9ed5e3f4 | ||
|
|
92bcc36266 | ||
|
|
7d19f83982 | ||
|
|
7d87a1b127 | ||
|
|
6874c3a3cc | ||
|
|
24179dc99f | ||
|
|
ec25d398a5 | ||
|
|
98adf204b2 | ||
|
|
499c9551c8 | ||
|
|
144dc12e55 | ||
|
|
09389d2b20 | ||
|
|
c3ee1c17bc | ||
|
|
4350f499b2 | ||
|
|
61ede096a3 | ||
|
|
2f9d5c2586 | ||
|
|
f05d65dc42 | ||
|
|
573866bfad | ||
|
|
2305d3271d | ||
|
|
406355fd10 | ||
|
|
393167b94f | ||
|
|
ef9dd14963 | ||
|
|
2b5c2f3ed5 | ||
|
|
ede7478dcc | ||
|
|
819942e964 | ||
|
|
8b0fb47c79 | ||
|
|
5854a71ece | ||
|
|
5d8d1db4df | ||
|
|
9118c0222f | ||
|
|
e53e45b55c | ||
|
|
e0cb046885 | ||
|
|
3a81d2e012 | ||
|
|
e9d7989140 | ||
|
|
54872e94b4 | ||
|
|
4f2db6c08d | ||
|
|
f3aead8e49 | ||
|
|
f8d4cadc3d | ||
|
|
c29130d400 | ||
|
|
ca093673fb | ||
|
|
d81ee9acfb | ||
|
|
72bf11cbe9 | ||
|
|
e6ff01ada3 | ||
|
|
8ddbca36c9 | ||
|
|
d21886c005 | ||
|
|
7f8e315285 | ||
|
|
c5424643f0 | ||
|
|
ab8b2e75a3 | ||
|
|
2a3cbdf4d1 | ||
|
|
308396ae3c | ||
|
|
feeee2803e | ||
|
|
1218392413 | ||
|
|
4d9e293b4d | ||
|
|
e2e6e31711 | ||
|
|
adc50ac72f | ||
|
|
66e66f61e8 | ||
|
|
accd962c9f | ||
|
|
b99aace8a9 | ||
|
|
bbc6c18448 | ||
|
|
7eea168106 | ||
|
|
b47f155d74 | ||
|
|
577cce2554 | ||
|
|
bdc29968b8 | ||
|
|
ed69424917 | ||
|
|
371fbe4388 | ||
|
|
fe4f23c1bf | ||
|
|
98acda426f | ||
|
|
d712428d33 | ||
|
|
366879db27 | ||
|
|
92323895c9 | ||
|
|
09d163aa3a | ||
|
|
70fdab4cfa | ||
|
|
3ad5b9de86 | ||
|
|
9b6dc93496 | ||
|
|
2c4b76b754 | ||
|
|
63ced4d486 | ||
|
|
97c89e6dc3 | ||
|
|
dd9c81ca26 | ||
|
|
74862b8f2f | ||
|
|
057007fb52 | ||
|
|
2898dffb9e | ||
|
|
7305adfdba | ||
|
|
fad6266a47 | ||
|
|
b5bd4d959d | ||
|
|
f423f08b45 | ||
|
|
5c8b0fb523 | ||
|
|
978bb190a1 | ||
|
|
a2a904466c | ||
|
|
77c28825df | ||
|
|
d3dcc2b8f1 | ||
|
|
85541113eb | ||
|
|
7ef06822cb | ||
|
|
289e0091de | ||
|
|
28efdebfcd | ||
|
|
fb2e7cc727 | ||
|
|
fb8ad714b1 | ||
|
|
945072b89a | ||
|
|
0d80a9c729 | ||
|
|
357a7c79ef | ||
|
|
158f3cfaea | ||
|
|
82c74fe7e6 | ||
|
|
afc5c7e4f6 | ||
|
|
03eb9203fd | ||
|
|
ae4dff0e0a | ||
|
|
d217b52508 | ||
|
|
8c1be624a6 | ||
|
|
16794feaf6 | ||
|
|
436e13ac25 | ||
|
|
9fb5ddf778 | ||
|
|
5ab5a11544 | ||
|
|
26b526967e | ||
|
|
d6dfb1a393 | ||
|
|
85393d34b6 | ||
|
|
be7a86270c | ||
|
|
d4c9fa9f1a | ||
|
|
ec5e05834f | ||
|
|
6f98cd6faa | ||
|
|
561a5fb558 | ||
|
|
47739291cf | ||
|
|
8a39869347 | ||
|
|
eab762ea99 | ||
|
|
c49863aa63 | ||
|
|
01d2ff804b | ||
|
|
68f658b6cc | ||
|
|
4bde70d060 | ||
|
|
8a94c66e68 | ||
|
|
0d07b64571 | ||
|
|
98dd2ce75c | ||
|
|
308e76e19c | ||
|
|
60212e8831 | ||
|
|
75abfbe8d4 | ||
|
|
6cc56b4a1b | ||
|
|
9733127951 | ||
|
|
fdee88f086 | ||
|
|
53429ed8b8 | ||
|
|
b9faf78d51 | ||
|
|
79927428d1 | ||
|
|
56855893ca | ||
|
|
52babba33e | ||
|
|
8552860a9b | ||
|
|
f02f72e651 | ||
|
|
6d661ace0a | ||
|
|
f51c34ef31 | ||
|
|
b220c96bf8 | ||
|
|
aa87395c39 | ||
|
|
75160547f2 | ||
|
|
4c552d4ef7 | ||
|
|
b607f6728f | ||
|
|
a986b8f945 | ||
|
|
c24e8e01b4 | ||
|
|
1a37035d71 | ||
|
|
99c4b6fdc3 | ||
|
|
dd2425675b | ||
|
|
6a3c943ce2 | ||
|
|
98430a17f2 | ||
|
|
fe6e1b2059 | ||
|
|
7ce5873da2 | ||
|
|
0eb80bcb5a | ||
|
|
fb45f82840 | ||
|
|
0f7aeafe7c | ||
|
|
e3bc6faa2b | ||
|
|
909d72b474 | ||
|
|
0c38aacc33 | ||
|
|
c578226d43 | ||
|
|
23a8e2b469 | ||
|
|
08671fcf4a | ||
|
|
491b30c5d9 | ||
|
|
b631fc0bc1 | ||
|
|
5af5ba11d3 | ||
|
|
053c052190 | ||
|
|
9b438d56e8 | ||
|
|
56156bb119 | ||
|
|
e048315d9b | ||
|
|
89df701ae9 | ||
|
|
fed89ef549 | ||
|
|
3ffce6ae4a | ||
|
|
c66aedfa65 | ||
|
|
3155d0ca9c | ||
|
|
53d319144d | ||
|
|
412042d987 | ||
|
|
b8c15a0215 | ||
|
|
1f46015de5 | ||
|
|
6ddfd60ce7 | ||
|
|
653a53226a | ||
|
|
1fbd19b8cb | ||
|
|
4405dd0ffe | ||
|
|
890c2fa555 | ||
|
|
725290a8ab | ||
|
|
da1045151f | ||
|
|
98c707561c | ||
|
|
a341dddc74 | ||
|
|
dd384f7f78 | ||
|
|
7885e43b78 | ||
|
|
32f44ce2fd | ||
|
|
16e49d96d1 | ||
|
|
ec62a3c9e4 | ||
|
|
f70ed83479 | ||
|
|
dff914cadd | ||
|
|
3135463573 | ||
|
|
266b53dfc2 | ||
|
|
bdb3581a52 | ||
|
|
27b62740e3 | ||
|
|
52bc2f8616 | ||
|
|
a736e26734 | ||
|
|
fbc5ba08d9 | ||
|
|
4b0237c8ee | ||
|
|
877ca5a0bf | ||
|
|
a8cfd54871 | ||
|
|
be1954afbc | ||
|
|
2cfcf12d09 | ||
|
|
5fcce8a7d6 | ||
|
|
a70e92777f | ||
|
|
3c011c05db | ||
|
|
168daaa71f | ||
|
|
43fc1b47c0 | ||
|
|
699f094b0c | ||
|
|
9bdf51fcc5 | ||
|
|
1d23999033 | ||
|
|
1d35a03812 | ||
|
|
ceacc12b52 | ||
|
|
fa6acdcfd4 | ||
|
|
d70596da91 | ||
|
|
a1277aaf0e | ||
|
|
5fd82078ad | ||
|
|
5ceee08590 | ||
|
|
0dcc862a56 | ||
|
|
9e7206a1cf | ||
|
|
19cec79363 | ||
|
|
0cacdef6c5 | ||
|
|
981fcb2798 | ||
|
|
1ee3d40dbe | ||
|
|
861265411f | ||
|
|
bdddc9c38b | ||
|
|
916c0a8b36 | ||
|
|
078448008c | ||
|
|
b46c7dddaa | ||
|
|
42a7e0488d | ||
|
|
4636bf6160 | ||
|
|
52a5acda92 | ||
|
|
2b0ad4630d | ||
|
|
3ea987ef9d | ||
|
|
9577120592 | ||
|
|
7a186df271 | ||
|
|
7d155bd3cf | ||
|
|
6a902924f8 | ||
|
|
c9c73b8d8e | ||
|
|
4d0f066db7 | ||
|
|
5dab0954a0 | ||
|
|
b8a8382c2c | ||
|
|
bf97d5b817 | ||
|
|
dd28e28b34 | ||
|
|
6f5e1e386a | ||
|
|
6d4b14d7ee | ||
|
|
fd0010e6e9 | ||
|
|
8ce32b0795 | ||
|
|
5d4703852e | ||
|
|
9b51536a18 | ||
|
|
d8403d793f | ||
|
|
24d3e01548 | ||
|
|
3884398055 | ||
|
|
819e0ead44 | ||
|
|
c8ca1d12d7 | ||
|
|
3d2b7aeea5 | ||
|
|
7aa7c6bbfd | ||
|
|
c2b9196a7c | ||
|
|
4a76b2e9b6 | ||
|
|
28937a5cd9 | ||
|
|
2e02a1c370 | ||
|
|
28530836c9 | ||
|
|
055f1dc9ea | ||
|
|
0cbd58e16a | ||
|
|
b7b863b7bf | ||
|
|
4bcc06362d | ||
|
|
6dd2ff5332 | ||
|
|
891e029518 | ||
|
|
316e39872a | ||
|
|
5a2500800d | ||
|
|
c8c5a416ef | ||
|
|
81af1c024a | ||
|
|
e656dbb602 | ||
|
|
63b69326b8 | ||
|
|
19d05814d2 | ||
|
|
7d2b65813e | ||
|
|
f82c03833f | ||
|
|
486421fca2 | ||
|
|
2d7cfcd686 | ||
|
|
623e786524 | ||
|
|
5c09b33150 | ||
|
|
7e758a9dc6 | ||
|
|
3445bdaca2 | ||
|
|
89151b8c63 | ||
|
|
4194ddd5b4 | ||
|
|
ab90043adc | ||
|
|
a95fe3693b | ||
|
|
b64aaac7ec | ||
|
|
424b46de1b | ||
|
|
c9927bb66f | ||
|
|
cbb5d48fdd | ||
|
|
d98d655094 | ||
|
|
cf9a09e988 | ||
|
|
d9ede1bac2 | ||
|
|
5f90907509 | ||
|
|
310bada6f5 | ||
|
|
08b40396c9 | ||
|
|
a50209b29e | ||
|
|
a7b907d325 | ||
|
|
c78a8b28dc | ||
|
|
3a6a0f1220 | ||
|
|
fc4e240596 | ||
|
|
5507a4d239 | ||
|
|
01479189a5 | ||
|
|
ddb7060535 | ||
|
|
96a331e32f | ||
|
|
688622f5cf | ||
|
|
fa7b0086a2 | ||
|
|
3874d53ee1 | ||
|
|
b76de91474 | ||
|
|
6940f82235 | ||
|
|
f6a2f779ae | ||
|
|
19536c9f05 | ||
|
|
e4c1b9c1c4 | ||
|
|
25aed0dca8 | ||
|
|
b2f837acd8 | ||
|
|
3ef003f0ff | ||
|
|
baabde76ae | ||
|
|
c805412d47 | ||
|
|
e4d361b093 | ||
|
|
bed56e504a | ||
|
|
3dd50d039d | ||
|
|
0eeb466f11 | ||
|
|
ee88679c54 | ||
|
|
9af1f63ab6 | ||
|
|
fb7bbf816c | ||
|
|
9aae4b782b | ||
|
|
1d190dc618 | ||
|
|
7823838e69 | ||
|
|
a965386b9e | ||
|
|
6f0d90cd5a | ||
|
|
48424a6bf6 | ||
|
|
2a8d58814a | ||
|
|
fa601de5c4 | ||
|
|
8284d14ec4 | ||
|
|
238dcd8bae | ||
|
|
b95ff7104c | ||
|
|
03f8b50c8a | ||
|
|
dc7f76c276 | ||
|
|
20bd56f4b2 | ||
|
|
89c75b2c91 | ||
|
|
341bc33ff3 | ||
|
|
fe10057c15 | ||
|
|
48b62e4d89 | ||
|
|
1b431d6644 | ||
|
|
b1955c7f84 | ||
|
|
bfa2fadac1 | ||
|
|
48109b0e60 | ||
|
|
fdce2306a7 | ||
|
|
f00577f7c4 | ||
|
|
569dbeb087 | ||
|
|
c6938221ab | ||
|
|
2b764a0e73 | ||
|
|
51694b8295 | ||
|
|
2e04771893 | ||
|
|
7d107018e8 | ||
|
|
05aad5c381 | ||
|
|
1e0088a949 | ||
|
|
ba3b29e831 | ||
|
|
6218e20e88 | ||
|
|
190a52badb | ||
|
|
7b2956e10b | ||
|
|
de1a9f574c | ||
|
|
05a26d995b | ||
|
|
79722ae89b | ||
|
|
b5dc7d9be1 | ||
|
|
30e61f2777 | ||
|
|
58af604f82 | ||
|
|
c0024a723d | ||
|
|
545bf0d5a1 | ||
|
|
70b5d5aee9 | ||
|
|
fde44f4f30 | ||
|
|
fa4d832507 | ||
|
|
74a68b5ec6 | ||
|
|
e35ce98f89 | ||
|
|
dd0062c177 | ||
|
|
6c37a51f95 | ||
|
|
52ac6cd7a9 | ||
|
|
4825678e1a | ||
|
|
e43eaffd93 | ||
|
|
9f85d4c952 | ||
|
|
7a6f902f6f | ||
|
|
a912d3745b | ||
|
|
f23307b06d | ||
|
|
6c3e6401d4 | ||
|
|
3315b3a12f | ||
|
|
64d7d00218 | ||
|
|
7c747c97ec | ||
|
|
56c5db6e12 | ||
|
|
2d05f831fe | ||
|
|
cb6181255e | ||
|
|
cd9e30b221 | ||
|
|
d028fe1e66 | ||
|
|
b825af2e66 | ||
|
|
60e9698530 | ||
|
|
9e6bb74d71 | ||
|
|
ed3c96ee58 | ||
|
|
199fcf93d4 | ||
|
|
c8caa87759 | ||
|
|
b7de0401b8 | ||
|
|
01793ed8bc | ||
|
|
82d00efa8d | ||
|
|
61c569f960 | ||
|
|
dd56d7b7fc | ||
|
|
4de3d595c9 | ||
|
|
b28b3cc271 | ||
|
|
99072ea8c9 | ||
|
|
11a7bcaaa5 | ||
|
|
0caee1a673 | ||
|
|
4c87a6aa09 | ||
|
|
7b13c4bec0 | ||
|
|
aa8c352c10 | ||
|
|
ee75ecbda0 | ||
|
|
bc32e45bb6 | ||
|
|
3e5c9038ec | ||
|
|
b2c0864fbf | ||
|
|
a80efb038a | ||
|
|
5b29f579c5 | ||
|
|
808cb8e3ad | ||
|
|
8727503dd4 | ||
|
|
f46de68804 | ||
|
|
3c19cfcd9a | ||
|
|
b8784c28c9 | ||
|
|
29b05c8391 | ||
|
|
26c835eea5 | ||
|
|
eebf5c1d2c | ||
|
|
3daa0756eb | ||
|
|
3e34db50fb | ||
|
|
e2603d7050 | ||
|
|
369d9ecaa5 | ||
|
|
02dd6df6e6 | ||
|
|
6c170201d6 | ||
|
|
bf4de4bd68 | ||
|
|
80d6d94635 | ||
|
|
63cba1ed0d | ||
|
|
71ab6b8b05 | ||
|
|
c367180ab2 | ||
|
|
1bdf71730a | ||
|
|
0ea8dc0d40 | ||
|
|
638b3f5e39 | ||
|
|
309ecf7ab3 | ||
|
|
719d63085d | ||
|
|
5a5b732fe1 | ||
|
|
a0edbb75a4 | ||
|
|
0ef73ed3e0 | ||
|
|
7cfb750d7f | ||
|
|
70f72229c6 | ||
|
|
2e02579437 | ||
|
|
8d49abb0d1 | ||
|
|
c5631b6567 | ||
|
|
522224ee7c | ||
|
|
015e8e574a | ||
|
|
87ff7ee232 | ||
|
|
b5490b289d | ||
|
|
46039f8687 | ||
|
|
6b25fb4d64 | ||
|
|
99a5067edb | ||
|
|
5afb61ad26 | ||
|
|
fbfab6778c | ||
|
|
57bc14caa0 | ||
|
|
df3f21afb6 | ||
|
|
1a87bb2416 | ||
|
|
6e170a4a1c | ||
|
|
924a9667e1 | ||
|
|
319f6310f8 | ||
|
|
cd3a441304 | ||
|
|
8180165229 | ||
|
|
ec5a429c77 | ||
|
|
713069ebd4 | ||
|
|
f7af08d309 | ||
|
|
943099ddd1 | ||
|
|
379562107e | ||
|
|
25c392196f | ||
|
|
ec597e81a4 | ||
|
|
81588d7f63 | ||
|
|
af893aab26 | ||
|
|
8bf7e7cc4b | ||
|
|
7b20288c2b | ||
|
|
2b2bec6b97 | ||
|
|
fcc20d4181 | ||
|
|
6ac31088c5 | ||
|
|
7eea6b3b02 | ||
|
|
e87facfb22 | ||
|
|
855b115dab | ||
|
|
4263b8b407 | ||
|
|
7d150c20cf | ||
|
|
a124163425 | ||
|
|
2382546112 | ||
|
|
946bb08da5 | ||
|
|
85d1f0404a | ||
|
|
1d60f61ba8 | ||
|
|
ad05cbe6da | ||
|
|
1216a27b44 | ||
|
|
2b2240e904 | ||
|
|
74f7efd2a3 | ||
|
|
34db8aed34 | ||
|
|
926c6028bb | ||
|
|
af54e09759 | ||
|
|
dfaeefd692 | ||
|
|
86b6ce5042 | ||
|
|
8f880e1625 | ||
|
|
46c85bc352 | ||
|
|
8b61a332ba | ||
|
|
139c97930b | ||
|
|
9cfee82f9b | ||
|
|
c0a5f3df10 | ||
|
|
8220c05b01 | ||
|
|
d0f5f6676b | ||
|
|
ec02f694ef | ||
|
|
6351e2846c | ||
|
|
1c70827f33 | ||
|
|
ccfd962170 | ||
|
|
b417d7cb79 | ||
|
|
1c46462991 | ||
|
|
5ccb7b1ced | ||
|
|
eabf2f9091 | ||
|
|
1db4cbcc9f | ||
|
|
fbac936596 | ||
|
|
ffa572531a | ||
|
|
fde2a6f5fd | ||
|
|
7b7737bf96 | ||
|
|
04e9ae75c8 | ||
|
|
9ea7826427 | ||
|
|
09cc45b0c5 | ||
|
|
0aa54101c9 | ||
|
|
5eef6a2821 | ||
|
|
518c88f149 | ||
|
|
5f5a7995b9 | ||
|
|
0528e5b45f | ||
|
|
9b04958303 | ||
|
|
faed54d6c7 | ||
|
|
1f609f96e6 | ||
|
|
0664ae137c | ||
|
|
d0107c898e | ||
|
|
9128fec4c4 | ||
|
|
80bcf8d624 | ||
|
|
b8df5446c0 | ||
|
|
2a31df072b | ||
|
|
02f5defd89 | ||
|
|
efb5332023 | ||
|
|
b9908cc036 | ||
|
|
c727860241 | ||
|
|
8c17c7cd12 | ||
|
|
b7459b8a64 | ||
|
|
b920f09a95 | ||
|
|
a3353c49fd | ||
|
|
a4a12b8356 | ||
|
|
13ae2fe28b | ||
|
|
141a463fed | ||
|
|
f508a52ca9 | ||
|
|
b48a02fdb1 | ||
|
|
382efc6363 | ||
|
|
1bed514eb6 | ||
|
|
41f19796e8 | ||
|
|
427e6c3b4d | ||
|
|
14bc3c4009 | ||
|
|
7e063eec08 | ||
|
|
61934ae82d | ||
|
|
8c74bb0d25 | ||
|
|
bb4771cedf | ||
|
|
9475cd3fb8 | ||
|
|
464e16deca | ||
|
|
d9b78f2a95 | ||
|
|
7232b45f25 | ||
|
|
a54e4e64cd | ||
|
|
edfb567091 | ||
|
|
6a2ebddc7c | ||
|
|
5040dde0c5 | ||
|
|
095abfd035 | ||
|
|
69ef0ab189 | ||
|
|
d851a8fd07 | ||
|
|
0704fcacd7 | ||
|
|
4f17d56ecb | ||
|
|
b1f6dc23da | ||
|
|
c6f90c25e3 | ||
|
|
f0e5cb362e | ||
|
|
c7cf4adfd0 | ||
|
|
def543924b | ||
|
|
6be6798cdf | ||
|
|
ce4eb51ee0 | ||
|
|
0d2668017d | ||
|
|
e7e4860ded | ||
|
|
aba55a0fb2 | ||
|
|
b5d65e5139 | ||
|
|
3a3f0f5c56 | ||
|
|
ba9146c131 | ||
|
|
c790f7475e | ||
|
|
f9b1e39b8a | ||
|
|
81ad1689b9 | ||
|
|
44f60ba141 | ||
|
|
bced5a3f81 | ||
|
|
a8d7e513f4 | ||
|
|
604a021a2a | ||
|
|
603d81ef2f | ||
|
|
6378cdf7a9 | ||
|
|
84eacf3e3c | ||
|
|
320c95ca43 | ||
|
|
b20803f0a6 | ||
|
|
df767cca8f | ||
|
|
b3166a538c | ||
|
|
1f148a93ec | ||
|
|
af46ffe021 | ||
|
|
cee828130c | ||
|
|
67236d6de3 | ||
|
|
470e4f9e91 | ||
|
|
0e55a8793f | ||
|
|
49d46a0059 | ||
|
|
616d8251f3 | ||
|
|
a24126effb | ||
|
|
8984177448 | ||
|
|
750442909c | ||
|
|
df874db817 | ||
|
|
00d0c74657 | ||
|
|
122980ecad | ||
|
|
fc0bd9412c | ||
|
|
5ff9a0ff54 | ||
|
|
25d74a5919 | ||
|
|
213dbe7a5f | ||
|
|
9e57954b03 | ||
|
|
1b5aa2868d | ||
|
|
eee24138b0 | ||
|
|
04545f8a54 | ||
|
|
d1628944a6 | ||
|
|
abc27f56fc | ||
|
|
771aef9ddb | ||
|
|
dc7153e33c | ||
|
|
5ec08d3081 | ||
|
|
61b8443723 | ||
|
|
ad0b8e31b8 | ||
|
|
f144666f8b | ||
|
|
4e94135d36 | ||
|
|
b71add27da | ||
|
|
cb58eaa611 | ||
|
|
0c05ca1fd5 | ||
|
|
4867554eec | ||
|
|
36924b59bd | ||
|
|
7c088d1104 | ||
|
|
48df2f6842 | ||
|
|
79587d4b70 | ||
|
|
8a4517fd17 | ||
|
|
97f7815feb | ||
|
|
d8fbb0b8e3 | ||
|
|
9f77a8507e | ||
|
|
beaa8e55bd |
8
.github/ISSUE_TEMPLATE.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
Thanks for submitting an issue!
|
||||
|
||||
Here's a quick checklist in what to include:
|
||||
|
||||
- [ ] Include a detailed description of the bug or suggestion
|
||||
- [ ] `pip list` of the virtual environment you are using
|
||||
- [ ] pytest and operating system versions
|
||||
- [ ] Minimal example if possible
|
||||
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
14
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,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`;
|
||||
|
||||
Unless your change is trivial 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.
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -16,6 +16,7 @@ include/
|
||||
*.class
|
||||
*.orig
|
||||
*~
|
||||
.hypothesis/
|
||||
|
||||
.eggs/
|
||||
|
||||
@@ -32,3 +33,4 @@ env/
|
||||
.coverage
|
||||
.ropeproject
|
||||
.idea
|
||||
.hypothesis
|
||||
|
||||
51
.travis.yml
51
.travis.yml
@@ -7,27 +7,38 @@ install: "pip install -U tox"
|
||||
# # command to run tests
|
||||
env:
|
||||
matrix:
|
||||
- TESTENV=coveralls
|
||||
- TESTENV=doctesting
|
||||
- TESTENV=flakes
|
||||
- TESTENV=py26
|
||||
- TESTENV=py27
|
||||
- TESTENV=py27-cxfreeze
|
||||
- TESTENV=py27-nobyte
|
||||
- TESTENV=py27-pexpect
|
||||
- TESTENV=py27-subprocess
|
||||
- TESTENV=py27-trial
|
||||
- TESTENV=py27-xdist
|
||||
- TESTENV=py33
|
||||
- TESTENV=py33
|
||||
- TESTENV=py34
|
||||
- TESTENV=py35-pexpect
|
||||
- TESTENV=py35-trial
|
||||
- TESTENV=py35-xdist
|
||||
- TESTENV=py35
|
||||
- TESTENV=pypy
|
||||
# coveralls is not listed in tox's envlist, but should run in travis
|
||||
- TOXENV=coveralls
|
||||
# note: please use "tox --listenvs" to populate the build matrix below
|
||||
- TOXENV=linting
|
||||
- TOXENV=py26
|
||||
- TOXENV=py27
|
||||
- TOXENV=py33
|
||||
- TOXENV=py34
|
||||
- TOXENV=py35
|
||||
- TOXENV=pypy
|
||||
- TOXENV=py27-pexpect
|
||||
- TOXENV=py27-xdist
|
||||
- TOXENV=py27-trial
|
||||
- TOXENV=py35-pexpect
|
||||
- TOXENV=py35-xdist
|
||||
- TOXENV=py35-trial
|
||||
- TOXENV=py27-nobyte
|
||||
- TOXENV=doctesting
|
||||
- TOXENV=freeze
|
||||
- TOXENV=docs
|
||||
|
||||
script: tox --recreate -e $TESTENV
|
||||
matrix:
|
||||
include:
|
||||
- env: TOXENV=py36
|
||||
python: '3.6-dev'
|
||||
- env: TOXENV=py37
|
||||
python: 'nightly'
|
||||
allow_failures:
|
||||
- env: TOXENV=py37
|
||||
python: 'nightly'
|
||||
|
||||
script: tox --recreate
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
|
||||
88
AUTHORS
88
AUTHORS
@@ -3,39 +3,68 @@ merlinux GmbH, Germany, office at merlinux eu
|
||||
|
||||
Contributors include::
|
||||
|
||||
Abdeali JK
|
||||
Abhijeet Kasurde
|
||||
Ahn Ki-Wook
|
||||
Alexei Kozlenok
|
||||
Anatoly Bubenkoff
|
||||
Andreas Zeidler
|
||||
Andrzej Ostrowski
|
||||
Andy Freeland
|
||||
Anthon van der Neut
|
||||
Antony Lee
|
||||
Armin Rigo
|
||||
Aron Curzon
|
||||
Aviv Palivoda
|
||||
Barney Gale
|
||||
Ben Webb
|
||||
Benjamin Peterson
|
||||
Bernard Pratz
|
||||
Bob Ippolito
|
||||
Brian Dorsey
|
||||
Brian Okken
|
||||
Brianna Laugher
|
||||
Bruno Oliveira
|
||||
Cal Leeming
|
||||
Carl Friedrich Bolz
|
||||
Charles Cloud
|
||||
Charnjit SiNGH (CCSJ)
|
||||
Chris Lamb
|
||||
Christian Boelsen
|
||||
Christian Theunert
|
||||
Christian Tismer
|
||||
Christopher Gilling
|
||||
Daniel Grana
|
||||
Daniel Hahler
|
||||
Daniel Nuri
|
||||
Daniel Wandschneider
|
||||
Danielle Jenkins
|
||||
Dave Hunt
|
||||
David Díaz-Barquero
|
||||
David Mohr
|
||||
David Vierra
|
||||
Denis Kirisov
|
||||
Diego Russo
|
||||
Dmitry Dygalo
|
||||
Duncan Betts
|
||||
Edison Gustavo Muenz
|
||||
Edoardo Batini
|
||||
Eduardo Schettino
|
||||
Eli Boyarski
|
||||
Elizaveta Shashkova
|
||||
Endre Galaczi
|
||||
Eric Hunsberger
|
||||
Eric Siegerman
|
||||
Erik M. Bray
|
||||
Feng Ma
|
||||
Florian Bruhin
|
||||
Floris Bruynooghe
|
||||
Gabriel Reis
|
||||
Georgy Dyuldin
|
||||
Graham Horler
|
||||
Greg Price
|
||||
Grig Gheorghiu
|
||||
Grigorii Eremeev (budulianin)
|
||||
Guido Wesdorp
|
||||
Harald Armin Massa
|
||||
Ian Bicking
|
||||
@@ -43,31 +72,80 @@ Jaap Broekhuizen
|
||||
Jan Balster
|
||||
Janne Vanhala
|
||||
Jason R. Coombs
|
||||
Javier Domingo Cansino
|
||||
Javier Romero
|
||||
Jeff Widman
|
||||
John Towler
|
||||
Jon Sonesen
|
||||
Jordan Guymon
|
||||
Joshua Bronson
|
||||
Jurko Gospodnetić
|
||||
Justyna Janczyszyn
|
||||
Kale Kundert
|
||||
Katarzyna Jachim
|
||||
Kevin Cox
|
||||
Lee Kamentsky
|
||||
Lev Maximov
|
||||
Loic Esteve
|
||||
Lukas Bednar
|
||||
Luke Murphy
|
||||
Maciek Fijalkowski
|
||||
Maho
|
||||
Marc Schlaich
|
||||
Marcin Bachry
|
||||
Mark Abramowitz
|
||||
Markus Unterwaditzer
|
||||
Martijn Faassen
|
||||
Martin K. Scherer
|
||||
Martin Prusse
|
||||
Mathieu Clabaut
|
||||
Matt Bachmann
|
||||
Matt Williams
|
||||
Matthias Hafner
|
||||
mbyt
|
||||
Michael Aquilina
|
||||
Michael Birtwell
|
||||
Michael Droettboom
|
||||
Michael Seifert
|
||||
Mike Lundy
|
||||
Ned Batchelder
|
||||
Neven Mundar
|
||||
Nicolas Delaby
|
||||
Oleg Pidsadnyi
|
||||
Oliver Bestwalter
|
||||
Omar Kohl
|
||||
Omer Hadari
|
||||
Patrick Hayes
|
||||
Pieter Mulder
|
||||
Piotr Banaszkiewicz
|
||||
Punyashloka Biswal
|
||||
Quentin Pradet
|
||||
Ralf Schmitt
|
||||
Ran Benita
|
||||
Raphael Pierzina
|
||||
Raquel Alegre
|
||||
Roberto Polli
|
||||
Romain Dorgueil
|
||||
Roman Bolshakov
|
||||
Ronny Pfannschmidt
|
||||
Ross Lawley
|
||||
Russel Winder
|
||||
Ryan Wooden
|
||||
Samuele Pedroni
|
||||
Simon Gomizelj
|
||||
Stefan Farmbauer
|
||||
Stefan Zimmermann
|
||||
Stefano Taschini
|
||||
Steffen Allner
|
||||
Stephan Obermann
|
||||
Tareq Alayan
|
||||
Ted Xiao
|
||||
Thomas Grainger
|
||||
Tom Viner
|
||||
Trevor Bekolay
|
||||
Tyler Goodlet
|
||||
Vasily Kuznetsov
|
||||
Victor Uriarte
|
||||
Vidar T. Fauske
|
||||
Wouter van Ackooy
|
||||
David Díaz-Barquero
|
||||
Eric Hunsberger
|
||||
Simon Gomizelj
|
||||
Russel Winder
|
||||
Ben Webb
|
||||
Xuecong Liao
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
177
CONTRIBUTING.rst
177
CONTRIBUTING.rst
@@ -9,46 +9,18 @@ so do not hesitate!
|
||||
:depth: 2
|
||||
|
||||
|
||||
.. _submitplugin:
|
||||
.. _submitfeedback:
|
||||
|
||||
Submit a plugin, co-develop pytest
|
||||
----------------------------------
|
||||
Feature requests and feedback
|
||||
-----------------------------
|
||||
|
||||
Pytest development of the core, some plugins and support code happens
|
||||
in repositories living under:
|
||||
Do you like pytest? Share some love on Twitter or in your blog posts!
|
||||
|
||||
- `the pytest-dev github organisation <https://github.com/pytest-dev>`_
|
||||
We'd also like to hear about your propositions and suggestions. Feel free to
|
||||
`submit them as issues <https://github.com/pytest-dev/pytest/issues>`_ and:
|
||||
|
||||
- `the pytest-dev bitbucket team <https://bitbucket.org/pytest-dev>`_
|
||||
|
||||
All pytest-dev Contributors team members have write access to all contained
|
||||
repositories. pytest core and plugins are generally developed
|
||||
using `pull requests`_ to respective repositories.
|
||||
|
||||
You can submit your plugin by subscribing to the `pytest-dev mail list
|
||||
<https://mail.python.org/mailman/listinfo/pytest-dev>`_ and writing a
|
||||
mail pointing to your existing pytest plugin repository which must have
|
||||
the following:
|
||||
|
||||
- PyPI presence with a ``setup.py`` that contains a license, ``pytest-``
|
||||
prefixed, version number, authors, short and long description.
|
||||
|
||||
- a ``tox.ini`` for running tests using `tox <http://tox.testrun.org>`_.
|
||||
|
||||
- a ``README.txt`` describing how to use the plugin and on which
|
||||
platforms it runs.
|
||||
|
||||
- a ``LICENSE.txt`` file or equivalent containing the licensing
|
||||
information, with matching info in ``setup.py``.
|
||||
|
||||
- an issue tracker unless you rather want to use the core ``pytest``
|
||||
issue tracker.
|
||||
|
||||
If no contributor strongly objects and two agree, the repo will be
|
||||
transferred to the ``pytest-dev`` organisation and you'll become a
|
||||
member of the ``pytest-dev Contributors`` team, with commit rights
|
||||
to all projects. We recommend that each plugin has at least three
|
||||
people who have the right to release to pypi.
|
||||
* Explain in detail how they should work.
|
||||
* Keep the scope as narrow as possible. This will make it easier to implement.
|
||||
|
||||
|
||||
.. _reportbugs:
|
||||
@@ -56,7 +28,7 @@ people who have the right to release to pypi.
|
||||
Report bugs
|
||||
-----------
|
||||
|
||||
Report bugs for pytest at https://github.com/pytest-dev/pytest/issues
|
||||
Report bugs for pytest in the `issue tracker <https://github.com/pytest-dev/pytest/issues>`_.
|
||||
|
||||
If you are reporting a bug, please include:
|
||||
|
||||
@@ -70,29 +42,13 @@ If you can write a demonstration test that currently fails but should pass (xfai
|
||||
that is a very useful commit to make as well, even if you can't find how
|
||||
to fix the bug yet.
|
||||
|
||||
.. _submitfeedback:
|
||||
|
||||
Submit feedback for developers
|
||||
------------------------------
|
||||
|
||||
Do you like pytest? Share some love on Twitter or in your blog posts!
|
||||
|
||||
We'd also like to hear about your propositions and suggestions. Feel free to
|
||||
`submit them as issues <https://github.com/pytest-dev/pytest/issues>`__ and:
|
||||
|
||||
* Set the "kind" to "enhancement" or "proposal" so that we can quickly find
|
||||
about them.
|
||||
* Explain in detail how they should work.
|
||||
* Keep the scope as narrow as possible. This will make it easier to implement.
|
||||
* If you have required skills and/or knowledge, we are very happy for
|
||||
:ref:`pull requests <pull-requests>`.
|
||||
|
||||
.. _fixbugs:
|
||||
|
||||
Fix bugs
|
||||
--------
|
||||
|
||||
Look through the GitHub issues for bugs. Here is sample filter you can use:
|
||||
Look through the GitHub issues for bugs. Here is a filter you can use:
|
||||
https://github.com/pytest-dev/pytest/labels/bug
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can fix specific bugs.
|
||||
@@ -104,8 +60,7 @@ Don't forget to check the issue trackers of your favourite plugins, too!
|
||||
Implement features
|
||||
------------------
|
||||
|
||||
Look through the GitHub issues for enhancements. Here is sample filter you
|
||||
can use:
|
||||
Look through the GitHub issues for enhancements. Here is a filter you can use:
|
||||
https://github.com/pytest-dev/pytest/labels/enhancement
|
||||
|
||||
:ref:`Talk <contact>` to developers to find out how you can implement specific
|
||||
@@ -114,16 +69,91 @@ features.
|
||||
Write documentation
|
||||
-------------------
|
||||
|
||||
pytest could always use more documentation. What exactly is needed?
|
||||
Pytest could always use more documentation. What exactly is needed?
|
||||
|
||||
* More complementary documentation. Have you perhaps found something unclear?
|
||||
* Documentation translations. We currently have only English.
|
||||
* Docstrings. There can never be too many of them.
|
||||
* Blog posts, articles and such -- they're all very appreciated.
|
||||
|
||||
You can also edit documentation files directly in the Github web interface
|
||||
without needing to make a fork and local copy. This can be convenient for
|
||||
small fixes.
|
||||
You can also edit documentation files directly in the GitHub web interface,
|
||||
without using a local copy. This can be convenient for small fixes.
|
||||
|
||||
.. note::
|
||||
Build the documentation locally with the following command:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
$ tox -e docs
|
||||
|
||||
The built documentation should be available in the ``doc/en/_build/``.
|
||||
|
||||
Where 'en' refers to the documentation language.
|
||||
|
||||
.. _submitplugin:
|
||||
|
||||
Submitting Plugins to pytest-dev
|
||||
--------------------------------
|
||||
|
||||
Pytest development of the core, some plugins and support code happens
|
||||
in repositories living under the ``pytest-dev`` organisations:
|
||||
|
||||
- `pytest-dev on GitHub <https://github.com/pytest-dev>`_
|
||||
|
||||
- `pytest-dev on Bitbucket <https://bitbucket.org/pytest-dev>`_
|
||||
|
||||
All pytest-dev Contributors team members have write access to all contained
|
||||
repositories. Pytest core and plugins are generally developed
|
||||
using `pull requests`_ to respective repositories.
|
||||
|
||||
The objectives of the ``pytest-dev`` organisation are:
|
||||
|
||||
* Having a central location for popular pytest plugins
|
||||
* Sharing some of the maintenance responsibility (in case a maintainer no
|
||||
longer wishes to maintain a plugin)
|
||||
|
||||
You can submit your plugin by subscribing to the `pytest-dev mail list
|
||||
<https://mail.python.org/mailman/listinfo/pytest-dev>`_ and writing a
|
||||
mail pointing to your existing pytest plugin repository which must have
|
||||
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 ``README.txt`` describing how to use the plugin and on which
|
||||
platforms it runs.
|
||||
|
||||
- a ``LICENSE.txt`` file or equivalent containing the licensing
|
||||
information, with matching info in ``setup.py``.
|
||||
|
||||
- an issue tracker for bug reports and enhancement requests.
|
||||
|
||||
- a `changelog <http://keepachangelog.com/>`_
|
||||
|
||||
If no contributor strongly objects and two agree, the repository can then be
|
||||
transferred to the ``pytest-dev`` organisation.
|
||||
|
||||
Here's a rundown of how a repository transfer usually proceeds
|
||||
(using a repository named ``joedoe/pytest-xyz`` as example):
|
||||
|
||||
* ``joedoe`` transfers repository ownership to ``pytest-dev`` administrator ``calvin``.
|
||||
* ``calvin`` creates ``pytest-xyz-admin`` and ``pytest-xyz-developers`` teams, inviting ``joedoe`` to both as **maintainer**.
|
||||
* ``calvin`` transfers repository to ``pytest-dev`` and configures team access:
|
||||
|
||||
- ``pytest-xyz-admin`` **admin** access;
|
||||
- ``pytest-xyz-developers`` **write** access;
|
||||
|
||||
The ``pytest-dev/Contributors`` team has write access to all projects, and
|
||||
every project administrator is in it. We recommend that each plugin has at least three
|
||||
people who have the right to release to PyPI.
|
||||
|
||||
Repository owners can rest assured that no ``pytest-dev`` administrator will ever make
|
||||
releases of your repository or take ownership in any way, except in rare cases
|
||||
where someone becomes unresponsive after months of contact attempts.
|
||||
As stated, the objective is to share maintenance and avoid "plugin-abandon".
|
||||
|
||||
|
||||
.. _`pull requests`:
|
||||
.. _pull-requests:
|
||||
@@ -135,7 +165,7 @@ Preparing Pull Requests on GitHub
|
||||
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 pull request, we can discuss it's potential modifications and
|
||||
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
|
||||
@@ -179,35 +209,32 @@ but here is a simple overview:
|
||||
You need to have Python 2.7 and 3.5 available in your system. Now
|
||||
running tests is as simple as issuing this command::
|
||||
|
||||
$ python runtox.py -e py27,py35,flakes
|
||||
$ tox -e linting,py27,py35
|
||||
|
||||
This command will run tests via the "tox" tool against Python 2.7 and 3.5
|
||||
and also perform "flakes" coding-style checks. ``runtox.py`` is
|
||||
a thin wrapper around ``tox`` which installs from a development package
|
||||
index where newer (not yet released to pypi) versions of dependencies
|
||||
(especially ``py``) might be present.
|
||||
and also perform "lint" coding-style checks.
|
||||
|
||||
#. You can now edit your local working copy.
|
||||
|
||||
You can now make the changes you want and run the tests again as necessary.
|
||||
|
||||
To run tests on py27 and pass options to pytest (e.g. enter pdb on failure)
|
||||
to pytest you can do::
|
||||
To run tests on Python 2.7 and pass options to pytest (e.g. enter pdb on
|
||||
failure) to pytest you can do::
|
||||
|
||||
$ python runtox.py -e py27 -- --pdb
|
||||
$ tox -e py27 -- --pdb
|
||||
|
||||
or to only run tests in a particular test module on py35::
|
||||
Or to only run tests in a particular test module on Python 3.5::
|
||||
|
||||
$ python runtox.py -e py35 -- testing/test_config.py
|
||||
$ tox -e py35 -- 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 CHANGELOG message, 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.
|
||||
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.
|
||||
|
||||
#. Finally, submit a pull request through the GitHub website using this data::
|
||||
|
||||
@@ -216,6 +243,6 @@ but here is a simple overview:
|
||||
|
||||
base-fork: pytest-dev/pytest
|
||||
base: master # if it's a bugfix
|
||||
base: feature # if it's a feature
|
||||
base: features # if it's a feature
|
||||
|
||||
|
||||
|
||||
111
HOWTORELEASE.rst
111
HOWTORELEASE.rst
@@ -3,88 +3,83 @@ How to release pytest
|
||||
|
||||
Note: this assumes you have already registered on pypi.
|
||||
|
||||
1. Bump version numbers in _pytest/__init__.py (setup.py reads it)
|
||||
1. Bump version numbers in ``_pytest/__init__.py`` (``setup.py`` reads it).
|
||||
|
||||
2. Check and finalize CHANGELOG
|
||||
2. Check and finalize ``CHANGELOG.rst``.
|
||||
|
||||
3. Write doc/en/announce/release-VERSION.txt and include
|
||||
it in doc/en/announce/index.txt::
|
||||
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::
|
||||
|
||||
git log 2.8.2..HEAD --format='%aN' | sort -u # lists the names of authors involved
|
||||
git log $(git describe --abbrev=0 --tags)..HEAD --format='%aN' | sort -u
|
||||
|
||||
4. Use devpi for uploading a release tarball to a staging area::
|
||||
4. Regenerate the docs examples using tox::
|
||||
|
||||
tox -e regen
|
||||
|
||||
5. At this point, open a PR named ``release-X`` so others can help find regressions or provide suggestions.
|
||||
|
||||
6. Use devpi for uploading a release tarball to a staging area::
|
||||
|
||||
devpi use https://devpi.net/USER/dev
|
||||
devpi upload --formats sdist,bdist_wheel
|
||||
|
||||
5. Run from multiple machines::
|
||||
7. Run from multiple machines::
|
||||
|
||||
devpi use https://devpi.net/USER/dev
|
||||
devpi test pytest==VERSION
|
||||
|
||||
6. Check that tests pass for relevant combinations with::
|
||||
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``).
|
||||
|
||||
8. Check that tests pass for relevant combinations with::
|
||||
|
||||
devpi list pytest
|
||||
|
||||
or look at failures with "devpi list -f pytest".
|
||||
There will be some failed environments like e.g. the py33-trial
|
||||
or py27-pexpect tox environments on Win32 platforms
|
||||
which is ok (tox does not support skipping on
|
||||
per-platform basis yet).
|
||||
|
||||
7. Regenerate the docs examples using tox, and check for regressions::
|
||||
9. Feeling confident? Publish to pypi::
|
||||
|
||||
tox -e regen
|
||||
git diff
|
||||
|
||||
|
||||
8. Build the docs, you need a virtualenv with py and sphinx
|
||||
installed::
|
||||
|
||||
cd doc/en
|
||||
python plugins_index/plugins_index.py
|
||||
make html
|
||||
|
||||
Commit any changes before tagging the release.
|
||||
|
||||
9. Tag the release::
|
||||
|
||||
git tag VERSION
|
||||
git push
|
||||
|
||||
10. Upload the docs using doc/en/Makefile::
|
||||
|
||||
cd doc/en
|
||||
make install # or "installall" if you have LaTeX installed for PDF
|
||||
|
||||
This requires ssh-login permission on pytest.org because it uses
|
||||
rsync.
|
||||
Note that the ``install`` target of ``doc/en/Makefile`` defines where the
|
||||
rsync goes to, typically to the "latest" section of pytest.org.
|
||||
|
||||
If you are making a minor release (e.g. 5.4), you also need to manually
|
||||
create a symlink for "latest"::
|
||||
|
||||
ssh pytest-dev@pytest.org
|
||||
ln -s 5.4 latest
|
||||
|
||||
Browse to pytest.org to verify.
|
||||
|
||||
11. Publish to pypi::
|
||||
|
||||
devpi push pytest-VERSION pypi:NAME
|
||||
devpi push pytest==VERSION pypi:NAME
|
||||
|
||||
where 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::
|
||||
|
||||
12. Send release announcement to mailing lists:
|
||||
git tag VERSION <hash>
|
||||
git push origin VERSION
|
||||
|
||||
- pytest-dev
|
||||
- testing-in-python
|
||||
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**
|
||||
|
||||
|
||||
13. **after the release** Bump the version number in ``_pytest/__init__.py``,
|
||||
to the next Minor release version (i.e. if you released ``pytest-2.8.0``,
|
||||
set it to ``pytest-2.9.0.dev1``).
|
||||
|
||||
364
ISSUES.txt
364
ISSUES.txt
@@ -1,364 +0,0 @@
|
||||
|
||||
|
||||
recorder = monkeypatch.function(".......")
|
||||
-------------------------------------------------------------
|
||||
tags: nice feature
|
||||
|
||||
Like monkeypatch.replace but sets a mock-like call recorder:
|
||||
|
||||
recorder = monkeypatch.function("os.path.abspath")
|
||||
recorder.set_return("/hello")
|
||||
os.path.abspath("hello")
|
||||
call, = recorder.calls
|
||||
assert call.args.path == "hello"
|
||||
assert call.returned == "/hello"
|
||||
...
|
||||
|
||||
Unlike mock, "args.path" acts on the parsed auto-spec'ed ``os.path.abspath``
|
||||
so it's independent from if the client side called "os.path.abspath(path=...)"
|
||||
or "os.path.abspath('positional')".
|
||||
|
||||
refine parametrize API
|
||||
-------------------------------------------------------------
|
||||
tags: critical feature
|
||||
|
||||
extend metafunc.parametrize to directly support indirection, example:
|
||||
|
||||
def setupdb(request, config):
|
||||
# setup "resource" based on test request and the values passed
|
||||
# in to parametrize. setupfunc is called for each such value.
|
||||
# you may use request.addfinalizer() or request.cached_setup ...
|
||||
return dynamic_setup_database(val)
|
||||
|
||||
@pytest.mark.parametrize("db", ["pg", "mysql"], setupfunc=setupdb)
|
||||
def test_heavy_functional_test(db):
|
||||
...
|
||||
|
||||
There would be no need to write or explain funcarg factories and
|
||||
their special __ syntax.
|
||||
|
||||
The examples and improvements should also show how to put the parametrize
|
||||
decorator to a class, to a module or even to a directory. For the directory
|
||||
part a conftest.py content like this::
|
||||
|
||||
pytestmark = [
|
||||
@pytest.mark.parametrize_setup("db", ...),
|
||||
]
|
||||
|
||||
probably makes sense in order to keep the declarative nature. This mirrors
|
||||
the marker-mechanism with respect to a test module but puts it to a directory
|
||||
scale.
|
||||
|
||||
When doing larger scoped parametrization it probably becomes necessary
|
||||
to allow parametrization to be ignored if the according parameter is not
|
||||
used (currently any parametrized argument that is not present in a function will cause a ValueError). Example:
|
||||
|
||||
@pytest.mark.parametrize("db", ..., mustmatch=False)
|
||||
|
||||
means to not raise an error but simply ignore the parametrization
|
||||
if the signature of a decorated function does not match. XXX is it
|
||||
not sufficient to always allow non-matches?
|
||||
|
||||
|
||||
allow parametrized attributes on classes
|
||||
--------------------------------------------------
|
||||
|
||||
tags: wish 2.4
|
||||
|
||||
example:
|
||||
|
||||
@pytest.mark.parametrize_attr("db", setupfunc, [1,2,3], scope="class")
|
||||
@pytest.mark.parametrize_attr("tmp", setupfunc, scope="...")
|
||||
class TestMe:
|
||||
def test_hello(self):
|
||||
access self.db ...
|
||||
|
||||
this would run the test_hello() function three times with three
|
||||
different values for self.db. This could also work with unittest/nose
|
||||
style tests, i.e. it leverages existing test suites without needing
|
||||
to rewrite them. Together with the previously mentioned setup_test()
|
||||
maybe the setupfunc could be omitted?
|
||||
|
||||
optimizations
|
||||
---------------------------------------------------------------
|
||||
tags: 2.4 core
|
||||
|
||||
- look at ihook optimization such that all lookups for
|
||||
hooks relating to the same fspath are cached.
|
||||
|
||||
fix start/finish partial finailization problem
|
||||
---------------------------------------------------------------
|
||||
tags: bug core
|
||||
|
||||
if a configure/runtest_setup/sessionstart/... hook invocation partially
|
||||
fails the sessionfinishes is not called. Each hook implementation
|
||||
should better be repsonsible for registering a cleanup/finalizer
|
||||
appropriately to avoid this issue. Moreover/Alternatively, we could
|
||||
record which implementations of a hook succeeded and only call their
|
||||
teardown.
|
||||
|
||||
|
||||
relax requirement to have tests/testing contain an __init__
|
||||
----------------------------------------------------------------
|
||||
tags: feature
|
||||
bb: http://bitbucket.org/hpk42/py-trunk/issue/64
|
||||
|
||||
A local test run of a "tests" directory may work
|
||||
but a remote one fail because the tests directory
|
||||
does not contain an "__init__.py". Either give
|
||||
an error or make it work without the __init__.py
|
||||
i.e. port the nose-logic of unloading a test module.
|
||||
|
||||
customize test function collection
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
- introduce pytest.mark.nocollect for not considering a function for
|
||||
test collection at all. maybe also introduce a pytest.mark.test to
|
||||
explicitely mark a function to become a tested one. Lookup JUnit ways
|
||||
of tagging tests.
|
||||
|
||||
introduce pytest.mark.importorskip
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
in addition to the imperative pytest.importorskip also introduce
|
||||
a pytest.mark.importorskip so that the test count is more correct.
|
||||
|
||||
|
||||
introduce pytest.mark.platform
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
Introduce nice-to-spell platform-skipping, examples:
|
||||
|
||||
@pytest.mark.platform("python3")
|
||||
@pytest.mark.platform("not python3")
|
||||
@pytest.mark.platform("win32 and not python3")
|
||||
@pytest.mark.platform("darwin")
|
||||
@pytest.mark.platform("not (jython and win32)")
|
||||
@pytest.mark.platform("not (jython and win32)", xfail=True)
|
||||
|
||||
etc. Idea is to allow Python expressions which can operate
|
||||
on common spellings for operating systems and python
|
||||
interpreter versions.
|
||||
|
||||
pytest.mark.xfail signature change
|
||||
-------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
change to pytest.mark.xfail(reason, (optional)condition)
|
||||
to better implement the word meaning. It also signals
|
||||
better that we always have some kind of an implementation
|
||||
reason that can be formualated.
|
||||
Compatibility? how to introduce a new name/keep compat?
|
||||
|
||||
allow to non-intrusively apply skipfs/xfail/marks
|
||||
---------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
use case: mark a module or directory structures
|
||||
to be skipped on certain platforms (i.e. no import
|
||||
attempt will be made).
|
||||
|
||||
consider introducing a hook/mechanism that allows to apply marks
|
||||
from conftests or plugins. (See extended parametrization)
|
||||
|
||||
|
||||
explicit referencing of conftest.py files
|
||||
-----------------------------------------
|
||||
tags: feature
|
||||
|
||||
allow to name conftest.py files (in sub directories) that should
|
||||
be imported early, as to include command line options.
|
||||
|
||||
improve central pytest ini file
|
||||
-------------------------------
|
||||
tags: feature
|
||||
|
||||
introduce more declarative configuration options:
|
||||
- (to-be-collected test directories)
|
||||
- required plugins
|
||||
- test func/class/file matching patterns
|
||||
- skip/xfail (non-intrusive)
|
||||
- pytest.ini and tox.ini and setup.cfg configuration in the same file
|
||||
|
||||
new documentation
|
||||
----------------------------------
|
||||
tags: feature
|
||||
|
||||
- logo pytest
|
||||
- examples for unittest or functional testing
|
||||
- resource management for functional testing
|
||||
- patterns: page object
|
||||
|
||||
have imported module mismatch honour relative paths
|
||||
--------------------------------------------------------
|
||||
tags: bug
|
||||
|
||||
With 1.1.1 pytest fails at least on windows if an import
|
||||
is relative and compared against an absolute conftest.py
|
||||
path. Normalize.
|
||||
|
||||
consider globals: pytest.ensuretemp and config
|
||||
--------------------------------------------------------------
|
||||
tags: experimental-wish
|
||||
|
||||
consider deprecating pytest.ensuretemp and pytest.config
|
||||
to further reduce pytest globality. Also consider
|
||||
having pytest.config and ensuretemp coming from
|
||||
a plugin rather than being there from the start.
|
||||
|
||||
|
||||
consider pytest_addsyspath hook
|
||||
-----------------------------------------
|
||||
tags: wish
|
||||
|
||||
pytest could call a new pytest_addsyspath() in order to systematically
|
||||
allow manipulation of sys.path and to inhibit it via --no-addsyspath
|
||||
in order to more easily run against installed packages.
|
||||
|
||||
Alternatively it could also be done via the config object
|
||||
and pytest_configure.
|
||||
|
||||
|
||||
|
||||
deprecate global pytest.config usage
|
||||
----------------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
pytest.ensuretemp and pytest.config are probably the last
|
||||
objects containing global state. Often using them is not
|
||||
necessary. This is about trying to get rid of them, i.e.
|
||||
deprecating them and checking with PyPy's usages as well
|
||||
as others.
|
||||
|
||||
remove deprecated bits in collect.py
|
||||
-------------------------------------------------------------------
|
||||
tags: feature
|
||||
|
||||
In an effort to further simplify code, review and remove deprecated bits
|
||||
in collect.py. Probably good:
|
||||
- inline consider_file/dir methods, no need to have them
|
||||
subclass-overridable because of hooks
|
||||
|
||||
implement fslayout decorator
|
||||
---------------------------------
|
||||
tags: feature
|
||||
|
||||
Improve the way how tests can work with pre-made examples,
|
||||
keeping the layout close to the test function:
|
||||
|
||||
@pytest.mark.fslayout("""
|
||||
conftest.py:
|
||||
# empty
|
||||
tests/
|
||||
test_%(NAME)s: # becomes test_run1.py
|
||||
def test_function(self):
|
||||
pass
|
||||
""")
|
||||
def test_run(pytester, fslayout):
|
||||
p = fslayout.findone("test_*.py")
|
||||
result = pytester.runpytest(p)
|
||||
assert result.ret == 0
|
||||
assert result.passed == 1
|
||||
|
||||
Another idea is to allow to define a full scenario including the run
|
||||
in one content string::
|
||||
|
||||
runscenario("""
|
||||
test_{TESTNAME}.py:
|
||||
import pytest
|
||||
@pytest.mark.xfail
|
||||
def test_that_fails():
|
||||
assert 0
|
||||
|
||||
@pytest.mark.skipif("True")
|
||||
def test_hello():
|
||||
pass
|
||||
|
||||
conftest.py:
|
||||
import pytest
|
||||
def pytest_runsetup_setup(item):
|
||||
pytest.skip("abc")
|
||||
|
||||
runpytest -rsxX
|
||||
*SKIP*{TESTNAME}*
|
||||
*1 skipped*
|
||||
""")
|
||||
|
||||
This could be run with at least three different ways to invoke pytest:
|
||||
through the shell, through "python -m pytest" and inlined. As inlined
|
||||
would be the fastest it could be run first (or "--fast" mode).
|
||||
|
||||
|
||||
Create isolate plugin
|
||||
---------------------
|
||||
tags: feature
|
||||
|
||||
The idea is that you can e.g. import modules in a test and afterwards
|
||||
sys.modules, sys.meta_path etc would be reverted. It can go further
|
||||
then just importing however, e.g. current working directory, file
|
||||
descriptors, ...
|
||||
|
||||
This would probably be done by marking::
|
||||
|
||||
@pytest.mark.isolate(importing=True, cwd=True, fds=False)
|
||||
def test_foo():
|
||||
...
|
||||
|
||||
With the possibility of doing this globally in an ini-file.
|
||||
|
||||
|
||||
fnmatch for test names
|
||||
----------------------
|
||||
tags: feature-wish
|
||||
|
||||
various testsuites use suffixes instead of prefixes for test classes
|
||||
also it lends itself to bdd style test names::
|
||||
|
||||
class UserBehaviour:
|
||||
def anonymous_should_not_have_inbox(user):
|
||||
...
|
||||
def registred_should_have_inbox(user):
|
||||
..
|
||||
|
||||
using the following in pytest.ini::
|
||||
|
||||
[pytest]
|
||||
python_classes = Test *Behaviour *Test
|
||||
python_functions = test *_should_*
|
||||
|
||||
|
||||
mechanism for running named parts of tests with different reporting behaviour
|
||||
------------------------------------------------------------------------------
|
||||
tags: feature-wish-incomplete
|
||||
|
||||
a few use-cases come to mind:
|
||||
|
||||
* fail assertions and record that without stopping a complete test
|
||||
|
||||
* this is in particular hepfull if a small bit of a test is known to fail/xfail::
|
||||
|
||||
def test_fun():
|
||||
with pytest.section('fdcheck, marks=pytest.mark.xfail_if(...)):
|
||||
breaks_on_windows()
|
||||
|
||||
* divide functional/acceptance tests into sections
|
||||
* provide a different mechanism for generators, maybe something like::
|
||||
|
||||
def pytest_runtest_call(item)
|
||||
if not generator:
|
||||
...
|
||||
prepare_check = GeneratorCheckprepare()
|
||||
|
||||
gen = item.obj(**fixtures)
|
||||
for check in gen
|
||||
id, call = prepare_check(check)
|
||||
# bubble should only prevent exception propagation after a failure
|
||||
# the whole test should still fail
|
||||
# there might be need for a lower level api and taking custom markers into account
|
||||
with pytest.section(id, bubble=False):
|
||||
call()
|
||||
|
||||
|
||||
36
LICENSE
36
LICENSE
@@ -1,19 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
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.
|
||||
Copyright (c) 2004-2016 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
|
||||
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.
|
||||
|
||||
39
MANIFEST.in
39
MANIFEST.in
@@ -1,7 +1,36 @@
|
||||
include CHANGELOG
|
||||
include README.rst
|
||||
include setup.py
|
||||
include tox.ini
|
||||
include CHANGELOG.rst
|
||||
include LICENSE
|
||||
graft doc
|
||||
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
|
||||
|
||||
128
README.rst
128
README.rst
@@ -1,66 +1,102 @@
|
||||
======
|
||||
pytest
|
||||
======
|
||||
.. image:: http://docs.pytest.org/en/latest/_static/pytest1.png
|
||||
:target: http://docs.pytest.org
|
||||
:align: center
|
||||
:alt: pytest
|
||||
|
||||
The ``pytest`` testing tool makes it easy to write small tests, yet
|
||||
scales to support complex functional testing.
|
||||
------
|
||||
|
||||
.. image:: http://img.shields.io/pypi/v/pytest.svg
|
||||
.. image:: https://img.shields.io/pypi/v/pytest.svg
|
||||
:target: https://pypi.python.org/pypi/pytest
|
||||
.. image:: http://img.shields.io/coveralls/pytest-dev/pytest/master.svg
|
||||
.. image:: https://img.shields.io/pypi/pyversions/pytest.svg
|
||||
: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
|
||||
.. 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
|
||||
|
||||
Documentation: http://pytest.org/latest/
|
||||
The ``pytest`` framework makes it easy to write small tests, yet
|
||||
scales to support complex functional testing for applications and libraries.
|
||||
|
||||
Changelog: http://pytest.org/latest/changelog.html
|
||||
An example of a simple test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_sample.py
|
||||
def inc(x):
|
||||
return x + 1
|
||||
|
||||
def test_answer():
|
||||
assert inc(3) == 5
|
||||
|
||||
|
||||
To execute it::
|
||||
|
||||
$ pytest
|
||||
============================= test session starts =============================
|
||||
collected 1 items
|
||||
|
||||
test_sample.py F
|
||||
|
||||
================================== FAILURES ===================================
|
||||
_________________________________ test_answer _________________________________
|
||||
|
||||
def test_answer():
|
||||
> assert inc(3) == 5
|
||||
E assert 4 == 5
|
||||
E + where 4 = inc(3)
|
||||
|
||||
test_sample.py:5: AssertionError
|
||||
========================== 1 failed in 0.04 seconds ===========================
|
||||
|
||||
|
||||
Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See `getting-started <http://docs.pytest.org/en/latest/getting-started.html#our-first-test-run>`_ for more examples.
|
||||
|
||||
Issues: https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
- `auto-discovery
|
||||
<http://pytest.org/latest/goodpractises.html#python-test-discovery>`_
|
||||
of test modules and functions,
|
||||
- detailed info on failing `assert statements <http://pytest.org/latest/assert.html>`_ (no need to remember ``self.assert*`` names)
|
||||
- `modular fixtures <http://pytest.org/latest/fixture.html>`_ for
|
||||
managing small or parametrized long-lived test resources.
|
||||
- multi-paradigm support: you can use ``pytest`` to run test suites based
|
||||
on `unittest <http://pytest.org/latest/unittest.html>`_ (or trial),
|
||||
`nose <http://pytest.org/latest/nose.html>`_
|
||||
- single-source compatibility from Python2.6 all the way up to
|
||||
Python3.5, PyPy-2.3, (jython-2.5 untested)
|
||||
- Detailed info on failing `assert statements <http://docs.pytest.org/en/latest/assert.html>`_ (no need to remember ``self.assert*`` names);
|
||||
|
||||
- `Auto-discovery
|
||||
<http://docs.pytest.org/en/latest/goodpractices.html#python-test-discovery>`_
|
||||
of test modules and functions;
|
||||
|
||||
- `Modular fixtures <http://docs.pytest.org/en/latest/fixture.html>`_ for
|
||||
managing small or parametrized long-lived test resources;
|
||||
|
||||
- Can run `unittest <http://docs.pytest.org/en/latest/unittest.html>`_ (or trial),
|
||||
`nose <http://docs.pytest.org/en/latest/nose.html>`_ test suites out of the box;
|
||||
|
||||
- Python2.6+, Python3.3+, 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;
|
||||
|
||||
|
||||
- many `external plugins <http://pytest.org/latest/plugins.html#installing-external-plugins-searching>`_.
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
A simple example for a test:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# content of test_module.py
|
||||
def test_function():
|
||||
i = 4
|
||||
assert i == 3
|
||||
|
||||
which can be run with ``py.test test_module.py``. See `getting-started <http://pytest.org/latest/getting-started.html#our-first-test-run>`_ for more examples.
|
||||
|
||||
For much more info, including PDF docs, see
|
||||
|
||||
http://pytest.org
|
||||
|
||||
and report bugs at:
|
||||
|
||||
https://github.com/pytest-dev/pytest/issues
|
||||
|
||||
and checkout or fork repo at:
|
||||
|
||||
https://github.com/pytest-dev/pytest
|
||||
For full documentation, including installation, tutorials and PDF documents, please see http://docs.pytest.org.
|
||||
|
||||
|
||||
Copyright Holger Krekel and others, 2004-2015
|
||||
Licensed under the MIT license.
|
||||
Bugs/Requests
|
||||
-------------
|
||||
|
||||
Please use the `GitHub issue tracker <https://github.com/pytest-dev/pytest/issues>`_ to submit bugs or request features.
|
||||
|
||||
|
||||
Changelog
|
||||
---------
|
||||
|
||||
Consult the `Changelog <http://docs.pytest.org/en/latest/changelog.html>`__ page for fixes and enhancements of each version.
|
||||
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Copyright Holger Krekel and others, 2004-2016.
|
||||
|
||||
Distributed under the terms of the `MIT`_ license, pytest is free and open source software.
|
||||
|
||||
.. _`MIT`: https://github.com/pytest-dev/pytest/blob/master/LICENSE
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#
|
||||
__version__ = '2.8.3'
|
||||
__version__ = '3.0.7'
|
||||
|
||||
@@ -87,10 +87,8 @@ class FastFilesCompleter:
|
||||
completion.append(x[prefix_dir:])
|
||||
return completion
|
||||
|
||||
|
||||
if os.environ.get('_ARGCOMPLETE'):
|
||||
# argcomplete 0.5.6 is not compatible with python 2.5.6: print/with/format
|
||||
if sys.version_info[:2] < (2, 6):
|
||||
sys.exit(1)
|
||||
try:
|
||||
import argcomplete.completers
|
||||
except ImportError:
|
||||
|
||||
9
_pytest/_code/__init__.py
Normal file
9
_pytest/_code/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
""" python inspection/code generation API """
|
||||
from .code import Code # noqa
|
||||
from .code import ExceptionInfo # noqa
|
||||
from .code import Frame # noqa
|
||||
from .code import Traceback # noqa
|
||||
from .code import getrawcode # noqa
|
||||
from .source import Source # noqa
|
||||
from .source import compile_ as compile # noqa
|
||||
from .source import getfslineno # noqa
|
||||
81
_pytest/_code/_py2traceback.py
Normal file
81
_pytest/_code/_py2traceback.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# copied from python-2.7.3's traceback.py
|
||||
# CHANGES:
|
||||
# - some_str is replaced, trying to create unicode strings
|
||||
#
|
||||
import types
|
||||
|
||||
def format_exception_only(etype, value):
|
||||
"""Format the exception part of a traceback.
|
||||
|
||||
The arguments are the exception type and value such as given by
|
||||
sys.last_type and sys.last_value. The return value is a list of
|
||||
strings, each ending in a newline.
|
||||
|
||||
Normally, the list contains a single string; however, for
|
||||
SyntaxError exceptions, it contains several lines that (when
|
||||
printed) display detailed information about where the syntax
|
||||
error occurred.
|
||||
|
||||
The message indicating which exception occurred is always the last
|
||||
string in the list.
|
||||
|
||||
"""
|
||||
|
||||
# An instance should not have a meaningful value parameter, but
|
||||
# sometimes does, particularly for string exceptions, such as
|
||||
# >>> raise string1, string2 # deprecated
|
||||
#
|
||||
# Clear these out first because issubtype(string1, SyntaxError)
|
||||
# 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):
|
||||
return [_format_final_exc_line(etype, value)]
|
||||
|
||||
stype = etype.__name__
|
||||
|
||||
if not issubclass(etype, SyntaxError):
|
||||
return [_format_final_exc_line(stype, value)]
|
||||
|
||||
# It was a syntax error; show exactly where the problem was found.
|
||||
lines = []
|
||||
try:
|
||||
msg, (filename, lineno, offset, badline) = value.args
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
filename = filename or "<string>"
|
||||
lines.append(' File "%s", line %d\n' % (filename, lineno))
|
||||
if badline is not None:
|
||||
if isinstance(badline, bytes): # python 2 only
|
||||
badline = badline.decode('utf-8', 'replace')
|
||||
lines.append(u' %s\n' % badline.strip())
|
||||
if offset is not None:
|
||||
caretspace = badline.rstrip('\n')[:offset].lstrip()
|
||||
# non-space whitespace (likes tabs) must be kept for alignment
|
||||
caretspace = ((c.isspace() and c or ' ') for c in caretspace)
|
||||
# only three spaces to account for offset1 == pos 0
|
||||
lines.append(' %s^\n' % ''.join(caretspace))
|
||||
value = msg
|
||||
|
||||
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)
|
||||
if value is None or not valuestr:
|
||||
line = "%s\n" % etype
|
||||
else:
|
||||
line = "%s: %s\n" % (etype, valuestr)
|
||||
return line
|
||||
|
||||
def _some_str(value):
|
||||
try:
|
||||
return unicode(value)
|
||||
except Exception:
|
||||
try:
|
||||
return str(value)
|
||||
except Exception:
|
||||
pass
|
||||
return '<unprintable %s object>' % type(value).__name__
|
||||
863
_pytest/_code/code.py
Normal file
863
_pytest/_code/code.py
Normal file
@@ -0,0 +1,863 @@
|
||||
import sys
|
||||
from inspect import CO_VARARGS, CO_VARKEYWORDS
|
||||
import re
|
||||
from weakref import ref
|
||||
|
||||
import py
|
||||
builtin_repr = repr
|
||||
|
||||
reprlib = py.builtin._tryimport('repr', 'reprlib')
|
||||
|
||||
if sys.version_info[0] >= 3:
|
||||
from traceback import format_exception_only
|
||||
else:
|
||||
from ._py2traceback import format_exception_only
|
||||
|
||||
|
||||
class Code(object):
|
||||
""" wrapper around Python code objects """
|
||||
def __init__(self, rawcode):
|
||||
if not hasattr(rawcode, "co_filename"):
|
||||
rawcode = getrawcode(rawcode)
|
||||
try:
|
||||
self.filename = rawcode.co_filename
|
||||
self.firstlineno = rawcode.co_firstlineno - 1
|
||||
self.name = rawcode.co_name
|
||||
except AttributeError:
|
||||
raise TypeError("not a code object: %r" %(rawcode,))
|
||||
self.raw = rawcode
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.raw == other.raw
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
""" return a path object pointing to source code (note that it
|
||||
might not point to an actually existing file). """
|
||||
try:
|
||||
p = py.path.local(self.raw.co_filename)
|
||||
# maybe don't try this checking
|
||||
if not p.check():
|
||||
raise OSError("py.path check failed.")
|
||||
except OSError:
|
||||
# XXX maybe try harder like the weird logic
|
||||
# in the standard lib [linecache.updatecache] does?
|
||||
p = self.raw.co_filename
|
||||
|
||||
return p
|
||||
|
||||
@property
|
||||
def fullsource(self):
|
||||
""" return a _pytest._code.Source object for the full source file of the code
|
||||
"""
|
||||
from _pytest._code import source
|
||||
full, _ = source.findsource(self.raw)
|
||||
return full
|
||||
|
||||
def source(self):
|
||||
""" return a _pytest._code.Source object for the code object's source only
|
||||
"""
|
||||
# return source only for that part of code
|
||||
import _pytest._code
|
||||
return _pytest._code.Source(self.raw)
|
||||
|
||||
def getargs(self, var=False):
|
||||
""" return a tuple with the argument names for the code object
|
||||
|
||||
if 'var' is set True also return the names of the variable and
|
||||
keyword arguments when present
|
||||
"""
|
||||
# handfull shortcut for getting args
|
||||
raw = self.raw
|
||||
argcount = raw.co_argcount
|
||||
if var:
|
||||
argcount += raw.co_flags & CO_VARARGS
|
||||
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."""
|
||||
|
||||
def __init__(self, frame):
|
||||
self.lineno = frame.f_lineno - 1
|
||||
self.f_globals = frame.f_globals
|
||||
self.f_locals = frame.f_locals
|
||||
self.raw = frame
|
||||
self.code = Code(frame.f_code)
|
||||
|
||||
@property
|
||||
def statement(self):
|
||||
""" statement this frame is at """
|
||||
import _pytest._code
|
||||
if self.code.fullsource is None:
|
||||
return _pytest._code.Source("")
|
||||
return self.code.fullsource.getstatement(self.lineno)
|
||||
|
||||
def eval(self, code, **vars):
|
||||
""" evaluate 'code' in the frame
|
||||
|
||||
'vars' are optional additional local variables
|
||||
|
||||
returns the result of the evaluation
|
||||
"""
|
||||
f_locals = self.f_locals.copy()
|
||||
f_locals.update(vars)
|
||||
return eval(code, self.f_globals, f_locals)
|
||||
|
||||
def exec_(self, code, **vars):
|
||||
""" exec 'code' in the frame
|
||||
|
||||
'vars' are optiona; additional local variables
|
||||
"""
|
||||
f_locals = self.f_locals.copy()
|
||||
f_locals.update(vars)
|
||||
py.builtin.exec_(code, self.f_globals, f_locals )
|
||||
|
||||
def repr(self, object):
|
||||
""" return a 'safe' (non-recursive, one-line) string repr for 'object'
|
||||
"""
|
||||
return py.io.saferepr(object)
|
||||
|
||||
def is_true(self, object):
|
||||
return object
|
||||
|
||||
def getargs(self, var=False):
|
||||
""" return a list of tuples (name, value) for all arguments
|
||||
|
||||
if 'var' is set True also include the variable and keyword
|
||||
arguments when present
|
||||
"""
|
||||
retval = []
|
||||
for arg in self.code.getargs(var):
|
||||
try:
|
||||
retval.append((arg, self.f_locals[arg]))
|
||||
except KeyError:
|
||||
pass # this can occur when using Psyco
|
||||
return retval
|
||||
|
||||
class TracebackEntry(object):
|
||||
""" a single entry in a traceback """
|
||||
|
||||
_repr_style = None
|
||||
exprinfo = None
|
||||
|
||||
def __init__(self, rawentry, excinfo=None):
|
||||
self._excinfo = excinfo
|
||||
self._rawentry = rawentry
|
||||
self.lineno = rawentry.tb_lineno - 1
|
||||
|
||||
def set_repr_style(self, mode):
|
||||
assert mode in ("short", "long")
|
||||
self._repr_style = mode
|
||||
|
||||
@property
|
||||
def frame(self):
|
||||
import _pytest._code
|
||||
return _pytest._code.Frame(self._rawentry.tb_frame)
|
||||
|
||||
@property
|
||||
def relline(self):
|
||||
return self.lineno - self.frame.code.firstlineno
|
||||
|
||||
def __repr__(self):
|
||||
return "<TracebackEntry %s:%d>" %(self.frame.code.path, self.lineno+1)
|
||||
|
||||
@property
|
||||
def statement(self):
|
||||
""" _pytest._code.Source object for the current statement """
|
||||
source = self.frame.code.fullsource
|
||||
return source.getstatement(self.lineno)
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
""" path to the source code """
|
||||
return self.frame.code.path
|
||||
|
||||
def getlocals(self):
|
||||
return self.frame.f_locals
|
||||
locals = property(getlocals, None, None, "locals of underlaying frame")
|
||||
|
||||
def getfirstlinesource(self):
|
||||
# on Jython this firstlineno can be -1 apparently
|
||||
return max(self.frame.code.firstlineno, 0)
|
||||
|
||||
def getsource(self, astcache=None):
|
||||
""" return failing source code. """
|
||||
# we use the passed in astcache to not reparse asttrees
|
||||
# within exception info printing
|
||||
from _pytest._code.source import getstatementrange_ast
|
||||
source = self.frame.code.fullsource
|
||||
if source is None:
|
||||
return None
|
||||
key = astnode = None
|
||||
if astcache is not None:
|
||||
key = self.frame.code.path
|
||||
if key is not None:
|
||||
astnode = astcache.get(key, None)
|
||||
start = self.getfirstlinesource()
|
||||
try:
|
||||
astnode, _, end = getstatementrange_ast(self.lineno, source,
|
||||
astnode=astnode)
|
||||
except SyntaxError:
|
||||
end = self.lineno + 1
|
||||
else:
|
||||
if key is not None:
|
||||
astcache[key] = astnode
|
||||
return source[start:end]
|
||||
|
||||
source = property(getsource)
|
||||
|
||||
def ishidden(self):
|
||||
""" return True if the current frame has a var __tracebackhide__
|
||||
resolving to True
|
||||
|
||||
If __tracebackhide__ is a callable, it gets called with the
|
||||
ExceptionInfo instance and can decide whether to hide the traceback.
|
||||
|
||||
mostly for internal use
|
||||
"""
|
||||
try:
|
||||
tbh = self.frame.f_locals['__tracebackhide__']
|
||||
except KeyError:
|
||||
try:
|
||||
tbh = self.frame.f_globals['__tracebackhide__']
|
||||
except KeyError:
|
||||
return False
|
||||
|
||||
if py.builtin.callable(tbh):
|
||||
return tbh(None if self._excinfo is None else self._excinfo())
|
||||
else:
|
||||
return tbh
|
||||
|
||||
def __str__(self):
|
||||
try:
|
||||
fn = str(self.path)
|
||||
except py.error.Error:
|
||||
fn = '???'
|
||||
name = self.frame.code.name
|
||||
try:
|
||||
line = str(self.statement).lstrip()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
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
|
||||
if hasattr(tb, 'tb_next'):
|
||||
def f(cur):
|
||||
while cur is not None:
|
||||
yield self.Entry(cur, excinfo=excinfo)
|
||||
cur = cur.tb_next
|
||||
list.__init__(self, f(tb))
|
||||
else:
|
||||
list.__init__(self, tb)
|
||||
|
||||
def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None):
|
||||
""" return a Traceback instance wrapping part of this Traceback
|
||||
|
||||
by provding any combination of path, lineno and firstlineno, the
|
||||
first frame to start the to-be-returned traceback is determined
|
||||
|
||||
this allows cutting the first part of a Traceback instance e.g.
|
||||
for formatting reasons (removing some uninteresting bits that deal
|
||||
with handling of the exception/traceback)
|
||||
"""
|
||||
for x in self:
|
||||
code = x.frame.code
|
||||
codepath = code.path
|
||||
if ((path is None or codepath == path) and
|
||||
(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)):
|
||||
return Traceback(x._rawentry, self._excinfo)
|
||||
return self
|
||||
|
||||
def __getitem__(self, key):
|
||||
val = super(Traceback, self).__getitem__(key)
|
||||
if isinstance(key, type(slice(0))):
|
||||
val = self.__class__(val)
|
||||
return val
|
||||
|
||||
def filter(self, fn=lambda x: not x.ishidden()):
|
||||
""" return a Traceback instance with certain items removed
|
||||
|
||||
fn is a function that gets a single argument, a TracebackEntry
|
||||
instance, and should return True when the item should be added
|
||||
to the Traceback, False when not
|
||||
|
||||
by default this removes all the TracebackEntries which are hidden
|
||||
(see ishidden() above)
|
||||
"""
|
||||
return Traceback(filter(fn, self), self._excinfo)
|
||||
|
||||
def getcrashentry(self):
|
||||
""" return last non-hidden traceback entry that lead
|
||||
to the exception of a traceback.
|
||||
"""
|
||||
for i in range(-1, -len(self)-1, -1):
|
||||
entry = self[i]
|
||||
if not entry.ishidden():
|
||||
return entry
|
||||
return self[-1]
|
||||
|
||||
def recursionindex(self):
|
||||
""" return the index of the frame/TracebackEntry where recursion
|
||||
originates if appropriate, None if no recursion occurred
|
||||
"""
|
||||
cache = {}
|
||||
for i, entry in enumerate(self):
|
||||
# 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
|
||||
key = entry.frame.code.path, id(entry.frame.code.raw), entry.lineno
|
||||
#print "checking for recursion at", key
|
||||
l = cache.setdefault(key, [])
|
||||
if l:
|
||||
f = entry.frame
|
||||
loc = f.f_locals
|
||||
for otherloc in l:
|
||||
if f.is_true(f.eval(co_equal,
|
||||
__recursioncache_locals_1=loc,
|
||||
__recursioncache_locals_2=otherloc)):
|
||||
return i
|
||||
l.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 "
|
||||
|
||||
def __init__(self, tup=None, exprinfo=None):
|
||||
import _pytest._code
|
||||
if tup is None:
|
||||
tup = sys.exc_info()
|
||||
if exprinfo is None and isinstance(tup[1], AssertionError):
|
||||
exprinfo = getattr(tup[1], 'msg', None)
|
||||
if exprinfo is None:
|
||||
exprinfo = py.io.saferepr(tup[1])
|
||||
if exprinfo and exprinfo.startswith(self._assert_start_repr):
|
||||
self._striptext = 'AssertionError: '
|
||||
self._excinfo = tup
|
||||
#: the exception class
|
||||
self.type = tup[0]
|
||||
#: the exception instance
|
||||
self.value = tup[1]
|
||||
#: the exception raw traceback
|
||||
self.tb = tup[2]
|
||||
#: the exception type name
|
||||
self.typename = self.type.__name__
|
||||
#: the exception traceback (_pytest._code.Traceback instance)
|
||||
self.traceback = _pytest._code.Traceback(self.tb, excinfo=ref(self))
|
||||
|
||||
def __repr__(self):
|
||||
return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback))
|
||||
|
||||
def exconly(self, tryshort=False):
|
||||
""" return the exception as a string
|
||||
|
||||
when 'tryshort' resolves to True, and the exception is a
|
||||
_pytest._code._AssertionError, only the actual exception part of
|
||||
the exception representation is returned (so 'AssertionError: ' is
|
||||
removed from the beginning)
|
||||
"""
|
||||
lines = format_exception_only(self.type, self.value)
|
||||
text = ''.join(lines)
|
||||
text = text.rstrip()
|
||||
if tryshort:
|
||||
if text.startswith(self._striptext):
|
||||
text = text[len(self._striptext):]
|
||||
return text
|
||||
|
||||
def errisinstance(self, exc):
|
||||
""" return True if the exception is an instance of exc """
|
||||
return isinstance(self.value, exc)
|
||||
|
||||
def _getreprcrash(self):
|
||||
exconly = self.exconly(tryshort=True)
|
||||
entry = self.traceback.getcrashentry()
|
||||
path, lineno = entry.frame.code.raw.co_filename, entry.lineno
|
||||
return ReprFileLocation(path, lineno+1, exconly)
|
||||
|
||||
def getrepr(self, showlocals=False, style="long",
|
||||
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
|
||||
tbfilter: hide entries (where __tracebackhide__ is true)
|
||||
|
||||
in case of style==native, tbfilter and showlocals is ignored.
|
||||
"""
|
||||
if style == 'native':
|
||||
return ReprExceptionInfo(ReprTracebackNative(
|
||||
py.std.traceback.format_exception(
|
||||
self.type,
|
||||
self.value,
|
||||
self.traceback[0]._rawentry,
|
||||
)), self._getreprcrash())
|
||||
|
||||
fmt = FormattedExcinfo(showlocals=showlocals, style=style,
|
||||
abspath=abspath, tbfilter=tbfilter, funcargs=funcargs)
|
||||
return fmt.repr_excinfo(self)
|
||||
|
||||
def __str__(self):
|
||||
entry = self.traceback[-1]
|
||||
loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly())
|
||||
return str(loc)
|
||||
|
||||
def __unicode__(self):
|
||||
entry = self.traceback[-1]
|
||||
loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly())
|
||||
return unicode(loc)
|
||||
|
||||
def match(self, regexp):
|
||||
"""
|
||||
Match the regular expression 'regexp' on the string representation of
|
||||
the exception. If it matches then True is returned (so that it is
|
||||
possible to write 'assert excinfo.match()'). If it doesn't match an
|
||||
AssertionError is raised.
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
if not re.search(regexp, str(self.value)):
|
||||
assert 0, "Pattern '{0!s}' not found in '{1!s}'".format(
|
||||
regexp, self.value)
|
||||
return True
|
||||
|
||||
|
||||
class FormattedExcinfo(object):
|
||||
""" presenting information about failing Functions and Generators. """
|
||||
# for traceback entries
|
||||
flow_marker = ">"
|
||||
fail_marker = "E"
|
||||
|
||||
def __init__(self, showlocals=False, style="long", abspath=True, tbfilter=True, funcargs=False):
|
||||
self.showlocals = showlocals
|
||||
self.style = style
|
||||
self.tbfilter = tbfilter
|
||||
self.funcargs = funcargs
|
||||
self.abspath = abspath
|
||||
self.astcache = {}
|
||||
|
||||
def _getindent(self, source):
|
||||
# figure out indent for given source
|
||||
try:
|
||||
s = str(source.getstatement(len(source)-1))
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
try:
|
||||
s = str(source[-1])
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
return 0
|
||||
return 4 + (len(s) - len(s.lstrip()))
|
||||
|
||||
def _getentrysource(self, entry):
|
||||
source = entry.getsource(self.astcache)
|
||||
if source is not None:
|
||||
source = source.deindent()
|
||||
return source
|
||||
|
||||
def _saferepr(self, obj):
|
||||
return py.io.saferepr(obj)
|
||||
|
||||
def repr_args(self, entry):
|
||||
if self.funcargs:
|
||||
args = []
|
||||
for argname, argvalue in entry.frame.getargs(var=True):
|
||||
args.append((argname, self._saferepr(argvalue)))
|
||||
return ReprFuncArgs(args)
|
||||
|
||||
def get_source(self, source, line_index=-1, excinfo=None, short=False):
|
||||
""" return formatted and marked up source lines. """
|
||||
import _pytest._code
|
||||
lines = []
|
||||
if source is None or line_index >= len(source.lines):
|
||||
source = _pytest._code.Source("???")
|
||||
line_index = 0
|
||||
if line_index < 0:
|
||||
line_index += len(source)
|
||||
space_prefix = " "
|
||||
if short:
|
||||
lines.append(space_prefix + source.lines[line_index].strip())
|
||||
else:
|
||||
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:]:
|
||||
lines.append(space_prefix + line)
|
||||
if excinfo is not None:
|
||||
indent = 4 if short else self._getindent(source)
|
||||
lines.extend(self.get_exconly(excinfo, indent=indent, markall=True))
|
||||
return lines
|
||||
|
||||
def get_exconly(self, excinfo, indent=4, markall=False):
|
||||
lines = []
|
||||
indent = " " * indent
|
||||
# get the real exception information out
|
||||
exlines = excinfo.exconly(tryshort=True).split('\n')
|
||||
failindent = self.fail_marker + indent[1:]
|
||||
for line in exlines:
|
||||
lines.append(failindent + line)
|
||||
if not markall:
|
||||
failindent = indent
|
||||
return lines
|
||||
|
||||
def repr_locals(self, locals):
|
||||
if self.showlocals:
|
||||
lines = []
|
||||
keys = [loc for loc in locals if loc[0] != "@"]
|
||||
keys.sort()
|
||||
for name in keys:
|
||||
value = locals[name]
|
||||
if name == '__builtins__':
|
||||
lines.append("__builtins__ = <builtins>")
|
||||
else:
|
||||
# This formatting could all be handled by the
|
||||
# _repr() function, which is only reprlib.Repr in
|
||||
# disguise, so is very configurable.
|
||||
str_repr = self._saferepr(value)
|
||||
#if len(str_repr) < 70 or not isinstance(value,
|
||||
# (list, tuple, dict)):
|
||||
lines.append("%-10s = %s" %(name, str_repr))
|
||||
#else:
|
||||
# self._line("%-10s =\\" % (name,))
|
||||
# # XXX
|
||||
# py.std.pprint.pprint(value, stream=self.excinfowriter)
|
||||
return ReprLocals(lines)
|
||||
|
||||
def repr_traceback_entry(self, entry, excinfo=None):
|
||||
import _pytest._code
|
||||
source = self._getentrysource(entry)
|
||||
if source is None:
|
||||
source = _pytest._code.Source("???")
|
||||
line_index = 0
|
||||
else:
|
||||
# entry.getfirstlinesource() can be -1, should be 0 on jython
|
||||
line_index = entry.lineno - max(entry.getfirstlinesource(), 0)
|
||||
|
||||
lines = []
|
||||
style = entry._repr_style
|
||||
if style is None:
|
||||
style = self.style
|
||||
if style in ("short", "long"):
|
||||
short = style == "short"
|
||||
reprargs = self.repr_args(entry) if not short else None
|
||||
s = self.get_source(source, line_index, excinfo, short=short)
|
||||
lines.extend(s)
|
||||
if short:
|
||||
message = "in %s" %(entry.name)
|
||||
else:
|
||||
message = excinfo and excinfo.typename or ""
|
||||
path = self._makepath(entry.path)
|
||||
filelocrepr = ReprFileLocation(path, entry.lineno+1, message)
|
||||
localsrepr = None
|
||||
if not short:
|
||||
localsrepr = self.repr_locals(entry.locals)
|
||||
return ReprEntry(lines, reprargs, localsrepr, filelocrepr, style)
|
||||
if excinfo:
|
||||
lines.extend(self.get_exconly(excinfo, indent=4))
|
||||
return ReprEntry(lines, None, None, None, style)
|
||||
|
||||
def _makepath(self, path):
|
||||
if not self.abspath:
|
||||
try:
|
||||
np = py.path.local().bestrelpath(path)
|
||||
except OSError:
|
||||
return path
|
||||
if len(np) < len(str(path)):
|
||||
path = np
|
||||
return path
|
||||
|
||||
def repr_traceback(self, excinfo):
|
||||
traceback = excinfo.traceback
|
||||
if self.tbfilter:
|
||||
traceback = traceback.filter()
|
||||
recursionindex = None
|
||||
if is_recursion_error(excinfo):
|
||||
recursionindex = traceback.recursionindex()
|
||||
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 repr_excinfo(self, excinfo):
|
||||
if sys.version_info[0] < 3:
|
||||
reprtraceback = self.repr_traceback(excinfo)
|
||||
reprcrash = excinfo._getreprcrash()
|
||||
|
||||
return ReprExceptionInfo(reprtraceback, reprcrash)
|
||||
else:
|
||||
repr_chain = []
|
||||
e = excinfo.value
|
||||
descr = None
|
||||
while e is not None:
|
||||
if excinfo:
|
||||
reprtraceback = self.repr_traceback(excinfo)
|
||||
reprcrash = excinfo._getreprcrash()
|
||||
else:
|
||||
# fallback to native repr if the exception doesn't have a traceback:
|
||||
# ExceptionInfo objects require a full traceback to work
|
||||
reprtraceback = ReprTracebackNative(py.std.traceback.format_exception(type(e), e, None))
|
||||
reprcrash = None
|
||||
|
||||
repr_chain += [(reprtraceback, reprcrash, descr)]
|
||||
if e.__cause__ is not None:
|
||||
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:
|
||||
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:'
|
||||
else:
|
||||
e = None
|
||||
repr_chain.reverse()
|
||||
return ExceptionChainRepr(repr_chain)
|
||||
|
||||
|
||||
class TerminalRepr(object):
|
||||
def __str__(self):
|
||||
s = self.__unicode__()
|
||||
if sys.version_info[0] < 3:
|
||||
s = s.encode('utf-8')
|
||||
return s
|
||||
|
||||
def __unicode__(self):
|
||||
# FYI this is called from pytest-xdist's serialization of exception
|
||||
# information.
|
||||
io = py.io.TextIO()
|
||||
tw = py.io.TerminalWriter(file=io)
|
||||
self.toterminal(tw)
|
||||
return io.getvalue().strip()
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s instance at %0x>" %(self.__class__, id(self))
|
||||
|
||||
|
||||
class ExceptionRepr(TerminalRepr):
|
||||
def __init__(self):
|
||||
self.sections = []
|
||||
|
||||
def addsection(self, name, content, sep="-"):
|
||||
self.sections.append((name, content, sep))
|
||||
|
||||
def toterminal(self, tw):
|
||||
for name, content, sep in self.sections:
|
||||
tw.sep(sep, name)
|
||||
tw.line(content)
|
||||
|
||||
|
||||
class ExceptionChainRepr(ExceptionRepr):
|
||||
def __init__(self, chain):
|
||||
super(ExceptionChainRepr, self).__init__()
|
||||
self.chain = chain
|
||||
# reprcrash and reprtraceback of the outermost (the newest) exception
|
||||
# in the chain
|
||||
self.reprtraceback = chain[-1][0]
|
||||
self.reprcrash = chain[-1][1]
|
||||
|
||||
def toterminal(self, tw):
|
||||
for element in self.chain:
|
||||
element[0].toterminal(tw)
|
||||
if element[2] is not None:
|
||||
tw.line("")
|
||||
tw.line(element[2], yellow=True)
|
||||
super(ExceptionChainRepr, self).toterminal(tw)
|
||||
|
||||
|
||||
class ReprExceptionInfo(ExceptionRepr):
|
||||
def __init__(self, reprtraceback, reprcrash):
|
||||
super(ReprExceptionInfo, self).__init__()
|
||||
self.reprtraceback = reprtraceback
|
||||
self.reprcrash = reprcrash
|
||||
|
||||
def toterminal(self, tw):
|
||||
self.reprtraceback.toterminal(tw)
|
||||
super(ReprExceptionInfo, self).toterminal(tw)
|
||||
|
||||
class ReprTraceback(TerminalRepr):
|
||||
entrysep = "_ "
|
||||
|
||||
def __init__(self, reprentries, extraline, style):
|
||||
self.reprentries = reprentries
|
||||
self.extraline = extraline
|
||||
self.style = style
|
||||
|
||||
def toterminal(self, tw):
|
||||
# the entries might have different styles
|
||||
for i, entry in enumerate(self.reprentries):
|
||||
if entry.style == "long":
|
||||
tw.line("")
|
||||
entry.toterminal(tw)
|
||||
if i < len(self.reprentries) - 1:
|
||||
next_entry = self.reprentries[i+1]
|
||||
if entry.style == "long" or \
|
||||
entry.style == "short" and next_entry.style == "long":
|
||||
tw.sep(self.entrysep)
|
||||
|
||||
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"
|
||||
|
||||
def __init__(self, tblines):
|
||||
self.lines = tblines
|
||||
|
||||
def toterminal(self, tw):
|
||||
tw.write("".join(self.lines))
|
||||
|
||||
class ReprEntry(TerminalRepr):
|
||||
localssep = "_ "
|
||||
|
||||
def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style):
|
||||
self.lines = lines
|
||||
self.reprfuncargs = reprfuncargs
|
||||
self.reprlocals = reprlocals
|
||||
self.reprfileloc = filelocrepr
|
||||
self.style = style
|
||||
|
||||
def toterminal(self, tw):
|
||||
if self.style == "short":
|
||||
self.reprfileloc.toterminal(tw)
|
||||
for line in self.lines:
|
||||
red = line.startswith("E ")
|
||||
tw.line(line, bold=True, red=red)
|
||||
#tw.line("")
|
||||
return
|
||||
if self.reprfuncargs:
|
||||
self.reprfuncargs.toterminal(tw)
|
||||
for line in self.lines:
|
||||
red = line.startswith("E ")
|
||||
tw.line(line, bold=True, red=red)
|
||||
if self.reprlocals:
|
||||
#tw.sep(self.localssep, "Locals")
|
||||
tw.line("")
|
||||
self.reprlocals.toterminal(tw)
|
||||
if self.reprfileloc:
|
||||
if self.lines:
|
||||
tw.line("")
|
||||
self.reprfileloc.toterminal(tw)
|
||||
|
||||
def __str__(self):
|
||||
return "%s\n%s\n%s" % ("\n".join(self.lines),
|
||||
self.reprlocals,
|
||||
self.reprfileloc)
|
||||
|
||||
class ReprFileLocation(TerminalRepr):
|
||||
def __init__(self, path, lineno, message):
|
||||
self.path = str(path)
|
||||
self.lineno = lineno
|
||||
self.message = message
|
||||
|
||||
def toterminal(self, tw):
|
||||
# filename and lineno output for each entry,
|
||||
# using an output format that most editors unterstand
|
||||
msg = self.message
|
||||
i = msg.find("\n")
|
||||
if i != -1:
|
||||
msg = msg[:i]
|
||||
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
|
||||
|
||||
def toterminal(self, tw):
|
||||
for line in self.lines:
|
||||
tw.line(line)
|
||||
|
||||
class ReprFuncArgs(TerminalRepr):
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
def toterminal(self, tw):
|
||||
if self.args:
|
||||
linesofar = ""
|
||||
for name, value in self.args:
|
||||
ns = "%s = %s" %(name, value)
|
||||
if len(ns) + len(linesofar) + 2 > tw.fullwidth:
|
||||
if linesofar:
|
||||
tw.line(linesofar)
|
||||
linesofar = ns
|
||||
else:
|
||||
if linesofar:
|
||||
linesofar += ", " + ns
|
||||
else:
|
||||
linesofar = ns
|
||||
if linesofar:
|
||||
tw.line(linesofar)
|
||||
tw.line("")
|
||||
|
||||
|
||||
def getrawcode(obj, trycall=True):
|
||||
""" return code object for given function. """
|
||||
try:
|
||||
return obj.__code__
|
||||
except AttributeError:
|
||||
obj = getattr(obj, 'im_func', obj)
|
||||
obj = getattr(obj, 'func_code', obj)
|
||||
obj = getattr(obj, 'f_code', obj)
|
||||
obj = getattr(obj, '__code__', obj)
|
||||
if trycall and not hasattr(obj, 'co_firstlineno'):
|
||||
if hasattr(obj, '__call__') and not py.std.inspect.isclass(obj):
|
||||
x = getrawcode(obj.__call__, trycall=False)
|
||||
if hasattr(x, 'co_firstlineno'):
|
||||
return x
|
||||
return obj
|
||||
|
||||
|
||||
if sys.version_info[:2] >= (3, 5): # RecursionError introduced in 3.5
|
||||
def is_recursion_error(excinfo):
|
||||
return excinfo.errisinstance(RecursionError) # noqa
|
||||
else:
|
||||
def is_recursion_error(excinfo):
|
||||
if not excinfo.errisinstance(RuntimeError):
|
||||
return False
|
||||
try:
|
||||
return "maximum recursion depth exceeded" in str(excinfo.value)
|
||||
except UnicodeError:
|
||||
return False
|
||||
414
_pytest/_code/source.py
Normal file
414
_pytest/_code/source.py
Normal file
@@ -0,0 +1,414 @@
|
||||
from __future__ import generators
|
||||
|
||||
from bisect import bisect_right
|
||||
import sys
|
||||
import inspect, tokenize
|
||||
import py
|
||||
cpy_compile = compile
|
||||
|
||||
try:
|
||||
import _ast
|
||||
from _ast import PyCF_ONLY_AST as _AST_FLAG
|
||||
except ImportError:
|
||||
_AST_FLAG = 0
|
||||
_ast = None
|
||||
|
||||
|
||||
class Source(object):
|
||||
""" a immutable object holding a source code fragment,
|
||||
possibly deindenting it.
|
||||
"""
|
||||
_compilecounter = 0
|
||||
def __init__(self, *parts, **kwargs):
|
||||
self.lines = lines = []
|
||||
de = kwargs.get('deindent', True)
|
||||
rstrip = kwargs.get('rstrip', True)
|
||||
for part in parts:
|
||||
if not part:
|
||||
partlines = []
|
||||
if isinstance(part, Source):
|
||||
partlines = part.lines
|
||||
elif isinstance(part, (tuple, list)):
|
||||
partlines = [x.rstrip("\n") for x in part]
|
||||
elif isinstance(part, py.builtin._basestring):
|
||||
partlines = part.split('\n')
|
||||
if rstrip:
|
||||
while partlines:
|
||||
if partlines[-1].strip():
|
||||
break
|
||||
partlines.pop()
|
||||
else:
|
||||
partlines = getsource(part, deindent=de).lines
|
||||
if de:
|
||||
partlines = deindent(partlines)
|
||||
lines.extend(partlines)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
return self.lines == other.lines
|
||||
except AttributeError:
|
||||
if isinstance(other, str):
|
||||
return str(self) == other
|
||||
return False
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __getitem__(self, key):
|
||||
if isinstance(key, int):
|
||||
return self.lines[key]
|
||||
else:
|
||||
if key.step not in (None, 1):
|
||||
raise IndexError("cannot slice a Source with a step")
|
||||
newsource = Source()
|
||||
newsource.lines = self.lines[key.start:key.stop]
|
||||
return newsource
|
||||
|
||||
def __len__(self):
|
||||
return len(self.lines)
|
||||
|
||||
def strip(self):
|
||||
""" return new source object with trailing
|
||||
and leading blank lines removed.
|
||||
"""
|
||||
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():
|
||||
end -= 1
|
||||
source = Source()
|
||||
source.lines[:] = self.lines[start:end]
|
||||
return source
|
||||
|
||||
def putaround(self, before='', after='', indent=' ' * 4):
|
||||
""" return a copy of the source object with
|
||||
'before' and 'after' wrapped around it.
|
||||
"""
|
||||
before = Source(before)
|
||||
after = Source(after)
|
||||
newsource = Source()
|
||||
lines = [ (indent + line) for line in self.lines]
|
||||
newsource.lines = before.lines + lines + after.lines
|
||||
return newsource
|
||||
|
||||
def indent(self, indent=' ' * 4):
|
||||
""" return a copy of the source object with
|
||||
all lines indented by the given indent-string.
|
||||
"""
|
||||
newsource = Source()
|
||||
newsource.lines = [(indent+line) for line in self.lines]
|
||||
return newsource
|
||||
|
||||
def getstatement(self, lineno, assertion=False):
|
||||
""" return Source statement which contains the
|
||||
given linenumber (counted from 0).
|
||||
"""
|
||||
start, end = self.getstatementrange(lineno, assertion)
|
||||
return self[start:end]
|
||||
|
||||
def getstatementrange(self, lineno, assertion=False):
|
||||
""" return (start, end) tuple which spans the minimal
|
||||
statement region which containing the given lineno.
|
||||
"""
|
||||
if not (0 <= lineno < len(self)):
|
||||
raise IndexError("lineno out of range")
|
||||
ast, start, end = getstatementrange_ast(lineno, self)
|
||||
return start, end
|
||||
|
||||
def deindent(self, offset=None):
|
||||
""" return a new source object deindented by offset.
|
||||
If offset is None then guess an indentation offset from
|
||||
the first non-blank line. Subsequent lines which have a
|
||||
lower indentation offset will be copied verbatim as
|
||||
they are assumed to be part of multilines.
|
||||
"""
|
||||
# XXX maybe use the tokenizer to properly handle multiline
|
||||
# strings etc.pp?
|
||||
newsource = Source()
|
||||
newsource.lines[:] = deindent(self.lines, offset)
|
||||
return newsource
|
||||
|
||||
def isparseable(self, deindent=True):
|
||||
""" return True if source is parseable, heuristically
|
||||
deindenting it by default.
|
||||
"""
|
||||
try:
|
||||
import parser
|
||||
except ImportError:
|
||||
syntax_checker = lambda x: compile(x, 'asd', 'exec')
|
||||
else:
|
||||
syntax_checker = parser.suite
|
||||
|
||||
if deindent:
|
||||
source = str(self.deindent())
|
||||
else:
|
||||
source = str(self)
|
||||
try:
|
||||
#compile(source+'\n', "x", "exec")
|
||||
syntax_checker(source+'\n')
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return "\n".join(self.lines)
|
||||
|
||||
def compile(self, filename=None, mode='exec',
|
||||
flag=generators.compiler_flag,
|
||||
dont_inherit=0, _genframe=None):
|
||||
""" return compiled code object. if filename is None
|
||||
invent an artificial filename which displays
|
||||
the source/line position of the caller frame.
|
||||
"""
|
||||
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
|
||||
base = "<%d-codegen " % self._compilecounter
|
||||
self.__class__._compilecounter += 1
|
||||
if not filename:
|
||||
filename = base + '%s:%d>' % (fn, lineno)
|
||||
else:
|
||||
filename = base + '%r %s:%d>' % (filename, fn, lineno)
|
||||
source = "\n".join(self.lines) + '\n'
|
||||
try:
|
||||
co = cpy_compile(source, filename, mode, flag)
|
||||
except SyntaxError:
|
||||
ex = sys.exc_info()[1]
|
||||
# re-represent syntax errors from parsing python strings
|
||||
msglines = self.lines[:ex.lineno]
|
||||
if 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
|
||||
newex.lineno = ex.lineno
|
||||
newex.text = ex.text
|
||||
raise newex
|
||||
else:
|
||||
if flag & _AST_FLAG:
|
||||
return co
|
||||
lines = [(x + "\n") for x in self.lines]
|
||||
py.std.linecache.cache[filename] = (1, None, lines, filename)
|
||||
return co
|
||||
|
||||
#
|
||||
# public API shortcut functions
|
||||
#
|
||||
|
||||
def compile_(source, filename=None, mode='exec', flags=
|
||||
generators.compiler_flag, dont_inherit=0):
|
||||
""" compile the given source to a raw code object,
|
||||
and maintain an internal cache which allows later
|
||||
retrieval of the source code for the code object
|
||||
and any recursively created code objects.
|
||||
"""
|
||||
if _ast is not None and isinstance(source, _ast.AST):
|
||||
# XXX should Source support having AST?
|
||||
return cpy_compile(source, filename, mode, flags, dont_inherit)
|
||||
_genframe = sys._getframe(1) # the caller
|
||||
s = Source(source)
|
||||
co = s.compile(filename, mode, flags, _genframe=_genframe)
|
||||
return co
|
||||
|
||||
|
||||
def getfslineno(obj):
|
||||
""" Return source location (path, lineno) for the given object.
|
||||
If the source cannot be determined return ("", -1)
|
||||
"""
|
||||
import _pytest._code
|
||||
try:
|
||||
code = _pytest._code.Code(obj)
|
||||
except TypeError:
|
||||
try:
|
||||
fn = (py.std.inspect.getsourcefile(obj) or
|
||||
py.std.inspect.getfile(obj))
|
||||
except TypeError:
|
||||
return "", -1
|
||||
|
||||
fspath = fn and py.path.local(fn) or None
|
||||
lineno = -1
|
||||
if fspath:
|
||||
try:
|
||||
_, lineno = findsource(obj)
|
||||
except IOError:
|
||||
pass
|
||||
else:
|
||||
fspath = code.path
|
||||
lineno = code.firstlineno
|
||||
assert isinstance(lineno, int)
|
||||
return fspath, lineno
|
||||
|
||||
#
|
||||
# helper functions
|
||||
#
|
||||
|
||||
def findsource(obj):
|
||||
try:
|
||||
sourcelines, lineno = py.std.inspect.findsource(obj)
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
return None, -1
|
||||
source = Source()
|
||||
source.lines = [line.rstrip() for line in sourcelines]
|
||||
return source, lineno
|
||||
|
||||
|
||||
def getsource(obj, **kwargs):
|
||||
import _pytest._code
|
||||
obj = _pytest._code.getrawcode(obj)
|
||||
try:
|
||||
strsrc = inspect.getsource(obj)
|
||||
except IndentationError:
|
||||
strsrc = "\"Buggy python version consider upgrading, cannot get source\""
|
||||
assert isinstance(strsrc, str)
|
||||
return Source(strsrc, **kwargs)
|
||||
|
||||
|
||||
def deindent(lines, offset=None):
|
||||
if offset is None:
|
||||
for line in lines:
|
||||
line = line.expandtabs()
|
||||
s = line.lstrip()
|
||||
if s:
|
||||
offset = len(line)-len(s)
|
||||
break
|
||||
else:
|
||||
offset = 0
|
||||
if offset == 0:
|
||||
return list(lines)
|
||||
newlines = []
|
||||
|
||||
def readline_generator(lines):
|
||||
for line in lines:
|
||||
yield line + '\n'
|
||||
while True:
|
||||
yield ''
|
||||
|
||||
it = readline_generator(lines)
|
||||
|
||||
try:
|
||||
for _, _, (sline, _), (eline, _), _ in tokenize.generate_tokens(lambda: next(it)):
|
||||
if sline > len(lines):
|
||||
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
|
||||
newlines.append(line)
|
||||
|
||||
for i in range(sline, eline):
|
||||
# Don't deindent continuing lines of
|
||||
# multiline tokens (i.e. multiline strings)
|
||||
newlines.append(lines[i])
|
||||
except (IndentationError, tokenize.TokenError):
|
||||
pass
|
||||
# Add any lines we didn't see. E.g. if an exception was raised.
|
||||
newlines.extend(lines[len(newlines):])
|
||||
return newlines
|
||||
|
||||
|
||||
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 = []
|
||||
for x in ast.walk(node):
|
||||
if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler):
|
||||
l.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):
|
||||
end = None
|
||||
else:
|
||||
end = l[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:
|
||||
start, end = getstatementrange_old(lineno, source, assertion)
|
||||
return None, start, end
|
||||
start, end = get_statement_startend2(lineno, astnode)
|
||||
# we need to correct the end:
|
||||
# - ast-parsing strips comments
|
||||
# - there might be empty lines
|
||||
# - we might have lesser indented code blocks at the end
|
||||
if end is None:
|
||||
end = len(source.lines)
|
||||
|
||||
if end > start + 1:
|
||||
# make sure we don't span differently indented code blocks
|
||||
# by using the BlockFinder helper used which inspect.getsource() uses itself
|
||||
block_finder = inspect.BlockFinder()
|
||||
# if we start with an indented line, put blockfinder to "started" mode
|
||||
block_finder.started = source.lines[start][0].isspace()
|
||||
it = ((x + "\n") for x in source.lines[start:end])
|
||||
try:
|
||||
for tok in tokenize.generate_tokens(lambda: next(it)):
|
||||
block_finder.tokeneater(*tok)
|
||||
except (inspect.EndOfBlock, IndentationError):
|
||||
end = block_finder.last + start
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# the end might still point to a comment or empty line, correct it
|
||||
while end:
|
||||
line = source.lines[end - 1].lstrip()
|
||||
if line.startswith("#") or not line:
|
||||
end -= 1
|
||||
else:
|
||||
break
|
||||
return astnode, start, end
|
||||
|
||||
|
||||
def getstatementrange_old(lineno, source, assertion=False):
|
||||
""" return (start, end) tuple which spans the minimal
|
||||
statement region which containing the given lineno.
|
||||
raise an IndexError if no such statementrange can be found.
|
||||
"""
|
||||
# XXX this logic is only used on python2.4 and below
|
||||
# 1. find the start of the statement
|
||||
from codeop import compile_command
|
||||
for start in range(lineno, -1, -1):
|
||||
if assertion:
|
||||
line = source.lines[start]
|
||||
# the following lines are not fully tested, change with care
|
||||
if 'super' in line and 'self' in line and '__init__' in line:
|
||||
raise IndexError("likely a subclass")
|
||||
if "assert" not in line and "raise" not in line:
|
||||
continue
|
||||
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():')
|
||||
trysource = '\n '.join(trylines)
|
||||
# ^ space here
|
||||
try:
|
||||
compile_command(trysource)
|
||||
except (SyntaxError, OverflowError, ValueError):
|
||||
continue
|
||||
|
||||
# 2. find the end of the statement
|
||||
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,))
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
support for presenting detailed information in failing assertions.
|
||||
"""
|
||||
import py
|
||||
import os
|
||||
import sys
|
||||
from _pytest.monkeypatch import monkeypatch
|
||||
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion import rewrite
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -12,25 +14,49 @@ def pytest_addoption(parser):
|
||||
group.addoption('--assert',
|
||||
action="store",
|
||||
dest="assertmode",
|
||||
choices=("rewrite", "reinterp", "plain",),
|
||||
choices=("rewrite", "plain",),
|
||||
default="rewrite",
|
||||
metavar="MODE",
|
||||
help="""control assertion debugging tools. 'plain'
|
||||
performs no assertion debugging. 'reinterp'
|
||||
reinterprets assert statements after they failed
|
||||
to provide assertion expression information.
|
||||
'rewrite' (the default) rewrites assert
|
||||
statements in test modules on import to
|
||||
provide assert expression information. """)
|
||||
group.addoption('--no-assert',
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="noassert",
|
||||
help="DEPRECATED equivalent to --assert=plain")
|
||||
group.addoption('--nomagic', '--no-magic',
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="DEPRECATED equivalent to --assert=plain")
|
||||
help="""Control assertion debugging tools. 'plain'
|
||||
performs no assertion debugging. 'rewrite'
|
||||
(the default) rewrites assert statements in
|
||||
test modules on import to provide assert
|
||||
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.
|
||||
|
||||
This function will make sure that this module or all modules inside
|
||||
the package will get their assert statements rewritten.
|
||||
Thus you should make sure to call this before the module is
|
||||
actually imported, usually in your __init__.py if you are a plugin
|
||||
using a package.
|
||||
|
||||
:raise TypeError: if the given module names are not strings.
|
||||
"""
|
||||
for name in names:
|
||||
if not isinstance(name, str):
|
||||
msg = 'expected module names as *args, got {0} instead'
|
||||
raise TypeError(msg.format(repr(names)))
|
||||
for hook in sys.meta_path:
|
||||
if isinstance(hook, rewrite.AssertionRewritingHook):
|
||||
importhook = hook
|
||||
break
|
||||
else:
|
||||
importhook = DummyRewriteHook()
|
||||
importhook.mark_rewrite(*names)
|
||||
|
||||
|
||||
class DummyRewriteHook(object):
|
||||
"""A no-op import hook for when rewriting is disabled."""
|
||||
|
||||
def mark_rewrite(self, *names):
|
||||
pass
|
||||
|
||||
|
||||
class AssertionState:
|
||||
@@ -39,51 +65,45 @@ class AssertionState:
|
||||
def __init__(self, config, mode):
|
||||
self.mode = mode
|
||||
self.trace = config.trace.root.get("assertion")
|
||||
self.hook = None
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
mode = config.getvalue("assertmode")
|
||||
if config.getvalue("noassert") or config.getvalue("nomagic"):
|
||||
mode = "plain"
|
||||
if mode == "rewrite":
|
||||
try:
|
||||
import ast # noqa
|
||||
except ImportError:
|
||||
mode = "reinterp"
|
||||
else:
|
||||
# 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)):
|
||||
mode = "reinterp"
|
||||
if mode != "plain":
|
||||
_load_modules(mode)
|
||||
m = monkeypatch()
|
||||
config._cleanup.append(m.undo)
|
||||
m.setattr(py.builtin.builtins, 'AssertionError',
|
||||
reinterpret.AssertionError) # noqa
|
||||
hook = None
|
||||
if mode == "rewrite":
|
||||
hook = rewrite.AssertionRewritingHook() # noqa
|
||||
sys.meta_path.insert(0, hook)
|
||||
warn_about_missing_assertion(mode)
|
||||
config._assertstate = AssertionState(config, mode)
|
||||
config._assertstate.hook = hook
|
||||
config._assertstate.trace("configured with mode set to %r" % (mode,))
|
||||
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)):
|
||||
raise SystemError('rewrite not supported')
|
||||
|
||||
config._assertstate = AssertionState(config, 'rewrite')
|
||||
config._assertstate.hook = hook = rewrite.AssertionRewritingHook(config)
|
||||
sys.meta_path.insert(0, hook)
|
||||
config._assertstate.trace('installed rewrite import hook')
|
||||
|
||||
def undo():
|
||||
hook = config._assertstate.hook
|
||||
if hook is not None and hook in sys.meta_path:
|
||||
sys.meta_path.remove(hook)
|
||||
|
||||
config.add_cleanup(undo)
|
||||
return hook
|
||||
|
||||
|
||||
def pytest_collection(session):
|
||||
# this hook is only called when test modules are collected
|
||||
# so for example not in the master process of pytest-xdist
|
||||
# (which does not collect test modules)
|
||||
hook = session.config._assertstate.hook
|
||||
if hook is not None:
|
||||
hook.set_session(session)
|
||||
assertstate = getattr(session.config, '_assertstate', None)
|
||||
if assertstate:
|
||||
if assertstate.hook is not None:
|
||||
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):
|
||||
@@ -99,7 +119,8 @@ def pytest_runtest_setup(item):
|
||||
|
||||
This uses the first result from the hook and then ensures the
|
||||
following:
|
||||
* Overly verbose explanations are dropped unles -vv was used.
|
||||
* Overly verbose explanations are dropped unless -vv was used or
|
||||
running on a CI.
|
||||
* Embedded newlines are escaped to help util.format_explanation()
|
||||
later.
|
||||
* If the rewrite mode is used embedded %-characters are replaced
|
||||
@@ -112,8 +133,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):
|
||||
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(
|
||||
@@ -132,35 +154,10 @@ def pytest_runtest_teardown(item):
|
||||
|
||||
|
||||
def pytest_sessionfinish(session):
|
||||
hook = session.config._assertstate.hook
|
||||
if hook is not None:
|
||||
hook.session = None
|
||||
|
||||
|
||||
def _load_modules(mode):
|
||||
"""Lazily import assertion related code."""
|
||||
global rewrite, reinterpret
|
||||
from _pytest.assertion import reinterpret # noqa
|
||||
if mode == "rewrite":
|
||||
from _pytest.assertion import rewrite # noqa
|
||||
|
||||
|
||||
def warn_about_missing_assertion(mode):
|
||||
try:
|
||||
assert False
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
if mode == "rewrite":
|
||||
specifically = ("assertions which are not in test modules "
|
||||
"will be ignored")
|
||||
else:
|
||||
specifically = "failing tests may report as passing"
|
||||
|
||||
sys.stderr.write("WARNING: " + specifically +
|
||||
" because assert statements are not executed "
|
||||
"by the underlying Python interpreter "
|
||||
"(are you using python -O?)\n")
|
||||
assertstate = getattr(session.config, '_assertstate', None)
|
||||
if assertstate:
|
||||
if assertstate.hook is not None:
|
||||
assertstate.hook.set_session(None)
|
||||
|
||||
|
||||
# Expose this plugin's implementation for the pytest_assertrepr_compare hook
|
||||
|
||||
@@ -1,365 +0,0 @@
|
||||
"""
|
||||
Find intermediate evalutation results in assert statements through builtin AST.
|
||||
This should replace oldinterpret.py eventually.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import ast
|
||||
|
||||
import py
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion.reinterpret import BuiltinAssertionError
|
||||
|
||||
|
||||
if sys.platform.startswith("java"):
|
||||
# See http://bugs.jython.org/issue1497
|
||||
_exprs = ("BoolOp", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict",
|
||||
"ListComp", "GeneratorExp", "Yield", "Compare", "Call",
|
||||
"Repr", "Num", "Str", "Attribute", "Subscript", "Name",
|
||||
"List", "Tuple")
|
||||
_stmts = ("FunctionDef", "ClassDef", "Return", "Delete", "Assign",
|
||||
"AugAssign", "Print", "For", "While", "If", "With", "Raise",
|
||||
"TryExcept", "TryFinally", "Assert", "Import", "ImportFrom",
|
||||
"Exec", "Global", "Expr", "Pass", "Break", "Continue")
|
||||
_expr_nodes = set(getattr(ast, name) for name in _exprs)
|
||||
_stmt_nodes = set(getattr(ast, name) for name in _stmts)
|
||||
def _is_ast_expr(node):
|
||||
return node.__class__ in _expr_nodes
|
||||
def _is_ast_stmt(node):
|
||||
return node.__class__ in _stmt_nodes
|
||||
else:
|
||||
def _is_ast_expr(node):
|
||||
return isinstance(node, ast.expr)
|
||||
def _is_ast_stmt(node):
|
||||
return isinstance(node, ast.stmt)
|
||||
|
||||
try:
|
||||
_Starred = ast.Starred
|
||||
except AttributeError:
|
||||
# Python 2. Define a dummy class so isinstance() will always be False.
|
||||
class _Starred(object): pass
|
||||
|
||||
|
||||
class Failure(Exception):
|
||||
"""Error found while interpreting AST."""
|
||||
|
||||
def __init__(self, explanation=""):
|
||||
self.cause = sys.exc_info()
|
||||
self.explanation = explanation
|
||||
|
||||
|
||||
def interpret(source, frame, should_fail=False):
|
||||
mod = ast.parse(source)
|
||||
visitor = DebugInterpreter(frame)
|
||||
try:
|
||||
visitor.visit(mod)
|
||||
except Failure:
|
||||
failure = sys.exc_info()[1]
|
||||
return getfailure(failure)
|
||||
if should_fail:
|
||||
return ("(assertion failed, but when it was re-run for "
|
||||
"printing intermediate values, it did not fail. Suggestions: "
|
||||
"compute assert expression before the assert or use --assert=plain)")
|
||||
|
||||
def run(offending_line, frame=None):
|
||||
if frame is None:
|
||||
frame = py.code.Frame(sys._getframe(1))
|
||||
return interpret(offending_line, frame)
|
||||
|
||||
def getfailure(e):
|
||||
explanation = util.format_explanation(e.explanation)
|
||||
value = e.cause[1]
|
||||
if str(value):
|
||||
lines = explanation.split('\n')
|
||||
lines[0] += " << %s" % (value,)
|
||||
explanation = '\n'.join(lines)
|
||||
text = "%s: %s" % (e.cause[0].__name__, explanation)
|
||||
if text.startswith('AssertionError: assert '):
|
||||
text = text[16:]
|
||||
return text
|
||||
|
||||
operator_map = {
|
||||
ast.BitOr : "|",
|
||||
ast.BitXor : "^",
|
||||
ast.BitAnd : "&",
|
||||
ast.LShift : "<<",
|
||||
ast.RShift : ">>",
|
||||
ast.Add : "+",
|
||||
ast.Sub : "-",
|
||||
ast.Mult : "*",
|
||||
ast.Div : "/",
|
||||
ast.FloorDiv : "//",
|
||||
ast.Mod : "%",
|
||||
ast.Eq : "==",
|
||||
ast.NotEq : "!=",
|
||||
ast.Lt : "<",
|
||||
ast.LtE : "<=",
|
||||
ast.Gt : ">",
|
||||
ast.GtE : ">=",
|
||||
ast.Pow : "**",
|
||||
ast.Is : "is",
|
||||
ast.IsNot : "is not",
|
||||
ast.In : "in",
|
||||
ast.NotIn : "not in"
|
||||
}
|
||||
|
||||
unary_map = {
|
||||
ast.Not : "not %s",
|
||||
ast.Invert : "~%s",
|
||||
ast.USub : "-%s",
|
||||
ast.UAdd : "+%s"
|
||||
}
|
||||
|
||||
|
||||
class DebugInterpreter(ast.NodeVisitor):
|
||||
"""Interpret AST nodes to gleam useful debugging information. """
|
||||
|
||||
def __init__(self, frame):
|
||||
self.frame = frame
|
||||
|
||||
def generic_visit(self, node):
|
||||
# Fallback when we don't have a special implementation.
|
||||
if _is_ast_expr(node):
|
||||
mod = ast.Expression(node)
|
||||
co = self._compile(mod)
|
||||
try:
|
||||
result = self.frame.eval(co)
|
||||
except Exception:
|
||||
raise Failure()
|
||||
explanation = self.frame.repr(result)
|
||||
return explanation, result
|
||||
elif _is_ast_stmt(node):
|
||||
mod = ast.Module([node])
|
||||
co = self._compile(mod, "exec")
|
||||
try:
|
||||
self.frame.exec_(co)
|
||||
except Exception:
|
||||
raise Failure()
|
||||
return None, None
|
||||
else:
|
||||
raise AssertionError("can't handle %s" %(node,))
|
||||
|
||||
def _compile(self, source, mode="eval"):
|
||||
return compile(source, "<assertion interpretation>", mode)
|
||||
|
||||
def visit_Expr(self, expr):
|
||||
return self.visit(expr.value)
|
||||
|
||||
def visit_Module(self, mod):
|
||||
for stmt in mod.body:
|
||||
self.visit(stmt)
|
||||
|
||||
def visit_Name(self, name):
|
||||
explanation, result = self.generic_visit(name)
|
||||
# See if the name is local.
|
||||
source = "%r in locals() is not globals()" % (name.id,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
local = self.frame.eval(co)
|
||||
except Exception:
|
||||
# have to assume it isn't
|
||||
local = None
|
||||
if local is None or not self.frame.is_true(local):
|
||||
return name.id, result
|
||||
return explanation, result
|
||||
|
||||
def visit_Compare(self, comp):
|
||||
left = comp.left
|
||||
left_explanation, left_result = self.visit(left)
|
||||
for op, next_op in zip(comp.ops, comp.comparators):
|
||||
next_explanation, next_result = self.visit(next_op)
|
||||
op_symbol = operator_map[op.__class__]
|
||||
explanation = "%s %s %s" % (left_explanation, op_symbol,
|
||||
next_explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % (op_symbol,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_left=left_result,
|
||||
__exprinfo_right=next_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
try:
|
||||
if not self.frame.is_true(result):
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
break
|
||||
left_explanation, left_result = next_explanation, next_result
|
||||
|
||||
if util._reprcompare is not None:
|
||||
res = util._reprcompare(op_symbol, left_result, next_result)
|
||||
if res:
|
||||
explanation = res
|
||||
return explanation, result
|
||||
|
||||
def visit_BoolOp(self, boolop):
|
||||
is_or = isinstance(boolop.op, ast.Or)
|
||||
explanations = []
|
||||
for operand in boolop.values:
|
||||
explanation, result = self.visit(operand)
|
||||
explanations.append(explanation)
|
||||
if result == is_or:
|
||||
break
|
||||
name = is_or and " or " or " and "
|
||||
explanation = "(" + name.join(explanations) + ")"
|
||||
return explanation, result
|
||||
|
||||
def visit_UnaryOp(self, unary):
|
||||
pattern = unary_map[unary.op.__class__]
|
||||
operand_explanation, operand_result = self.visit(unary.operand)
|
||||
explanation = pattern % (operand_explanation,)
|
||||
co = self._compile(pattern % ("__exprinfo_expr",))
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=operand_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_BinOp(self, binop):
|
||||
left_explanation, left_result = self.visit(binop.left)
|
||||
right_explanation, right_result = self.visit(binop.right)
|
||||
symbol = operator_map[binop.op.__class__]
|
||||
explanation = "(%s %s %s)" % (left_explanation, symbol,
|
||||
right_explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % (symbol,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_left=left_result,
|
||||
__exprinfo_right=right_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_Call(self, call):
|
||||
func_explanation, func = self.visit(call.func)
|
||||
arg_explanations = []
|
||||
ns = {"__exprinfo_func" : func}
|
||||
arguments = []
|
||||
for arg in call.args:
|
||||
arg_explanation, arg_result = self.visit(arg)
|
||||
if isinstance(arg, _Starred):
|
||||
arg_name = "__exprinfo_star"
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append("*%s" % (arg_name,))
|
||||
arg_explanations.append("*%s" % (arg_explanation,))
|
||||
else:
|
||||
arg_name = "__exprinfo_%s" % (len(ns),)
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append(arg_name)
|
||||
arg_explanations.append(arg_explanation)
|
||||
for keyword in call.keywords:
|
||||
arg_explanation, arg_result = self.visit(keyword.value)
|
||||
if keyword.arg:
|
||||
arg_name = "__exprinfo_%s" % (len(ns),)
|
||||
keyword_source = "%s=%%s" % (keyword.arg)
|
||||
arguments.append(keyword_source % (arg_name,))
|
||||
arg_explanations.append(keyword_source % (arg_explanation,))
|
||||
else:
|
||||
arg_name = "__exprinfo_kwds"
|
||||
arguments.append("**%s" % (arg_name,))
|
||||
arg_explanations.append("**%s" % (arg_explanation,))
|
||||
|
||||
ns[arg_name] = arg_result
|
||||
|
||||
if getattr(call, 'starargs', None):
|
||||
arg_explanation, arg_result = self.visit(call.starargs)
|
||||
arg_name = "__exprinfo_star"
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append("*%s" % (arg_name,))
|
||||
arg_explanations.append("*%s" % (arg_explanation,))
|
||||
|
||||
if getattr(call, 'kwargs', None):
|
||||
arg_explanation, arg_result = self.visit(call.kwargs)
|
||||
arg_name = "__exprinfo_kwds"
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append("**%s" % (arg_name,))
|
||||
arg_explanations.append("**%s" % (arg_explanation,))
|
||||
args_explained = ", ".join(arg_explanations)
|
||||
explanation = "%s(%s)" % (func_explanation, args_explained)
|
||||
args = ", ".join(arguments)
|
||||
source = "__exprinfo_func(%s)" % (args,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, **ns)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
pattern = "%s\n{%s = %s\n}"
|
||||
rep = self.frame.repr(result)
|
||||
explanation = pattern % (rep, rep, explanation)
|
||||
return explanation, result
|
||||
|
||||
def _is_builtin_name(self, name):
|
||||
pattern = "%r not in globals() and %r not in locals()"
|
||||
source = pattern % (name.id, name.id)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
return self.frame.eval(co)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def visit_Attribute(self, attr):
|
||||
if not isinstance(attr.ctx, ast.Load):
|
||||
return self.generic_visit(attr)
|
||||
source_explanation, source_result = self.visit(attr.value)
|
||||
explanation = "%s.%s" % (source_explanation, attr.attr)
|
||||
source = "__exprinfo_expr.%s" % (attr.attr,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except AttributeError:
|
||||
# Maybe the attribute name needs to be mangled?
|
||||
if not attr.attr.startswith("__") or attr.attr.endswith("__"):
|
||||
raise
|
||||
source = "getattr(__exprinfo_expr.__class__, '__name__', '')"
|
||||
co = self._compile(source)
|
||||
class_name = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
mangled_attr = "_" + class_name + attr.attr
|
||||
source = "__exprinfo_expr.%s" % (mangled_attr,)
|
||||
co = self._compile(source)
|
||||
result = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
explanation = "%s\n{%s = %s.%s\n}" % (self.frame.repr(result),
|
||||
self.frame.repr(result),
|
||||
source_explanation, attr.attr)
|
||||
# Check if the attr is from an instance.
|
||||
source = "%r in getattr(__exprinfo_expr, '__dict__', {})"
|
||||
source = source % (attr.attr,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
from_instance = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except Exception:
|
||||
from_instance = None
|
||||
if from_instance is None or self.frame.is_true(from_instance):
|
||||
rep = self.frame.repr(result)
|
||||
pattern = "%s\n{%s = %s\n}"
|
||||
explanation = pattern % (rep, rep, explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_Assert(self, assrt):
|
||||
test_explanation, test_result = self.visit(assrt.test)
|
||||
explanation = "assert %s" % (test_explanation,)
|
||||
if not self.frame.is_true(test_result):
|
||||
try:
|
||||
raise BuiltinAssertionError
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, test_result
|
||||
|
||||
def visit_Assign(self, assign):
|
||||
value_explanation, value_result = self.visit(assign.value)
|
||||
explanation = "... = %s" % (value_explanation,)
|
||||
name = ast.Name("__exprinfo_expr", ast.Load(),
|
||||
lineno=assign.value.lineno,
|
||||
col_offset=assign.value.col_offset)
|
||||
new_assign = ast.Assign(assign.targets, name, lineno=assign.lineno,
|
||||
col_offset=assign.col_offset)
|
||||
mod = ast.Module([new_assign])
|
||||
co = self._compile(mod, "exec")
|
||||
try:
|
||||
self.frame.exec_(co, __exprinfo_expr=value_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, value_result
|
||||
@@ -1,566 +0,0 @@
|
||||
import traceback
|
||||
import types
|
||||
import py
|
||||
import sys, inspect
|
||||
from compiler import parse, ast, pycodegen
|
||||
from _pytest.assertion.util import format_explanation, BuiltinAssertionError
|
||||
|
||||
passthroughex = py.builtin._sysex
|
||||
|
||||
class Failure:
|
||||
def __init__(self, node):
|
||||
self.exc, self.value, self.tb = sys.exc_info()
|
||||
self.node = node
|
||||
|
||||
class View(object):
|
||||
"""View base class.
|
||||
|
||||
If C is a subclass of View, then C(x) creates a proxy object around
|
||||
the object x. The actual class of the proxy is not C in general,
|
||||
but a *subclass* of C determined by the rules below. To avoid confusion
|
||||
we call view class the class of the proxy (a subclass of C, so of View)
|
||||
and object class the class of x.
|
||||
|
||||
Attributes and methods not found in the proxy are automatically read on x.
|
||||
Other operations like setting attributes are performed on the proxy, as
|
||||
determined by its view class. The object x is available from the proxy
|
||||
as its __obj__ attribute.
|
||||
|
||||
The view class selection is determined by the __view__ tuples and the
|
||||
optional __viewkey__ method. By default, the selected view class is the
|
||||
most specific subclass of C whose __view__ mentions the class of x.
|
||||
If no such subclass is found, the search proceeds with the parent
|
||||
object classes. For example, C(True) will first look for a subclass
|
||||
of C with __view__ = (..., bool, ...) and only if it doesn't find any
|
||||
look for one with __view__ = (..., int, ...), and then ..., object,...
|
||||
If everything fails the class C itself is considered to be the default.
|
||||
|
||||
Alternatively, the view class selection can be driven by another aspect
|
||||
of the object x, instead of the class of x, by overriding __viewkey__.
|
||||
See last example at the end of this module.
|
||||
"""
|
||||
|
||||
_viewcache = {}
|
||||
__view__ = ()
|
||||
|
||||
def __new__(rootclass, obj, *args, **kwds):
|
||||
self = object.__new__(rootclass)
|
||||
self.__obj__ = obj
|
||||
self.__rootclass__ = rootclass
|
||||
key = self.__viewkey__()
|
||||
try:
|
||||
self.__class__ = self._viewcache[key]
|
||||
except KeyError:
|
||||
self.__class__ = self._selectsubclass(key)
|
||||
return self
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# attributes not found in the normal hierarchy rooted on View
|
||||
# are looked up in the object's real class
|
||||
return getattr(object.__getattribute__(self, '__obj__'), attr)
|
||||
|
||||
def __viewkey__(self):
|
||||
return self.__obj__.__class__
|
||||
|
||||
def __matchkey__(self, key, subclasses):
|
||||
if inspect.isclass(key):
|
||||
keys = inspect.getmro(key)
|
||||
else:
|
||||
keys = [key]
|
||||
for key in keys:
|
||||
result = [C for C in subclasses if key in C.__view__]
|
||||
if result:
|
||||
return result
|
||||
return []
|
||||
|
||||
def _selectsubclass(self, key):
|
||||
subclasses = list(enumsubclasses(self.__rootclass__))
|
||||
for C in subclasses:
|
||||
if not isinstance(C.__view__, tuple):
|
||||
C.__view__ = (C.__view__,)
|
||||
choices = self.__matchkey__(key, subclasses)
|
||||
if not choices:
|
||||
return self.__rootclass__
|
||||
elif len(choices) == 1:
|
||||
return choices[0]
|
||||
else:
|
||||
# combine the multiple choices
|
||||
return type('?', tuple(choices), {})
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__)
|
||||
|
||||
|
||||
def enumsubclasses(cls):
|
||||
for subcls in cls.__subclasses__():
|
||||
for subsubclass in enumsubclasses(subcls):
|
||||
yield subsubclass
|
||||
yield cls
|
||||
|
||||
|
||||
class Interpretable(View):
|
||||
"""A parse tree node with a few extra methods."""
|
||||
explanation = None
|
||||
|
||||
def is_builtin(self, frame):
|
||||
return False
|
||||
|
||||
def eval(self, frame):
|
||||
# fall-back for unknown expression nodes
|
||||
try:
|
||||
expr = ast.Expression(self.__obj__)
|
||||
expr.filename = '<eval>'
|
||||
self.__obj__.filename = '<eval>'
|
||||
co = pycodegen.ExpressionCodeGenerator(expr).getCode()
|
||||
result = frame.eval(co)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
self.result = result
|
||||
self.explanation = self.explanation or frame.repr(self.result)
|
||||
|
||||
def run(self, frame):
|
||||
# fall-back for unknown statement nodes
|
||||
try:
|
||||
expr = ast.Module(None, ast.Stmt([self.__obj__]))
|
||||
expr.filename = '<run>'
|
||||
co = pycodegen.ModuleCodeGenerator(expr).getCode()
|
||||
frame.exec_(co)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
def nice_explanation(self):
|
||||
return format_explanation(self.explanation)
|
||||
|
||||
|
||||
class Name(Interpretable):
|
||||
__view__ = ast.Name
|
||||
|
||||
def is_local(self, frame):
|
||||
source = '%r in locals() is not globals()' % self.name
|
||||
try:
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_global(self, frame):
|
||||
source = '%r in globals()' % self.name
|
||||
try:
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_builtin(self, frame):
|
||||
source = '%r not in locals() and %r not in globals()' % (
|
||||
self.name, self.name)
|
||||
try:
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def eval(self, frame):
|
||||
super(Name, self).eval(frame)
|
||||
if not self.is_local(frame):
|
||||
self.explanation = self.name
|
||||
|
||||
class Compare(Interpretable):
|
||||
__view__ = ast.Compare
|
||||
|
||||
def eval(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
for operation, expr2 in self.ops:
|
||||
if hasattr(self, 'result'):
|
||||
# shortcutting in chained expressions
|
||||
if not frame.is_true(self.result):
|
||||
break
|
||||
expr2 = Interpretable(expr2)
|
||||
expr2.eval(frame)
|
||||
self.explanation = "%s %s %s" % (
|
||||
expr.explanation, operation, expr2.explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % operation
|
||||
try:
|
||||
self.result = frame.eval(source,
|
||||
__exprinfo_left=expr.result,
|
||||
__exprinfo_right=expr2.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
expr = expr2
|
||||
|
||||
class And(Interpretable):
|
||||
__view__ = ast.And
|
||||
|
||||
def eval(self, frame):
|
||||
explanations = []
|
||||
for expr in self.nodes:
|
||||
expr = Interpretable(expr)
|
||||
expr.eval(frame)
|
||||
explanations.append(expr.explanation)
|
||||
self.result = expr.result
|
||||
if not frame.is_true(expr.result):
|
||||
break
|
||||
self.explanation = '(' + ' and '.join(explanations) + ')'
|
||||
|
||||
class Or(Interpretable):
|
||||
__view__ = ast.Or
|
||||
|
||||
def eval(self, frame):
|
||||
explanations = []
|
||||
for expr in self.nodes:
|
||||
expr = Interpretable(expr)
|
||||
expr.eval(frame)
|
||||
explanations.append(expr.explanation)
|
||||
self.result = expr.result
|
||||
if frame.is_true(expr.result):
|
||||
break
|
||||
self.explanation = '(' + ' or '.join(explanations) + ')'
|
||||
|
||||
|
||||
# == Unary operations ==
|
||||
keepalive = []
|
||||
for astclass, astpattern in {
|
||||
ast.Not : 'not __exprinfo_expr',
|
||||
ast.Invert : '(~__exprinfo_expr)',
|
||||
}.items():
|
||||
|
||||
class UnaryArith(Interpretable):
|
||||
__view__ = astclass
|
||||
|
||||
def eval(self, frame, astpattern=astpattern):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
self.explanation = astpattern.replace('__exprinfo_expr',
|
||||
expr.explanation)
|
||||
try:
|
||||
self.result = frame.eval(astpattern,
|
||||
__exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
keepalive.append(UnaryArith)
|
||||
|
||||
# == Binary operations ==
|
||||
for astclass, astpattern in {
|
||||
ast.Add : '(__exprinfo_left + __exprinfo_right)',
|
||||
ast.Sub : '(__exprinfo_left - __exprinfo_right)',
|
||||
ast.Mul : '(__exprinfo_left * __exprinfo_right)',
|
||||
ast.Div : '(__exprinfo_left / __exprinfo_right)',
|
||||
ast.Mod : '(__exprinfo_left % __exprinfo_right)',
|
||||
ast.Power : '(__exprinfo_left ** __exprinfo_right)',
|
||||
}.items():
|
||||
|
||||
class BinaryArith(Interpretable):
|
||||
__view__ = astclass
|
||||
|
||||
def eval(self, frame, astpattern=astpattern):
|
||||
left = Interpretable(self.left)
|
||||
left.eval(frame)
|
||||
right = Interpretable(self.right)
|
||||
right.eval(frame)
|
||||
self.explanation = (astpattern
|
||||
.replace('__exprinfo_left', left .explanation)
|
||||
.replace('__exprinfo_right', right.explanation))
|
||||
try:
|
||||
self.result = frame.eval(astpattern,
|
||||
__exprinfo_left=left.result,
|
||||
__exprinfo_right=right.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
keepalive.append(BinaryArith)
|
||||
|
||||
|
||||
class CallFunc(Interpretable):
|
||||
__view__ = ast.CallFunc
|
||||
|
||||
def is_bool(self, frame):
|
||||
source = 'isinstance(__exprinfo_value, bool)'
|
||||
try:
|
||||
return frame.is_true(frame.eval(source,
|
||||
__exprinfo_value=self.result))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def eval(self, frame):
|
||||
node = Interpretable(self.node)
|
||||
node.eval(frame)
|
||||
explanations = []
|
||||
vars = {'__exprinfo_fn': node.result}
|
||||
source = '__exprinfo_fn('
|
||||
for a in self.args:
|
||||
if isinstance(a, ast.Keyword):
|
||||
keyword = a.name
|
||||
a = a.expr
|
||||
else:
|
||||
keyword = None
|
||||
a = Interpretable(a)
|
||||
a.eval(frame)
|
||||
argname = '__exprinfo_%d' % len(vars)
|
||||
vars[argname] = a.result
|
||||
if keyword is None:
|
||||
source += argname + ','
|
||||
explanations.append(a.explanation)
|
||||
else:
|
||||
source += '%s=%s,' % (keyword, argname)
|
||||
explanations.append('%s=%s' % (keyword, a.explanation))
|
||||
if self.star_args:
|
||||
star_args = Interpretable(self.star_args)
|
||||
star_args.eval(frame)
|
||||
argname = '__exprinfo_star'
|
||||
vars[argname] = star_args.result
|
||||
source += '*' + argname + ','
|
||||
explanations.append('*' + star_args.explanation)
|
||||
if self.dstar_args:
|
||||
dstar_args = Interpretable(self.dstar_args)
|
||||
dstar_args.eval(frame)
|
||||
argname = '__exprinfo_kwds'
|
||||
vars[argname] = dstar_args.result
|
||||
source += '**' + argname + ','
|
||||
explanations.append('**' + dstar_args.explanation)
|
||||
self.explanation = "%s(%s)" % (
|
||||
node.explanation, ', '.join(explanations))
|
||||
if source.endswith(','):
|
||||
source = source[:-1]
|
||||
source += ')'
|
||||
try:
|
||||
self.result = frame.eval(source, **vars)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
if not node.is_builtin(frame) or not self.is_bool(frame):
|
||||
r = frame.repr(self.result)
|
||||
self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation)
|
||||
|
||||
class Getattr(Interpretable):
|
||||
__view__ = ast.Getattr
|
||||
|
||||
def eval(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
source = '__exprinfo_expr.%s' % self.attrname
|
||||
try:
|
||||
try:
|
||||
self.result = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
except AttributeError:
|
||||
# Maybe the attribute name needs to be mangled?
|
||||
if (not self.attrname.startswith("__") or
|
||||
self.attrname.endswith("__")):
|
||||
raise
|
||||
source = "getattr(__exprinfo_expr.__class__, '__name__', '')"
|
||||
class_name = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
mangled_attr = "_" + class_name + self.attrname
|
||||
source = "__exprinfo_expr.%s" % (mangled_attr,)
|
||||
self.result = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
self.explanation = '%s.%s' % (expr.explanation, self.attrname)
|
||||
# if the attribute comes from the instance, its value is interesting
|
||||
source = ('hasattr(__exprinfo_expr, "__dict__") and '
|
||||
'%r in __exprinfo_expr.__dict__' % self.attrname)
|
||||
try:
|
||||
from_instance = frame.is_true(
|
||||
frame.eval(source, __exprinfo_expr=expr.result))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
from_instance = True
|
||||
if from_instance:
|
||||
r = frame.repr(self.result)
|
||||
self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation)
|
||||
|
||||
# == Re-interpretation of full statements ==
|
||||
|
||||
class Assert(Interpretable):
|
||||
__view__ = ast.Assert
|
||||
|
||||
def run(self, frame):
|
||||
test = Interpretable(self.test)
|
||||
test.eval(frame)
|
||||
# print the result as 'assert <explanation>'
|
||||
self.result = test.result
|
||||
self.explanation = 'assert ' + test.explanation
|
||||
if not frame.is_true(test.result):
|
||||
try:
|
||||
raise BuiltinAssertionError
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
class Assign(Interpretable):
|
||||
__view__ = ast.Assign
|
||||
|
||||
def run(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
self.result = expr.result
|
||||
self.explanation = '... = ' + expr.explanation
|
||||
# fall-back-run the rest of the assignment
|
||||
ass = ast.Assign(self.nodes, ast.Name('__exprinfo_expr'))
|
||||
mod = ast.Module(None, ast.Stmt([ass]))
|
||||
mod.filename = '<run>'
|
||||
co = pycodegen.ModuleCodeGenerator(mod).getCode()
|
||||
try:
|
||||
frame.exec_(co, __exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
class Discard(Interpretable):
|
||||
__view__ = ast.Discard
|
||||
|
||||
def run(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
self.result = expr.result
|
||||
self.explanation = expr.explanation
|
||||
|
||||
class Stmt(Interpretable):
|
||||
__view__ = ast.Stmt
|
||||
|
||||
def run(self, frame):
|
||||
for stmt in self.nodes:
|
||||
stmt = Interpretable(stmt)
|
||||
stmt.run(frame)
|
||||
|
||||
|
||||
def report_failure(e):
|
||||
explanation = e.node.nice_explanation()
|
||||
if explanation:
|
||||
explanation = ", in: " + explanation
|
||||
else:
|
||||
explanation = ""
|
||||
sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation))
|
||||
|
||||
def check(s, frame=None):
|
||||
if frame is None:
|
||||
frame = sys._getframe(1)
|
||||
frame = py.code.Frame(frame)
|
||||
expr = parse(s, 'eval')
|
||||
assert isinstance(expr, ast.Expression)
|
||||
node = Interpretable(expr.node)
|
||||
try:
|
||||
node.eval(frame)
|
||||
except passthroughex:
|
||||
raise
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
report_failure(e)
|
||||
else:
|
||||
if not frame.is_true(node.result):
|
||||
sys.stderr.write("assertion failed: %s\n" % node.nice_explanation())
|
||||
|
||||
|
||||
###########################################################
|
||||
# API / Entry points
|
||||
# #########################################################
|
||||
|
||||
def interpret(source, frame, should_fail=False):
|
||||
module = Interpretable(parse(source, 'exec').node)
|
||||
#print "got module", module
|
||||
if isinstance(frame, types.FrameType):
|
||||
frame = py.code.Frame(frame)
|
||||
try:
|
||||
module.run(frame)
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
return getfailure(e)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
traceback.print_exc()
|
||||
if should_fail:
|
||||
return ("(assertion failed, but when it was re-run for "
|
||||
"printing intermediate values, it did not fail. Suggestions: "
|
||||
"compute assert expression before the assert or use --assert=plain)")
|
||||
else:
|
||||
return None
|
||||
|
||||
def getmsg(excinfo):
|
||||
if isinstance(excinfo, tuple):
|
||||
excinfo = py.code.ExceptionInfo(excinfo)
|
||||
#frame, line = gettbline(tb)
|
||||
#frame = py.code.Frame(frame)
|
||||
#return interpret(line, frame)
|
||||
|
||||
tb = excinfo.traceback[-1]
|
||||
source = str(tb.statement).strip()
|
||||
x = interpret(source, tb.frame, should_fail=True)
|
||||
if not isinstance(x, str):
|
||||
raise TypeError("interpret returned non-string %r" % (x,))
|
||||
return x
|
||||
|
||||
def getfailure(e):
|
||||
explanation = e.node.nice_explanation()
|
||||
if str(e.value):
|
||||
lines = explanation.split('\n')
|
||||
lines[0] += " << %s" % (e.value,)
|
||||
explanation = '\n'.join(lines)
|
||||
text = "%s: %s" % (e.exc.__name__, explanation)
|
||||
if text.startswith('AssertionError: assert '):
|
||||
text = text[16:]
|
||||
return text
|
||||
|
||||
def run(s, frame=None):
|
||||
if frame is None:
|
||||
frame = sys._getframe(1)
|
||||
frame = py.code.Frame(frame)
|
||||
module = Interpretable(parse(s, 'exec').node)
|
||||
try:
|
||||
module.run(frame)
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
report_failure(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# example:
|
||||
def f():
|
||||
return 5
|
||||
|
||||
def g():
|
||||
return 3
|
||||
|
||||
def h(x):
|
||||
return 'never'
|
||||
|
||||
check("f() * g() == 5")
|
||||
check("not f()")
|
||||
check("not (f() and g() or 0)")
|
||||
check("f() == g()")
|
||||
i = 4
|
||||
check("i == f()")
|
||||
check("len(f()) == 0")
|
||||
check("isinstance(2+3+4, float)")
|
||||
|
||||
run("x = i")
|
||||
check("x == 5")
|
||||
|
||||
run("assert not f(), 'oops'")
|
||||
run("a, b, c = 1, 2")
|
||||
run("a, b, c = f()")
|
||||
|
||||
check("max([f(),g()]) == 4")
|
||||
check("'hello'[g()] == 'h'")
|
||||
run("'guk%d' % h(f())")
|
||||
@@ -1,52 +0,0 @@
|
||||
import sys
|
||||
import py
|
||||
from _pytest.assertion.util import BuiltinAssertionError
|
||||
u = py.builtin._totext
|
||||
|
||||
|
||||
class AssertionError(BuiltinAssertionError):
|
||||
def __init__(self, *args):
|
||||
BuiltinAssertionError.__init__(self, *args)
|
||||
if args:
|
||||
# on Python2.6 we get len(args)==2 for: assert 0, (x,y)
|
||||
# on Python2.7 and above we always get len(args) == 1
|
||||
# with args[0] being the (x,y) tuple.
|
||||
if len(args) > 1:
|
||||
toprint = args
|
||||
else:
|
||||
toprint = args[0]
|
||||
try:
|
||||
self.msg = u(toprint)
|
||||
except Exception:
|
||||
self.msg = u(
|
||||
"<[broken __repr__] %s at %0xd>"
|
||||
% (toprint.__class__, id(toprint)))
|
||||
else:
|
||||
f = py.code.Frame(sys._getframe(1))
|
||||
try:
|
||||
source = f.code.fullsource
|
||||
if source is not None:
|
||||
try:
|
||||
source = source.getstatement(f.lineno, assertion=True)
|
||||
except IndexError:
|
||||
source = None
|
||||
else:
|
||||
source = str(source.deindent()).strip()
|
||||
except py.error.ENOENT:
|
||||
source = None
|
||||
# this can also occur during reinterpretation, when the
|
||||
# co_filename is set to "<run>".
|
||||
if source:
|
||||
self.msg = reinterpret(source, f, should_fail=True)
|
||||
else:
|
||||
self.msg = "<could not determine information>"
|
||||
if not self.args:
|
||||
self.args = (self.msg,)
|
||||
|
||||
if sys.version_info > (3, 0):
|
||||
AssertionError.__module__ = "builtins"
|
||||
|
||||
if sys.version_info >= (2, 6) or sys.platform.startswith("java"):
|
||||
from _pytest.assertion.newinterpret import interpret as reinterpret
|
||||
else:
|
||||
from _pytest.assertion.oldinterpret import interpret as reinterpret
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Rewrite assertion AST to produce nice error messages"""
|
||||
|
||||
import ast
|
||||
import _ast
|
||||
import errno
|
||||
import itertools
|
||||
import imp
|
||||
@@ -10,6 +11,7 @@ import re
|
||||
import struct
|
||||
import sys
|
||||
import types
|
||||
from fnmatch import fnmatch
|
||||
|
||||
import py
|
||||
from _pytest.assertion import util
|
||||
@@ -44,20 +46,20 @@ else:
|
||||
class AssertionRewritingHook(object):
|
||||
"""PEP302 Import hook which rewrites asserts."""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.fnpats = config.getini("python_files")
|
||||
self.session = None
|
||||
self.modules = {}
|
||||
self._rewritten_names = set()
|
||||
self._register_with_pkg_resources()
|
||||
self._must_rewrite = set()
|
||||
|
||||
def set_session(self, session):
|
||||
self.fnpats = session.config.getini("python_files")
|
||||
self.session = session
|
||||
|
||||
def find_module(self, name, path=None):
|
||||
if self.session is None:
|
||||
return None
|
||||
sess = self.session
|
||||
state = sess.config._assertstate
|
||||
state = self.config._assertstate
|
||||
state.trace("find_module called for: %s" % name)
|
||||
names = name.rsplit(".", 1)
|
||||
lastname = names[-1]
|
||||
@@ -78,7 +80,12 @@ class AssertionRewritingHook(object):
|
||||
tp = desc[2]
|
||||
if tp == imp.PY_COMPILED:
|
||||
if hasattr(imp, "source_from_cache"):
|
||||
fn = imp.source_from_cache(fn)
|
||||
try:
|
||||
fn = imp.source_from_cache(fn)
|
||||
except ValueError:
|
||||
# Python 3 doesn't like orphaned but still-importable
|
||||
# .pyc files.
|
||||
fn = fn[:-1]
|
||||
else:
|
||||
fn = fn[:-1]
|
||||
elif tp != imp.PY_SOURCE:
|
||||
@@ -86,24 +93,13 @@ class AssertionRewritingHook(object):
|
||||
return None
|
||||
else:
|
||||
fn = os.path.join(pth, name.rpartition(".")[2] + ".py")
|
||||
|
||||
fn_pypath = py.path.local(fn)
|
||||
# Is this a test file?
|
||||
if not sess.isinitpath(fn):
|
||||
# We have to be very careful here because imports in this code can
|
||||
# trigger a cycle.
|
||||
self.session = None
|
||||
try:
|
||||
for pat in self.fnpats:
|
||||
if fn_pypath.fnmatch(pat):
|
||||
state.trace("matched test file %r" % (fn,))
|
||||
break
|
||||
else:
|
||||
return None
|
||||
finally:
|
||||
self.session = sess
|
||||
else:
|
||||
state.trace("matched test file (was specified on cmdline): %r" %
|
||||
(fn,))
|
||||
if not self._should_rewrite(name, fn_pypath, state):
|
||||
return None
|
||||
|
||||
self._rewritten_names.add(name)
|
||||
|
||||
# The requested module looks like a test file, so rewrite it. This is
|
||||
# the most magical part of the process: load the source, rewrite the
|
||||
# asserts, and load the rewritten source. We also cache the rewritten
|
||||
@@ -140,7 +136,7 @@ class AssertionRewritingHook(object):
|
||||
co = _read_pyc(fn_pypath, pyc, state.trace)
|
||||
if co is None:
|
||||
state.trace("rewriting %r" % (fn,))
|
||||
source_stat, co = _rewrite_test(state, fn_pypath)
|
||||
source_stat, co = _rewrite_test(self.config, fn_pypath)
|
||||
if co is None:
|
||||
# Probably a SyntaxError in the test.
|
||||
return None
|
||||
@@ -151,6 +147,55 @@ class AssertionRewritingHook(object):
|
||||
self.modules[name] = co, pyc
|
||||
return self
|
||||
|
||||
def _should_rewrite(self, name, fn_pypath, state):
|
||||
# always rewrite conftest files
|
||||
fn = str(fn_pypath)
|
||||
if fn_pypath.basename == 'conftest.py':
|
||||
state.trace("rewriting conftest file: %r" % (fn,))
|
||||
return True
|
||||
|
||||
if self.session is not None:
|
||||
if self.session.isinitpath(fn):
|
||||
state.trace("matched test file (was specified on cmdline): %r" %
|
||||
(fn,))
|
||||
return True
|
||||
|
||||
# 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):
|
||||
state.trace("matched test file %r" % (fn,))
|
||||
return True
|
||||
|
||||
for marked in self._must_rewrite:
|
||||
if 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.
|
||||
|
||||
The named module or package as well as any nested modules will
|
||||
be re-written on import.
|
||||
"""
|
||||
already_imported = set(names).intersection(set(sys.modules))
|
||||
if already_imported:
|
||||
for name in already_imported:
|
||||
if name not in self._rewritten_names:
|
||||
self._warn_already_imported(name)
|
||||
self._must_rewrite.update(names)
|
||||
|
||||
def _warn_already_imported(self, name):
|
||||
self.config.warn(
|
||||
'P1',
|
||||
'Module already imported so can not be re-written: %s' % name)
|
||||
|
||||
def load_module(self, name):
|
||||
# If there is an existing module object named 'fullname' in
|
||||
# sys.modules, the loader must use that existing module. (Otherwise,
|
||||
@@ -170,7 +215,8 @@ class AssertionRewritingHook(object):
|
||||
mod.__loader__ = self
|
||||
py.builtin.exec_(co, mod.__dict__)
|
||||
except:
|
||||
del sys.modules[name]
|
||||
if name in sys.modules:
|
||||
del sys.modules[name]
|
||||
raise
|
||||
return sys.modules[name]
|
||||
|
||||
@@ -235,14 +281,16 @@ def _write_pyc(state, co, source_stat, pyc):
|
||||
fp.close()
|
||||
return True
|
||||
|
||||
|
||||
RN = "\r\n".encode("utf-8")
|
||||
N = "\n".encode("utf-8")
|
||||
|
||||
cookie_re = re.compile(r"^[ \t\f]*#.*coding[:=][ \t]*[-\w.]+")
|
||||
BOM_UTF8 = '\xef\xbb\xbf'
|
||||
|
||||
def _rewrite_test(state, fn):
|
||||
def _rewrite_test(config, fn):
|
||||
"""Try to read and rewrite *fn* and return the code object."""
|
||||
state = config._assertstate
|
||||
try:
|
||||
stat = fn.stat()
|
||||
source = fn.read("rb")
|
||||
@@ -287,7 +335,7 @@ def _rewrite_test(state, fn):
|
||||
# Let this pop up again in the real import.
|
||||
state.trace("failed to parse: %r" % (fn,))
|
||||
return None, None
|
||||
rewrite_asserts(tree)
|
||||
rewrite_asserts(tree, fn, config)
|
||||
try:
|
||||
co = compile(tree, fn.strpath, "exec")
|
||||
except SyntaxError:
|
||||
@@ -343,9 +391,9 @@ def _read_pyc(source, pyc, trace=lambda x: None):
|
||||
return co
|
||||
|
||||
|
||||
def rewrite_asserts(mod):
|
||||
def rewrite_asserts(mod, module_path=None, config=None):
|
||||
"""Rewrite the assert statements in mod."""
|
||||
AssertionRewriter().run(mod)
|
||||
AssertionRewriter(module_path, config).run(mod)
|
||||
|
||||
|
||||
def _saferepr(obj):
|
||||
@@ -453,6 +501,11 @@ binop_map = {
|
||||
ast.In: "in",
|
||||
ast.NotIn: "not in"
|
||||
}
|
||||
# Python 3.5+ compatibility
|
||||
try:
|
||||
binop_map[ast.MatMult] = "@"
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
# Python 3.4+ compatibility
|
||||
if hasattr(ast, "NameConstant"):
|
||||
@@ -527,6 +580,11 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, module_path, config):
|
||||
super(AssertionRewriter, self).__init__()
|
||||
self.module_path = module_path
|
||||
self.config = config
|
||||
|
||||
def run(self, mod):
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
if not mod.body:
|
||||
@@ -667,6 +725,10 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
the expression is false.
|
||||
|
||||
"""
|
||||
if isinstance(assert_.test, ast.Tuple) and self.config is not None:
|
||||
fslocation = (self.module_path, assert_.lineno)
|
||||
self.config.warn('R1', 'assertion is always true, perhaps '
|
||||
'remove parentheses?', fslocation=fslocation)
|
||||
self.statements = []
|
||||
self.variables = []
|
||||
self.variable_counter = itertools.count()
|
||||
@@ -850,6 +912,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
def visit_Compare(self, comp):
|
||||
self.push_format_context()
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
if isinstance(comp.left, (_ast.Compare, _ast.BoolOp)):
|
||||
left_expl = "({0})".format(left_expl)
|
||||
res_variables = [self.variable() for i in range(len(comp.ops))]
|
||||
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
|
||||
store_names = [ast.Name(v, ast.Store()) for v in res_variables]
|
||||
@@ -859,6 +923,8 @@ class AssertionRewriter(ast.NodeVisitor):
|
||||
results = [left_res]
|
||||
for i, op, next_operand in it:
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
if isinstance(next_operand, (_ast.Compare, _ast.BoolOp)):
|
||||
next_expl = "({0})".format(next_expl)
|
||||
results.append(next_res)
|
||||
sym = binop_map[op.__class__]
|
||||
syms.append(ast.Str(sym))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Utilities for assertion debugging"""
|
||||
import pprint
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
try:
|
||||
from collections import Sequence
|
||||
@@ -17,6 +18,15 @@ u = py.builtin._totext
|
||||
_reprcompare = None
|
||||
|
||||
|
||||
# the re-encoding is needed for python2 repr
|
||||
# with non-ascii characters (see issue 877 and 1379)
|
||||
def ecu(s):
|
||||
try:
|
||||
return u(s, 'utf-8', 'replace')
|
||||
except TypeError:
|
||||
return s
|
||||
|
||||
|
||||
def format_explanation(explanation):
|
||||
"""This formats an explanation
|
||||
|
||||
@@ -27,44 +37,12 @@ def format_explanation(explanation):
|
||||
for when one explanation needs to span multiple lines, e.g. when
|
||||
displaying diffs.
|
||||
"""
|
||||
explanation = _collapse_false(explanation)
|
||||
explanation = ecu(explanation)
|
||||
lines = _split_explanation(explanation)
|
||||
result = _format_lines(lines)
|
||||
return u('\n').join(result)
|
||||
|
||||
|
||||
def _collapse_false(explanation):
|
||||
"""Collapse expansions of False
|
||||
|
||||
So this strips out any "assert False\n{where False = ...\n}"
|
||||
blocks.
|
||||
"""
|
||||
where = 0
|
||||
while True:
|
||||
start = where = explanation.find("False\n{False = ", where)
|
||||
if where == -1:
|
||||
break
|
||||
level = 0
|
||||
prev_c = explanation[start]
|
||||
for i, c in enumerate(explanation[start:]):
|
||||
if prev_c + c == "\n{":
|
||||
level += 1
|
||||
elif prev_c + c == "\n}":
|
||||
level -= 1
|
||||
if not level:
|
||||
break
|
||||
prev_c = c
|
||||
else:
|
||||
raise AssertionError("unbalanced braces: %r" % (explanation,))
|
||||
end = start + i
|
||||
where = end
|
||||
if explanation[end - 1] == '\n':
|
||||
explanation = (explanation[:start] + explanation[start+15:end-1] +
|
||||
explanation[end+1:])
|
||||
where -= 17
|
||||
return explanation
|
||||
|
||||
|
||||
def _split_explanation(explanation):
|
||||
"""Return a list of individual lines in the explanation
|
||||
|
||||
@@ -127,21 +105,13 @@ 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))
|
||||
left_repr = py.io.saferepr(left, maxsize=int(width//2))
|
||||
right_repr = py.io.saferepr(right, maxsize=width-len(left_repr))
|
||||
|
||||
# the re-encoding is needed for python2 repr
|
||||
# with non-ascii characters (see issue 877)
|
||||
def ecu(s):
|
||||
try:
|
||||
return u(s, 'utf-8', 'replace')
|
||||
except TypeError:
|
||||
return s
|
||||
|
||||
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))
|
||||
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))
|
||||
@@ -179,7 +149,7 @@ def assertrepr_compare(config, op, left, right):
|
||||
explanation = [
|
||||
u('(pytest_assertion plugin: representation of details failed. '
|
||||
'Probably an object has a faulty __repr__.)'),
|
||||
u(py.code.ExceptionInfo())]
|
||||
u(_pytest._code.ExceptionInfo())]
|
||||
|
||||
if not explanation:
|
||||
return None
|
||||
@@ -222,9 +192,10 @@ def _diff_text(left, right, verbose=False):
|
||||
'characters in diff, use -v to show') % i]
|
||||
left = left[:-i]
|
||||
right = right[:-i]
|
||||
keepends = True
|
||||
explanation += [line.strip('\n')
|
||||
for line in ndiff(left.splitlines(),
|
||||
right.splitlines())]
|
||||
for line in ndiff(left.splitlines(keepends),
|
||||
right.splitlines(keepends))]
|
||||
return explanation
|
||||
|
||||
|
||||
@@ -263,8 +234,7 @@ def _compare_eq_sequence(left, right, verbose=False):
|
||||
explanation += [
|
||||
u('Right contains more items, first extra item: %s') %
|
||||
py.io.saferepr(right[len(left)],)]
|
||||
return explanation # + _diff_text(pprint.pformat(left),
|
||||
# pprint.pformat(right))
|
||||
return explanation
|
||||
|
||||
|
||||
def _compare_eq_set(left, right, verbose=False):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
merged implementation of the cache provider
|
||||
|
||||
the name cache was not choosen to ensure pluggy automatically
|
||||
the name cache was not chosen to ensure pluggy automatically
|
||||
ignores the external pytest-cache
|
||||
"""
|
||||
|
||||
@@ -149,17 +149,19 @@ class LFPlugin:
|
||||
config = self.config
|
||||
if config.getvalue("cacheshow") or hasattr(config, "slaveinput"):
|
||||
return
|
||||
config.cache.set("cache/lastfailed", self.lastfailed)
|
||||
prev_failed = config.cache.get("cache/lastfailed", None) is not None
|
||||
if (session.testscollected and prev_failed) or self.lastfailed:
|
||||
config.cache.set("cache/lastfailed", self.lastfailed)
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group.addoption(
|
||||
'--lf', action='store_true', dest="lf",
|
||||
'--lf', '--last-failed', action='store_true', dest="lf",
|
||||
help="rerun only the tests that failed "
|
||||
"at the last run (or all if none failed)")
|
||||
group.addoption(
|
||||
'--ff', action='store_true', dest="failedfirst",
|
||||
'--ff', '--failed-first', action='store_true', dest="failedfirst",
|
||||
help="run all tests but run the last failures first. "
|
||||
"This may re-order tests and thus lead to "
|
||||
"repeated fixture setup/teardown")
|
||||
|
||||
@@ -4,6 +4,7 @@ per-test stdout/stderr capturing mechanism.
|
||||
"""
|
||||
from __future__ import with_statement
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
import os
|
||||
from tempfile import TemporaryFile
|
||||
@@ -31,6 +32,7 @@ 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
|
||||
pluginmanager = early_config.pluginmanager
|
||||
capman = CaptureManager(ns.capture)
|
||||
@@ -145,46 +147,48 @@ class CaptureManager:
|
||||
def pytest_internalerror(self, excinfo):
|
||||
self.reset_capturings()
|
||||
|
||||
def suspendcapture_item(self, item, when):
|
||||
out, err = self.suspendcapture()
|
||||
def suspendcapture_item(self, item, when, in_=False):
|
||||
out, err = self.suspendcapture(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"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def capsys(request):
|
||||
"""enables capturing of writes to sys.stdout/sys.stderr and makes
|
||||
"""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.
|
||||
"""
|
||||
if "capfd" in request._funcargs:
|
||||
if "capfd" in request.fixturenames:
|
||||
raise request.raiseerror(error_capsysfderror)
|
||||
request.node._capfuncarg = c = CaptureFixture(SysCapture)
|
||||
request.node._capfuncarg = c = CaptureFixture(SysCapture, request)
|
||||
return c
|
||||
|
||||
@pytest.fixture
|
||||
def capfd(request):
|
||||
"""enables capturing of writes to file descriptors 1 and 2 and makes
|
||||
"""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.
|
||||
"""
|
||||
if "capsys" in request._funcargs:
|
||||
if "capsys" in request.fixturenames:
|
||||
request.raiseerror(error_capsysfderror)
|
||||
if not hasattr(os, 'dup'):
|
||||
pytest.skip("capfd funcarg needs os.dup")
|
||||
request.node._capfuncarg = c = CaptureFixture(FDCapture)
|
||||
request.node._capfuncarg = c = CaptureFixture(FDCapture, request)
|
||||
return c
|
||||
|
||||
|
||||
class CaptureFixture:
|
||||
def __init__(self, captureclass):
|
||||
def __init__(self, captureclass, request):
|
||||
self.captureclass = captureclass
|
||||
self.request = request
|
||||
|
||||
def _start(self):
|
||||
self._capture = MultiCapture(out=True, err=True, in_=False,
|
||||
Capture=self.captureclass)
|
||||
Capture=self.captureclass)
|
||||
self._capture.start_capturing()
|
||||
|
||||
def close(self):
|
||||
@@ -199,6 +203,15 @@ class CaptureFixture:
|
||||
except AttributeError:
|
||||
return self._outerr
|
||||
|
||||
@contextlib.contextmanager
|
||||
def disabled(self):
|
||||
capmanager = self.request.config.pluginmanager.getplugin('capturemanager')
|
||||
capmanager.suspendcapture_item(self.request.node, "call", in_=True)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
capmanager.resumecapture()
|
||||
|
||||
|
||||
def safe_text_dupfile(f, mode, default_encoding="UTF8"):
|
||||
""" return a open text file object that's a duplicate of f on the
|
||||
@@ -442,3 +455,37 @@ class DontReadFromInput:
|
||||
|
||||
def close(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def buffer(self):
|
||||
if sys.version_info >= (3,0):
|
||||
return self
|
||||
else:
|
||||
raise AttributeError('redirected stdin has no attribute buffer')
|
||||
|
||||
|
||||
def _readline_workaround():
|
||||
"""
|
||||
Ensure readline is imported so that it attaches to the correct stdio
|
||||
handles on Windows.
|
||||
|
||||
Pdb uses readline support where available--when not running from the Python
|
||||
prompt, the readline module is not imported until running the pdb REPL. If
|
||||
running pytest with the --pdb option this means the readline module is not
|
||||
imported until after I/O capture has been started.
|
||||
|
||||
This is a problem for pyreadline, which is often used to implement readline
|
||||
support on Windows, as it does not attach to the correct handles for stdout
|
||||
and/or stdin if they have been redirected by the FDCapture mechanism. This
|
||||
workaround ensures that readline is imported before I/O capture is setup so
|
||||
that it can attach to the actual stdin/out for the console.
|
||||
|
||||
See https://github.com/pytest-dev/pytest/pull/1281
|
||||
"""
|
||||
|
||||
if not sys.platform.startswith('win32'):
|
||||
return
|
||||
try:
|
||||
import readline # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
241
_pytest/compat.py
Normal file
241
_pytest/compat.py
Normal file
@@ -0,0 +1,241 @@
|
||||
"""
|
||||
python version compatibility code
|
||||
"""
|
||||
import sys
|
||||
import inspect
|
||||
import types
|
||||
import re
|
||||
import functools
|
||||
|
||||
import py
|
||||
|
||||
import _pytest
|
||||
|
||||
|
||||
|
||||
try:
|
||||
import enum
|
||||
except ImportError: # pragma: no cover
|
||||
# Only available in Python 3.4+ or as a backport
|
||||
enum = None
|
||||
|
||||
|
||||
_PY3 = sys.version_info > (3, 0)
|
||||
_PY2 = not _PY3
|
||||
|
||||
|
||||
NoneType = type(None)
|
||||
NOTSET = object()
|
||||
|
||||
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))
|
||||
|
||||
isfunction = inspect.isfunction
|
||||
isclass = inspect.isclass
|
||||
# used to work around a python2 exception info leak
|
||||
exc_clear = getattr(sys, 'exc_clear', lambda: None)
|
||||
# The type of re.compile objects is not exposed in Python.
|
||||
REGEX_TYPE = type(re.compile(''))
|
||||
|
||||
|
||||
def is_generator(func):
|
||||
genfunc = inspect.isgeneratorfunction(func)
|
||||
return genfunc and not iscoroutinefunction(func)
|
||||
|
||||
|
||||
def iscoroutinefunction(func):
|
||||
"""Return True if func is a decorated coroutine function.
|
||||
|
||||
Note: copied and modified from Python 3.5's builtin couroutines.py to avoid import asyncio directly,
|
||||
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)))
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def num_mock_patch_args(function):
|
||||
""" return number of arguments used up by mock arguments (if any) """
|
||||
patchings = getattr(function, "patchings", None)
|
||||
if not patchings:
|
||||
return 0
|
||||
mock = sys.modules.get("mock", sys.modules.get("unittest.mock", None))
|
||||
if mock is not None:
|
||||
return len([p for p in patchings
|
||||
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:])
|
||||
|
||||
|
||||
|
||||
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))
|
||||
|
||||
|
||||
if _PY3:
|
||||
import codecs
|
||||
|
||||
STRING_TYPES = bytes, str
|
||||
|
||||
def _escape_strings(val):
|
||||
"""If val is pure ascii, returns it as a str(). Otherwise, escapes
|
||||
bytes objects into a sequence of escaped bytes:
|
||||
|
||||
b'\xc3\xb4\xc5\xd6' -> u'\\xc3\\xb4\\xc5\\xd6'
|
||||
|
||||
and escapes unicode objects into a sequence of escaped unicode
|
||||
ids, e.g.:
|
||||
|
||||
'4\\nV\\U00043efa\\x0eMXWB\\x1e\\u3028\\u15fd\\xcd\\U0007d944'
|
||||
|
||||
note:
|
||||
the obvious "v.decode('unicode-escape')" will return
|
||||
valid utf-8 unicode if it finds them in bytes, but we
|
||||
want to return escaped bytes for any byte, even if they match
|
||||
a utf-8 string.
|
||||
|
||||
"""
|
||||
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 ''
|
||||
else:
|
||||
return val.encode('unicode_escape').decode('ascii')
|
||||
else:
|
||||
STRING_TYPES = bytes, str, unicode
|
||||
|
||||
def _escape_strings(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.
|
||||
|
||||
If it's a unicode string, change the unicode characters into
|
||||
unicode escapes.
|
||||
|
||||
"""
|
||||
if isinstance(val, bytes):
|
||||
try:
|
||||
return val.encode('ascii')
|
||||
except UnicodeDecodeError:
|
||||
return val.encode('string-escape')
|
||||
else:
|
||||
return val.encode('unicode-escape')
|
||||
|
||||
|
||||
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__
|
||||
if isinstance(obj, functools.partial):
|
||||
obj = obj.func
|
||||
return obj
|
||||
|
||||
|
||||
def getfslineno(obj):
|
||||
# xxx let decorators etc specify a sane ordering
|
||||
obj = get_real_func(obj)
|
||||
if hasattr(obj, 'place_as'):
|
||||
obj = obj.place_as
|
||||
fslineno = _pytest._code.getfslineno(obj)
|
||||
assert isinstance(fslineno[1], int), obj
|
||||
return fslineno
|
||||
|
||||
|
||||
def getimfunc(func):
|
||||
try:
|
||||
return func.__func__
|
||||
except AttributeError:
|
||||
try:
|
||||
return func.im_func
|
||||
except AttributeError:
|
||||
return func
|
||||
|
||||
|
||||
def safe_getattr(object, name, default):
|
||||
""" Like getattr but return default upon any Exception.
|
||||
|
||||
Attribute access can potentially fail for 'evil' Python objects.
|
||||
See issue214
|
||||
"""
|
||||
try:
|
||||
return getattr(object, name, default)
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def _is_unittest_unexpected_success_a_failure():
|
||||
"""Return if the test suite should fail if a @expectedFailure unittest test PASSES.
|
||||
|
||||
From https://docs.python.org/3/library/unittest.html?highlight=unittest#unittest.TestResult.wasSuccessful:
|
||||
Changed in version 3.4: Returns False if there were any
|
||||
unexpectedSuccesses from tests marked with the expectedFailure() decorator.
|
||||
"""
|
||||
return sys.version_info >= (3, 4)
|
||||
|
||||
|
||||
if _PY3:
|
||||
def safe_str(v):
|
||||
"""returns v as string"""
|
||||
return str(v)
|
||||
else:
|
||||
def safe_str(v):
|
||||
"""returns v as string, converting to ascii if necessary"""
|
||||
try:
|
||||
return str(v)
|
||||
except UnicodeError:
|
||||
errors = 'replace'
|
||||
return v.encode('ascii', errors)
|
||||
@@ -8,8 +8,11 @@ import warnings
|
||||
import py
|
||||
# DON't import pytest here because it causes import cycle troubles
|
||||
import sys, os
|
||||
import _pytest._code
|
||||
import _pytest.hookspec # the extension point definitions
|
||||
import _pytest.assertion
|
||||
from _pytest._pluggy import PluginManager, HookimplMarker, HookspecMarker
|
||||
from _pytest.compat import safe_str
|
||||
|
||||
hookimpl = HookimplMarker("pytest")
|
||||
hookspec = HookspecMarker("pytest")
|
||||
@@ -24,6 +27,12 @@ class ConftestImportFailure(Exception):
|
||||
self.path = path
|
||||
self.excinfo = excinfo
|
||||
|
||||
def __str__(self):
|
||||
etype, evalue, etb = self.excinfo
|
||||
formatted = traceback.format_tb(etb)
|
||||
# The level of the tracebacks we want to print is hand crafted :(
|
||||
return repr(evalue) + '\n' + ''.join(formatted[2:])
|
||||
|
||||
|
||||
def main(args=None, plugins=None):
|
||||
""" return exit code, after performing an in-process test run.
|
||||
@@ -56,15 +65,40 @@ def main(args=None, plugins=None):
|
||||
class cmdline: # compatibility namespace
|
||||
main = staticmethod(main)
|
||||
|
||||
|
||||
class UsageError(Exception):
|
||||
""" error in pytest usage or invocation"""
|
||||
|
||||
|
||||
def filename_arg(path, optname):
|
||||
""" Argparse type validator for filename arguments.
|
||||
|
||||
:path: path of filename
|
||||
:optname: name of the option
|
||||
"""
|
||||
if os.path.isdir(path):
|
||||
raise UsageError("{0} must be a filename, given: {1}".format(optname, path))
|
||||
return path
|
||||
|
||||
|
||||
def directory_arg(path, optname):
|
||||
"""Argparse type validator for directory arguments.
|
||||
|
||||
:path: path of directory
|
||||
:optname: name of the option
|
||||
"""
|
||||
if not os.path.isdir(path):
|
||||
raise UsageError("{0} must be a directory, given: {1}".format(optname, path))
|
||||
return path
|
||||
|
||||
|
||||
_preinit = []
|
||||
|
||||
default_plugins = (
|
||||
"mark main terminal runner python pdb unittest capture skipping "
|
||||
"tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript "
|
||||
"junitxml resultlog doctest cacheprovider").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").split()
|
||||
|
||||
builtin_plugins = set(default_plugins)
|
||||
builtin_plugins.add("pytester")
|
||||
@@ -96,6 +130,7 @@ def get_plugin_manager():
|
||||
return get_config().pluginmanager
|
||||
|
||||
def _prepareconfig(args=None, plugins=None):
|
||||
warning = None
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
elif isinstance(args, py.path.local):
|
||||
@@ -103,7 +138,9 @@ def _prepareconfig(args=None, plugins=None):
|
||||
elif not isinstance(args, (tuple, list)):
|
||||
if not isinstance(args, str):
|
||||
raise ValueError("not a string or argument list: %r" % (args,))
|
||||
args = shlex.split(args)
|
||||
args = shlex.split(args, posix=sys.platform != "win32")
|
||||
from _pytest import deprecated
|
||||
warning = deprecated.MAIN_STR_ARGS
|
||||
config = get_config()
|
||||
pluginmanager = config.pluginmanager
|
||||
try:
|
||||
@@ -113,6 +150,8 @@ def _prepareconfig(args=None, plugins=None):
|
||||
pluginmanager.consider_pluginarg(plugin)
|
||||
else:
|
||||
pluginmanager.register(plugin)
|
||||
if warning:
|
||||
config.warn('C1', warning)
|
||||
return pluginmanager.hook.pytest_cmdline_parse(
|
||||
pluginmanager=pluginmanager, args=args)
|
||||
except BaseException:
|
||||
@@ -138,6 +177,7 @@ class PytestPluginManager(PluginManager):
|
||||
self._conftestpath2mod = {}
|
||||
self._confcutdir = None
|
||||
self._noconftest = False
|
||||
self._duplicatepaths = set()
|
||||
|
||||
self.add_hookspecs(_pytest.hookspec)
|
||||
self.register(self)
|
||||
@@ -151,6 +191,9 @@ class PytestPluginManager(PluginManager):
|
||||
self.trace.root.setwriter(err.write)
|
||||
self.enable_tracing()
|
||||
|
||||
# Config._consider_importhook will set a real object if required.
|
||||
self.rewrite_hook = _pytest.assertion.DummyRewriteHook()
|
||||
|
||||
def addhooks(self, module_or_class):
|
||||
"""
|
||||
.. deprecated:: 2.8
|
||||
@@ -158,7 +201,7 @@ class PytestPluginManager(PluginManager):
|
||||
Use :py:meth:`pluggy.PluginManager.add_hookspecs` instead.
|
||||
"""
|
||||
warning = dict(code="I2",
|
||||
fslocation=py.code.getfslineno(sys._getframe(1)),
|
||||
fslocation=_pytest._code.getfslineno(sys._getframe(1)),
|
||||
nodeid=None,
|
||||
message="use pluginmanager.add_hookspecs instead of "
|
||||
"deprecated addhooks() method.")
|
||||
@@ -195,7 +238,7 @@ class PytestPluginManager(PluginManager):
|
||||
def _verify_hook(self, hook, hookmethod):
|
||||
super(PytestPluginManager, self)._verify_hook(hook, hookmethod)
|
||||
if "__multicall__" in hookmethod.argnames:
|
||||
fslineno = py.code.getfslineno(hookmethod.function)
|
||||
fslineno = _pytest._code.getfslineno(hookmethod.function)
|
||||
warning = dict(code="I1",
|
||||
fslocation=fslineno,
|
||||
nodeid=None,
|
||||
@@ -359,31 +402,35 @@ class PytestPluginManager(PluginManager):
|
||||
self._import_plugin_specs(os.environ.get("PYTEST_PLUGINS"))
|
||||
|
||||
def consider_module(self, mod):
|
||||
self._import_plugin_specs(getattr(mod, "pytest_plugins", None))
|
||||
self._import_plugin_specs(getattr(mod, 'pytest_plugins', []))
|
||||
|
||||
def _import_plugin_specs(self, spec):
|
||||
if spec:
|
||||
if isinstance(spec, str):
|
||||
spec = spec.split(",")
|
||||
for import_spec in spec:
|
||||
self.import_plugin(import_spec)
|
||||
plugins = _get_plugin_specs_as_list(spec)
|
||||
for import_spec in plugins:
|
||||
self.import_plugin(import_spec)
|
||||
|
||||
def import_plugin(self, modname):
|
||||
# most often modname refers to builtin modules, e.g. "pytester",
|
||||
# "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)
|
||||
assert isinstance(modname, str), "module name as string required, got %r" % modname
|
||||
if self.get_plugin(modname) is not None:
|
||||
return
|
||||
if modname in builtin_plugins:
|
||||
importspec = "_pytest." + modname
|
||||
else:
|
||||
importspec = modname
|
||||
self.rewrite_hook.mark_rewrite(importspec)
|
||||
try:
|
||||
__import__(importspec)
|
||||
except ImportError:
|
||||
raise
|
||||
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
|
||||
except Exception as e:
|
||||
import pytest
|
||||
if not hasattr(pytest, 'skip') or not isinstance(e, pytest.skip.Exception):
|
||||
@@ -395,6 +442,24 @@ class PytestPluginManager(PluginManager):
|
||||
self.consider_module(mod)
|
||||
|
||||
|
||||
def _get_plugin_specs_as_list(specs):
|
||||
"""
|
||||
Parses a list of "plugin specs" and returns a list of plugin names.
|
||||
|
||||
Plugin specs can be given as a list of strings separated by "," or already as a list/tuple in
|
||||
which case it is returned as a list. Specs can also be `None` in which case an
|
||||
empty list is returned.
|
||||
"""
|
||||
if specs is not None:
|
||||
if isinstance(specs, str):
|
||||
specs = specs.split(',') if specs else []
|
||||
if not isinstance(specs, (list, tuple)):
|
||||
raise UsageError("Plugin specs must be a ','-separated string or a "
|
||||
"list/tuple of strings for plugin names. Given: %r" % specs)
|
||||
return list(specs)
|
||||
return []
|
||||
|
||||
|
||||
class Parser:
|
||||
""" Parser for command line arguments and ini-file values.
|
||||
|
||||
@@ -455,11 +520,11 @@ class Parser:
|
||||
"""
|
||||
self._anonymous.addoption(*opts, **attrs)
|
||||
|
||||
def parse(self, args):
|
||||
def parse(self, args, namespace=None):
|
||||
from _pytest._argcomplete import try_argcomplete
|
||||
self.optparser = self._getparser()
|
||||
try_argcomplete(self.optparser)
|
||||
return self.optparser.parse_args([str(x) for x in args])
|
||||
return self.optparser.parse_args([str(x) for x in args], namespace=namespace)
|
||||
|
||||
def _getparser(self):
|
||||
from _pytest._argcomplete import filescompleter
|
||||
@@ -477,37 +542,38 @@ class Parser:
|
||||
optparser.add_argument(FILE_OR_DIR, nargs='*').completer=filescompleter
|
||||
return optparser
|
||||
|
||||
def parse_setoption(self, args, option):
|
||||
parsedoption = self.parse(args)
|
||||
def parse_setoption(self, args, option, namespace=None):
|
||||
parsedoption = self.parse(args, namespace=namespace)
|
||||
for name, value in parsedoption.__dict__.items():
|
||||
setattr(option, name, value)
|
||||
return getattr(parsedoption, FILE_OR_DIR)
|
||||
|
||||
def parse_known_args(self, args):
|
||||
def parse_known_args(self, args, namespace=None):
|
||||
"""parses and returns a namespace object with known arguments at this
|
||||
point.
|
||||
"""
|
||||
return self.parse_known_and_unknown_args(args)[0]
|
||||
return self.parse_known_and_unknown_args(args, namespace=namespace)[0]
|
||||
|
||||
def parse_known_and_unknown_args(self, args):
|
||||
def parse_known_and_unknown_args(self, args, namespace=None):
|
||||
"""parses and returns a namespace object with known arguments, and
|
||||
the remaining arguments unknown at this point.
|
||||
"""
|
||||
optparser = self._getparser()
|
||||
args = [str(x) for x in args]
|
||||
return optparser.parse_known_args(args)
|
||||
return optparser.parse_known_args(args, namespace=namespace)
|
||||
|
||||
def addini(self, name, help, type=None, default=None):
|
||||
""" register an ini-file option.
|
||||
|
||||
:name: name of the ini-variable
|
||||
:type: type of the variable, can be ``pathlist``, ``args`` or ``linelist``.
|
||||
:type: type of the variable, can be ``pathlist``, ``args``, ``linelist``
|
||||
or ``bool``.
|
||||
:default: default value if no ini-file option exists but is queried.
|
||||
|
||||
The value of ini-variables can be retrieved via a call to
|
||||
:py:func:`config.getini(name) <_pytest.config.Config.getini>`.
|
||||
"""
|
||||
assert type in (None, "pathlist", "args", "linelist")
|
||||
assert type in (None, "pathlist", "args", "linelist", "bool")
|
||||
self._inidict[name] = (help, type, default)
|
||||
self._ininames.append(name)
|
||||
|
||||
@@ -530,13 +596,18 @@ class ArgumentError(Exception):
|
||||
|
||||
|
||||
class Argument:
|
||||
"""class that mimics the necessary behaviour of optparse.Option """
|
||||
"""class that mimics the necessary behaviour of optparse.Option
|
||||
|
||||
its currently a least effort implementation
|
||||
and ignoring choices and integer prefixes
|
||||
https://docs.python.org/3/library/optparse.html#optparse-standard-option-types
|
||||
"""
|
||||
_typ_map = {
|
||||
'int': int,
|
||||
'string': str,
|
||||
}
|
||||
# enable after some grace period for plugin writers
|
||||
TYPE_WARN = False
|
||||
'float': float,
|
||||
'complex': complex,
|
||||
}
|
||||
|
||||
def __init__(self, *names, **attrs):
|
||||
"""store parms in private vars for use in add_argument"""
|
||||
@@ -544,17 +615,12 @@ class Argument:
|
||||
self._short_opts = []
|
||||
self._long_opts = []
|
||||
self.dest = attrs.get('dest')
|
||||
if self.TYPE_WARN:
|
||||
try:
|
||||
help = attrs['help']
|
||||
if '%default' in help:
|
||||
warnings.warn(
|
||||
'pytest now uses argparse. "%default" should be'
|
||||
' changed to "%(default)s" ',
|
||||
FutureWarning,
|
||||
stacklevel=3)
|
||||
except KeyError:
|
||||
pass
|
||||
if '%default' in (attrs.get('help') or ''):
|
||||
warnings.warn(
|
||||
'pytest now uses argparse. "%default" should be'
|
||||
' changed to "%(default)s" ',
|
||||
DeprecationWarning,
|
||||
stacklevel=3)
|
||||
try:
|
||||
typ = attrs['type']
|
||||
except KeyError:
|
||||
@@ -563,25 +629,23 @@ class Argument:
|
||||
# this might raise a keyerror as well, don't want to catch that
|
||||
if isinstance(typ, py.builtin._basestring):
|
||||
if typ == 'choice':
|
||||
if self.TYPE_WARN:
|
||||
warnings.warn(
|
||||
'type argument to addoption() is a string %r.'
|
||||
' For parsearg this is optional and when supplied '
|
||||
' should be a type.'
|
||||
' (options: %s)' % (typ, names),
|
||||
FutureWarning,
|
||||
stacklevel=3)
|
||||
warnings.warn(
|
||||
'type argument to addoption() is a string %r.'
|
||||
' For parsearg this is optional and when supplied'
|
||||
' should be a type.'
|
||||
' (options: %s)' % (typ, names),
|
||||
DeprecationWarning,
|
||||
stacklevel=3)
|
||||
# argparse expects a type here take it from
|
||||
# the type of the first element
|
||||
attrs['type'] = type(attrs['choices'][0])
|
||||
else:
|
||||
if self.TYPE_WARN:
|
||||
warnings.warn(
|
||||
'type argument to addoption() is a string %r.'
|
||||
' For parsearg this should be a type.'
|
||||
' (options: %s)' % (typ, names),
|
||||
FutureWarning,
|
||||
stacklevel=3)
|
||||
warnings.warn(
|
||||
'type argument to addoption() is a string %r.'
|
||||
' For parsearg this should be a type.'
|
||||
' (options: %s)' % (typ, names),
|
||||
DeprecationWarning,
|
||||
stacklevel=3)
|
||||
attrs['type'] = Argument._typ_map[typ]
|
||||
# used in test_parseopt -> test_parse_defaultgetter
|
||||
self.type = attrs['type']
|
||||
@@ -648,20 +712,17 @@ class Argument:
|
||||
self._long_opts.append(opt)
|
||||
|
||||
def __repr__(self):
|
||||
retval = 'Argument('
|
||||
args = []
|
||||
if self._short_opts:
|
||||
retval += '_short_opts: ' + repr(self._short_opts) + ', '
|
||||
args += ['_short_opts: ' + repr(self._short_opts)]
|
||||
if self._long_opts:
|
||||
retval += '_long_opts: ' + repr(self._long_opts) + ', '
|
||||
retval += 'dest: ' + repr(self.dest) + ', '
|
||||
args += ['_long_opts: ' + repr(self._long_opts)]
|
||||
args += ['dest: ' + repr(self.dest)]
|
||||
if hasattr(self, 'type'):
|
||||
retval += 'type: ' + repr(self.type) + ', '
|
||||
args += ['type: ' + repr(self.type)]
|
||||
if hasattr(self, 'default'):
|
||||
retval += 'default: ' + repr(self.default) + ', '
|
||||
if retval[-2:] == ', ': # always long enough to test ("Argument(" )
|
||||
retval = retval[:-2]
|
||||
retval += ')'
|
||||
return retval
|
||||
args += ['default: ' + repr(self.default)]
|
||||
return 'Argument({0})'.format(', '.join(args))
|
||||
|
||||
|
||||
class OptionGroup:
|
||||
@@ -679,6 +740,10 @@ class OptionGroup:
|
||||
results in help showing '--two-words' only, but --twowords gets
|
||||
accepted **and** the automatic destination is in args.twowords
|
||||
"""
|
||||
conflict = set(optnames).intersection(
|
||||
name for opt in self.options for name in opt.names())
|
||||
if conflict:
|
||||
raise ValueError("option names %s already added" % conflict)
|
||||
option = Argument(*optnames, **attrs)
|
||||
self._addoption_instance(option, shortupper=False)
|
||||
|
||||
@@ -765,7 +830,7 @@ class DropShorterLongHelpFormatter(argparse.HelpFormatter):
|
||||
if len(option) == 2 or option[2] == ' ':
|
||||
return_list.append(option)
|
||||
if option[2:] == short_long.get(option.replace('-', '')):
|
||||
return_list.append(option.replace(' ', '='))
|
||||
return_list.append(option.replace(' ', '=', 1))
|
||||
action._formatted_action_invocation = ', '.join(return_list)
|
||||
return action._formatted_action_invocation
|
||||
|
||||
@@ -779,18 +844,22 @@ def _ensure_removed_sysmodule(modname):
|
||||
|
||||
class CmdOptions(object):
|
||||
""" holds cmdline options as attributes."""
|
||||
def __init__(self, **kwargs):
|
||||
self.__dict__.update(kwargs)
|
||||
def __init__(self, values=()):
|
||||
self.__dict__.update(values)
|
||||
def __repr__(self):
|
||||
return "<CmdOptions %r>" %(self.__dict__,)
|
||||
def copy(self):
|
||||
return CmdOptions(self.__dict__)
|
||||
|
||||
class Notset:
|
||||
def __repr__(self):
|
||||
return "<NOTSET>"
|
||||
|
||||
|
||||
notset = Notset()
|
||||
FILE_OR_DIR = 'file_or_dir'
|
||||
|
||||
|
||||
class Config(object):
|
||||
""" access to configuration values, pluginmanager and plugin hooks. """
|
||||
|
||||
@@ -808,14 +877,17 @@ class Config(object):
|
||||
self.trace = self.pluginmanager.trace.root.get("config")
|
||||
self.hook = self.pluginmanager.hook
|
||||
self._inicache = {}
|
||||
self._override_ini = ()
|
||||
self._opt2dest = {}
|
||||
self._cleanup = []
|
||||
self._warn = self.pluginmanager._warn
|
||||
self.pluginmanager.register(self, "pytestconfig")
|
||||
self._configured = False
|
||||
|
||||
def do_setns(dic):
|
||||
import pytest
|
||||
setns(pytest, dic)
|
||||
|
||||
self.hook.pytest_namespace.call_historic(do_setns, {})
|
||||
self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser))
|
||||
|
||||
@@ -879,8 +951,8 @@ class Config(object):
|
||||
def fromdictargs(cls, option_dict, args):
|
||||
""" constructor useable for subprocesses. """
|
||||
config = get_config()
|
||||
config._preparse(args, addopts=False)
|
||||
config.option.__dict__.update(option_dict)
|
||||
config.parse(args, addopts=False)
|
||||
for x in config.option.plugins:
|
||||
config.pluginmanager.consider_pluginarg(x)
|
||||
return config
|
||||
@@ -898,14 +970,67 @@ class Config(object):
|
||||
self.pluginmanager._set_initial_conftests(early_config.known_args_namespace)
|
||||
|
||||
def _initini(self, args):
|
||||
ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
|
||||
r = determine_setup(ns.inifilename, ns.file_or_dir + unknown_args)
|
||||
ns, unknown_args = self._parser.parse_known_and_unknown_args(args, namespace=self.option.copy())
|
||||
r = determine_setup(ns.inifilename, ns.file_or_dir + unknown_args, warnfunc=self.warn)
|
||||
self.rootdir, self.inifile, self.inicfg = r
|
||||
self._parser.extra_info['rootdir'] = self.rootdir
|
||||
self._parser.extra_info['inifile'] = self.inifile
|
||||
self.invocation_dir = py.path.local()
|
||||
self._parser.addini('addopts', 'extra command line options', 'args')
|
||||
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.
|
||||
|
||||
Needs to parse the --assert=<mode> option from the commandline
|
||||
and find all the installed plugins to mark them for re-writing
|
||||
by the importhook.
|
||||
"""
|
||||
ns, unknown_args = self._parser.parse_known_and_unknown_args(args)
|
||||
mode = ns.assertmode
|
||||
if mode == 'rewrite':
|
||||
try:
|
||||
hook = _pytest.assertion.install_importhook(self)
|
||||
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._warn_about_missing_assertion(mode)
|
||||
|
||||
def _warn_about_missing_assertion(self, mode):
|
||||
try:
|
||||
assert False
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
if mode == 'plain':
|
||||
sys.stderr.write("WARNING: ASSERTIONS ARE NOT EXECUTED"
|
||||
" and FAILING TESTS WILL PASS. Are you"
|
||||
" using python -O?")
|
||||
else:
|
||||
sys.stderr.write("WARNING: assertions not in test modules or"
|
||||
" plugins will be ignored"
|
||||
" because assert statements are not executed "
|
||||
"by the underlying Python interpreter "
|
||||
"(are you using python -O?)\n")
|
||||
|
||||
def _preparse(self, args, addopts=True):
|
||||
self._initini(args)
|
||||
@@ -913,13 +1038,13 @@ class Config(object):
|
||||
args[:] = shlex.split(os.environ.get('PYTEST_ADDOPTS', '')) + args
|
||||
args[:] = self.getini("addopts") + args
|
||||
self._checkversion()
|
||||
entrypoint_name = 'pytest11'
|
||||
self._consider_importhook(args, entrypoint_name)
|
||||
self.pluginmanager.consider_preparse(args)
|
||||
try:
|
||||
self.pluginmanager.load_setuptools_entrypoints("pytest11")
|
||||
except ImportError as e:
|
||||
self.warn("I2", "could not load setuptools entry import: %s" % (e,))
|
||||
self.pluginmanager.load_setuptools_entrypoints(entrypoint_name)
|
||||
self.pluginmanager.consider_env()
|
||||
self.known_args_namespace = ns = self._parser.parse_known_args(args)
|
||||
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
|
||||
@@ -947,17 +1072,17 @@ class Config(object):
|
||||
self.inicfg.config.path, self.inicfg.lineof('minversion'),
|
||||
minver, pytest.__version__))
|
||||
|
||||
def parse(self, args):
|
||||
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")
|
||||
self._origargs = args
|
||||
self.hook.pytest_addhooks.call_historic(
|
||||
kwargs=dict(pluginmanager=self.pluginmanager))
|
||||
self._preparse(args)
|
||||
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)
|
||||
args = self._parser.parse_setoption(args, self.option, namespace=self.option)
|
||||
if not args:
|
||||
cwd = os.getcwd()
|
||||
if cwd == self.rootdir:
|
||||
@@ -990,14 +1115,16 @@ class Config(object):
|
||||
description, type, default = self._parser._inidict[name]
|
||||
except KeyError:
|
||||
raise ValueError("unknown configuration value: %r" %(name,))
|
||||
try:
|
||||
value = self.inicfg[name]
|
||||
except KeyError:
|
||||
if default is not None:
|
||||
return default
|
||||
if type is None:
|
||||
return ''
|
||||
return []
|
||||
value = self._get_override_ini_value(name)
|
||||
if value is None:
|
||||
try:
|
||||
value = self.inicfg[name]
|
||||
except KeyError:
|
||||
if default is not None:
|
||||
return default
|
||||
if type is None:
|
||||
return ''
|
||||
return []
|
||||
if type == "pathlist":
|
||||
dp = py.path.local(self.inicfg.config.path).dirpath()
|
||||
l = []
|
||||
@@ -1008,6 +1135,8 @@ class Config(object):
|
||||
return shlex.split(value)
|
||||
elif type == "linelist":
|
||||
return [t for t in map(lambda x: x.strip(), value.split("\n")) if t]
|
||||
elif type == "bool":
|
||||
return bool(_strtobool(value.strip()))
|
||||
else:
|
||||
assert type is None
|
||||
return value
|
||||
@@ -1026,6 +1155,22 @@ class Config(object):
|
||||
l.append(relroot)
|
||||
return l
|
||||
|
||||
def _get_override_ini_value(self, name):
|
||||
value = None
|
||||
# override_ini is a list of list, to support both -o foo1=bar1 foo2=bar2 and
|
||||
# and -o foo1=bar1 -o foo2=bar2 options
|
||||
# always use the last item if multiple value set for same ini-name,
|
||||
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2
|
||||
for ini_config_list in self._override_ini:
|
||||
for ini_config in ini_config_list:
|
||||
try:
|
||||
(key, user_ini_value) = ini_config.split("=", 1)
|
||||
except ValueError:
|
||||
raise UsageError("-o/--override-ini expects option=value style.")
|
||||
if key == name:
|
||||
value = user_ini_value
|
||||
return value
|
||||
|
||||
def getoption(self, name, default=notset, skip=False):
|
||||
""" return command line option value.
|
||||
|
||||
@@ -1063,7 +1208,18 @@ def exists(path, ignore=EnvironmentError):
|
||||
except ignore:
|
||||
return False
|
||||
|
||||
def getcfg(args, inibasenames):
|
||||
def getcfg(args, warnfunc=None):
|
||||
"""
|
||||
Search the list of arguments for a valid ini-file for pytest,
|
||||
and return a tuple of (rootdir, inifile, cfg-dict).
|
||||
|
||||
note: warnfunc is an optional function used to warn
|
||||
about ini-files that use deprecated features.
|
||||
This parameter should be removed when pytest
|
||||
adopts standard deprecation warnings (#1804).
|
||||
"""
|
||||
from _pytest.deprecated import SETUP_CFG_PYTEST
|
||||
inibasenames = ["pytest.ini", "tox.ini", "setup.cfg"]
|
||||
args = [x for x in args if not str(x).startswith("-")]
|
||||
if not args:
|
||||
args = [py.path.local()]
|
||||
@@ -1075,57 +1231,89 @@ def getcfg(args, inibasenames):
|
||||
if exists(p):
|
||||
iniconfig = py.iniconfig.IniConfig(p)
|
||||
if 'pytest' in iniconfig.sections:
|
||||
if inibasename == 'setup.cfg' and warnfunc:
|
||||
warnfunc('C1', SETUP_CFG_PYTEST)
|
||||
return base, p, iniconfig['pytest']
|
||||
if inibasename == 'setup.cfg' and 'tool:pytest' in iniconfig.sections:
|
||||
return base, p, iniconfig['tool:pytest']
|
||||
elif inibasename == "pytest.ini":
|
||||
# allowed to be empty
|
||||
return base, p, {}
|
||||
return None, None, None
|
||||
|
||||
|
||||
def get_common_ancestor(args):
|
||||
# args are what we get after early command line parsing (usually
|
||||
# strings, but can be py.path.local objects as well)
|
||||
def get_common_ancestor(paths):
|
||||
common_ancestor = None
|
||||
for arg in args:
|
||||
if str(arg)[0] == "-":
|
||||
for path in paths:
|
||||
if not path.exists():
|
||||
continue
|
||||
p = py.path.local(arg)
|
||||
if common_ancestor is None:
|
||||
common_ancestor = p
|
||||
common_ancestor = path
|
||||
else:
|
||||
if p.relto(common_ancestor) or p == common_ancestor:
|
||||
if path.relto(common_ancestor) or path == common_ancestor:
|
||||
continue
|
||||
elif common_ancestor.relto(p):
|
||||
common_ancestor = p
|
||||
elif common_ancestor.relto(path):
|
||||
common_ancestor = path
|
||||
else:
|
||||
shared = p.common(common_ancestor)
|
||||
shared = path.common(common_ancestor)
|
||||
if shared is not None:
|
||||
common_ancestor = shared
|
||||
if common_ancestor is None:
|
||||
common_ancestor = py.path.local()
|
||||
elif not common_ancestor.isdir():
|
||||
elif common_ancestor.isfile():
|
||||
common_ancestor = common_ancestor.dirpath()
|
||||
return common_ancestor
|
||||
|
||||
|
||||
def determine_setup(inifile, args):
|
||||
def get_dirs_from_args(args):
|
||||
def is_option(x):
|
||||
return str(x).startswith('-')
|
||||
|
||||
def get_file_part_from_node_id(x):
|
||||
return str(x).split('::')[0]
|
||||
|
||||
def get_dir_from_path(path):
|
||||
if path.isdir():
|
||||
return path
|
||||
return py.path.local(path.dirname)
|
||||
|
||||
# These look like paths but may not exist
|
||||
possible_paths = (
|
||||
py.path.local(get_file_part_from_node_id(arg))
|
||||
for arg in args
|
||||
if not is_option(arg)
|
||||
)
|
||||
|
||||
return [
|
||||
get_dir_from_path(path)
|
||||
for path in possible_paths
|
||||
if path.exists()
|
||||
]
|
||||
|
||||
|
||||
def determine_setup(inifile, args, warnfunc=None):
|
||||
dirs = get_dirs_from_args(args)
|
||||
if inifile:
|
||||
iniconfig = py.iniconfig.IniConfig(inifile)
|
||||
try:
|
||||
inicfg = iniconfig["pytest"]
|
||||
except KeyError:
|
||||
inicfg = None
|
||||
rootdir = get_common_ancestor(args)
|
||||
rootdir = get_common_ancestor(dirs)
|
||||
else:
|
||||
ancestor = get_common_ancestor(args)
|
||||
rootdir, inifile, inicfg = getcfg(
|
||||
[ancestor], ["pytest.ini", "tox.ini", "setup.cfg"])
|
||||
ancestor = get_common_ancestor(dirs)
|
||||
rootdir, inifile, inicfg = getcfg([ancestor], warnfunc=warnfunc)
|
||||
if rootdir is None:
|
||||
for rootdir in ancestor.parts(reverse=True):
|
||||
if rootdir.join("setup.py").exists():
|
||||
break
|
||||
else:
|
||||
rootdir = ancestor
|
||||
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
|
||||
if is_fs_root:
|
||||
rootdir = ancestor
|
||||
return rootdir, inifile, inicfg or {}
|
||||
|
||||
|
||||
@@ -1161,3 +1349,21 @@ def create_terminal_writer(config, *args, **kwargs):
|
||||
if config.option.color == 'no':
|
||||
tw.hasmarkup = False
|
||||
return tw
|
||||
|
||||
|
||||
def _strtobool(val):
|
||||
"""Convert a string representation of truth to true (1) or false (0).
|
||||
|
||||
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
|
||||
are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if
|
||||
'val' is anything else.
|
||||
|
||||
.. note:: copied from distutils.util
|
||||
"""
|
||||
val = val.lower()
|
||||
if val in ('y', 'yes', 't', 'true', 'on', '1'):
|
||||
return 1
|
||||
elif val in ('n', 'no', 'f', 'false', 'off', '0'):
|
||||
return 0
|
||||
else:
|
||||
raise ValueError("invalid truth value %r" % (val,))
|
||||
|
||||
@@ -8,21 +8,35 @@ import pytest
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
group._addoption('--pdb',
|
||||
action="store_true", dest="usepdb", default=False,
|
||||
help="start the interactive Python debugger on errors.")
|
||||
group._addoption(
|
||||
'--pdb', dest="usepdb", action="store_true",
|
||||
help="start the interactive Python debugger on errors.")
|
||||
group._addoption(
|
||||
'--pdbcls', dest="usepdb_cls", metavar="modulename:classname",
|
||||
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"):
|
||||
if config.getvalue("usepdb") or config.getvalue("usepdb_cls"):
|
||||
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)
|
||||
|
||||
def fin():
|
||||
pdb.set_trace, pytestPDB._pluginmanager = old
|
||||
pytestPDB._config = None
|
||||
pytestPDB._pdb_cls = pdb.Pdb
|
||||
|
||||
pdb.set_trace = pytest.set_trace
|
||||
pytestPDB._pluginmanager = config.pluginmanager
|
||||
pytestPDB._config = config
|
||||
@@ -32,12 +46,12 @@ class pytestPDB:
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
_pluginmanager = None
|
||||
_config = None
|
||||
_pdb_cls = pdb.Pdb
|
||||
|
||||
def set_trace(self):
|
||||
""" invoke PDB set_trace debugging, dropping any IO capturing. """
|
||||
import _pytest.config
|
||||
frame = sys._getframe().f_back
|
||||
capman = None
|
||||
if self._pluginmanager is not None:
|
||||
capman = self._pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
@@ -45,15 +59,17 @@ class pytestPDB:
|
||||
tw = _pytest.config.create_terminal_writer(self._config)
|
||||
tw.line()
|
||||
tw.sep(">", "PDB set_trace (IO-capturing turned off)")
|
||||
self._pluginmanager.hook.pytest_enter_pdb()
|
||||
pdb.Pdb().set_trace(frame)
|
||||
self._pluginmanager.hook.pytest_enter_pdb(config=self._config)
|
||||
self._pdb_cls().set_trace(frame)
|
||||
|
||||
|
||||
class PdbInvoke:
|
||||
def pytest_exception_interact(self, node, call, report):
|
||||
capman = node.config.pluginmanager.getplugin("capturemanager")
|
||||
if capman:
|
||||
capman.suspendcapture(in_=True)
|
||||
out, err = capman.suspendcapture(in_=True)
|
||||
sys.stdout.write(out)
|
||||
sys.stdout.write(err)
|
||||
_enter_pdb(node, call.excinfo, report)
|
||||
|
||||
def pytest_internalerror(self, excrepr, excinfo):
|
||||
@@ -97,7 +113,7 @@ def _find_last_non_hidden_frame(stack):
|
||||
|
||||
|
||||
def post_mortem(t):
|
||||
class Pdb(pdb.Pdb):
|
||||
class Pdb(pytestPDB._pdb_cls):
|
||||
def get_stack(self, f, t):
|
||||
stack, i = pdb.Pdb.get_stack(self, f, t)
|
||||
if f is None:
|
||||
24
_pytest/deprecated.py
Normal file
24
_pytest/deprecated.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""
|
||||
This module contains deprecation messages and bits of code used elsewhere in the codebase
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
MAIN_STR_ARGS = 'passing a string to pytest.main() is deprecated, ' \
|
||||
'pass a list of arguments instead.'
|
||||
|
||||
YIELD_TESTS = 'yield tests are deprecated, and scheduled to be removed in pytest 4.0'
|
||||
|
||||
FUNCARG_PREFIX = (
|
||||
'{name}: declaring fixtures using "pytest_funcarg__" prefix is deprecated '
|
||||
'and scheduled to be removed in pytest 4.0. '
|
||||
'Please remove the prefix and use the @pytest.fixture decorator instead.')
|
||||
|
||||
SETUP_CFG_PYTEST = '[pytest] section in setup.cfg files is deprecated, use [tool:pytest] instead.'
|
||||
|
||||
GETFUNCARGVALUE = "use of getfuncargvalue is deprecated, use getfixturevalue"
|
||||
|
||||
RESULT_LOG = '--result-log is deprecated and scheduled for removal in pytest 4.0'
|
||||
@@ -1,10 +1,26 @@
|
||||
""" discover and run doctests in modules and test files."""
|
||||
from __future__ import absolute_import
|
||||
import traceback
|
||||
import pytest, py
|
||||
from _pytest.python import FixtureRequest
|
||||
from py._code.code import TerminalRepr, ReprFileLocation
|
||||
|
||||
import traceback
|
||||
|
||||
import pytest
|
||||
from _pytest._code.code import ExceptionInfo, ReprFileLocation, TerminalRepr
|
||||
from _pytest.fixtures import FixtureRequest
|
||||
|
||||
|
||||
DOCTEST_REPORT_CHOICE_NONE = 'none'
|
||||
DOCTEST_REPORT_CHOICE_CDIFF = 'cdiff'
|
||||
DOCTEST_REPORT_CHOICE_NDIFF = 'ndiff'
|
||||
DOCTEST_REPORT_CHOICE_UDIFF = 'udiff'
|
||||
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE = 'only_first_failure'
|
||||
|
||||
DOCTEST_REPORT_CHOICES = (
|
||||
DOCTEST_REPORT_CHOICE_NONE,
|
||||
DOCTEST_REPORT_CHOICE_CDIFF,
|
||||
DOCTEST_REPORT_CHOICE_NDIFF,
|
||||
DOCTEST_REPORT_CHOICE_UDIFF,
|
||||
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE,
|
||||
)
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addini('doctest_optionflags', 'option flags for doctests',
|
||||
@@ -14,8 +30,13 @@ def pytest_addoption(parser):
|
||||
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")
|
||||
group.addoption("--doctest-glob",
|
||||
action="store", default="test*.txt", metavar="pat",
|
||||
action="append", default=[], metavar="pat",
|
||||
help="doctests file matching pattern, default: test*.txt",
|
||||
dest="doctestglob")
|
||||
group.addoption("--doctest-ignore-import-errors",
|
||||
@@ -29,11 +50,20 @@ def pytest_collect_file(path, parent):
|
||||
if path.ext == ".py":
|
||||
if config.option.doctestmodules:
|
||||
return DoctestModule(path, parent)
|
||||
elif (path.ext in ('.txt', '.rst') and parent.session.isinitpath(path)) or \
|
||||
path.check(fnmatch=config.getvalue("doctestglob")):
|
||||
elif _is_doctest(config, path, parent):
|
||||
return DoctestTextfile(path, parent)
|
||||
|
||||
|
||||
def _is_doctest(config, path, parent):
|
||||
if path.ext in ('.txt', '.rst') and parent.session.isinitpath(path):
|
||||
return True
|
||||
globs = config.getoption("doctestglob") or ['test*.txt']
|
||||
for glob in globs:
|
||||
if path.check(fnmatch=glob):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ReprFailDoctest(TerminalRepr):
|
||||
|
||||
def __init__(self, reprlocation, lines):
|
||||
@@ -47,7 +77,6 @@ class ReprFailDoctest(TerminalRepr):
|
||||
|
||||
|
||||
class DoctestItem(pytest.Item):
|
||||
|
||||
def __init__(self, name, parent, runner=None, dtest=None):
|
||||
super(DoctestItem, self).__init__(name, parent)
|
||||
self.runner = runner
|
||||
@@ -58,7 +87,9 @@ class DoctestItem(pytest.Item):
|
||||
def setup(self):
|
||||
if self.dtest is not None:
|
||||
self.fixture_request = _setup_fixtures(self)
|
||||
globs = dict(getfixture=self.fixture_request.getfuncargvalue)
|
||||
globs = dict(getfixture=self.fixture_request.getfixturevalue)
|
||||
for name, value in self.fixture_request.getfixturevalue('doctest_namespace').items():
|
||||
globs[name] = value
|
||||
self.dtest.globs.update(globs)
|
||||
|
||||
def runtest(self):
|
||||
@@ -79,26 +110,26 @@ class DoctestItem(pytest.Item):
|
||||
lineno = test.lineno + example.lineno + 1
|
||||
message = excinfo.type.__name__
|
||||
reprlocation = ReprFileLocation(filename, lineno, message)
|
||||
checker = _get_unicode_checker()
|
||||
REPORT_UDIFF = doctest.REPORT_UDIFF
|
||||
filelines = py.path.local(filename).readlines(cr=0)
|
||||
lines = []
|
||||
checker = _get_checker()
|
||||
report_choice = _get_report_choice(self.config.getoption("doctestreport"))
|
||||
if lineno is not None:
|
||||
i = max(test.lineno, max(0, lineno - 10)) # XXX?
|
||||
for line in filelines[i:lineno]:
|
||||
lines.append("%03d %s" % (i+1, line))
|
||||
i += 1
|
||||
lines = doctestfailure.test.docstring.splitlines(False)
|
||||
# add line numbers to the left of the error message
|
||||
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]
|
||||
else:
|
||||
lines.append('EXAMPLE LOCATION UNKNOWN, not showing all tests of that example')
|
||||
lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example']
|
||||
indent = '>>>'
|
||||
for line in example.source.splitlines():
|
||||
lines.append('??? %s %s' % (indent, line))
|
||||
indent = '...'
|
||||
if excinfo.errisinstance(doctest.DocTestFailure):
|
||||
lines += checker.output_difference(example,
|
||||
doctestfailure.got, REPORT_UDIFF).split("\n")
|
||||
doctestfailure.got, report_choice).split("\n")
|
||||
else:
|
||||
inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info)
|
||||
inner_excinfo = ExceptionInfo(excinfo.value.exc_info)
|
||||
lines += ["UNEXPECTED EXCEPTION: %s" %
|
||||
repr(inner_excinfo.value)]
|
||||
lines += traceback.format_exception(*excinfo.value.exc_info)
|
||||
@@ -118,7 +149,9 @@ def _get_flag_lookup():
|
||||
ELLIPSIS=doctest.ELLIPSIS,
|
||||
IGNORE_EXCEPTION_DETAIL=doctest.IGNORE_EXCEPTION_DETAIL,
|
||||
COMPARISON_FLAGS=doctest.COMPARISON_FLAGS,
|
||||
ALLOW_UNICODE=_get_allow_unicode_flag())
|
||||
ALLOW_UNICODE=_get_allow_unicode_flag(),
|
||||
ALLOW_BYTES=_get_allow_bytes_flag(),
|
||||
)
|
||||
|
||||
|
||||
def get_optionflags(parent):
|
||||
@@ -130,29 +163,28 @@ def get_optionflags(parent):
|
||||
return flag_acc
|
||||
|
||||
|
||||
class DoctestTextfile(DoctestItem, pytest.Module):
|
||||
class DoctestTextfile(pytest.Module):
|
||||
obj = None
|
||||
|
||||
def runtest(self):
|
||||
def collect(self):
|
||||
import doctest
|
||||
fixture_request = _setup_fixtures(self)
|
||||
|
||||
# inspired by doctest.testfile; ideally we would use it directly,
|
||||
# but it doesn't support passing a custom checker
|
||||
text = self.fspath.read()
|
||||
filename = str(self.fspath)
|
||||
name = self.fspath.basename
|
||||
globs = dict(getfixture=fixture_request.getfuncargvalue)
|
||||
if '__name__' not in globs:
|
||||
globs['__name__'] = '__main__'
|
||||
globs = {'__name__': '__main__'}
|
||||
|
||||
|
||||
optionflags = get_optionflags(self)
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
|
||||
checker=_get_unicode_checker())
|
||||
checker=_get_checker())
|
||||
|
||||
parser = doctest.DocTestParser()
|
||||
test = parser.get_doctest(text, globs, name, filename, 0)
|
||||
_check_all_skipped(test)
|
||||
runner.run(test)
|
||||
if test.examples:
|
||||
yield DoctestItem(test.name, self, runner, test)
|
||||
|
||||
|
||||
def _check_all_skipped(test):
|
||||
@@ -182,7 +214,7 @@ class DoctestModule(pytest.Module):
|
||||
finder = doctest.DocTestFinder()
|
||||
optionflags = get_optionflags(self)
|
||||
runner = doctest.DebugRunner(verbose=0, optionflags=optionflags,
|
||||
checker=_get_unicode_checker())
|
||||
checker=_get_checker())
|
||||
for test in finder.find(module, module.__name__):
|
||||
if test.examples: # skip empty doctests
|
||||
yield DoctestItem(test.name, self, runner, test)
|
||||
@@ -204,28 +236,32 @@ def _setup_fixtures(doctest_item):
|
||||
return fixture_request
|
||||
|
||||
|
||||
def _get_unicode_checker():
|
||||
def _get_checker():
|
||||
"""
|
||||
Returns a doctest.OutputChecker subclass that takes in account the
|
||||
ALLOW_UNICODE option to ignore u'' prefixes in strings. Useful
|
||||
when the same doctest should run in Python 2 and Python 3.
|
||||
ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES
|
||||
to strip b'' prefixes.
|
||||
Useful when the same doctest should run in Python 2 and Python 3.
|
||||
|
||||
An inner class is used to avoid importing "doctest" at the module
|
||||
level.
|
||||
"""
|
||||
if hasattr(_get_unicode_checker, 'UnicodeOutputChecker'):
|
||||
return _get_unicode_checker.UnicodeOutputChecker()
|
||||
if hasattr(_get_checker, 'LiteralsOutputChecker'):
|
||||
return _get_checker.LiteralsOutputChecker()
|
||||
|
||||
import doctest
|
||||
import re
|
||||
|
||||
class UnicodeOutputChecker(doctest.OutputChecker):
|
||||
class LiteralsOutputChecker(doctest.OutputChecker):
|
||||
"""
|
||||
Copied from doctest_nose_plugin.py from the nltk project:
|
||||
https://github.com/nltk/nltk
|
||||
|
||||
Further extended to also support byte literals.
|
||||
"""
|
||||
|
||||
_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
|
||||
_unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE)
|
||||
_bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE)
|
||||
|
||||
def check_output(self, want, got, optionflags):
|
||||
res = doctest.OutputChecker.check_output(self, want, got,
|
||||
@@ -233,23 +269,27 @@ def _get_unicode_checker():
|
||||
if res:
|
||||
return True
|
||||
|
||||
if not (optionflags & _get_allow_unicode_flag()):
|
||||
allow_unicode = optionflags & _get_allow_unicode_flag()
|
||||
allow_bytes = optionflags & _get_allow_bytes_flag()
|
||||
if not allow_unicode and not allow_bytes:
|
||||
return False
|
||||
|
||||
else: # pragma: no cover
|
||||
# the code below will end up executed only in Python 2 in
|
||||
# our tests, and our coverage check runs in Python 3 only
|
||||
def remove_u_prefixes(txt):
|
||||
return re.sub(self._literal_re, r'\1\2', txt)
|
||||
def remove_prefixes(regex, txt):
|
||||
return re.sub(regex, r'\1\2', txt)
|
||||
|
||||
want = remove_u_prefixes(want)
|
||||
got = remove_u_prefixes(got)
|
||||
if allow_unicode:
|
||||
want = remove_prefixes(self._unicode_literal_re, want)
|
||||
got = remove_prefixes(self._unicode_literal_re, got)
|
||||
if allow_bytes:
|
||||
want = remove_prefixes(self._bytes_literal_re, want)
|
||||
got = remove_prefixes(self._bytes_literal_re, got)
|
||||
res = doctest.OutputChecker.check_output(self, want, got,
|
||||
optionflags)
|
||||
return res
|
||||
|
||||
_get_unicode_checker.UnicodeOutputChecker = UnicodeOutputChecker
|
||||
return _get_unicode_checker.UnicodeOutputChecker()
|
||||
_get_checker.LiteralsOutputChecker = LiteralsOutputChecker
|
||||
return _get_checker.LiteralsOutputChecker()
|
||||
|
||||
|
||||
def _get_allow_unicode_flag():
|
||||
@@ -258,3 +298,34 @@ def _get_allow_unicode_flag():
|
||||
"""
|
||||
import doctest
|
||||
return doctest.register_optionflag('ALLOW_UNICODE')
|
||||
|
||||
|
||||
def _get_allow_bytes_flag():
|
||||
"""
|
||||
Registers and returns the ALLOW_BYTES flag.
|
||||
"""
|
||||
import doctest
|
||||
return doctest.register_optionflag('ALLOW_BYTES')
|
||||
|
||||
|
||||
def _get_report_choice(key):
|
||||
"""
|
||||
This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid
|
||||
importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests.
|
||||
"""
|
||||
import doctest
|
||||
|
||||
return {
|
||||
DOCTEST_REPORT_CHOICE_UDIFF: doctest.REPORT_UDIFF,
|
||||
DOCTEST_REPORT_CHOICE_CDIFF: doctest.REPORT_CDIFF,
|
||||
DOCTEST_REPORT_CHOICE_NDIFF: doctest.REPORT_NDIFF,
|
||||
DOCTEST_REPORT_CHOICE_ONLY_FIRST_FAILURE: doctest.REPORT_ONLY_FIRST_FAILURE,
|
||||
DOCTEST_REPORT_CHOICE_NONE: 0,
|
||||
}[key]
|
||||
|
||||
@pytest.fixture(scope='session')
|
||||
def doctest_namespace():
|
||||
"""
|
||||
Inject names into the doctest namespace.
|
||||
"""
|
||||
return dict()
|
||||
|
||||
1135
_pytest/fixtures.py
Normal file
1135
_pytest/fixtures.py
Normal file
File diff suppressed because it is too large
Load Diff
45
_pytest/freeze_support.py
Normal file
45
_pytest/freeze_support.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Provides a function to report all internal modules for using freezing tools
|
||||
pytest
|
||||
"""
|
||||
|
||||
def pytest_namespace():
|
||||
return {'freeze_includes': freeze_includes}
|
||||
|
||||
|
||||
def freeze_includes():
|
||||
"""
|
||||
Returns a list of module names used by py.test that should be
|
||||
included by cx_freeze.
|
||||
"""
|
||||
import py
|
||||
import _pytest
|
||||
result = list(_iter_all_modules(py))
|
||||
result += list(_iter_all_modules(_pytest))
|
||||
return result
|
||||
|
||||
|
||||
def _iter_all_modules(package, prefix=''):
|
||||
"""
|
||||
Iterates over the names of all modules that can be found in the given
|
||||
package, recursively.
|
||||
Example:
|
||||
_iter_all_modules(_pytest) ->
|
||||
['_pytest.assertion.newinterpret',
|
||||
'_pytest.capture',
|
||||
'_pytest.core',
|
||||
...
|
||||
]
|
||||
"""
|
||||
import os
|
||||
import pkgutil
|
||||
if type(package) is not str:
|
||||
path, prefix = package.__path__[0], package.__name__ + '.'
|
||||
else:
|
||||
path = package
|
||||
for _, name, is_package in pkgutil.iter_modules([path]):
|
||||
if is_package:
|
||||
for m in _iter_all_modules(os.path.join(path, name), prefix=name + '.'):
|
||||
yield prefix + m
|
||||
else:
|
||||
yield prefix + name
|
||||
@@ -1,132 +0,0 @@
|
||||
""" (deprecated) generate a single-file self-contained version of pytest """
|
||||
import os
|
||||
import sys
|
||||
import pkgutil
|
||||
|
||||
import py
|
||||
import _pytest
|
||||
|
||||
|
||||
|
||||
def find_toplevel(name):
|
||||
for syspath in sys.path:
|
||||
base = py.path.local(syspath)
|
||||
lib = base/name
|
||||
if lib.check(dir=1):
|
||||
return lib
|
||||
mod = base.join("%s.py" % name)
|
||||
if mod.check(file=1):
|
||||
return mod
|
||||
raise LookupError(name)
|
||||
|
||||
def pkgname(toplevel, rootpath, path):
|
||||
parts = path.parts()[len(rootpath.parts()):]
|
||||
return '.'.join([toplevel] + [x.purebasename for x in parts])
|
||||
|
||||
def pkg_to_mapping(name):
|
||||
toplevel = find_toplevel(name)
|
||||
name2src = {}
|
||||
if toplevel.check(file=1): # module
|
||||
name2src[toplevel.purebasename] = toplevel.read()
|
||||
else: # package
|
||||
for pyfile in toplevel.visit('*.py'):
|
||||
pkg = pkgname(name, toplevel, pyfile)
|
||||
name2src[pkg] = pyfile.read()
|
||||
# with wheels py source code might be not be installed
|
||||
# and the resulting genscript is useless, just bail out.
|
||||
assert name2src, "no source code found for %r at %r" %(name, toplevel)
|
||||
return name2src
|
||||
|
||||
def compress_mapping(mapping):
|
||||
import base64, pickle, zlib
|
||||
data = pickle.dumps(mapping, 2)
|
||||
data = zlib.compress(data, 9)
|
||||
data = base64.encodestring(data)
|
||||
data = data.decode('ascii')
|
||||
return data
|
||||
|
||||
|
||||
def compress_packages(names):
|
||||
mapping = {}
|
||||
for name in names:
|
||||
mapping.update(pkg_to_mapping(name))
|
||||
return compress_mapping(mapping)
|
||||
|
||||
def generate_script(entry, packages):
|
||||
data = compress_packages(packages)
|
||||
tmpl = py.path.local(__file__).dirpath().join('standalonetemplate.py')
|
||||
exe = tmpl.read()
|
||||
exe = exe.replace('@SOURCES@', data)
|
||||
exe = exe.replace('@ENTRY@', entry)
|
||||
return exe
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption("--genscript", action="store", default=None,
|
||||
dest="genscript", metavar="path",
|
||||
help="create standalone pytest script at given target path.")
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
import _pytest.config
|
||||
genscript = config.getvalue("genscript")
|
||||
if genscript:
|
||||
tw = _pytest.config.create_terminal_writer(config)
|
||||
tw.line("WARNING: usage of genscript is deprecated.",
|
||||
red=True)
|
||||
deps = ['py', '_pytest', 'pytest'] # pluggy is vendored
|
||||
if sys.version_info < (2,7):
|
||||
deps.append("argparse")
|
||||
tw.line("generated script will run on python2.6-python3.3++")
|
||||
else:
|
||||
tw.line("WARNING: generated script will not run on python2.6 "
|
||||
"due to 'argparse' dependency. Use python2.6 "
|
||||
"to generate a python2.6 compatible script", red=True)
|
||||
script = generate_script(
|
||||
'import pytest; raise SystemExit(pytest.cmdline.main())',
|
||||
deps,
|
||||
)
|
||||
genscript = py.path.local(genscript)
|
||||
genscript.write(script)
|
||||
tw.line("generated pytest standalone script: %s" % genscript,
|
||||
bold=True)
|
||||
return 0
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {'freeze_includes': freeze_includes}
|
||||
|
||||
|
||||
def freeze_includes():
|
||||
"""
|
||||
Returns a list of module names used by py.test that should be
|
||||
included by cx_freeze.
|
||||
"""
|
||||
result = list(_iter_all_modules(py))
|
||||
result += list(_iter_all_modules(_pytest))
|
||||
return result
|
||||
|
||||
|
||||
def _iter_all_modules(package, prefix=''):
|
||||
"""
|
||||
Iterates over the names of all modules that can be found in the given
|
||||
package, recursively.
|
||||
|
||||
Example:
|
||||
_iter_all_modules(_pytest) ->
|
||||
['_pytest.assertion.newinterpret',
|
||||
'_pytest.capture',
|
||||
'_pytest.core',
|
||||
...
|
||||
]
|
||||
"""
|
||||
if type(package) is not str:
|
||||
path, prefix = package.__path__[0], package.__name__ + '.'
|
||||
else:
|
||||
path = package
|
||||
for _, name, is_package in pkgutil.iter_modules([path]):
|
||||
if is_package:
|
||||
for m in _iter_all_modules(os.path.join(path, name), prefix=name + '.'):
|
||||
yield prefix + m
|
||||
else:
|
||||
yield prefix + name
|
||||
@@ -20,6 +20,10 @@ def pytest_addoption(parser):
|
||||
group.addoption('--debug',
|
||||
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",
|
||||
help="override config option with option=value style, e.g. `-o xfail_strict=True`.")
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
@@ -37,12 +41,14 @@ def pytest_cmdline_parse():
|
||||
config.trace.root.setwriter(debugfile.write)
|
||||
undo_tracing = config.pluginmanager.enable_tracing()
|
||||
sys.stderr.write("writing pytestdebug information to %s\n" % path)
|
||||
|
||||
def unset_tracing():
|
||||
debugfile.close()
|
||||
sys.stderr.write("wrote pytestdebug information to %s\n" %
|
||||
debugfile.name)
|
||||
config.trace.root.setwriter(None)
|
||||
undo_tracing()
|
||||
|
||||
config.add_cleanup(unset_tracing)
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
@@ -67,9 +73,8 @@ def showhelp(config):
|
||||
tw.write(config._parser.optparser.format_help())
|
||||
tw.line()
|
||||
tw.line()
|
||||
#tw.sep( "=", "config file settings")
|
||||
tw.line("[pytest] ini-options in the next "
|
||||
"pytest.ini|tox.ini|setup.cfg file:")
|
||||
tw.line("[pytest] ini-options in the first "
|
||||
"pytest.ini|tox.ini|setup.cfg file found:")
|
||||
tw.line()
|
||||
|
||||
for name in config._parser._ininames:
|
||||
@@ -92,8 +97,8 @@ def showhelp(config):
|
||||
tw.line()
|
||||
tw.line()
|
||||
|
||||
tw.line("to see available markers type: py.test --markers")
|
||||
tw.line("to see available fixtures type: py.test --fixtures")
|
||||
tw.line("to see available markers type: pytest --markers")
|
||||
tw.line("to see available fixtures type: pytest --fixtures")
|
||||
tw.line("(shown according to specified file_or_dir or current dir "
|
||||
"if not specified)")
|
||||
|
||||
|
||||
@@ -28,17 +28,14 @@ def pytest_plugin_registered(plugin, manager):
|
||||
|
||||
@hookspec(historic=True)
|
||||
def pytest_addoption(parser):
|
||||
"""register argparse-style options and ini-style config values.
|
||||
"""register argparse-style options and ini-style config values,
|
||||
called once at the beginning of a test run.
|
||||
|
||||
.. warning::
|
||||
.. note::
|
||||
|
||||
This function must be implemented in a :ref:`plugin <pluginorder>`
|
||||
and is called once at the beginning of a test run.
|
||||
|
||||
Implementing this hook from ``conftest.py`` files is **strongly**
|
||||
discouraged because ``conftest.py`` files are lazily loaded and
|
||||
may give strange *unknown option* errors depending on the directory
|
||||
``py.test`` is invoked from.
|
||||
This function should be implemented only in plugins or ``conftest.py``
|
||||
files situated at the tests root directory due to how pytest
|
||||
:ref:`discovers plugins during startup <pluginorder>`.
|
||||
|
||||
:arg parser: To add command line options, call
|
||||
:py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`.
|
||||
@@ -84,7 +81,7 @@ def pytest_cmdline_main(config):
|
||||
""" called for performing the main command line action. The default
|
||||
implementation will invoke the configure hooks and runtest_mainloop. """
|
||||
|
||||
def pytest_load_initial_conftests(args, early_config, parser):
|
||||
def pytest_load_initial_conftests(early_config, parser, args):
|
||||
""" implements the loading of initial conftest files ahead
|
||||
of command line option parsing. """
|
||||
|
||||
@@ -159,6 +156,12 @@ def pytest_pyfunc_call(pyfuncitem):
|
||||
def pytest_generate_tests(metafunc):
|
||||
""" generate (multiple) parametrized calls to a test function."""
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_make_parametrize_id(config, val):
|
||||
"""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``.
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# generic runtest related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -215,6 +218,19 @@ def pytest_runtest_logreport(report):
|
||||
""" process a test setup/call/teardown report relating to
|
||||
the respective phase of executing a test. """
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Fixture related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
""" performs fixture setup execution. """
|
||||
|
||||
def pytest_fixture_post_finalizer(fixturedef):
|
||||
""" called after fixture teardown, but before the cache is cleared so
|
||||
the fixture result cache ``fixturedef.cached_result`` can
|
||||
still be accessed."""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# test session related hooks
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -230,7 +246,7 @@ def pytest_unconfigure(config):
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# hooks for customising the assert methods
|
||||
# hooks for customizing the assert methods
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_assertrepr_compare(config, op, left, right):
|
||||
@@ -239,7 +255,7 @@ def pytest_assertrepr_compare(config, op, left, right):
|
||||
Return None for no custom explanation, otherwise return a list
|
||||
of strings. The strings will be joined by newlines but any newlines
|
||||
*in* a string will be escaped. Note that all but the first line will
|
||||
be indented sligthly, the intention is for the first line to be a summary.
|
||||
be indented slightly, the intention is for the first line to be a summary.
|
||||
"""
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -247,13 +263,20 @@ def pytest_assertrepr_compare(config, op, left, right):
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
""" return a string to be displayed as header info for terminal reporting."""
|
||||
""" return a string to be displayed as header info for terminal reporting.
|
||||
|
||||
.. note::
|
||||
|
||||
This function should be implemented only in plugins or ``conftest.py``
|
||||
files situated at the tests root directory due to how pytest
|
||||
:ref:`discovers plugins during startup <pluginorder>`.
|
||||
"""
|
||||
|
||||
@hookspec(firstresult=True)
|
||||
def pytest_report_teststatus(report):
|
||||
""" return result-category, shortletter and verbose word for reporting."""
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
def pytest_terminal_summary(terminalreporter, exitstatus):
|
||||
""" add additional section in terminal summary reporting. """
|
||||
|
||||
|
||||
@@ -282,13 +305,17 @@ def pytest_keyboard_interrupt(excinfo):
|
||||
""" called for keyboard interrupt. """
|
||||
|
||||
def pytest_exception_interact(node, call, report):
|
||||
""" (experimental, new in 2.4) called when
|
||||
an exception was raised which can potentially be
|
||||
"""called when an exception was raised which can potentially be
|
||||
interactively handled.
|
||||
|
||||
This hook is only called if an exception was raised
|
||||
that is not an internal exception like "skip.Exception".
|
||||
that is not an internal exception like ``skip.Exception``.
|
||||
"""
|
||||
|
||||
def pytest_enter_pdb():
|
||||
""" called upon pdb.set_trace()"""
|
||||
def pytest_enter_pdb(config):
|
||||
""" called upon pdb.set_trace(), can be used by plugins to take special
|
||||
action just before the python debugger enters in interactive mode.
|
||||
|
||||
:arg config: pytest config object
|
||||
:type config: _pytest.config.Config
|
||||
"""
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
""" report test results in JUnit-XML format, for use with Hudson and build integration servers.
|
||||
"""
|
||||
report test results in JUnit-XML format,
|
||||
for use with Jenkins and build integration servers.
|
||||
|
||||
Output conforms to https://github.com/jenkinsci/xunit-plugin/blob/master/src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd
|
||||
|
||||
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
|
||||
|
||||
import functools
|
||||
import py
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import pytest
|
||||
from _pytest.config import filename_arg
|
||||
|
||||
# Python 2.X and 3.X compatibility
|
||||
if sys.version_info[0] < 3:
|
||||
@@ -19,6 +25,7 @@ else:
|
||||
unicode = str
|
||||
long = int
|
||||
|
||||
|
||||
class Junit(py.xml.Namespace):
|
||||
pass
|
||||
|
||||
@@ -30,21 +37,21 @@ class Junit(py.xml.Namespace):
|
||||
# | [#x10000-#x10FFFF]
|
||||
_legal_chars = (0x09, 0x0A, 0x0d)
|
||||
_legal_ranges = (
|
||||
(0x20, 0x7E),
|
||||
(0x80, 0xD7FF),
|
||||
(0xE000, 0xFFFD),
|
||||
(0x10000, 0x10FFFF),
|
||||
(0x20, 0x7E), (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF),
|
||||
)
|
||||
_legal_xml_re = [unicode("%s-%s") % (unichr(low), unichr(high))
|
||||
for (low, high) in _legal_ranges
|
||||
if low < sys.maxunicode]
|
||||
_legal_xml_re = [
|
||||
unicode("%s-%s") % (unichr(low), unichr(high))
|
||||
for (low, high) in _legal_ranges if low < sys.maxunicode
|
||||
]
|
||||
_legal_xml_re = [unichr(x) for x in _legal_chars] + _legal_xml_re
|
||||
illegal_xml_re = re.compile(unicode('[^%s]') %
|
||||
unicode('').join(_legal_xml_re))
|
||||
illegal_xml_re = re.compile(unicode('[^%s]') % unicode('').join(_legal_xml_re))
|
||||
del _legal_chars
|
||||
del _legal_ranges
|
||||
del _legal_xml_re
|
||||
|
||||
_py_ext_re = re.compile(r"\.py$")
|
||||
|
||||
|
||||
def bin_xml_escape(arg):
|
||||
def repl(matchobj):
|
||||
i = ord(matchobj.group())
|
||||
@@ -52,122 +59,82 @@ def bin_xml_escape(arg):
|
||||
return unicode('#x%02X') % i
|
||||
else:
|
||||
return unicode('#x%04X') % i
|
||||
|
||||
return py.xml.raw(illegal_xml_re.sub(repl, py.xml.escape(arg)))
|
||||
|
||||
@pytest.fixture
|
||||
def record_xml_property(request):
|
||||
"""Fixture that adds extra xml properties to the tag for the calling test.
|
||||
The fixture is callable with (name, value), with value being automatically
|
||||
xml-encoded.
|
||||
"""
|
||||
def inner(name, value):
|
||||
if hasattr(request.config, "_xml"):
|
||||
request.config._xml.add_custom_property(name, value)
|
||||
msg = 'record_xml_property is an experimental feature'
|
||||
request.config.warn(code='C3', message=msg,
|
||||
fslocation=request.node.location[:2])
|
||||
return inner
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group.addoption('--junitxml', '--junit-xml', action="store",
|
||||
dest="xmlpath", metavar="path", default=None,
|
||||
help="create junit-xml style report file at given path.")
|
||||
group.addoption('--junitprefix', '--junit-prefix', action="store",
|
||||
metavar="str", default=None,
|
||||
help="prepend prefix to classnames in junit-xml output")
|
||||
class _NodeReporter(object):
|
||||
def __init__(self, nodeid, xml):
|
||||
|
||||
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.pluginmanager.register(config._xml)
|
||||
self.id = nodeid
|
||||
self.xml = xml
|
||||
self.add_stats = self.xml.add_stats
|
||||
self.duration = 0
|
||||
self.properties = []
|
||||
self.nodes = []
|
||||
self.testcase = None
|
||||
self.attrs = {}
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
xml = getattr(config, '_xml', None)
|
||||
if xml:
|
||||
del config._xml
|
||||
config.pluginmanager.unregister(xml)
|
||||
def append(self, node):
|
||||
self.xml.add_stats(type(node).__name__)
|
||||
self.nodes.append(node)
|
||||
|
||||
def mangle_testnames(names):
|
||||
names = [x.replace(".py", "") for x in names if x != '()']
|
||||
names[0] = names[0].replace("/", '.')
|
||||
return names
|
||||
def add_property(self, name, value):
|
||||
self.properties.append((str(name), bin_xml_escape(value)))
|
||||
|
||||
class LogXML(object):
|
||||
def __init__(self, logfile, prefix):
|
||||
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
||||
self.logfile = os.path.normpath(os.path.abspath(logfile))
|
||||
self.prefix = prefix
|
||||
self.tests = []
|
||||
self.tests_by_nodeid = {} # nodeid -> Junit.testcase
|
||||
self.durations = {} # nodeid -> total duration (setup+call+teardown)
|
||||
self.passed = self.skipped = 0
|
||||
self.failed = self.errors = 0
|
||||
self.custom_properties = {}
|
||||
def make_properties_node(self):
|
||||
"""Return a Junit node containing custom properties, if any.
|
||||
"""
|
||||
if self.properties:
|
||||
return Junit.properties([
|
||||
Junit.property(name=name, value=value)
|
||||
for name, value in self.properties
|
||||
])
|
||||
return ''
|
||||
|
||||
def add_custom_property(self, name, value):
|
||||
self.custom_properties[str(name)] = bin_xml_escape(str(value))
|
||||
|
||||
def _opentestcase(self, report):
|
||||
names = mangle_testnames(report.nodeid.split("::"))
|
||||
def record_testreport(self, testreport):
|
||||
assert not self.testcase
|
||||
names = mangle_test_address(testreport.nodeid)
|
||||
classnames = names[:-1]
|
||||
if self.prefix:
|
||||
classnames.insert(0, self.prefix)
|
||||
if self.xml.prefix:
|
||||
classnames.insert(0, self.xml.prefix)
|
||||
attrs = {
|
||||
"classname": ".".join(classnames),
|
||||
"name": bin_xml_escape(names[-1]),
|
||||
"file": report.location[0],
|
||||
"time": self.durations.get(report.nodeid, 0),
|
||||
"file": testreport.location[0],
|
||||
}
|
||||
if report.location[1] is not None:
|
||||
attrs["line"] = report.location[1]
|
||||
testcase = Junit.testcase(**attrs)
|
||||
custom_properties = self.pop_custom_properties()
|
||||
if custom_properties:
|
||||
testcase.append(custom_properties)
|
||||
self.tests.append(testcase)
|
||||
self.tests_by_nodeid[report.nodeid] = testcase
|
||||
if testreport.location[1] is not None:
|
||||
attrs["line"] = testreport.location[1]
|
||||
self.attrs = attrs
|
||||
|
||||
def _write_captured_output(self, report):
|
||||
def to_xml(self):
|
||||
testcase = Junit.testcase(time=self.duration, **self.attrs)
|
||||
testcase.append(self.make_properties_node())
|
||||
for node in self.nodes:
|
||||
testcase.append(node)
|
||||
return testcase
|
||||
|
||||
def _add_simple(self, kind, message, data=None):
|
||||
data = bin_xml_escape(data)
|
||||
node = kind(data, message=message)
|
||||
self.append(node)
|
||||
|
||||
def write_captured_output(self, report):
|
||||
for capname in ('out', 'err'):
|
||||
allcontent = ""
|
||||
for name, content in report.get_sections("Captured std%s" %
|
||||
capname):
|
||||
allcontent += content
|
||||
if allcontent:
|
||||
tag = getattr(Junit, 'system-'+capname)
|
||||
self.append(tag(bin_xml_escape(allcontent)))
|
||||
|
||||
def append(self, obj):
|
||||
self.tests[-1].append(obj)
|
||||
|
||||
def pop_custom_properties(self):
|
||||
"""Return a Junit node containing custom properties set for
|
||||
the current test, if any, and reset the current custom properties.
|
||||
"""
|
||||
if self.custom_properties:
|
||||
result = Junit.properties(
|
||||
[
|
||||
Junit.property(name=name, value=value)
|
||||
for name, value in self.custom_properties.items()
|
||||
]
|
||||
)
|
||||
self.custom_properties.clear()
|
||||
return result
|
||||
return None
|
||||
content = getattr(report, 'capstd' + capname)
|
||||
if content:
|
||||
tag = getattr(Junit, 'system-' + capname)
|
||||
self.append(tag(bin_xml_escape(content)))
|
||||
|
||||
def append_pass(self, report):
|
||||
self.passed += 1
|
||||
self._write_captured_output(report)
|
||||
self.add_stats('passed')
|
||||
|
||||
def append_failure(self, report):
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
# msg = str(report.longrepr.reprtraceback.extraline)
|
||||
if hasattr(report, "wasxfail"):
|
||||
self.append(
|
||||
Junit.skipped(message="xfail-marked test passes unexpectedly"))
|
||||
self.skipped += 1
|
||||
self._add_simple(
|
||||
Junit.skipped,
|
||||
"xfail-marked test passes unexpectedly")
|
||||
else:
|
||||
if hasattr(report.longrepr, "reprcrash"):
|
||||
message = report.longrepr.reprcrash.message
|
||||
@@ -179,30 +146,29 @@ class LogXML(object):
|
||||
fail = Junit.failure(message=message)
|
||||
fail.append(bin_xml_escape(report.longrepr))
|
||||
self.append(fail)
|
||||
self.failed += 1
|
||||
self._write_captured_output(report)
|
||||
|
||||
def append_collect_error(self, report):
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
# msg = str(report.longrepr.reprtraceback.extraline)
|
||||
self.append(Junit.error(bin_xml_escape(report.longrepr),
|
||||
message="collection failure"))
|
||||
self.errors += 1
|
||||
|
||||
def append_collect_skipped(self, report):
|
||||
#msg = str(report.longrepr.reprtraceback.extraline)
|
||||
self.append(Junit.skipped(bin_xml_escape(report.longrepr),
|
||||
message="collection skipped"))
|
||||
self.skipped += 1
|
||||
self._add_simple(
|
||||
Junit.skipped, "collection skipped", report.longrepr)
|
||||
|
||||
def append_error(self, report):
|
||||
self.append(Junit.error(bin_xml_escape(report.longrepr),
|
||||
message="test setup failure"))
|
||||
self.errors += 1
|
||||
if getattr(report, 'when', None) == 'teardown':
|
||||
msg = "test teardown failure"
|
||||
else:
|
||||
msg = "test setup failure"
|
||||
self._add_simple(
|
||||
Junit.error, msg, report.longrepr)
|
||||
|
||||
def append_skipped(self, report):
|
||||
if hasattr(report, "wasxfail"):
|
||||
self.append(Junit.skipped(bin_xml_escape(report.wasxfail),
|
||||
message="expected test failure"))
|
||||
self._add_simple(
|
||||
Junit.skipped, "expected test failure", report.wasxfail
|
||||
)
|
||||
else:
|
||||
filename, lineno, skipreason = report.longrepr
|
||||
if skipreason.startswith("Skipped: "):
|
||||
@@ -210,10 +176,133 @@ class LogXML(object):
|
||||
self.append(
|
||||
Junit.skipped("%s:%s: %s" % (filename, lineno, skipreason),
|
||||
type="pytest.skip",
|
||||
message=skipreason
|
||||
))
|
||||
self.skipped += 1
|
||||
self._write_captured_output(report)
|
||||
message=skipreason))
|
||||
self.write_captured_output(report)
|
||||
|
||||
def finalize(self):
|
||||
data = self.to_xml().unicode(indent=0)
|
||||
self.__dict__.clear()
|
||||
self.to_xml = lambda: py.xml.raw(data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def record_xml_property(request):
|
||||
"""Add extra xml properties to the tag for the calling test.
|
||||
The fixture is callable with ``(name, value)``, with value being automatically
|
||||
xml-encoded.
|
||||
"""
|
||||
request.node.warn(
|
||||
code='C3',
|
||||
message='record_xml_property is an experimental feature',
|
||||
)
|
||||
xml = getattr(request.config, "_xml", None)
|
||||
if xml is not None:
|
||||
node_reporter = xml.node_reporter(request.node.nodeid)
|
||||
return node_reporter.add_property
|
||||
else:
|
||||
def add_property_noop(name, value):
|
||||
pass
|
||||
|
||||
return add_property_noop
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting")
|
||||
group.addoption(
|
||||
'--junitxml', '--junit-xml',
|
||||
action="store",
|
||||
dest="xmlpath",
|
||||
metavar="path",
|
||||
type=functools.partial(filename_arg, optname="--junitxml"),
|
||||
default=None,
|
||||
help="create junit-xml style report file at given path.")
|
||||
group.addoption(
|
||||
'--junitprefix', '--junit-prefix',
|
||||
action="store",
|
||||
metavar="str",
|
||||
default=None,
|
||||
help="prepend prefix to classnames in junit-xml output")
|
||||
|
||||
|
||||
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.pluginmanager.register(config._xml)
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
xml = getattr(config, '_xml', None)
|
||||
if xml:
|
||||
del config._xml
|
||||
config.pluginmanager.unregister(xml)
|
||||
|
||||
|
||||
def mangle_test_address(address):
|
||||
path, possible_open_bracket, params = address.partition('[')
|
||||
names = path.split("::")
|
||||
try:
|
||||
names.remove('()')
|
||||
except ValueError:
|
||||
pass
|
||||
# convert file path to dotted path
|
||||
names[0] = names[0].replace("/", '.')
|
||||
names[0] = _py_ext_re.sub("", names[0])
|
||||
# put any params back
|
||||
names[-1] += possible_open_bracket + params
|
||||
return names
|
||||
|
||||
|
||||
class LogXML(object):
|
||||
def __init__(self, logfile, prefix):
|
||||
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
||||
self.logfile = os.path.normpath(os.path.abspath(logfile))
|
||||
self.prefix = prefix
|
||||
self.stats = dict.fromkeys([
|
||||
'error',
|
||||
'passed',
|
||||
'failure',
|
||||
'skipped',
|
||||
], 0)
|
||||
self.node_reporters = {} # nodeid -> _NodeReporter
|
||||
self.node_reporters_ordered = []
|
||||
self.global_properties = []
|
||||
|
||||
def finalize(self, report):
|
||||
nodeid = getattr(report, 'nodeid', report)
|
||||
# local hack to handle xdist report order
|
||||
slavenode = getattr(report, 'node', None)
|
||||
reporter = self.node_reporters.pop((nodeid, slavenode))
|
||||
if reporter is not None:
|
||||
reporter.finalize()
|
||||
|
||||
def node_reporter(self, report):
|
||||
nodeid = getattr(report, 'nodeid', report)
|
||||
# local hack to handle xdist report order
|
||||
slavenode = getattr(report, 'node', None)
|
||||
|
||||
key = nodeid, slavenode
|
||||
|
||||
if key in self.node_reporters:
|
||||
# TODO: breasks for --dist=each
|
||||
return self.node_reporters[key]
|
||||
|
||||
reporter = _NodeReporter(nodeid, self)
|
||||
|
||||
self.node_reporters[key] = reporter
|
||||
self.node_reporters_ordered.append(reporter)
|
||||
|
||||
return reporter
|
||||
|
||||
def add_stats(self, key):
|
||||
if key in self.stats:
|
||||
self.stats[key] += 1
|
||||
|
||||
def _opentestcase(self, report):
|
||||
reporter = self.node_reporter(report)
|
||||
reporter.record_testreport(report)
|
||||
return reporter
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
"""handle a setup/call/teardown report, generating the appropriate
|
||||
@@ -240,47 +329,42 @@ class LogXML(object):
|
||||
"""
|
||||
if report.passed:
|
||||
if report.when == "call": # ignore setup/teardown
|
||||
self._opentestcase(report)
|
||||
self.append_pass(report)
|
||||
reporter = self._opentestcase(report)
|
||||
reporter.append_pass(report)
|
||||
elif report.failed:
|
||||
self._opentestcase(report)
|
||||
if report.when != "call":
|
||||
self.append_error(report)
|
||||
reporter = self._opentestcase(report)
|
||||
if report.when == "call":
|
||||
reporter.append_failure(report)
|
||||
else:
|
||||
self.append_failure(report)
|
||||
reporter.append_error(report)
|
||||
elif report.skipped:
|
||||
self._opentestcase(report)
|
||||
self.append_skipped(report)
|
||||
reporter = self._opentestcase(report)
|
||||
reporter.append_skipped(report)
|
||||
self.update_testcase_duration(report)
|
||||
if report.when == "teardown":
|
||||
reporter = self._opentestcase(report)
|
||||
reporter.write_captured_output(report)
|
||||
self.finalize(report)
|
||||
|
||||
def update_testcase_duration(self, report):
|
||||
"""accumulates total duration for nodeid from given report and updates
|
||||
the Junit.testcase with the new total if already created.
|
||||
"""
|
||||
total = self.durations.get(report.nodeid, 0.0)
|
||||
total += getattr(report, 'duration', 0.0)
|
||||
self.durations[report.nodeid] = total
|
||||
|
||||
testcase = self.tests_by_nodeid.get(report.nodeid)
|
||||
if testcase is not None:
|
||||
testcase.attr.time = total
|
||||
reporter = self.node_reporter(report)
|
||||
reporter.duration += getattr(report, 'duration', 0.0)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if not report.passed:
|
||||
self._opentestcase(report)
|
||||
reporter = self._opentestcase(report)
|
||||
if report.failed:
|
||||
self.append_collect_error(report)
|
||||
reporter.append_collect_error(report)
|
||||
else:
|
||||
self.append_collect_skipped(report)
|
||||
reporter.append_collect_skipped(report)
|
||||
|
||||
def pytest_internalerror(self, excrepr):
|
||||
self.errors += 1
|
||||
data = bin_xml_escape(excrepr)
|
||||
self.tests.append(
|
||||
Junit.testcase(
|
||||
Junit.error(data, message="internal error"),
|
||||
classname="pytest",
|
||||
name="internal"))
|
||||
reporter = self.node_reporter('internal')
|
||||
reporter.attrs.update(classname="pytest", name='internal')
|
||||
reporter._add_simple(Junit.error, 'internal error', excrepr)
|
||||
|
||||
def pytest_sessionstart(self):
|
||||
self.suite_start_time = time.time()
|
||||
@@ -292,19 +376,37 @@ class LogXML(object):
|
||||
logfile = open(self.logfile, 'w', encoding='utf-8')
|
||||
suite_stop_time = time.time()
|
||||
suite_time_delta = suite_stop_time - self.suite_start_time
|
||||
numtests = self.passed + self.failed
|
||||
|
||||
numtests = self.stats['passed'] + self.stats['failure'] + self.stats['skipped'] + self.stats['error']
|
||||
|
||||
logfile.write('<?xml version="1.0" encoding="utf-8"?>')
|
||||
|
||||
logfile.write(Junit.testsuite(
|
||||
self.tests,
|
||||
self._get_global_properties_node(),
|
||||
[x.to_xml() for x in self.node_reporters_ordered],
|
||||
name="pytest",
|
||||
errors=self.errors,
|
||||
failures=self.failed,
|
||||
skips=self.skipped,
|
||||
errors=self.stats['error'],
|
||||
failures=self.stats['failure'],
|
||||
skips=self.stats['skipped'],
|
||||
tests=numtests,
|
||||
time="%.3f" % suite_time_delta,
|
||||
).unicode(indent=0))
|
||||
time="%.3f" % suite_time_delta, ).unicode(indent=0))
|
||||
logfile.close()
|
||||
|
||||
def pytest_terminal_summary(self, terminalreporter):
|
||||
terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile))
|
||||
terminalreporter.write_sep("-",
|
||||
"generated xml file: %s" % (self.logfile))
|
||||
|
||||
def add_global_property(self, name, value):
|
||||
self.global_properties.append((str(name), bin_xml_escape(value)))
|
||||
|
||||
def _get_global_properties_node(self):
|
||||
"""Return a Junit node containing custom properties, if any.
|
||||
"""
|
||||
if self.global_properties:
|
||||
return Junit.properties(
|
||||
[
|
||||
Junit.property(name=name, value=value)
|
||||
for name, value in self.global_properties
|
||||
]
|
||||
)
|
||||
return ''
|
||||
|
||||
215
_pytest/main.py
215
_pytest/main.py
@@ -1,14 +1,18 @@
|
||||
""" core implementation of testing process: init, session, runtest loop. """
|
||||
import re
|
||||
import functools
|
||||
import os
|
||||
import sys
|
||||
|
||||
import _pytest
|
||||
import _pytest._code
|
||||
import py
|
||||
import pytest, _pytest
|
||||
import os, sys, imp
|
||||
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.runner import collect_one_node
|
||||
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
@@ -21,11 +25,9 @@ EXIT_INTERNALERROR = 3
|
||||
EXIT_USAGEERROR = 4
|
||||
EXIT_NOTESTSCOLLECTED = 5
|
||||
|
||||
name_re = re.compile("^[a-zA-Z_]\w*$")
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addini("norecursedirs", "directory patterns to avoid for recursion",
|
||||
type="args", default=['.*', 'CVS', '_darcs', '{arch}', '*.egg'])
|
||||
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",
|
||||
@@ -34,8 +36,8 @@ def pytest_addoption(parser):
|
||||
# "**/test_*.py", "**/*_test.py"]
|
||||
#)
|
||||
group = parser.getgroup("general", "running and selection options")
|
||||
group._addoption('-x', '--exitfirst', action="store_true", default=False,
|
||||
dest="exitfirst",
|
||||
group._addoption('-x', '--exitfirst', action="store_const",
|
||||
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,
|
||||
@@ -44,6 +46,9 @@ def pytest_addoption(parser):
|
||||
help="run pytest in strict mode, warnings become 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.")
|
||||
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.")
|
||||
|
||||
group = parser.getgroup("collect", "collection")
|
||||
group.addoption('--collectonly', '--collect-only', action="store_true",
|
||||
@@ -55,11 +60,14 @@ def pytest_addoption(parser):
|
||||
# when changing this to --conf-cut-dir, config.py Conftest.setinitial
|
||||
# needs upgrading as well
|
||||
group.addoption('--confcutdir', dest="confcutdir", default=None,
|
||||
metavar="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.")
|
||||
group.addoption('--keepduplicates', '--keep-duplicates', action="store_true",
|
||||
dest="keepduplicates", default=False,
|
||||
help="Keep duplicate tests.")
|
||||
|
||||
group = parser.getgroup("debugconfig",
|
||||
"test session debugging and configuration")
|
||||
@@ -71,10 +79,10 @@ def pytest_namespace():
|
||||
collect = dict(Item=Item, Collector=Collector, File=File, Session=Session)
|
||||
return dict(collect=collect)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
pytest.config = config # compatibiltiy
|
||||
if config.option.exitfirst:
|
||||
config.option.maxfail = 1
|
||||
pytest.config = config # compatibility
|
||||
|
||||
|
||||
def wrap_session(config, doit):
|
||||
"""Skeleton command line program"""
|
||||
@@ -91,11 +99,15 @@ def wrap_session(config, doit):
|
||||
except pytest.UsageError:
|
||||
raise
|
||||
except KeyboardInterrupt:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
if initstate < 2 and isinstance(
|
||||
excinfo.value, pytest.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:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
config.notify_exception(excinfo, config.option)
|
||||
session.exitstatus = EXIT_INTERNALERROR
|
||||
if excinfo.errisinstance(SystemExit):
|
||||
@@ -129,20 +141,16 @@ def pytest_collection(session):
|
||||
return session.perform_collect()
|
||||
|
||||
def pytest_runtestloop(session):
|
||||
if (session.testsfailed and
|
||||
not session.config.option.continue_on_collection_errors):
|
||||
raise session.Interrupted(
|
||||
"%d errors during collection" % session.testsfailed)
|
||||
|
||||
if session.config.option.collectonly:
|
||||
return True
|
||||
|
||||
def getnextitem(i):
|
||||
# this is a function to avoid python2
|
||||
# keeping sys.exc_info set when calling into a test
|
||||
# python2 keeps sys.exc_info till the frame is left
|
||||
try:
|
||||
return session.items[i+1]
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
for i, item in enumerate(session.items):
|
||||
nextitem = getnextitem(i)
|
||||
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.shouldstop:
|
||||
raise session.Interrupted(session.shouldstop)
|
||||
@@ -155,7 +163,21 @@ def pytest_ignore_collect(path, config):
|
||||
excludeopt = config.getoption("ignore")
|
||||
if excludeopt:
|
||||
ignore_paths.extend([py.path.local(x) for x in excludeopt])
|
||||
return path in ignore_paths
|
||||
|
||||
if path in ignore_paths:
|
||||
return True
|
||||
|
||||
# Skip duplicate paths.
|
||||
keepduplicates = config.getoption("keepduplicates")
|
||||
duplicate_paths = config.pluginmanager._duplicatepaths
|
||||
if not keepduplicates:
|
||||
if path in duplicate_paths:
|
||||
return True
|
||||
else:
|
||||
duplicate_paths.add(path)
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class FSHookProxy:
|
||||
def __init__(self, fspath, pm, remove_mods):
|
||||
@@ -168,12 +190,22 @@ class FSHookProxy:
|
||||
self.__dict__[name] = x
|
||||
return x
|
||||
|
||||
def compatproperty(name):
|
||||
def fget(self):
|
||||
# deprecated - use pytest.name
|
||||
return getattr(pytest, name)
|
||||
class _CompatProperty(object):
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __get__(self, obj, owner):
|
||||
if obj is None:
|
||||
return self
|
||||
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn(
|
||||
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
|
||||
# name=self.name, owner=type(owner).__name__),
|
||||
# PendingDeprecationWarning, stacklevel=2)
|
||||
return getattr(pytest, self.name)
|
||||
|
||||
|
||||
return property(fget)
|
||||
|
||||
class NodeKeywords(MappingMixin):
|
||||
def __init__(self, node):
|
||||
@@ -245,19 +277,23 @@ class Node(object):
|
||||
""" fspath sensitive hook proxy used to call pytest hooks"""
|
||||
return self.session.gethookproxy(self.fspath)
|
||||
|
||||
Module = compatproperty("Module")
|
||||
Class = compatproperty("Class")
|
||||
Instance = compatproperty("Instance")
|
||||
Function = compatproperty("Function")
|
||||
File = compatproperty("File")
|
||||
Item = compatproperty("Item")
|
||||
Module = _CompatProperty("Module")
|
||||
Class = _CompatProperty("Class")
|
||||
Instance = _CompatProperty("Instance")
|
||||
Function = _CompatProperty("Function")
|
||||
File = _CompatProperty("File")
|
||||
Item = _CompatProperty("Item")
|
||||
|
||||
def _getcustomclass(self, name):
|
||||
cls = getattr(self, name)
|
||||
if cls != getattr(pytest, name):
|
||||
py.log._apiwarn("2.0", "use of node.%s is deprecated, "
|
||||
"use pytest_pycollect_makeitem(...) to create custom "
|
||||
"collection nodes" % name)
|
||||
maybe_compatprop = getattr(type(self), name)
|
||||
if isinstance(maybe_compatprop, _CompatProperty):
|
||||
return getattr(pytest, name)
|
||||
else:
|
||||
cls = getattr(self, name)
|
||||
# TODO: reenable in the features branch
|
||||
# warnings.warn("use of node.%s is deprecated, "
|
||||
# "use pytest_pycollect_makeitem(...) to create custom "
|
||||
# "collection nodes" % name, category=DeprecationWarning)
|
||||
return cls
|
||||
|
||||
def __repr__(self):
|
||||
@@ -272,7 +308,7 @@ class Node(object):
|
||||
if fslocation is None:
|
||||
fslocation = getattr(self, "fspath", None)
|
||||
else:
|
||||
fslocation = "%s:%s" % fslocation[:2]
|
||||
fslocation = "%s:%s" % (fslocation[0], fslocation[1] + 1)
|
||||
|
||||
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
|
||||
code=code, message=message,
|
||||
@@ -388,7 +424,10 @@ class Node(object):
|
||||
if self.config.option.fulltrace:
|
||||
style="long"
|
||||
else:
|
||||
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
|
||||
self._prunetraceback(excinfo)
|
||||
if len(excinfo.traceback) == 0:
|
||||
excinfo.traceback = tb
|
||||
tbfilter = False # prunetraceback already does it
|
||||
if style == "auto":
|
||||
style = "long"
|
||||
@@ -399,7 +438,13 @@ class Node(object):
|
||||
else:
|
||||
style = "long"
|
||||
|
||||
return excinfo.getrepr(funcargs=True,
|
||||
try:
|
||||
os.getcwd()
|
||||
abspath = False
|
||||
except OSError:
|
||||
abspath = True
|
||||
|
||||
return excinfo.getrepr(funcargs=True, abspath=abspath,
|
||||
showlocals=self.config.option.showlocals,
|
||||
style=style, tbfilter=tbfilter)
|
||||
|
||||
@@ -506,7 +551,6 @@ class Session(FSCollector):
|
||||
def __init__(self, config):
|
||||
FSCollector.__init__(self, config.rootdir, parent=None,
|
||||
config=config, session=self)
|
||||
self._fs2hookproxy = {}
|
||||
self.testsfailed = 0
|
||||
self.testscollected = 0
|
||||
self.shouldstop = False
|
||||
@@ -537,23 +581,18 @@ class Session(FSCollector):
|
||||
return path in self._initialpaths
|
||||
|
||||
def gethookproxy(self, fspath):
|
||||
try:
|
||||
return self._fs2hookproxy[fspath]
|
||||
except KeyError:
|
||||
# check if we have the common case of running
|
||||
# hooks with all conftest.py filesall conftest.py
|
||||
pm = self.config.pluginmanager
|
||||
my_conftestmodules = pm._getconftestmodules(fspath)
|
||||
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
||||
if remove_mods:
|
||||
# one or more conftests are not in use at this fspath
|
||||
proxy = FSHookProxy(fspath, pm, remove_mods)
|
||||
else:
|
||||
# all plugis are active for this fspath
|
||||
proxy = self.config.hook
|
||||
|
||||
self._fs2hookproxy[fspath] = proxy
|
||||
return proxy
|
||||
# check if we have the common case of running
|
||||
# hooks with all conftest.py filesall conftest.py
|
||||
pm = self.config.pluginmanager
|
||||
my_conftestmodules = pm._getconftestmodules(fspath)
|
||||
remove_mods = pm._conftest_plugins.difference(my_conftestmodules)
|
||||
if remove_mods:
|
||||
# one or more conftests are not in use at this fspath
|
||||
proxy = FSHookProxy(fspath, pm, remove_mods)
|
||||
else:
|
||||
# all plugis are active for this fspath
|
||||
proxy = self.config.hook
|
||||
return proxy
|
||||
|
||||
def perform_collect(self, args=None, genitems=True):
|
||||
hook = self.config.hook
|
||||
@@ -645,44 +684,39 @@ class Session(FSCollector):
|
||||
return True
|
||||
|
||||
def _tryconvertpyarg(self, x):
|
||||
mod = None
|
||||
path = [os.path.abspath('.')] + sys.path
|
||||
for name in x.split('.'):
|
||||
# ignore anything that's not a proper name here
|
||||
# else something like --pyargs will mess up '.'
|
||||
# since imp.find_module will actually sometimes work for it
|
||||
# but it's supposed to be considered a filesystem path
|
||||
# not a package
|
||||
if name_re.match(name) is None:
|
||||
return x
|
||||
try:
|
||||
fd, mod, type_ = imp.find_module(name, path)
|
||||
except ImportError:
|
||||
return x
|
||||
else:
|
||||
if fd is not None:
|
||||
fd.close()
|
||||
"""Convert a dotted module name to path.
|
||||
|
||||
if type_[2] != imp.PKG_DIRECTORY:
|
||||
path = [os.path.dirname(mod)]
|
||||
else:
|
||||
path = [mod]
|
||||
return mod
|
||||
"""
|
||||
import pkgutil
|
||||
try:
|
||||
loader = pkgutil.find_loader(x)
|
||||
except ImportError:
|
||||
return x
|
||||
if loader is None:
|
||||
return x
|
||||
# This method is sometimes invoked when AssertionRewritingHook, which
|
||||
# does not define a get_filename method, is already in place:
|
||||
try:
|
||||
path = loader.get_filename(x)
|
||||
except AttributeError:
|
||||
# Retrieve path from AssertionRewritingHook:
|
||||
path = loader.modules[x][0].co_filename
|
||||
if loader.is_package(x):
|
||||
path = os.path.dirname(path)
|
||||
return path
|
||||
|
||||
def _parsearg(self, arg):
|
||||
""" return (fspath, names) tuple after checking the file exists. """
|
||||
arg = str(arg)
|
||||
if self.config.option.pyargs:
|
||||
arg = self._tryconvertpyarg(arg)
|
||||
parts = str(arg).split("::")
|
||||
if self.config.option.pyargs:
|
||||
parts[0] = self._tryconvertpyarg(parts[0])
|
||||
relpath = parts[0].replace("/", os.sep)
|
||||
path = self.config.invocation_dir.join(relpath, abs=True)
|
||||
if not path.check():
|
||||
if self.config.option.pyargs:
|
||||
msg = "file or package not found: "
|
||||
raise pytest.UsageError("file or package not found: " + arg + " (missing __init__.py?)")
|
||||
else:
|
||||
msg = "file not found: "
|
||||
raise pytest.UsageError(msg + arg)
|
||||
raise pytest.UsageError("file not found: " + arg)
|
||||
parts[0] = path
|
||||
return parts
|
||||
|
||||
@@ -714,7 +748,8 @@ class Session(FSCollector):
|
||||
if rep.passed:
|
||||
has_matched = False
|
||||
for x in rep.result:
|
||||
if x.name == name:
|
||||
# TODO: remove parametrized workaround once collection structure contains parametrization
|
||||
if x.name == name or x.name.split("[")[0] == name:
|
||||
resultnodes.extend(self.matchnodes([x], nextnames))
|
||||
has_matched = True
|
||||
# XXX accept IDs that don't have "()" for class instances
|
||||
|
||||
@@ -19,7 +19,7 @@ def pytest_addoption(parser):
|
||||
help="only run tests which match the given substring expression. "
|
||||
"An expression is a python evaluatable expression "
|
||||
"where all names are substring-matched against test names "
|
||||
"and their parent classes. Example: -k 'test_method or test "
|
||||
"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'. "
|
||||
"Additionally keywords are matched to classes and functions "
|
||||
@@ -54,17 +54,19 @@ def pytest_cmdline_main(config):
|
||||
tw.line()
|
||||
config._ensure_unconfigure()
|
||||
return 0
|
||||
|
||||
|
||||
pytest_cmdline_main.tryfirst = True
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(items, config):
|
||||
keywordexpr = config.option.keyword
|
||||
keywordexpr = config.option.keyword.lstrip()
|
||||
matchexpr = config.option.markexpr
|
||||
if not keywordexpr and not matchexpr:
|
||||
return
|
||||
# pytest used to allow "-" for negating
|
||||
# but today we just allow "-" at the beginning, use "not" instead
|
||||
# we probably remove "-" alltogether soon
|
||||
# we probably remove "-" altogether soon
|
||||
if keywordexpr.startswith("-"):
|
||||
keywordexpr = "not " + keywordexpr[1:]
|
||||
selectuntil = False
|
||||
@@ -169,7 +171,7 @@ class MarkGenerator:
|
||||
""" Factory for :class:`MarkDecorator` objects - exposed as
|
||||
a ``pytest.mark`` singleton instance. Example::
|
||||
|
||||
import py
|
||||
import pytest
|
||||
@pytest.mark.slowtest
|
||||
def test_function():
|
||||
pass
|
||||
@@ -283,6 +285,21 @@ class MarkDecorator:
|
||||
return self.__class__(self.name, args=args, kwargs=kw)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class MarkInfo:
|
||||
""" Marking object created by :class:`MarkDecorator` instances. """
|
||||
def __init__(self, name, args, kwargs):
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
""" monkeypatching and mocking functionality. """
|
||||
|
||||
import os, sys
|
||||
import re
|
||||
|
||||
from py.builtin import _basestring
|
||||
|
||||
def pytest_funcarg__monkeypatch(request):
|
||||
"""The returned ``monkeypatch`` funcarg provides these
|
||||
import pytest
|
||||
|
||||
RE_IMPORT_ERROR_NAME = re.compile("^No module named (.*)$")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def monkeypatch():
|
||||
"""The returned ``monkeypatch`` fixture provides these
|
||||
helper methods to modify objects, dictionaries or os.environ::
|
||||
|
||||
monkeypatch.setattr(obj, name, value, raising=True)
|
||||
@@ -17,57 +25,81 @@ def pytest_funcarg__monkeypatch(request):
|
||||
monkeypatch.chdir(path)
|
||||
|
||||
All modifications will be undone after the requesting
|
||||
test function has finished. The ``raising``
|
||||
test function or fixture has finished. The ``raising``
|
||||
parameter determines if a KeyError or AttributeError
|
||||
will be raised if the set/deletion operation has no target.
|
||||
"""
|
||||
mpatch = monkeypatch()
|
||||
request.addfinalizer(mpatch.undo)
|
||||
return mpatch
|
||||
mpatch = MonkeyPatch()
|
||||
yield mpatch
|
||||
mpatch.undo()
|
||||
|
||||
|
||||
def resolve(name):
|
||||
# simplified from zope.dottedname
|
||||
parts = name.split('.')
|
||||
|
||||
used = parts.pop(0)
|
||||
found = __import__(used)
|
||||
for part in parts:
|
||||
used += '.' + part
|
||||
try:
|
||||
found = getattr(found, part)
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
continue
|
||||
# we use explicit un-nesting of the handling block in order
|
||||
# to avoid nested exceptions on python 3
|
||||
try:
|
||||
__import__(used)
|
||||
except ImportError as ex:
|
||||
# str is used for py2 vs py3
|
||||
expected = str(ex).split()[-1]
|
||||
if expected == used:
|
||||
raise
|
||||
else:
|
||||
raise ImportError(
|
||||
'import error in %s: %s' % (used, ex)
|
||||
)
|
||||
found = annotated_getattr(found, part, used)
|
||||
return found
|
||||
|
||||
|
||||
def annotated_getattr(obj, name, ann):
|
||||
try:
|
||||
obj = getattr(obj, name)
|
||||
except AttributeError:
|
||||
raise AttributeError(
|
||||
'%r object at %s has no attribute %r' % (
|
||||
type(obj).__name__, ann, name
|
||||
)
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
def derive_importpath(import_path, raising):
|
||||
import pytest
|
||||
if not isinstance(import_path, _basestring) or "." not in import_path:
|
||||
raise TypeError("must be absolute import path string, not %r" %
|
||||
(import_path,))
|
||||
rest = []
|
||||
target = import_path
|
||||
while target:
|
||||
try:
|
||||
obj = __import__(target, None, None, "__doc__")
|
||||
except ImportError:
|
||||
if "." not in target:
|
||||
__tracebackhide__ = True
|
||||
pytest.fail("could not import any sub part: %s" %
|
||||
import_path)
|
||||
target, name = target.rsplit(".", 1)
|
||||
rest.append(name)
|
||||
else:
|
||||
assert rest
|
||||
try:
|
||||
while len(rest) > 1:
|
||||
attr = rest.pop()
|
||||
obj = getattr(obj, attr)
|
||||
attr = rest[0]
|
||||
if raising:
|
||||
getattr(obj, attr)
|
||||
except AttributeError:
|
||||
__tracebackhide__ = True
|
||||
pytest.fail("object %r has no attribute %r" % (obj, attr))
|
||||
return attr, obj
|
||||
|
||||
module, attr = import_path.rsplit('.', 1)
|
||||
target = resolve(module)
|
||||
if raising:
|
||||
annotated_getattr(target, attr, ann=module)
|
||||
return attr, target
|
||||
|
||||
|
||||
class Notset:
|
||||
def __repr__(self):
|
||||
return "<notset>"
|
||||
|
||||
|
||||
notset = Notset()
|
||||
|
||||
class monkeypatch:
|
||||
""" Object keeping a record of setattr/item/env/syspath changes. """
|
||||
|
||||
class MonkeyPatch:
|
||||
""" Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._setattr = []
|
||||
self._setitem = []
|
||||
@@ -94,19 +126,19 @@ class monkeypatch:
|
||||
if value is notset:
|
||||
if not isinstance(target, _basestring):
|
||||
raise TypeError("use setattr(target, name, value) or "
|
||||
"setattr(target, value) with target being a dotted "
|
||||
"import string")
|
||||
"setattr(target, value) with target being a dotted "
|
||||
"import string")
|
||||
value = name
|
||||
name, target = derive_importpath(target, raising)
|
||||
|
||||
oldval = getattr(target, name, notset)
|
||||
if raising and oldval is notset:
|
||||
raise AttributeError("%r has no attribute %r" %(target, name))
|
||||
raise AttributeError("%r has no attribute %r" % (target, name))
|
||||
|
||||
# avoid class descriptors like staticmethod/classmethod
|
||||
if inspect.isclass(target):
|
||||
oldval = target.__dict__.get(name, notset)
|
||||
self._setattr.insert(0, (target, name, oldval))
|
||||
self._setattr.append((target, name, oldval))
|
||||
setattr(target, name, value)
|
||||
|
||||
def delattr(self, target, name=notset, raising=True):
|
||||
@@ -132,13 +164,12 @@ class monkeypatch:
|
||||
if raising:
|
||||
raise AttributeError(name)
|
||||
else:
|
||||
self._setattr.insert(0, (target, name,
|
||||
getattr(target, name, notset)))
|
||||
self._setattr.append((target, name, getattr(target, name, notset)))
|
||||
delattr(target, name)
|
||||
|
||||
def setitem(self, dic, name, value):
|
||||
""" Set dictionary entry ``name`` to value. """
|
||||
self._setitem.insert(0, (dic, name, dic.get(name, notset)))
|
||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||
dic[name] = value
|
||||
|
||||
def delitem(self, dic, name, raising=True):
|
||||
@@ -151,7 +182,7 @@ class monkeypatch:
|
||||
if raising:
|
||||
raise KeyError(name)
|
||||
else:
|
||||
self._setitem.insert(0, (dic, name, dic.get(name, notset)))
|
||||
self._setitem.append((dic, name, dic.get(name, notset)))
|
||||
del dic[name]
|
||||
|
||||
def setenv(self, name, value, prepend=None):
|
||||
@@ -193,28 +224,28 @@ class monkeypatch:
|
||||
""" Undo previous changes. This call consumes the
|
||||
undo stack. Calling it a second time has no effect unless
|
||||
you do more monkeypatching after the undo call.
|
||||
|
||||
|
||||
There is generally no need to call `undo()`, since it is
|
||||
called automatically during tear-down.
|
||||
|
||||
|
||||
Note that the same `monkeypatch` fixture is used across a
|
||||
single test function invocation. If `monkeypatch` is used both by
|
||||
the test function itself and one of the test fixtures,
|
||||
calling `undo()` will undo all of the changes made in
|
||||
both functions.
|
||||
"""
|
||||
for obj, name, value in self._setattr:
|
||||
for obj, name, value in reversed(self._setattr):
|
||||
if value is not notset:
|
||||
setattr(obj, name, value)
|
||||
else:
|
||||
delattr(obj, name)
|
||||
self._setattr[:] = []
|
||||
for dictionary, name, value in self._setitem:
|
||||
for dictionary, name, value in reversed(self._setitem):
|
||||
if value is notset:
|
||||
try:
|
||||
del dictionary[name]
|
||||
except KeyError:
|
||||
pass # was already deleted, so we have the desired state
|
||||
pass # was already deleted, so we have the desired state
|
||||
else:
|
||||
dictionary[name] = value
|
||||
self._setitem[:] = []
|
||||
|
||||
@@ -11,21 +11,29 @@ def pytest_addoption(parser):
|
||||
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;
|
||||
# this can happen when this function executes in a slave node
|
||||
# when using pytest-xdist, for example
|
||||
if tr is not None:
|
||||
config._pastebinfile = tempfile.TemporaryFile('w+')
|
||||
# pastebin file will be utf-8 encoded binary file
|
||||
config._pastebinfile = tempfile.TemporaryFile('w+b')
|
||||
oldwrite = tr._tw.write
|
||||
|
||||
def tee_write(s, **kwargs):
|
||||
oldwrite(s, **kwargs)
|
||||
config._pastebinfile.write(str(s))
|
||||
if py.builtin._istext(s):
|
||||
s = s.encode('utf-8')
|
||||
config._pastebinfile.write(s)
|
||||
|
||||
tr._tw.write = tee_write
|
||||
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
if hasattr(config, '_pastebinfile'):
|
||||
# get terminal contents and delete file
|
||||
@@ -41,11 +49,12 @@ def pytest_unconfigure(config):
|
||||
pastebinurl = create_new_paste(sessionlog)
|
||||
tr.write_line("pastebin session-log: %s\n" % pastebinurl)
|
||||
|
||||
|
||||
def create_new_paste(contents):
|
||||
"""
|
||||
Creates a new paste using bpaste.net service.
|
||||
|
||||
:contents: paste contents
|
||||
:contents: paste contents as utf-8 encoded bytes
|
||||
:returns: url to the pasted contents
|
||||
"""
|
||||
import re
|
||||
@@ -61,13 +70,14 @@ def create_new_paste(contents):
|
||||
'expiry': '1week',
|
||||
}
|
||||
url = 'https://bpaste.net'
|
||||
response = urlopen(url, data=urlencode(params)).read()
|
||||
m = re.search(r'href="/raw/(\w+)"', response)
|
||||
response = urlopen(url, data=urlencode(params).encode('ascii')).read()
|
||||
m = re.search(r'href="/raw/(\w+)"', response.decode('utf-8'))
|
||||
if m:
|
||||
return '%s/show/%s' % (url, m.group(1))
|
||||
else:
|
||||
return 'bad response: ' + response
|
||||
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
import _pytest.config
|
||||
if terminalreporter.config.option.pastebin != "failed":
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
""" (disabled by default) support for testing pytest and pytest plugins. """
|
||||
import gc
|
||||
import sys
|
||||
import traceback
|
||||
import os
|
||||
import codecs
|
||||
import re
|
||||
import time
|
||||
import gc
|
||||
import os
|
||||
import platform
|
||||
from fnmatch import fnmatch
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
from fnmatch import fnmatch
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from py.builtin import print_
|
||||
|
||||
from _pytest._code import Source
|
||||
import py
|
||||
import pytest
|
||||
from _pytest.main import Session, EXIT_OK
|
||||
from _pytest.assertion.rewrite import AssertionRewritingHook
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -122,15 +124,18 @@ def getexecutable(name, cache={}):
|
||||
except KeyError:
|
||||
executable = py.path.local.sysfind(name)
|
||||
if executable:
|
||||
import subprocess
|
||||
popen = subprocess.Popen([str(executable), "--version"],
|
||||
universal_newlines=True, stderr=subprocess.PIPE)
|
||||
out, err = popen.communicate()
|
||||
if name == "jython":
|
||||
import subprocess
|
||||
popen = subprocess.Popen([str(executable), "--version"],
|
||||
universal_newlines=True, stderr=subprocess.PIPE)
|
||||
out, err = popen.communicate()
|
||||
if not err or "2.5" not in err:
|
||||
executable = None
|
||||
if "2.5.2" in err:
|
||||
executable = None # http://bugs.jython.org/issue1790
|
||||
elif popen.returncode != 0:
|
||||
# Handle pyenv's 127.
|
||||
executable = None
|
||||
cache[name] = executable
|
||||
return executable
|
||||
|
||||
@@ -317,7 +322,8 @@ def linecomp(request):
|
||||
return LineComp()
|
||||
|
||||
|
||||
def pytest_funcarg__LineMatcher(request):
|
||||
@pytest.fixture(name='LineMatcher')
|
||||
def LineMatcher_fixture(request):
|
||||
return LineMatcher
|
||||
|
||||
|
||||
@@ -326,7 +332,7 @@ def testdir(request, tmpdir_factory):
|
||||
return Testdir(request, tmpdir_factory)
|
||||
|
||||
|
||||
rex_outcome = re.compile("(\d+) ([\w-]+)")
|
||||
rex_outcome = re.compile(r"(\d+) ([\w-]+)")
|
||||
class RunResult:
|
||||
"""The result of running a command.
|
||||
|
||||
@@ -361,6 +367,7 @@ class RunResult:
|
||||
for num, cat in outcomes:
|
||||
d[cat] = int(num)
|
||||
return d
|
||||
raise ValueError("Pytest terminal report not found")
|
||||
|
||||
def assert_outcomes(self, passed=0, skipped=0, failed=0):
|
||||
""" assert that the specified outcomes appear with the respective
|
||||
@@ -373,10 +380,10 @@ class RunResult:
|
||||
|
||||
|
||||
class Testdir:
|
||||
"""Temporary test directory with tools to test/run py.test itself.
|
||||
"""Temporary test directory with tools to test/run pytest itself.
|
||||
|
||||
This is based on the ``tmpdir`` fixture but provides a number of
|
||||
methods which aid with testing py.test itself. Unless
|
||||
methods which aid with testing pytest itself. Unless
|
||||
:py:meth:`chdir` is used all methods will use :py:attr:`tmpdir` as
|
||||
current working directory.
|
||||
|
||||
@@ -440,9 +447,9 @@ class Testdir:
|
||||
the module is re-imported.
|
||||
"""
|
||||
for name in set(sys.modules).difference(self._savemodulekeys):
|
||||
# it seems zope.interfaces is keeping some state
|
||||
# (used by twisted related tests)
|
||||
if name != "zope.interface":
|
||||
# zope.interface (used by twisted-related tests) keeps internal
|
||||
# state and can't be deleted
|
||||
if not name.startswith("zope.interface"):
|
||||
del sys.modules[name]
|
||||
|
||||
def make_hook_recorder(self, pluginmanager):
|
||||
@@ -472,11 +479,14 @@ class Testdir:
|
||||
ret = None
|
||||
for name, value in items:
|
||||
p = self.tmpdir.join(name).new(ext=ext)
|
||||
source = py.code.Source(value)
|
||||
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"
|
||||
@@ -556,7 +566,7 @@ class Testdir:
|
||||
def mkpydir(self, name):
|
||||
"""Create a new python package.
|
||||
|
||||
This creates a (sub)direcotry with an empty ``__init__.py``
|
||||
This creates a (sub)directory with an empty ``__init__.py``
|
||||
file so that is recognised as a python package.
|
||||
|
||||
"""
|
||||
@@ -587,7 +597,7 @@ class Testdir:
|
||||
"""Return the collection node of a file.
|
||||
|
||||
This is like :py:meth:`getnode` but uses
|
||||
:py:meth:`parseconfigure` to create the (configured) py.test
|
||||
:py:meth:`parseconfigure` to create the (configured) pytest
|
||||
Config instance.
|
||||
|
||||
:param path: A :py:class:`py.path.local` instance of the file.
|
||||
@@ -651,11 +661,11 @@ class Testdir:
|
||||
def inline_genitems(self, *args):
|
||||
"""Run ``pytest.main(['--collectonly'])`` in-process.
|
||||
|
||||
Retuns a tuple of the collected items and a
|
||||
Returns a tuple of the collected items and a
|
||||
:py:class:`HookRecorder` instance.
|
||||
|
||||
This runs the :py:func:`pytest.main` function to run all of
|
||||
py.test inside the test process itself like
|
||||
pytest inside the test process itself like
|
||||
:py:meth:`inline_run`. However the return value is a tuple of
|
||||
the collection items and a :py:class:`HookRecorder` instance.
|
||||
|
||||
@@ -668,7 +678,7 @@ class Testdir:
|
||||
"""Run ``pytest.main()`` in-process, returning a HookRecorder.
|
||||
|
||||
This runs the :py:func:`pytest.main` function to run all of
|
||||
py.test inside the test process itself. This means it can
|
||||
pytest inside the test process itself. This means it can
|
||||
return a :py:class:`HookRecorder` instance which gives more
|
||||
detailed results from then run then can be done by matching
|
||||
stdout/stderr from :py:meth:`runpytest`.
|
||||
@@ -680,9 +690,21 @@ class Testdir:
|
||||
``pytest.main()`` instance should use.
|
||||
|
||||
:return: A :py:class:`HookRecorder` instance.
|
||||
|
||||
"""
|
||||
# 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.
|
||||
orig_warn = AssertionRewritingHook._warn_already_imported
|
||||
|
||||
def revert():
|
||||
AssertionRewritingHook._warn_already_imported = orig_warn
|
||||
|
||||
self.request.addfinalizer(revert)
|
||||
AssertionRewritingHook._warn_already_imported = lambda *a: None
|
||||
|
||||
rec = []
|
||||
|
||||
class Collect:
|
||||
def pytest_configure(x, config):
|
||||
rec.append(self.make_hook_recorder(config.pluginmanager))
|
||||
@@ -717,10 +739,13 @@ class Testdir:
|
||||
try:
|
||||
reprec = self.inline_run(*args, **kwargs)
|
||||
except SystemExit as e:
|
||||
|
||||
class reprec:
|
||||
ret = e.args[0]
|
||||
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
class reprec:
|
||||
ret = 3
|
||||
finally:
|
||||
@@ -754,9 +779,9 @@ class Testdir:
|
||||
return args
|
||||
|
||||
def parseconfig(self, *args):
|
||||
"""Return a new py.test Config instance from given commandline args.
|
||||
"""Return a new pytest Config instance from given commandline args.
|
||||
|
||||
This invokes the py.test bootstrapping code in _pytest.config
|
||||
This invokes the pytest bootstrapping code in _pytest.config
|
||||
to create a new :py:class:`_pytest.core.PluginManager` and
|
||||
call the pytest_cmdline_parse hook to create new
|
||||
:py:class:`_pytest.config.Config` instance.
|
||||
@@ -776,7 +801,7 @@ class Testdir:
|
||||
return config
|
||||
|
||||
def parseconfigure(self, *args):
|
||||
"""Return a new py.test configured Config instance.
|
||||
"""Return a new pytest configured Config instance.
|
||||
|
||||
This returns a new :py:class:`_pytest.config.Config` instance
|
||||
like :py:meth:`parseconfig`, but also calls the
|
||||
@@ -791,7 +816,7 @@ class Testdir:
|
||||
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 py.test's
|
||||
This writes the source to a python file and runs pytest's
|
||||
collection on the resulting module, returning the test item
|
||||
for the requested function name.
|
||||
|
||||
@@ -811,7 +836,7 @@ class Testdir:
|
||||
def getitems(self, source):
|
||||
"""Return all test items collected from the module.
|
||||
|
||||
This writes the source to a python file and runs py.test's
|
||||
This writes the source to a python file and runs pytest's
|
||||
collection on the resulting module, returning all test items
|
||||
contained within.
|
||||
|
||||
@@ -823,7 +848,7 @@ class Testdir:
|
||||
"""Return the module collection node for ``source``.
|
||||
|
||||
This writes ``source`` to a file using :py:meth:`makepyfile`
|
||||
and then runs the py.test collection on it, returning the
|
||||
and then runs the pytest collection on it, returning the
|
||||
collection node for the test module.
|
||||
|
||||
:param source: The source code of the module to collect.
|
||||
@@ -832,10 +857,10 @@ class Testdir:
|
||||
:py:meth:`parseconfigure`.
|
||||
|
||||
:param withinit: Whether to also write a ``__init__.py`` file
|
||||
to the temporarly directory to ensure it is a package.
|
||||
to the temporary directory to ensure it is a package.
|
||||
|
||||
"""
|
||||
kw = {self.request.function.__name__: py.code.Source(source).strip()}
|
||||
kw = {self.request.function.__name__: Source(source).strip()}
|
||||
path = self.makepyfile(**kw)
|
||||
if withinit:
|
||||
self.makepyfile(__init__ = "#")
|
||||
@@ -923,7 +948,7 @@ class Testdir:
|
||||
|
||||
def _getpytestargs(self):
|
||||
# we cannot use "(sys.executable,script)"
|
||||
# because on windows the script is e.g. a py.test.exe
|
||||
# because on windows the script is e.g. a pytest.exe
|
||||
return (sys.executable, _pytest_fullpath,) # noqa
|
||||
|
||||
def runpython(self, script):
|
||||
@@ -938,7 +963,7 @@ class Testdir:
|
||||
return self.run(sys.executable, "-c", command)
|
||||
|
||||
def runpytest_subprocess(self, *args, **kwargs):
|
||||
"""Run py.test as a subprocess with given arguments.
|
||||
"""Run pytest as a subprocess with given arguments.
|
||||
|
||||
Any plugins added to the :py:attr:`plugins` list will added
|
||||
using the ``-p`` command line option. Addtionally
|
||||
@@ -966,9 +991,9 @@ class Testdir:
|
||||
return self.run(*args)
|
||||
|
||||
def spawn_pytest(self, string, expect_timeout=10.0):
|
||||
"""Run py.test using pexpect.
|
||||
"""Run pytest using pexpect.
|
||||
|
||||
This makes sure to use the right py.test and sets up the
|
||||
This makes sure to use the right pytest and sets up the
|
||||
temporary directory locations.
|
||||
|
||||
The pexpect child is returned.
|
||||
@@ -987,8 +1012,6 @@ class Testdir:
|
||||
pexpect = pytest.importorskip("pexpect", "3.0")
|
||||
if hasattr(sys, 'pypy_version_info') and '64' in platform.machine():
|
||||
pytest.skip("pypy-64 bit not supported")
|
||||
if sys.platform == "darwin":
|
||||
pytest.xfail("pexpect does not work reliably on darwin?!")
|
||||
if sys.platform.startswith("freebsd"):
|
||||
pytest.xfail("pexpect does not work reliably on freebsd")
|
||||
logfile = self.tmpdir.join("spawn.out").open("wb")
|
||||
@@ -1034,6 +1057,7 @@ class LineMatcher:
|
||||
|
||||
def __init__(self, lines):
|
||||
self.lines = lines
|
||||
self._log_output = []
|
||||
|
||||
def str(self):
|
||||
"""Return the entire original text."""
|
||||
@@ -1041,8 +1065,8 @@ class LineMatcher:
|
||||
|
||||
def _getlines(self, lines2):
|
||||
if isinstance(lines2, str):
|
||||
lines2 = py.code.Source(lines2)
|
||||
if isinstance(lines2, py.code.Source):
|
||||
lines2 = Source(lines2)
|
||||
if isinstance(lines2, Source):
|
||||
lines2 = lines2.strip().lines
|
||||
return lines2
|
||||
|
||||
@@ -1057,10 +1081,11 @@ class LineMatcher:
|
||||
for line in lines2:
|
||||
for x in self.lines:
|
||||
if line == x or fnmatch(x, line):
|
||||
print_("matched: ", repr(line))
|
||||
self._log("matched: ", repr(line))
|
||||
break
|
||||
else:
|
||||
raise ValueError("line %r not found in output" % line)
|
||||
self._log("line %r not found in output" % line)
|
||||
raise ValueError(self._log_text)
|
||||
|
||||
def get_lines_after(self, fnline):
|
||||
"""Return all lines following the given line in the text.
|
||||
@@ -1072,6 +1097,13 @@ class LineMatcher:
|
||||
return self.lines[i+1:]
|
||||
raise ValueError("line %r not found in output" % fnline)
|
||||
|
||||
def _log(self, *args):
|
||||
self._log_output.append(' '.join((str(x) for x in args)))
|
||||
|
||||
@property
|
||||
def _log_text(self):
|
||||
return '\n'.join(self._log_output)
|
||||
|
||||
def fnmatch_lines(self, lines2):
|
||||
"""Search the text for matching lines.
|
||||
|
||||
@@ -1081,8 +1113,6 @@ class LineMatcher:
|
||||
stdout.
|
||||
|
||||
"""
|
||||
def show(arg1, arg2):
|
||||
py.builtin.print_(arg1, arg2, file=sys.stderr)
|
||||
lines2 = self._getlines(lines2)
|
||||
lines1 = self.lines[:]
|
||||
nextline = None
|
||||
@@ -1093,17 +1123,18 @@ class LineMatcher:
|
||||
while lines1:
|
||||
nextline = lines1.pop(0)
|
||||
if line == nextline:
|
||||
show("exact match:", repr(line))
|
||||
self._log("exact match:", repr(line))
|
||||
break
|
||||
elif fnmatch(nextline, line):
|
||||
show("fnmatch:", repr(line))
|
||||
show(" with:", repr(nextline))
|
||||
self._log("fnmatch:", repr(line))
|
||||
self._log(" with:", repr(nextline))
|
||||
break
|
||||
else:
|
||||
if not nomatchprinted:
|
||||
show("nomatch:", repr(line))
|
||||
self._log("nomatch:", repr(line))
|
||||
nomatchprinted = True
|
||||
show(" and:", repr(nextline))
|
||||
self._log(" and:", repr(nextline))
|
||||
extralines.append(nextline)
|
||||
else:
|
||||
pytest.fail("remains unmatched: %r, see stderr" % (line,))
|
||||
self._log("remains unmatched: %r" % (line,))
|
||||
pytest.fail(self._log_text)
|
||||
|
||||
2023
_pytest/python.py
2023
_pytest/python.py
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,8 @@
|
||||
""" recording warnings during test function execution. """
|
||||
|
||||
import inspect
|
||||
|
||||
import _pytest._code
|
||||
import py
|
||||
import sys
|
||||
import warnings
|
||||
@@ -28,19 +30,53 @@ def pytest_namespace():
|
||||
'warns': warns}
|
||||
|
||||
|
||||
def deprecated_call(func, *args, **kwargs):
|
||||
"""Assert that ``func(*args, **kwargs)`` triggers a DeprecationWarning.
|
||||
"""
|
||||
wrec = WarningsRecorder()
|
||||
with wrec:
|
||||
warnings.simplefilter('always') # ensure all warnings are triggered
|
||||
ret = func(*args, **kwargs)
|
||||
def deprecated_call(func=None, *args, **kwargs):
|
||||
""" assert that calling ``func(*args, **kwargs)`` triggers a
|
||||
``DeprecationWarning`` or ``PendingDeprecationWarning``.
|
||||
|
||||
depwarnings = (DeprecationWarning, PendingDeprecationWarning)
|
||||
if not any(r.category in depwarnings for r in wrec):
|
||||
This function can be used as a context manager::
|
||||
|
||||
>>> import warnings
|
||||
>>> def api_call_v2():
|
||||
... warnings.warn('use v3 of this api', DeprecationWarning)
|
||||
... return 200
|
||||
|
||||
>>> 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.
|
||||
"""
|
||||
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):
|
||||
__tracebackhide__ = True
|
||||
raise AssertionError("%r did not produce DeprecationWarning" % (func,))
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
@@ -71,7 +107,7 @@ def warns(expected_warning, *args, **kwargs):
|
||||
loc.update(kwargs)
|
||||
|
||||
with wcheck:
|
||||
code = py.code.Source(code).compile()
|
||||
code = _pytest._code.Source(code).compile()
|
||||
py.builtin.exec_(code, frame.f_globals, loc)
|
||||
else:
|
||||
func = args[0]
|
||||
@@ -150,8 +186,8 @@ class WarningsRecorder(object):
|
||||
self._module.showwarning = showwarning
|
||||
|
||||
# allow the same warning to be raised more than once
|
||||
self._module.simplefilter('always', append=True)
|
||||
|
||||
self._module.simplefilter('always')
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc_info):
|
||||
@@ -187,4 +223,7 @@ class WarningsChecker(WarningsRecorder):
|
||||
if self.expected_warning is not None:
|
||||
if not any(r.category in self.expected_warning for r in self):
|
||||
__tracebackhide__ = True
|
||||
pytest.fail("DID NOT WARN")
|
||||
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]))
|
||||
|
||||
@@ -9,7 +9,7 @@ def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "resultlog plugin options")
|
||||
group.addoption('--resultlog', '--result-log', action="store",
|
||||
metavar="path", default=None,
|
||||
help="path for machine-readable result log.")
|
||||
help="DEPRECATED path for machine-readable result log.")
|
||||
|
||||
def pytest_configure(config):
|
||||
resultlog = config.option.resultlog
|
||||
@@ -22,6 +22,9 @@ def pytest_configure(config):
|
||||
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:
|
||||
|
||||
@@ -5,7 +5,8 @@ from time import time
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from py._code.code import TerminalRepr
|
||||
from _pytest._code.code import TerminalRepr, ExceptionInfo
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return {
|
||||
@@ -72,7 +73,10 @@ def runtestprotocol(item, log=True, nextitem=None):
|
||||
rep = call_and_report(item, "setup", log)
|
||||
reports = [rep]
|
||||
if rep.passed:
|
||||
reports.append(call_and_report(item, "call", log))
|
||||
if item.config.option.setupshow:
|
||||
show_test_item(item)
|
||||
if not item.config.option.setuponly:
|
||||
reports.append(call_and_report(item, "call", log))
|
||||
reports.append(call_and_report(item, "teardown", log,
|
||||
nextitem=nextitem))
|
||||
# after all teardown hooks have been called
|
||||
@@ -82,6 +86,16 @@ 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()
|
||||
tw.line()
|
||||
tw.write(' ' * 8)
|
||||
tw.write(item._nodeid)
|
||||
used_fixtures = sorted(item._fixtureinfo.name2fixturedefs.keys())
|
||||
if used_fixtures:
|
||||
tw.write(' (fixtures used: {0})'.format(', '.join(used_fixtures)))
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
item.session._setupstate.prepare(item)
|
||||
|
||||
@@ -151,7 +165,7 @@ class CallInfo:
|
||||
self.stop = time()
|
||||
raise
|
||||
except:
|
||||
self.excinfo = py.code.ExceptionInfo()
|
||||
self.excinfo = ExceptionInfo()
|
||||
self.stop = time()
|
||||
|
||||
def __repr__(self):
|
||||
@@ -177,9 +191,13 @@ class BaseReport(object):
|
||||
self.__dict__.update(kw)
|
||||
|
||||
def toterminal(self, out):
|
||||
longrepr = self.longrepr
|
||||
if hasattr(self, 'node'):
|
||||
out.line(getslaveinfoline(self.node))
|
||||
|
||||
longrepr = self.longrepr
|
||||
if longrepr is None:
|
||||
return
|
||||
|
||||
if hasattr(longrepr, 'toterminal'):
|
||||
longrepr.toterminal(out)
|
||||
else:
|
||||
@@ -193,6 +211,36 @@ class BaseReport(object):
|
||||
if name.startswith(prefix):
|
||||
yield prefix, content
|
||||
|
||||
@property
|
||||
def longreprtext(self):
|
||||
"""
|
||||
Read-only property that returns the full string representation
|
||||
of ``longrepr``.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
tw = py.io.TerminalWriter(stringio=True)
|
||||
tw.hasmarkup = False
|
||||
self.toterminal(tw)
|
||||
exc = tw.stringio.getvalue()
|
||||
return exc.strip()
|
||||
|
||||
@property
|
||||
def capstdout(self):
|
||||
"""Return captured text from stdout, if capturing is enabled
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return ''.join(content for (prefix, content) in self.get_sections('Captured stdout'))
|
||||
|
||||
@property
|
||||
def capstderr(self):
|
||||
"""Return captured text from stderr, if capturing is enabled
|
||||
|
||||
.. versionadded:: 3.0
|
||||
"""
|
||||
return ''.join(content for (prefix, content) in self.get_sections('Captured stderr'))
|
||||
|
||||
passed = property(lambda x: x.outcome == "passed")
|
||||
failed = property(lambda x: x.outcome == "failed")
|
||||
skipped = property(lambda x: x.outcome == "skipped")
|
||||
@@ -211,7 +259,7 @@ def pytest_runtest_makereport(item, call):
|
||||
outcome = "passed"
|
||||
longrepr = None
|
||||
else:
|
||||
if not isinstance(excinfo, py.code.ExceptionInfo):
|
||||
if not isinstance(excinfo, ExceptionInfo):
|
||||
outcome = "failed"
|
||||
longrepr = excinfo
|
||||
elif excinfo.errisinstance(pytest.skip.Exception):
|
||||
@@ -258,8 +306,10 @@ class TestReport(BaseReport):
|
||||
#: one of 'setup', 'call', 'teardown' to indicate runtest phase.
|
||||
self.when = when
|
||||
|
||||
#: list of (secname, data) extra information which needs to
|
||||
#: marshallable
|
||||
#: list of pairs ``(str, str)`` of extra information which needs to
|
||||
#: marshallable. Used by pytest to add captured text
|
||||
#: from ``stdout`` and ``stderr``, but may be used by other plugins
|
||||
#: to add arbitrary information to reports.
|
||||
self.sections = list(sections)
|
||||
|
||||
#: time it took to run just the test
|
||||
@@ -430,7 +480,10 @@ class OutcomeException(Exception):
|
||||
|
||||
def __repr__(self):
|
||||
if self.msg:
|
||||
return str(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__
|
||||
|
||||
@@ -439,10 +492,16 @@ class Skipped(OutcomeException):
|
||||
# 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"):
|
||||
@@ -456,8 +515,10 @@ def exit(msg):
|
||||
__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
|
||||
@@ -466,16 +527,21 @@ def skip(msg=""):
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
raise Skipped(msg=msg)
|
||||
|
||||
|
||||
skip.Exception = Skipped
|
||||
|
||||
|
||||
def fail(msg="", pytrace=True):
|
||||
""" explicitely fail an currently-executing test with the given Message.
|
||||
""" 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
|
||||
|
||||
|
||||
@@ -486,10 +552,14 @@ def importorskip(modname, minversion=None):
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
compile(modname, '', 'eval') # to catch syntaxerrors
|
||||
should_skip = False
|
||||
try:
|
||||
__import__(modname)
|
||||
except ImportError:
|
||||
skip("could not import %r" %(modname,))
|
||||
# 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
|
||||
@@ -498,10 +568,11 @@ def importorskip(modname, minversion=None):
|
||||
try:
|
||||
from pkg_resources import parse_version as pv
|
||||
except ImportError:
|
||||
skip("we have a required version for %r but can not import "
|
||||
"no pkg_resources to parse version strings." %(modname,))
|
||||
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):
|
||||
skip("module %r has __version__ %r, required is: %r" %(
|
||||
modname, verattr, minversion))
|
||||
raise Skipped("module %r has __version__ %r, required is: %r" %(
|
||||
modname, verattr, minversion), allow_module_level=True)
|
||||
return mod
|
||||
|
||||
|
||||
72
_pytest/setuponly.py
Normal file
72
_pytest/setuponly.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import pytest
|
||||
import sys
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption('--setuponly', '--setup-only', action="store_true",
|
||||
help="only setup fixtures, do not execute tests.")
|
||||
group.addoption('--setupshow', '--setup-show', action="store_true",
|
||||
help="show setup of fixtures while executing tests.")
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
yield
|
||||
config = request.config
|
||||
if config.option.setupshow:
|
||||
if hasattr(request, 'param'):
|
||||
# Save the fixture parameter so ._show_fixture_action() can
|
||||
# display it now and during the teardown (in .finish()).
|
||||
if fixturedef.ids:
|
||||
if callable(fixturedef.ids):
|
||||
fixturedef.cached_param = fixturedef.ids(request.param)
|
||||
else:
|
||||
fixturedef.cached_param = fixturedef.ids[
|
||||
request.param_index]
|
||||
else:
|
||||
fixturedef.cached_param = request.param
|
||||
_show_fixture_action(fixturedef, 'SETUP')
|
||||
|
||||
|
||||
def pytest_fixture_post_finalizer(fixturedef):
|
||||
if hasattr(fixturedef, "cached_result"):
|
||||
config = fixturedef._fixturemanager.config
|
||||
if config.option.setupshow:
|
||||
_show_fixture_action(fixturedef, 'TEARDOWN')
|
||||
if hasattr(fixturedef, "cached_param"):
|
||||
del fixturedef.cached_param
|
||||
|
||||
|
||||
def _show_fixture_action(fixturedef, msg):
|
||||
config = fixturedef._fixturemanager.config
|
||||
capman = config.pluginmanager.getplugin('capturemanager')
|
||||
if capman:
|
||||
out, err = capman.suspendcapture()
|
||||
|
||||
tw = config.get_terminal_writer()
|
||||
tw.line()
|
||||
tw.write(' ' * 2 * fixturedef.scopenum)
|
||||
tw.write('{step} {scope} {fixture}'.format(
|
||||
step=msg.ljust(8), # align the output to TEARDOWN
|
||||
scope=fixturedef.scope[0].upper(),
|
||||
fixture=fixturedef.argname))
|
||||
|
||||
if msg == 'SETUP':
|
||||
deps = sorted(arg for arg in fixturedef.argnames if arg != 'request')
|
||||
if deps:
|
||||
tw.write(' (fixtures used: {0})'.format(', '.join(deps)))
|
||||
|
||||
if hasattr(fixturedef, 'cached_param'):
|
||||
tw.write('[{0}]'.format(fixturedef.cached_param))
|
||||
|
||||
if capman:
|
||||
capman.resumecapture()
|
||||
sys.stdout.write(out)
|
||||
sys.stderr.write(err)
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_cmdline_main(config):
|
||||
if config.option.setuponly:
|
||||
config.option.setupshow = True
|
||||
23
_pytest/setupplan.py
Normal file
23
_pytest/setupplan.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption('--setupplan', '--setup-plan', action="store_true",
|
||||
help="show what fixtures and tests would be executed but "
|
||||
"don't execute anything.")
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_fixture_setup(fixturedef, request):
|
||||
# Will return a dummy fixture if the setuponly option is provided.
|
||||
if request.config.option.setupplan:
|
||||
fixturedef.cached_result = (None, None, None)
|
||||
return fixturedef.cached_result
|
||||
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_cmdline_main(config):
|
||||
if config.option.setupplan:
|
||||
config.option.setuponly = True
|
||||
config.option.setupshow = True
|
||||
@@ -5,6 +5,8 @@ import traceback
|
||||
|
||||
import py
|
||||
import pytest
|
||||
from _pytest.mark import MarkInfo, MarkDecorator
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("general")
|
||||
@@ -12,15 +14,29 @@ def pytest_addoption(parser):
|
||||
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: "
|
||||
"False)",
|
||||
default=False,
|
||||
type="bool")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.option.runxfail:
|
||||
old = pytest.xfail
|
||||
config._cleanup.append(lambda: setattr(pytest, "xfail", old))
|
||||
|
||||
def nop(*args, **kwargs):
|
||||
pass
|
||||
|
||||
nop.Exception = XFailed
|
||||
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."
|
||||
)
|
||||
config.addinivalue_line("markers",
|
||||
"skipif(condition): skip the given test function if eval(condition) "
|
||||
"results in a True value. Evaluation happens within the "
|
||||
@@ -29,27 +45,33 @@ def pytest_configure(config):
|
||||
"http://pytest.org/latest/skipping.html"
|
||||
)
|
||||
config.addinivalue_line("markers",
|
||||
"xfail(condition, reason=None, run=True, raises=None): mark the 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:
|
||||
def __init__(self, item, name):
|
||||
self.item = item
|
||||
@@ -90,19 +112,15 @@ class MarkEvaluator:
|
||||
|
||||
def _getglobals(self):
|
||||
d = {'os': os, 'sys': sys, 'config': self.item.config}
|
||||
func = self.item.obj
|
||||
try:
|
||||
d.update(func.__globals__)
|
||||
except AttributeError:
|
||||
d.update(func.func_globals)
|
||||
if hasattr(self.item, 'obj'):
|
||||
d.update(self.item.obj.__globals__)
|
||||
return d
|
||||
|
||||
def _istrue(self):
|
||||
if hasattr(self, 'result'):
|
||||
return self.result
|
||||
if self.holder:
|
||||
d = self._getglobals()
|
||||
if self.holder.args:
|
||||
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
|
||||
@@ -112,9 +130,12 @@ class MarkEvaluator:
|
||||
else:
|
||||
arglist = [(self.holder.args, self.holder.kwargs)]
|
||||
for args, kwargs in arglist:
|
||||
if 'condition' in kwargs:
|
||||
args = (kwargs['condition'],)
|
||||
for expr in args:
|
||||
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:
|
||||
@@ -147,23 +168,59 @@ class MarkEvaluator:
|
||||
|
||||
@pytest.hookimpl(tryfirst=True)
|
||||
def pytest_runtest_setup(item):
|
||||
evalskip = MarkEvaluator(item, 'skipif')
|
||||
if evalskip.istrue():
|
||||
item._evalskip = evalskip
|
||||
pytest.skip(evalskip.getexplanation())
|
||||
# Check if skip or skipif are specified as pytest marks
|
||||
|
||||
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())
|
||||
|
||||
skip_info = item.keywords.get('skip')
|
||||
if isinstance(skip_info, (MarkInfo, MarkDecorator)):
|
||||
item._evalskip = True
|
||||
if 'reason' in skip_info.kwargs:
|
||||
pytest.skip(skip_info.kwargs['reason'])
|
||||
elif skip_info.args:
|
||||
pytest.skip(skip_info.args[0])
|
||||
else:
|
||||
pytest.skip("unconditional skip")
|
||||
|
||||
item._evalxfail = MarkEvaluator(item, 'xfail')
|
||||
check_xfail_no_run(item)
|
||||
|
||||
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
check_xfail_no_run(pyfuncitem)
|
||||
outcome = yield
|
||||
passed = outcome.excinfo is None
|
||||
if passed:
|
||||
check_strict_xfail(pyfuncitem)
|
||||
|
||||
|
||||
def check_xfail_no_run(item):
|
||||
"""check xfail(run=False)"""
|
||||
if not item.config.option.runxfail:
|
||||
evalxfail = item._evalxfail
|
||||
if evalxfail.istrue():
|
||||
if not evalxfail.get('run', True):
|
||||
pytest.xfail("[NOTRUN] " + evalxfail.getexplanation())
|
||||
|
||||
|
||||
def check_strict_xfail(pyfuncitem):
|
||||
"""check xfail(strict=True) for the given PASSING test"""
|
||||
evalxfail = pyfuncitem._evalxfail
|
||||
if evalxfail.istrue():
|
||||
strict_default = pyfuncitem.config.getini('xfail_strict')
|
||||
is_strict_xfail = evalxfail.get('strict', strict_default)
|
||||
if is_strict_xfail:
|
||||
del pyfuncitem._evalxfail
|
||||
explanation = evalxfail.getexplanation()
|
||||
pytest.fail('[XPASS(strict)] ' + explanation, pytrace=False)
|
||||
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_makereport(item, call):
|
||||
outcome = yield
|
||||
@@ -172,9 +229,16 @@ def pytest_runtest_makereport(item, call):
|
||||
evalskip = getattr(item, '_evalskip', None)
|
||||
# unitttest special case, see setting of _unexpectedsuccess
|
||||
if hasattr(item, '_unexpectedsuccess') and rep.when == "call":
|
||||
# we need to translate into how pytest encodes xpass
|
||||
rep.wasxfail = "reason: " + repr(item._unexpectedsuccess)
|
||||
rep.outcome = "failed"
|
||||
from _pytest.compat import _is_unittest_unexpected_success_a_failure
|
||||
if item._unexpectedsuccess:
|
||||
rep.longrepr = "Unexpected success: {0}".format(item._unexpectedsuccess)
|
||||
else:
|
||||
rep.longrepr = "Unexpected success"
|
||||
if _is_unittest_unexpected_success_a_failure():
|
||||
rep.outcome = "failed"
|
||||
else:
|
||||
rep.outcome = "passed"
|
||||
rep.wasxfail = rep.longrepr
|
||||
elif item.config.option.runxfail:
|
||||
pass # don't interefere
|
||||
elif call.excinfo and call.excinfo.errisinstance(pytest.xfail.Exception):
|
||||
@@ -189,8 +253,15 @@ def pytest_runtest_makereport(item, call):
|
||||
rep.outcome = "skipped"
|
||||
rep.wasxfail = evalxfail.getexplanation()
|
||||
elif call.when == "call":
|
||||
rep.outcome = "failed" # xpass outcome
|
||||
rep.wasxfail = evalxfail.getexplanation()
|
||||
strict_default = item.config.getini('xfail_strict')
|
||||
is_strict_xfail = evalxfail.get('strict', strict_default)
|
||||
explanation = evalxfail.getexplanation()
|
||||
if is_strict_xfail:
|
||||
rep.outcome = "failed"
|
||||
rep.longrepr = "[XPASS(strict)] {0}".format(explanation)
|
||||
else:
|
||||
rep.outcome = "passed"
|
||||
rep.wasxfail = explanation
|
||||
elif evalskip is not None 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
|
||||
@@ -204,7 +275,7 @@ def pytest_report_teststatus(report):
|
||||
if hasattr(report, "wasxfail"):
|
||||
if report.skipped:
|
||||
return "xfailed", "x", "xfail"
|
||||
elif report.failed:
|
||||
elif report.passed:
|
||||
return "xpassed", "X", ("XPASS", {'yellow': True})
|
||||
|
||||
# called by the terminalreporter instance/plugin
|
||||
@@ -230,6 +301,9 @@ def pytest_terminal_summary(terminalreporter):
|
||||
show_skipped(terminalreporter, lines)
|
||||
elif char == "E":
|
||||
show_simple(terminalreporter, lines, 'error', "ERROR %s")
|
||||
elif char == 'p':
|
||||
show_simple(terminalreporter, lines, 'passed', "PASSED %s")
|
||||
|
||||
if lines:
|
||||
tr._tw.sep("=", "short test summary info")
|
||||
for line in lines:
|
||||
@@ -266,9 +340,8 @@ def cached_eval(config, expr, d):
|
||||
try:
|
||||
return config._evalcache[expr]
|
||||
except KeyError:
|
||||
#import sys
|
||||
#print >>sys.stderr, ("cache-miss: %r" % expr)
|
||||
exprcode = py.code.compile(expr, mode="eval")
|
||||
import _pytest._code
|
||||
exprcode = _pytest._code.compile(expr, mode="eval")
|
||||
config._evalcache[expr] = x = eval(exprcode, d)
|
||||
return x
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
#! /usr/bin/env python
|
||||
|
||||
# Hi There!
|
||||
# You may be wondering what this giant blob of binary data here is, you might
|
||||
# even be worried that we're up to something nefarious (good for you for being
|
||||
# paranoid!). This is a base64 encoding of a zip file, this zip file contains
|
||||
# a fully functional basic pytest script.
|
||||
#
|
||||
# Pytest is a thing that tests packages, pytest itself is a package that some-
|
||||
# one might want to install, especially if they're looking to run tests inside
|
||||
# some package they want to install. Pytest has a lot of code to collect and
|
||||
# execute tests, and other such sort of "tribal knowledge" that has been en-
|
||||
# coded in its code base. Because of this we basically include a basic copy
|
||||
# of pytest inside this blob. We do this because it let's you as a maintainer
|
||||
# or application developer who wants people who don't deal with python much to
|
||||
# easily run tests without installing the complete pytest package.
|
||||
#
|
||||
# If you're wondering how this is created: you can create it yourself if you
|
||||
# have a complete pytest installation by using this command on the command-
|
||||
# line: ``py.test --genscript=runtests.py``.
|
||||
|
||||
sources = """
|
||||
@SOURCES@"""
|
||||
|
||||
import sys
|
||||
import base64
|
||||
import zlib
|
||||
|
||||
class DictImporter(object):
|
||||
def __init__(self, sources):
|
||||
self.sources = sources
|
||||
|
||||
def find_module(self, fullname, path=None):
|
||||
if fullname == "argparse" and sys.version_info >= (2,7):
|
||||
# we were generated with <python2.7 (which pulls in argparse)
|
||||
# but we are running now on a stdlib which has it, so use that.
|
||||
return None
|
||||
if fullname in self.sources:
|
||||
return self
|
||||
if fullname + '.__init__' in self.sources:
|
||||
return self
|
||||
return None
|
||||
|
||||
def load_module(self, fullname):
|
||||
# print "load_module:", fullname
|
||||
from types import ModuleType
|
||||
try:
|
||||
s = self.sources[fullname]
|
||||
is_pkg = False
|
||||
except KeyError:
|
||||
s = self.sources[fullname + '.__init__']
|
||||
is_pkg = True
|
||||
|
||||
co = compile(s, fullname, 'exec')
|
||||
module = sys.modules.setdefault(fullname, ModuleType(fullname))
|
||||
module.__file__ = "%s/%s" % (__file__, fullname)
|
||||
module.__loader__ = self
|
||||
if is_pkg:
|
||||
module.__path__ = [fullname]
|
||||
|
||||
do_exec(co, module.__dict__) # noqa
|
||||
return sys.modules[fullname]
|
||||
|
||||
def get_source(self, name):
|
||||
res = self.sources.get(name)
|
||||
if res is None:
|
||||
res = self.sources.get(name + '.__init__')
|
||||
return res
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
import pkg_resources # noqa
|
||||
except ImportError:
|
||||
sys.stderr.write("ERROR: setuptools not installed\n")
|
||||
sys.exit(2)
|
||||
if sys.version_info >= (3, 0):
|
||||
exec("def do_exec(co, loc): exec(co, loc)\n")
|
||||
import pickle
|
||||
sources = sources.encode("ascii") # ensure bytes
|
||||
sources = pickle.loads(zlib.decompress(base64.decodebytes(sources)))
|
||||
else:
|
||||
import cPickle as pickle
|
||||
exec("def do_exec(co, loc): exec co in loc\n")
|
||||
sources = pickle.loads(zlib.decompress(base64.decodestring(sources)))
|
||||
|
||||
importer = DictImporter(sources)
|
||||
sys.meta_path.insert(0, importer)
|
||||
entry = "@ENTRY@"
|
||||
do_exec(entry, locals()) # noqa
|
||||
@@ -20,15 +20,18 @@ def pytest_addoption(parser):
|
||||
group._addoption('-q', '--quiet', action="count",
|
||||
dest="quiet", default=0, help="decrease verbosity."),
|
||||
group._addoption('-r',
|
||||
action="store", dest="reportchars", default=None, metavar="chars",
|
||||
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 (w)pytest-warnings (a)all.")
|
||||
"(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')
|
||||
group._addoption('-l', '--showlocals',
|
||||
action="store_true", dest="showlocals", default=False,
|
||||
help="show locals in tracebacks (disabled by default).")
|
||||
group._addoption('--report',
|
||||
action="store", dest="report", default=None, metavar="opts",
|
||||
help="(deprecated, use -r)")
|
||||
group._addoption('--tb', metavar="style",
|
||||
action="store", dest="tbstyle", default='auto',
|
||||
choices=['auto', 'long', 'short', 'no', 'line', 'native'],
|
||||
@@ -53,18 +56,11 @@ def pytest_configure(config):
|
||||
|
||||
def getreportopt(config):
|
||||
reportopts = ""
|
||||
optvalue = config.option.report
|
||||
if optvalue:
|
||||
py.builtin.print_("DEPRECATED: use -r instead of --report option.",
|
||||
file=sys.stderr)
|
||||
if optvalue:
|
||||
for setting in optvalue.split(","):
|
||||
setting = setting.strip()
|
||||
if setting == "skipped":
|
||||
reportopts += "s"
|
||||
elif setting == "xfailed":
|
||||
reportopts += "x"
|
||||
reportchars = config.option.reportchars
|
||||
if not config.option.disablepytestwarnings and 'w' not in reportchars:
|
||||
reportchars += 'w'
|
||||
elif config.option.disablepytestwarnings and 'w' in reportchars:
|
||||
reportchars = reportchars.replace('w', '')
|
||||
if reportchars:
|
||||
for char in reportchars:
|
||||
if char not in reportopts and char != 'a':
|
||||
@@ -111,6 +107,7 @@ class TerminalReporter:
|
||||
self.currentfspath = None
|
||||
self.reportchars = getreportopt(config)
|
||||
self.hasmarkup = self._tw.hasmarkup
|
||||
self.isatty = file.isatty()
|
||||
|
||||
def hasopt(self, char):
|
||||
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
|
||||
@@ -233,7 +230,7 @@ class TerminalReporter:
|
||||
self.currentfspath = -2
|
||||
|
||||
def pytest_collection(self):
|
||||
if not self.hasmarkup and self.config.option.verbose >= 1:
|
||||
if not self.isatty and self.config.option.verbose >= 1:
|
||||
self.write("collecting ... ", bold=True)
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
@@ -243,7 +240,7 @@ class TerminalReporter:
|
||||
self.stats.setdefault("skipped", []).append(report)
|
||||
items = [x for x in report.result if isinstance(x, pytest.Item)]
|
||||
self._numcollected += len(items)
|
||||
if self.hasmarkup:
|
||||
if self.isatty:
|
||||
#self.write_fspath_result(report.nodeid, 'E')
|
||||
self.report_collect()
|
||||
|
||||
@@ -262,7 +259,7 @@ class TerminalReporter:
|
||||
line += " / %d errors" % errors
|
||||
if skipped:
|
||||
line += " / %d skipped" % skipped
|
||||
if self.hasmarkup:
|
||||
if self.isatty:
|
||||
if final:
|
||||
line += " \n"
|
||||
self.rewrite(line, bold=True)
|
||||
@@ -298,8 +295,8 @@ class TerminalReporter:
|
||||
def pytest_report_header(self, config):
|
||||
inifile = ""
|
||||
if config.inifile:
|
||||
inifile = config.rootdir.bestrelpath(config.inifile)
|
||||
lines = ["rootdir: %s, inifile: %s" %(config.rootdir, inifile)]
|
||||
inifile = " " + config.rootdir.bestrelpath(config.inifile)
|
||||
lines = ["rootdir: %s, inifile:%s" % (config.rootdir, inifile)]
|
||||
|
||||
plugininfo = config.pluginmanager.list_plugin_distinfo()
|
||||
if plugininfo:
|
||||
@@ -364,10 +361,12 @@ class TerminalReporter:
|
||||
EXIT_OK, EXIT_TESTSFAILED, EXIT_INTERRUPTED, EXIT_USAGEERROR,
|
||||
EXIT_NOTESTSCOLLECTED)
|
||||
if exitstatus in summary_exit_codes:
|
||||
self.config.hook.pytest_terminal_summary(terminalreporter=self,
|
||||
exitstatus=exitstatus)
|
||||
self.summary_errors()
|
||||
self.summary_failures()
|
||||
self.summary_warnings()
|
||||
self.config.hook.pytest_terminal_summary(terminalreporter=self)
|
||||
self.summary_passes()
|
||||
if exitstatus == EXIT_INTERRUPTED:
|
||||
self._report_keyboardinterrupt()
|
||||
del self._keyboardinterrupt_memo
|
||||
@@ -389,6 +388,7 @@ class TerminalReporter:
|
||||
if self.config.option.fulltrace:
|
||||
excrepr.toterminal(self._tw)
|
||||
else:
|
||||
self._tw.line("to show a full traceback on KeyboardInterrupt use --fulltrace", yellow=True)
|
||||
excrepr.reprcrash.toterminal(self._tw)
|
||||
|
||||
def _locationline(self, nodeid, fspath, lineno, domain):
|
||||
@@ -446,6 +446,27 @@ class TerminalReporter:
|
||||
self._tw.line("W%s %s %s" % (w.code,
|
||||
w.fslocation, w.message))
|
||||
|
||||
def summary_passes(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
if self.hasopt("P"):
|
||||
reports = self.getreports('passed')
|
||||
if not reports:
|
||||
return
|
||||
self.write_sep("=", "PASSES")
|
||||
for rep in reports:
|
||||
msg = self._getfailureheadline(rep)
|
||||
self.write_sep("_", msg)
|
||||
self._outrep_summary(rep)
|
||||
|
||||
def print_teardown_sections(self, rep):
|
||||
for secname, content in rep.sections:
|
||||
if 'teardown' in secname:
|
||||
self._tw.sep('-', secname)
|
||||
if content[-1:] == "\n":
|
||||
content = content[:-1]
|
||||
self._tw.line(content)
|
||||
|
||||
|
||||
def summary_failures(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
reports = self.getreports('failed')
|
||||
@@ -461,6 +482,9 @@ class TerminalReporter:
|
||||
markup = {'red': True, 'bold': True}
|
||||
self.write_sep("_", msg, **markup)
|
||||
self._outrep_summary(rep)
|
||||
for report in self.getreports(''):
|
||||
if report.nodeid == rep.nodeid and report.when == 'teardown':
|
||||
self.print_teardown_sections(report)
|
||||
|
||||
def summary_errors(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
@@ -501,16 +525,8 @@ class TerminalReporter:
|
||||
|
||||
def summary_deselected(self):
|
||||
if 'deselected' in self.stats:
|
||||
l = []
|
||||
k = self.config.option.keyword
|
||||
if k:
|
||||
l.append("-k%s" % k)
|
||||
m = self.config.option.markexpr
|
||||
if m:
|
||||
l.append("-m %r" % m)
|
||||
if l:
|
||||
self.write_sep("=", "%d tests deselected by %r" % (
|
||||
len(self.stats['deselected']), " ".join(l)), bold=True)
|
||||
self.write_sep("=", "%d tests deselected" % (
|
||||
len(self.stats['deselected'])), bold=True)
|
||||
|
||||
def repr_pythonversion(v=None):
|
||||
if v is None:
|
||||
@@ -544,7 +560,11 @@ def build_summary_stats_line(stats):
|
||||
if val:
|
||||
key_name = key_translation.get(key, key)
|
||||
parts.append("%d %s" % (len(val), key_name))
|
||||
line = ", ".join(parts)
|
||||
|
||||
if parts:
|
||||
line = ", ".join(parts)
|
||||
else:
|
||||
line = "no tests ran"
|
||||
|
||||
if 'failed' in stats or 'error' in stats:
|
||||
color = 'red'
|
||||
|
||||
@@ -3,7 +3,7 @@ import re
|
||||
|
||||
import pytest
|
||||
import py
|
||||
from _pytest.monkeypatch import monkeypatch
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
|
||||
|
||||
class TempdirFactory:
|
||||
@@ -81,6 +81,7 @@ def get_user():
|
||||
except (ImportError, KeyError):
|
||||
return None
|
||||
|
||||
|
||||
# backward compatibility
|
||||
TempdirHandler = TempdirFactory
|
||||
|
||||
@@ -92,7 +93,7 @@ def pytest_configure(config):
|
||||
available at pytest_configure time, but ideally should be moved entirely
|
||||
to the tmpdir_factory session fixture.
|
||||
"""
|
||||
mp = monkeypatch()
|
||||
mp = MonkeyPatch()
|
||||
t = TempdirFactory(config)
|
||||
config._cleanup.extend([mp.undo, t.finish])
|
||||
mp.setattr(config, '_tmpdirhandler', t, raising=False)
|
||||
@@ -108,14 +109,14 @@ def tmpdir_factory(request):
|
||||
|
||||
@pytest.fixture
|
||||
def tmpdir(request, tmpdir_factory):
|
||||
"""return a temporary directory path object
|
||||
"""Return a temporary directory path object
|
||||
which is unique to each test function invocation,
|
||||
created as a sub directory of the base temporary
|
||||
directory. The returned object is a `py.path.local`_
|
||||
path object.
|
||||
"""
|
||||
name = request.node.name
|
||||
name = re.sub("[\W]", "_", name)
|
||||
name = re.sub(r"[\W]", "_", name)
|
||||
MAXVAL = 30
|
||||
if len(name) > MAXVAL:
|
||||
name = name[:MAXVAL]
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
""" discovery and running of std-library "unittest" style tests. """
|
||||
from __future__ import absolute_import
|
||||
import traceback
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import pytest
|
||||
import py
|
||||
|
||||
|
||||
# for transfering markers
|
||||
# for transferring markers
|
||||
import _pytest._code
|
||||
from _pytest.python import transfer_markers
|
||||
from _pytest.skipping import MarkEvaluator
|
||||
|
||||
@@ -24,9 +23,10 @@ def pytest_pycollect_makeitem(collector, name, obj):
|
||||
|
||||
|
||||
class UnitTestCase(pytest.Class):
|
||||
nofuncargs = True # marker for fixturemanger.getfixtureinfo()
|
||||
# to declare that our children do not support funcargs
|
||||
#
|
||||
# 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):
|
||||
@@ -50,6 +50,8 @@ class UnitTestCase(pytest.Class):
|
||||
foundsomething = False
|
||||
for name in loader.getTestCaseNames(self.obj):
|
||||
x = getattr(self.obj, name)
|
||||
if not getattr(x, '__test__', True):
|
||||
continue
|
||||
funcobj = getattr(x, 'im_func', x)
|
||||
transfer_markers(funcobj, cls, module)
|
||||
yield TestCaseFunction(name, parent=self)
|
||||
@@ -63,7 +65,6 @@ class UnitTestCase(pytest.Class):
|
||||
yield TestCaseFunction('runTest', parent=self)
|
||||
|
||||
|
||||
|
||||
class TestCaseFunction(pytest.Function):
|
||||
_excinfo = None
|
||||
|
||||
@@ -92,6 +93,9 @@ class TestCaseFunction(pytest.Function):
|
||||
def teardown(self):
|
||||
if hasattr(self._testcase, 'teardown_method'):
|
||||
self._testcase.teardown_method(self._obj)
|
||||
# Allow garbage collection on TestCase instance attributes.
|
||||
self._testcase = None
|
||||
self._obj = None
|
||||
|
||||
def startTest(self, testcase):
|
||||
pass
|
||||
@@ -100,7 +104,7 @@ class TestCaseFunction(pytest.Function):
|
||||
# unwrap potential exception info (see twisted trial support below)
|
||||
rawexcinfo = getattr(rawexcinfo, '_rawexcinfo', rawexcinfo)
|
||||
try:
|
||||
excinfo = py.code.ExceptionInfo(rawexcinfo)
|
||||
excinfo = _pytest._code.ExceptionInfo(rawexcinfo)
|
||||
except TypeError:
|
||||
try:
|
||||
try:
|
||||
@@ -116,7 +120,7 @@ class TestCaseFunction(pytest.Function):
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except pytest.fail.Exception:
|
||||
excinfo = py.code.ExceptionInfo()
|
||||
excinfo = _pytest._code.ExceptionInfo()
|
||||
self.__dict__.setdefault('_excinfo', []).append(excinfo)
|
||||
|
||||
def addError(self, testcase, rawexcinfo):
|
||||
@@ -147,8 +151,32 @@ class TestCaseFunction(pytest.Function):
|
||||
def stopTest(self, testcase):
|
||||
pass
|
||||
|
||||
def _handle_skip(self):
|
||||
# implements the skipping machinery (see #2137)
|
||||
# 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)):
|
||||
# If the class or method was skipped.
|
||||
skip_why = (getattr(self._testcase.__class__, '__unittest_skip_why__', '') or
|
||||
getattr(testMethod, '__unittest_skip_why__', ''))
|
||||
try: # PY3, unittest2 on PY2
|
||||
self._testcase._addSkip(self, self._testcase, skip_why)
|
||||
except TypeError: # PY2
|
||||
if sys.version_info[0] != 2:
|
||||
raise
|
||||
self._testcase._addSkip(self, skip_why)
|
||||
return True
|
||||
return False
|
||||
|
||||
def runtest(self):
|
||||
self._testcase(result=self)
|
||||
if self.config.pluginmanager.get_plugin("pdbinvoke") is None:
|
||||
self._testcase(result=self)
|
||||
else:
|
||||
# disables tearDown and cleanups for post mortem debugging (see #1890)
|
||||
if self._handle_skip():
|
||||
return
|
||||
self._testcase.debug()
|
||||
|
||||
def _prunetraceback(self, excinfo):
|
||||
pytest.Function._prunetraceback(self, excinfo)
|
||||
@@ -176,6 +204,7 @@ def pytest_runtest_protocol(item):
|
||||
ut = sys.modules['twisted.python.failure']
|
||||
Failure__init__ = ut.Failure.__init__
|
||||
check_testcase_implements_trial_reporter()
|
||||
|
||||
def excstore(self, exc_value=None, exc_type=None, exc_tb=None,
|
||||
captureVars=None):
|
||||
if exc_value is None:
|
||||
@@ -189,6 +218,7 @@ def pytest_runtest_protocol(item):
|
||||
captureVars=captureVars)
|
||||
except TypeError:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb)
|
||||
|
||||
ut.Failure.__init__ = excstore
|
||||
yield
|
||||
ut.Failure.__init__ = Failure__init__
|
||||
|
||||
@@ -10,4 +10,4 @@ $ pip install -U pluggy==<version> --no-compile --target=_pytest/vendored_packag
|
||||
```
|
||||
|
||||
And commit the modified files. The `pluggy-<version>.dist-info` directory
|
||||
created by `pip` should be ignored.
|
||||
created by `pip` should be added as well.
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
pluggy.py,sha256=v_RfWzyW6DPU1cJu_EFoL_OHq3t13qloVdR6UaMCXQA,29862
|
||||
pluggy-0.3.1.dist-info/top_level.txt,sha256=xKSCRhai-v9MckvMuWqNz16c1tbsmOggoMSwTgcpYHE,7
|
||||
pluggy-0.3.1.dist-info/pbr.json,sha256=xX3s6__wOcAyF-AZJX1sdZyW6PUXT-FkfBlM69EEUCg,47
|
||||
pluggy-0.3.1.dist-info/RECORD,,
|
||||
pluggy-0.3.1.dist-info/metadata.json,sha256=nLKltOT78dMV-00uXD6Aeemp4xNsz2q59j6ORSDeLjw,1027
|
||||
pluggy-0.3.1.dist-info/METADATA,sha256=1b85Ho2u4iK30M099k7axMzcDDhLcIMb-A82JUJZnSo,1334
|
||||
pluggy-0.3.1.dist-info/WHEEL,sha256=AvR0WeTpDaxT645bl5FQxUK6NPsTls2ttpcGJg3j1Xg,110
|
||||
pluggy-0.3.1.dist-info/DESCRIPTION.rst,sha256=P5Akh1EdIBR6CeqtV2P8ZwpGSpZiTKPw0NyS7jEiD-g,306
|
||||
@@ -1 +0,0 @@
|
||||
{"license": "MIT license", "name": "pluggy", "metadata_version": "2.0", "generator": "bdist_wheel (0.24.0)", "summary": "plugin and hook calling mechanisms for python", "platform": "unix", "version": "0.3.1", "extensions": {"python.details": {"document_names": {"description": "DESCRIPTION.rst"}, "contacts": [{"role": "author", "email": "holger at merlinux.eu", "name": "Holger Krekel"}]}}, "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"]}
|
||||
@@ -1 +0,0 @@
|
||||
{"is_release": false, "git_version": "7d4c9cd"}
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
Plugin registration and hook calling for Python
|
||||
===============================================
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
pip
|
||||
22
_pytest/vendored_packages/pluggy-0.4.0.dist-info/LICENSE.txt
Normal file
22
_pytest/vendored_packages/pluggy-0.4.0.dist-info/LICENSE.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
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,8 +1,8 @@
|
||||
Metadata-Version: 2.0
|
||||
Name: pluggy
|
||||
Version: 0.3.1
|
||||
Version: 0.4.0
|
||||
Summary: plugin and hook calling mechanisms for python
|
||||
Home-page: UNKNOWN
|
||||
Home-page: https://github.com/pytest-dev/pluggy
|
||||
Author: Holger Krekel
|
||||
Author-email: holger at merlinux.eu
|
||||
License: MIT license
|
||||
@@ -27,6 +27,7 @@ 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
|
||||
===============================================
|
||||
|
||||
9
_pytest/vendored_packages/pluggy-0.4.0.dist-info/RECORD
Normal file
9
_pytest/vendored_packages/pluggy-0.4.0.dist-info/RECORD
Normal file
@@ -0,0 +1,9 @@
|
||||
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,5 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: bdist_wheel (0.24.0)
|
||||
Generator: bdist_wheel (0.29.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py2-none-any
|
||||
Tag: py3-none-any
|
||||
@@ -0,0 +1 @@
|
||||
{"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"}
|
||||
@@ -67,8 +67,9 @@ Pluggy currently consists of functionality for:
|
||||
import sys
|
||||
import inspect
|
||||
|
||||
__version__ = '0.3.1'
|
||||
__all__ = ["PluginManager", "PluginValidationError",
|
||||
__version__ = '0.4.0'
|
||||
|
||||
__all__ = ["PluginManager", "PluginValidationError", "HookCallError",
|
||||
"HookspecMarker", "HookimplMarker"]
|
||||
|
||||
_py3 = sys.version_info > (3, 0)
|
||||
@@ -308,7 +309,7 @@ class PluginManager(object):
|
||||
""" Core Pluginmanager class which manages registration
|
||||
of plugin objects and 1:N hook calling.
|
||||
|
||||
You can register new hooks by calling ``addhooks(module_or_class)``.
|
||||
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
|
||||
@@ -374,7 +375,10 @@ class PluginManager(object):
|
||||
|
||||
def parse_hookimpl_opts(self, plugin, name):
|
||||
method = getattr(plugin, name)
|
||||
res = getattr(method, self.project_name + "_impl", None)
|
||||
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
|
||||
@@ -455,6 +459,10 @@ class PluginManager(object):
|
||||
""" 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():
|
||||
@@ -492,7 +500,8 @@ class PluginManager(object):
|
||||
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
|
||||
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):
|
||||
@@ -501,6 +510,9 @@ class PluginManager(object):
|
||||
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)
|
||||
@@ -590,7 +602,13 @@ class _MultiCall:
|
||||
|
||||
while self.hook_impls:
|
||||
hook_impl = self.hook_impls.pop()
|
||||
args = [all_kwargs[argname] for argname in hook_impl.argnames]
|
||||
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)
|
||||
@@ -629,7 +647,10 @@ def varnames(func, startindex=None):
|
||||
startindex = 1
|
||||
else:
|
||||
if not inspect.isfunction(func) and not inspect.ismethod(func):
|
||||
func = getattr(func, '__call__', func)
|
||||
try:
|
||||
func = getattr(func, '__call__', func)
|
||||
except Exception:
|
||||
return ()
|
||||
if startindex is None:
|
||||
startindex = int(inspect.ismethod(func))
|
||||
|
||||
@@ -763,6 +784,10 @@ class PluginValidationError(Exception):
|
||||
""" plugin failed validation. """
|
||||
|
||||
|
||||
class HookCallError(Exception):
|
||||
""" Hook was called wrongly. """
|
||||
|
||||
|
||||
if hasattr(inspect, 'signature'):
|
||||
def _formatdef(func):
|
||||
return "%s%s" % (
|
||||
|
||||
120
appveyor.yml
120
appveyor.yml
@@ -1,98 +1,42 @@
|
||||
environment:
|
||||
COVERALLS_REPO_TOKEN:
|
||||
secure: 2NJ5Ct55cHJ9WEg3xbSqCuv0rdgzzb6pnzOIG5OkMbTndw3wOBrXntWFoQrXiMFi
|
||||
# this is pytest's token in coveralls.io, encrypted
|
||||
# using pytestbot account as detailed here:
|
||||
# https://www.appveyor.com/docs/build-configuration#secure-variables
|
||||
|
||||
matrix:
|
||||
|
||||
# Pre-installed Python versions, which Appveyor may upgrade to
|
||||
# a later point release.
|
||||
|
||||
- PYTHON: "C:\\Python27"
|
||||
PYTHON_VERSION: "2.7.x" # currently 2.7.9
|
||||
PYTHON_ARCH: "32"
|
||||
TESTENV: "py27"
|
||||
|
||||
- PYTHON: "C:\\Python27-x64"
|
||||
PYTHON_VERSION: "2.7.x" # currently 2.7.9
|
||||
PYTHON_ARCH: "64"
|
||||
TESTENV: "py27"
|
||||
|
||||
- PYTHON: "C:\\Python33"
|
||||
PYTHON_VERSION: "3.3.x" # currently 3.3.5
|
||||
PYTHON_ARCH: "32"
|
||||
TESTENV: "py33"
|
||||
|
||||
- PYTHON: "C:\\Python33-x64"
|
||||
PYTHON_VERSION: "3.3.x" # currently 3.3.5
|
||||
PYTHON_ARCH: "64"
|
||||
TESTENV: "py33"
|
||||
|
||||
- PYTHON: "C:\\Python34"
|
||||
PYTHON_VERSION: "3.4.x" # currently 3.4.3
|
||||
PYTHON_ARCH: "32"
|
||||
TESTENV: "py34"
|
||||
|
||||
- PYTHON: "C:\\Python34-x64"
|
||||
PYTHON_VERSION: "3.4.x" # currently 3.4.3
|
||||
PYTHON_ARCH: "64"
|
||||
TESTENV: "py34"
|
||||
|
||||
- PYTHON: "C:\\Python35"
|
||||
PYTHON_VERSION: "3.5.x" # currently 3.5.0
|
||||
PYTHON_ARCH: "32"
|
||||
TESTENV: "py35"
|
||||
|
||||
- PYTHON: "C:\\Python35-x64"
|
||||
PYTHON_VERSION: "3.5.x" # currently 3.5.0
|
||||
PYTHON_ARCH: "64"
|
||||
TESTENV: "py35"
|
||||
|
||||
# Also test a Python version not pre-installed
|
||||
# See: https://github.com/ogrisel/python-appveyor-demo/issues/10
|
||||
|
||||
- PYTHON: "C:\\Python266"
|
||||
PYTHON_VERSION: "2.6.6"
|
||||
PYTHON_ARCH: "32"
|
||||
TESTENV: "py26"
|
||||
|
||||
# xdist testing
|
||||
|
||||
- PYTHON: "C:\\Python27"
|
||||
PYTHON_VERSION: "2.7.x" # currently 2.7.9
|
||||
PYTHON_ARCH: "32"
|
||||
TESTENV: "py27-xdist"
|
||||
|
||||
- PYTHON: "C:\\Python35"
|
||||
PYTHON_VERSION: "3.5.x" # currently 3.5.0
|
||||
PYTHON_ARCH: "32"
|
||||
TESTENV: "py35-xdist"
|
||||
|
||||
# coveralls is not in the default env list
|
||||
- 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"
|
||||
- TOXENV: "pypy"
|
||||
- TOXENV: "py27-pexpect"
|
||||
- TOXENV: "py27-xdist"
|
||||
- TOXENV: "py27-trial"
|
||||
- TOXENV: "py35-pexpect"
|
||||
- TOXENV: "py35-xdist"
|
||||
- TOXENV: "py35-trial"
|
||||
- TOXENV: "py27-nobyte"
|
||||
- TOXENV: "doctesting"
|
||||
- TOXENV: "freeze"
|
||||
- TOXENV: "docs"
|
||||
|
||||
install:
|
||||
- ECHO "Filesystem root:"
|
||||
- ps: "ls \"C:/\""
|
||||
- echo Installed Pythons
|
||||
- dir c:\Python*
|
||||
|
||||
- ECHO "Installed SDKs:"
|
||||
- ps: "ls \"C:/Program Files/Microsoft SDKs/Windows\""
|
||||
- if "%TOXENV%" == "pypy" call scripts\install-pypy.bat
|
||||
|
||||
# Install Python (from the official .msi of http://python.org) and pip when
|
||||
# not already installed.
|
||||
- ps: if (-not(Test-Path($env:PYTHON))) { & appveyor\install.ps1 }
|
||||
|
||||
# Prepend newly installed Python to the PATH of this build (this cannot be
|
||||
# done from inside the powershell script as it would require to restart
|
||||
# the parent CMD process).
|
||||
- "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%"
|
||||
|
||||
# Check that we have the expected version and architecture for Python
|
||||
- "python --version"
|
||||
- "python -c \"import struct; print(struct.calcsize('P') * 8)\""
|
||||
|
||||
# Install the build dependencies of the project. If some dependencies contain
|
||||
# compiled extensions and are not provided as pre-built wheel packages,
|
||||
# pip will build them from source using the MSVC compiler matching the
|
||||
# target Python version and architecture
|
||||
- C:\Python27\python -m pip install tox
|
||||
- C:\Python35\python -m pip install tox
|
||||
|
||||
build: false # Not a C# project, build stuff at the test step instead.
|
||||
|
||||
test_script:
|
||||
# Build the compiled extension and run the project tests
|
||||
- C:\Python27\python -m tox -e %TESTENV%
|
||||
- call scripts\call-tox.bat
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
# Sample script to install Python and pip under Windows
|
||||
# Authors: Olivier Grisel, Jonathan Helmus and Kyle Kastner
|
||||
# License: CC0 1.0 Universal: http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
$MINICONDA_URL = "http://repo.continuum.io/miniconda/"
|
||||
$BASE_URL = "https://www.python.org/ftp/python/"
|
||||
$GET_PIP_URL = "https://bootstrap.pypa.io/get-pip.py"
|
||||
$GET_PIP_PATH = "C:\get-pip.py"
|
||||
|
||||
|
||||
function DownloadPython ($python_version, $platform_suffix) {
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
$filename = "python-" + $python_version + $platform_suffix + ".msi"
|
||||
$url = $BASE_URL + $python_version + "/" + $filename
|
||||
|
||||
$basedir = $pwd.Path + "\"
|
||||
$filepath = $basedir + $filename
|
||||
if (Test-Path $filename) {
|
||||
Write-Host "Reusing" $filepath
|
||||
return $filepath
|
||||
}
|
||||
|
||||
# Download and retry up to 3 times in case of network transient errors.
|
||||
Write-Host "Downloading" $filename "from" $url
|
||||
$retry_attempts = 2
|
||||
for($i=0; $i -lt $retry_attempts; $i++){
|
||||
try {
|
||||
$webclient.DownloadFile($url, $filepath)
|
||||
break
|
||||
}
|
||||
Catch [Exception]{
|
||||
Start-Sleep 1
|
||||
}
|
||||
}
|
||||
if (Test-Path $filepath) {
|
||||
Write-Host "File saved at" $filepath
|
||||
} else {
|
||||
# Retry once to get the error message if any at the last try
|
||||
$webclient.DownloadFile($url, $filepath)
|
||||
}
|
||||
return $filepath
|
||||
}
|
||||
|
||||
|
||||
function InstallPython ($python_version, $architecture, $python_home) {
|
||||
Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home
|
||||
if (Test-Path $python_home) {
|
||||
Write-Host $python_home "already exists, skipping."
|
||||
return $false
|
||||
}
|
||||
if ($architecture -eq "32") {
|
||||
$platform_suffix = ""
|
||||
} else {
|
||||
$platform_suffix = ".amd64"
|
||||
}
|
||||
$msipath = DownloadPython $python_version $platform_suffix
|
||||
Write-Host "Installing" $msipath "to" $python_home
|
||||
$install_log = $python_home + ".log"
|
||||
$install_args = "/qn /log $install_log /i $msipath TARGETDIR=$python_home"
|
||||
$uninstall_args = "/qn /x $msipath"
|
||||
RunCommand "msiexec.exe" $install_args
|
||||
if (-not(Test-Path $python_home)) {
|
||||
Write-Host "Python seems to be installed else-where, reinstalling."
|
||||
RunCommand "msiexec.exe" $uninstall_args
|
||||
RunCommand "msiexec.exe" $install_args
|
||||
}
|
||||
if (Test-Path $python_home) {
|
||||
Write-Host "Python $python_version ($architecture) installation complete"
|
||||
} else {
|
||||
Write-Host "Failed to install Python in $python_home"
|
||||
Get-Content -Path $install_log
|
||||
Exit 1
|
||||
}
|
||||
}
|
||||
|
||||
function RunCommand ($command, $command_args) {
|
||||
Write-Host $command $command_args
|
||||
Start-Process -FilePath $command -ArgumentList $command_args -Wait -Passthru
|
||||
}
|
||||
|
||||
|
||||
function InstallPip ($python_home) {
|
||||
$pip_path = $python_home + "\Scripts\pip.exe"
|
||||
$python_path = $python_home + "\python.exe"
|
||||
if (-not(Test-Path $pip_path)) {
|
||||
Write-Host "Installing pip..."
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
$webclient.DownloadFile($GET_PIP_URL, $GET_PIP_PATH)
|
||||
Write-Host "Executing:" $python_path $GET_PIP_PATH
|
||||
Start-Process -FilePath "$python_path" -ArgumentList "$GET_PIP_PATH" -Wait -Passthru
|
||||
} else {
|
||||
Write-Host "pip already installed."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function DownloadMiniconda ($python_version, $platform_suffix) {
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($python_version -eq "3.4") {
|
||||
$filename = "Miniconda3-3.5.5-Windows-" + $platform_suffix + ".exe"
|
||||
} else {
|
||||
$filename = "Miniconda-3.5.5-Windows-" + $platform_suffix + ".exe"
|
||||
}
|
||||
$url = $MINICONDA_URL + $filename
|
||||
|
||||
$basedir = $pwd.Path + "\"
|
||||
$filepath = $basedir + $filename
|
||||
if (Test-Path $filename) {
|
||||
Write-Host "Reusing" $filepath
|
||||
return $filepath
|
||||
}
|
||||
|
||||
# Download and retry up to 3 times in case of network transient errors.
|
||||
Write-Host "Downloading" $filename "from" $url
|
||||
$retry_attempts = 2
|
||||
for($i=0; $i -lt $retry_attempts; $i++){
|
||||
try {
|
||||
$webclient.DownloadFile($url, $filepath)
|
||||
break
|
||||
}
|
||||
Catch [Exception]{
|
||||
Start-Sleep 1
|
||||
}
|
||||
}
|
||||
if (Test-Path $filepath) {
|
||||
Write-Host "File saved at" $filepath
|
||||
} else {
|
||||
# Retry once to get the error message if any at the last try
|
||||
$webclient.DownloadFile($url, $filepath)
|
||||
}
|
||||
return $filepath
|
||||
}
|
||||
|
||||
|
||||
function InstallMiniconda ($python_version, $architecture, $python_home) {
|
||||
Write-Host "Installing Python" $python_version "for" $architecture "bit architecture to" $python_home
|
||||
if (Test-Path $python_home) {
|
||||
Write-Host $python_home "already exists, skipping."
|
||||
return $false
|
||||
}
|
||||
if ($architecture -eq "32") {
|
||||
$platform_suffix = "x86"
|
||||
} else {
|
||||
$platform_suffix = "x86_64"
|
||||
}
|
||||
$filepath = DownloadMiniconda $python_version $platform_suffix
|
||||
Write-Host "Installing" $filepath "to" $python_home
|
||||
$install_log = $python_home + ".log"
|
||||
$args = "/S /D=$python_home"
|
||||
Write-Host $filepath $args
|
||||
Start-Process -FilePath $filepath -ArgumentList $args -Wait -Passthru
|
||||
if (Test-Path $python_home) {
|
||||
Write-Host "Python $python_version ($architecture) installation complete"
|
||||
} else {
|
||||
Write-Host "Failed to install Python in $python_home"
|
||||
Get-Content -Path $install_log
|
||||
Exit 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function InstallMinicondaPip ($python_home) {
|
||||
$pip_path = $python_home + "\Scripts\pip.exe"
|
||||
$conda_path = $python_home + "\Scripts\conda.exe"
|
||||
if (-not(Test-Path $pip_path)) {
|
||||
Write-Host "Installing pip..."
|
||||
$args = "install --yes pip"
|
||||
Write-Host $conda_path $args
|
||||
Start-Process -FilePath "$conda_path" -ArgumentList $args -Wait -Passthru
|
||||
} else {
|
||||
Write-Host "pip already installed."
|
||||
}
|
||||
}
|
||||
|
||||
function main () {
|
||||
InstallPython $env:PYTHON_VERSION $env:PYTHON_ARCH $env:PYTHON
|
||||
InstallPip $env:PYTHON
|
||||
}
|
||||
|
||||
main
|
||||
@@ -19,10 +19,9 @@ REGENDOC_ARGS := \
|
||||
--normalize "@/tmp/pytest-of-.*/pytest-\d+@PYTEST_TMPDIR@" \
|
||||
|
||||
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
|
||||
|
||||
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@@ -36,22 +35,6 @@ help:
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
SITETARGET=$(shell ./_getdoctarget.py)
|
||||
|
||||
showtarget:
|
||||
@echo $(SITETARGET)
|
||||
|
||||
install: html
|
||||
# for access talk to someone with login rights to
|
||||
# pytest-dev@pytest.org to add your ssh key
|
||||
rsync -avz _build/html/ pytest-dev@pytest.org:pytest.org/$(SITETARGET)
|
||||
|
||||
installpdf: latexpdf
|
||||
@scp $(BUILDDIR)/latex/pytest.pdf pytest-dev@pytest.org:pytest.org/$(SITETARGET)
|
||||
|
||||
installall: clean install installpdf
|
||||
@echo "done"
|
||||
|
||||
regen:
|
||||
PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.rst */*.rst ${REGENDOC_ARGS}
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import py
|
||||
|
||||
def get_version_string():
|
||||
fn = py.path.local(__file__).join("..", "..", "..",
|
||||
"_pytest", "__init__.py")
|
||||
for line in fn.readlines():
|
||||
if "version" in line and not line.strip().startswith('#'):
|
||||
return eval(line.split("=")[-1])
|
||||
|
||||
def get_minor_version_string():
|
||||
return ".".join(get_version_string().split(".")[:2])
|
||||
|
||||
if __name__ == "__main__":
|
||||
print (get_minor_version_string())
|
||||
@@ -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('license') }}">License</a></li>
|
||||
</ul>
|
||||
|
||||
{%- if display_toc %}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
{% extends "!layout.html" %}
|
||||
|
||||
{% block header %}
|
||||
{{super()}}
|
||||
{% endblock %}
|
||||
{% block footer %}
|
||||
{{ super() }}
|
||||
<script type="text/javascript">
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
<li><a href="{{ pathto('contributing') }}">Contribution Guide</a></li>
|
||||
<li><a href="https://pypi.python.org/pypi/pytest">pytest @ PyPI</a></li>
|
||||
<li><a href="https://github.com/pytest-dev/pytest/">pytest @ GitHub</a></li>
|
||||
<li><a href="http://pytest.org/latest/plugins_index/index.html">3rd party plugins</a></li>
|
||||
<li><a href="http://plugincompat.herokuapp.com/">3rd party plugins</a></li>
|
||||
<li><a href="https://github.com/pytest-dev/pytest/issues">Issue Tracker</a></li>
|
||||
<li><a href="http://pytest.org/latest/pytest.pdf">PDF Documentation</a>
|
||||
<li><a href="https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf">PDF Documentation</a>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
:orphan:
|
||||
|
||||
.. warnings about this file not being included in any toctree will be suppressed by :orphan:
|
||||
|
||||
|
||||
April 2015 is "adopt pytest month"
|
||||
=============================================
|
||||
|
||||
@@ -5,6 +5,23 @@ Release announcements
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
|
||||
release-3.0.7
|
||||
release-3.0.6
|
||||
release-3.0.5
|
||||
release-3.0.4
|
||||
release-3.0.3
|
||||
release-3.0.2
|
||||
release-3.0.1
|
||||
release-3.0.0
|
||||
sprint2016
|
||||
release-2.9.2
|
||||
release-2.9.1
|
||||
release-2.9.0
|
||||
release-2.8.7
|
||||
release-2.8.6
|
||||
release-2.8.5
|
||||
release-2.8.4
|
||||
release-2.8.3
|
||||
release-2.8.2
|
||||
release-2.7.2
|
||||
@@ -38,4 +55,3 @@ Release announcements
|
||||
release-2.0.2
|
||||
release-2.0.1
|
||||
release-2.0.0
|
||||
|
||||
|
||||
@@ -63,9 +63,9 @@ Changes between 2.0.1 and 2.0.2
|
||||
this.
|
||||
|
||||
- fixed typos in the docs (thanks Victor Garcia, Brianna Laugher) and particular
|
||||
thanks to Laura Creighton who also revieved parts of the documentation.
|
||||
thanks to Laura Creighton who also reviewed parts of the documentation.
|
||||
|
||||
- fix slighly wrong output of verbose progress reporting for classes
|
||||
- fix slightly wrong output of verbose progress reporting for classes
|
||||
(thanks Amaury)
|
||||
|
||||
- more precise (avoiding of) deprecation warnings for node.Class|Function accesses
|
||||
|
||||
@@ -13,7 +13,7 @@ If you want to install or upgrade pytest, just type one of::
|
||||
easy_install -U pytest
|
||||
|
||||
There also is a bugfix release 1.6 of pytest-xdist, the plugin
|
||||
that enables seemless distributed and "looponfail" testing for Python.
|
||||
that enables seamless distributed and "looponfail" testing for Python.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
@@ -33,7 +33,7 @@ Changes between 2.0.2 and 2.0.3
|
||||
- don't require zlib (and other libs) for genscript plugin without
|
||||
--genscript actually being used.
|
||||
|
||||
- speed up skips (by not doing a full traceback represenation
|
||||
- speed up skips (by not doing a full traceback representation
|
||||
internally)
|
||||
|
||||
- fix issue37: avoid invalid characters in junitxml's output
|
||||
|
||||
@@ -2,7 +2,7 @@ pytest-2.2.1: bug fixes, perfect teardowns
|
||||
===========================================================================
|
||||
|
||||
|
||||
pytest-2.2.1 is a minor backward-compatible release of the the py.test
|
||||
pytest-2.2.1 is a minor backward-compatible release of the py.test
|
||||
testing tool. It contains bug fixes and little improvements, including
|
||||
documentation fixes. If you are using the distributed testing
|
||||
pluginmake sure to upgrade it to pytest-xdist-1.8.
|
||||
|
||||
@@ -29,7 +29,7 @@ Changes between 2.2.3 and 2.2.4
|
||||
- fix issue with unittest: now @unittest.expectedFailure markers should
|
||||
be processed correctly (you can also use @pytest.mark markers)
|
||||
- document integration with the extended distribute/setuptools test commands
|
||||
- fix issue 140: propperly get the real functions
|
||||
- fix issue 140: properly get the real functions
|
||||
of bound classmethods for setup/teardown_class
|
||||
- fix issue #141: switch from the deceased paste.pocoo.org to bpaste.net
|
||||
- fix issue #143: call unconfigure/sessionfinish always when
|
||||
|
||||
@@ -89,7 +89,7 @@ Changes between 2.2.4 and 2.3.0
|
||||
|
||||
- fix issue128: show captured output when capsys/capfd are used
|
||||
|
||||
- fix issue179: propperly show the dependency chain of factories
|
||||
- fix issue179: properly show the dependency chain of factories
|
||||
|
||||
- pluginmanager.register(...) now raises ValueError if the
|
||||
plugin has been already registered or the name is taken
|
||||
@@ -130,5 +130,5 @@ Changes between 2.2.4 and 2.3.0
|
||||
|
||||
- don't show deselected reason line if there is none
|
||||
|
||||
- py.test -vv will show all of assert comparisations instead of truncating
|
||||
- py.test -vv will show all of assert comparisons instead of truncating
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pytest-2.3.2: some fixes and more traceback-printing speed
|
||||
===========================================================================
|
||||
|
||||
pytest-2.3.2 is a another stabilization release:
|
||||
pytest-2.3.2 is another stabilization release:
|
||||
|
||||
- issue 205: fixes a regression with conftest detection
|
||||
- issue 208/29: fixes traceback-printing speed in some bad cases
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pytest-2.3.3: integration fixes, py24 suport, ``*/**`` shown in traceback
|
||||
pytest-2.3.3: integration fixes, py24 support, ``*/**`` shown in traceback
|
||||
===========================================================================
|
||||
|
||||
pytest-2.3.3 is a another stabilization release of the py.test tool
|
||||
pytest-2.3.3 is another stabilization release of the py.test tool
|
||||
which offers uebersimple assertions, scalable fixture mechanisms
|
||||
and deep customization for testing with Python. Particularly,
|
||||
this release provides:
|
||||
@@ -46,7 +46,7 @@ Changes between 2.3.2 and 2.3.3
|
||||
- fix issue209 - reintroduce python2.4 support by depending on newer
|
||||
pylib which re-introduced statement-finding for pre-AST interpreters
|
||||
|
||||
- nose support: only call setup if its a callable, thanks Andrew
|
||||
- nose support: only call setup if it's a callable, thanks Andrew
|
||||
Taumoefolau
|
||||
|
||||
- fix issue219 - add py2.4-3.3 classifiers to TROVE list
|
||||
|
||||
@@ -44,11 +44,11 @@ Changes between 2.3.4 and 2.3.5
|
||||
(thanks Adam Goucher)
|
||||
|
||||
- Issue 265 - integrate nose setup/teardown with setupstate
|
||||
so it doesnt try to teardown if it did not setup
|
||||
so it doesn't try to teardown if it did not setup
|
||||
|
||||
- issue 271 - dont write junitxml on slave nodes
|
||||
- issue 271 - don't write junitxml on slave nodes
|
||||
|
||||
- Issue 274 - dont try to show full doctest example
|
||||
- Issue 274 - don't try to show full doctest example
|
||||
when doctest does not know the example location
|
||||
|
||||
- issue 280 - disable assertion rewriting on buggy CPython 2.6.0
|
||||
@@ -84,7 +84,7 @@ Changes between 2.3.4 and 2.3.5
|
||||
- allow to specify prefixes starting with "_" when
|
||||
customizing python_functions test discovery. (thanks Graham Horler)
|
||||
|
||||
- improve PYTEST_DEBUG tracing output by puting
|
||||
- improve PYTEST_DEBUG tracing output by putting
|
||||
extra data on a new lines with additional indent
|
||||
|
||||
- ensure OutcomeExceptions like skip/fail have initialized exception attributes
|
||||
|
||||
@@ -36,7 +36,7 @@ a full list of details. A few feature highlights:
|
||||
- reporting: color the last line red or green depending if
|
||||
failures/errors occurred or everything passed.
|
||||
|
||||
The documentation has been updated to accomodate the changes,
|
||||
The documentation has been updated to accommodate the changes,
|
||||
see `http://pytest.org <http://pytest.org>`_
|
||||
|
||||
To install or upgrade pytest::
|
||||
@@ -118,7 +118,7 @@ new features:
|
||||
|
||||
- fix issue322: tearDownClass is not run if setUpClass failed. Thanks
|
||||
Mathieu Agopian for the initial fix. Also make all of pytest/nose
|
||||
finalizer mimick the same generic behaviour: if a setupX exists and
|
||||
finalizer mimic the same generic behaviour: if a setupX exists and
|
||||
fails, don't run teardownX. This internally introduces a new method
|
||||
"node.addfinalizer()" helper which can only be called during the setup
|
||||
phase of a node.
|
||||
|
||||
@@ -70,7 +70,7 @@ holger krekel
|
||||
to problems for more than >966 non-function scoped parameters).
|
||||
|
||||
- fix issue290 - there is preliminary support now for parametrizing
|
||||
with repeated same values (sometimes useful to to test if calling
|
||||
with repeated same values (sometimes useful to test if calling
|
||||
a second time works as with the first time).
|
||||
|
||||
- close issue240 - document precisely how pytest module importing
|
||||
@@ -149,7 +149,7 @@ holger krekel
|
||||
|
||||
would not work correctly because pytest assumes @pytest.mark.some
|
||||
gets a function to be decorated already. We now at least detect if this
|
||||
arg is an lambda and thus the example will work. Thanks Alex Gaynor
|
||||
arg is a lambda and thus the example will work. Thanks Alex Gaynor
|
||||
for bringing it up.
|
||||
|
||||
- xfail a test on pypy that checks wrong encoding/ascii (pypy does
|
||||
|
||||
@@ -60,5 +60,5 @@ holger krekel
|
||||
- fix issue429: comparing byte strings with non-ascii chars in assert
|
||||
expressions now work better. Thanks Floris Bruynooghe.
|
||||
|
||||
- make capfd/capsys.capture private, its unused and shouldnt be exposed
|
||||
- make capfd/capsys.capture private, its unused and shouldn't be exposed
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ Changes 2.6.3
|
||||
dep). Thanks Charles Cloud for analysing the issue.
|
||||
|
||||
- fix conftest related fixture visibility issue: when running with a
|
||||
CWD outside a test package pytest would get fixture discovery wrong.
|
||||
Thanks to Wolfgang Schnerring for figuring out a reproducable example.
|
||||
CWD outside of a test package pytest would get fixture discovery wrong.
|
||||
Thanks to Wolfgang Schnerring for figuring out a reproducible example.
|
||||
|
||||
- Introduce pytest_enter_pdb hook (needed e.g. by pytest_timeout to cancel the
|
||||
timeout when interactively entering pdb). Thanks Wolfgang Schnerring.
|
||||
|
||||
@@ -32,7 +32,7 @@ The py.test Development Team
|
||||
explanations. Thanks Carl Meyer for the report and test case.
|
||||
|
||||
- fix issue553: properly handling inspect.getsourcelines failures in
|
||||
FixtureLookupError which would lead to to an internal error,
|
||||
FixtureLookupError which would lead to an internal error,
|
||||
obfuscating the original problem. Thanks talljosh for initial
|
||||
diagnose/patch and Bruno Oliveira for final patch.
|
||||
|
||||
|
||||
52
doc/en/announce/release-2.8.4.rst
Normal file
52
doc/en/announce/release-2.8.4.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
pytest-2.8.4
|
||||
============
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
This release is supposed to be drop-in compatible to 2.8.2.
|
||||
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
Bruno Oliveira
|
||||
Florian Bruhin
|
||||
Jeff Widman
|
||||
Mehdy Khoshnoody
|
||||
Nicholas Chammas
|
||||
Ronny Pfannschmidt
|
||||
Tim Chan
|
||||
|
||||
|
||||
Happy testing,
|
||||
The py.test Development Team
|
||||
|
||||
|
||||
2.8.4 (compared to 2.8.3)
|
||||
-----------------------------
|
||||
|
||||
- fix #1190: ``deprecated_call()`` now works when the deprecated
|
||||
function has been already called by another test in the same
|
||||
module. Thanks Mikhail Chernykh for the report and Bruno Oliveira for the
|
||||
PR.
|
||||
|
||||
- fix #1198: ``--pastebin`` option now works on Python 3. Thanks
|
||||
Mehdy Khoshnoody for the PR.
|
||||
|
||||
- fix #1219: ``--pastebin`` now works correctly when captured output contains
|
||||
non-ascii characters. Thanks Bruno Oliveira for the PR.
|
||||
|
||||
- fix #1204: another error when collecting with a nasty __getattr__().
|
||||
Thanks Florian Bruhin for the PR.
|
||||
|
||||
- fix the summary printed when no tests did run.
|
||||
Thanks Florian Bruhin for the PR.
|
||||
|
||||
- a number of documentation modernizations wrt good practices.
|
||||
Thanks Bruno Oliveira for the PR.
|
||||
39
doc/en/announce/release-2.8.5.rst
Normal file
39
doc/en/announce/release-2.8.5.rst
Normal file
@@ -0,0 +1,39 @@
|
||||
pytest-2.8.5
|
||||
============
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
This release is supposed to be drop-in compatible to 2.8.4.
|
||||
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://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 Gaynor
|
||||
aselus-hub
|
||||
Bruno Oliveira
|
||||
Ronny Pfannschmidt
|
||||
|
||||
|
||||
Happy testing,
|
||||
The py.test Development Team
|
||||
|
||||
|
||||
2.8.5 (compared to 2.8.4)
|
||||
-------------------------
|
||||
|
||||
- fix #1243: fixed issue where class attributes injected during collection could break pytest.
|
||||
PR by Alexei Kozlenok, thanks Ronny Pfannschmidt and Bruno Oliveira for the review and help.
|
||||
|
||||
- fix #1074: precompute junitxml chunks instead of storing the whole tree in objects
|
||||
Thanks Bruno Oliveira for the report and Ronny Pfannschmidt for the PR
|
||||
|
||||
- fix #1238: fix ``pytest.deprecated_call()`` receiving multiple arguments
|
||||
(Regression introduced in 2.8.4). Thanks Alex Gaynor for the report and
|
||||
Bruno Oliveira for the PR.
|
||||
67
doc/en/announce/release-2.8.6.rst
Normal file
67
doc/en/announce/release-2.8.6.rst
Normal file
@@ -0,0 +1,67 @@
|
||||
pytest-2.8.6
|
||||
============
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
This release is supposed to be drop-in compatible to 2.8.5.
|
||||
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
AMiT Kumar
|
||||
Bruno Oliveira
|
||||
Erik M. Bray
|
||||
Florian Bruhin
|
||||
Georgy Dyuldin
|
||||
Jeff Widman
|
||||
Kartik Singhal
|
||||
Loïc Estève
|
||||
Manu Phatak
|
||||
Peter Demin
|
||||
Rick van Hattem
|
||||
Ronny Pfannschmidt
|
||||
Ulrich Petri
|
||||
foxx
|
||||
|
||||
|
||||
Happy testing,
|
||||
The py.test Development Team
|
||||
|
||||
|
||||
2.8.6 (compared to 2.8.5)
|
||||
-------------------------
|
||||
|
||||
- fix #1259: allow for double nodeids in junitxml,
|
||||
this was a regression failing plugins combinations
|
||||
like pytest-pep8 + pytest-flakes
|
||||
|
||||
- Workaround for exception that occurs in pyreadline when using
|
||||
``--pdb`` with standard I/O capture enabled.
|
||||
Thanks Erik M. Bray for the PR.
|
||||
|
||||
- fix #900: Better error message in case the target of a ``monkeypatch`` call
|
||||
raises an ``ImportError``.
|
||||
|
||||
- fix #1292: monkeypatch calls (setattr, setenv, etc.) are now O(1).
|
||||
Thanks David R. MacIver for the report and Bruno Oliveira for the PR.
|
||||
|
||||
- fix #1223: captured stdout and stderr are now properly displayed before
|
||||
entering pdb when ``--pdb`` is used instead of being thrown away.
|
||||
Thanks Cal Leeming for the PR.
|
||||
|
||||
- fix #1305: pytest warnings emitted during ``pytest_terminal_summary`` are now
|
||||
properly displayed.
|
||||
Thanks Ionel Maries Cristian for the report and Bruno Oliveira for the PR.
|
||||
|
||||
- fix #628: fixed internal UnicodeDecodeError when doctests contain unicode.
|
||||
Thanks Jason R. Coombs for the report and Bruno Oliveira for the PR.
|
||||
|
||||
- fix #1334: Add captured stdout to jUnit XML report on setup error.
|
||||
Thanks Georgy Dyuldin for the PR.
|
||||
31
doc/en/announce/release-2.8.7.rst
Normal file
31
doc/en/announce/release-2.8.7.rst
Normal file
@@ -0,0 +1,31 @@
|
||||
pytest-2.8.7
|
||||
============
|
||||
|
||||
This is a hotfix release to solve a regression
|
||||
in the builtin monkeypatch plugin that got introduced in 2.8.6.
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
This release is supposed to be drop-in compatible to 2.8.5.
|
||||
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
Ronny Pfannschmidt
|
||||
|
||||
|
||||
Happy testing,
|
||||
The py.test Development Team
|
||||
|
||||
|
||||
2.8.7 (compared to 2.8.6)
|
||||
-------------------------
|
||||
|
||||
- fix #1338: use predictable object resolution for monkeypatch
|
||||
159
doc/en/announce/release-2.9.0.rst
Normal file
159
doc/en/announce/release-2.9.0.rst
Normal file
@@ -0,0 +1,159 @@
|
||||
pytest-2.9.0
|
||||
============
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
Anatoly Bubenkov
|
||||
Bruno Oliveira
|
||||
Buck Golemon
|
||||
David Vierra
|
||||
Florian Bruhin
|
||||
Galaczi Endre
|
||||
Georgy Dyuldin
|
||||
Lukas Bednar
|
||||
Luke Murphy
|
||||
Marcin Biernat
|
||||
Matt Williams
|
||||
Michael Aquilina
|
||||
Raphael Pierzina
|
||||
Ronny Pfannschmidt
|
||||
Ryan Wooden
|
||||
Tiemo Kieft
|
||||
TomV
|
||||
holger krekel
|
||||
jab
|
||||
|
||||
|
||||
Happy testing,
|
||||
The py.test Development Team
|
||||
|
||||
|
||||
2.9.0 (compared to 2.8.7)
|
||||
-------------------------
|
||||
|
||||
**New Features**
|
||||
|
||||
* New ``pytest.mark.skip`` mark, which unconditionally skips marked tests.
|
||||
Thanks `@MichaelAquilina`_ for the complete PR (`#1040`_).
|
||||
|
||||
* ``--doctest-glob`` may now be passed multiple times in the command-line.
|
||||
Thanks `@jab`_ and `@nicoddemus`_ for the PR.
|
||||
|
||||
* New ``-rp`` and ``-rP`` reporting options give the summary and full output
|
||||
of passing tests, respectively. Thanks to `@codewarrior0`_ for the PR.
|
||||
|
||||
* ``pytest.mark.xfail`` now has a ``strict`` option which makes ``XPASS``
|
||||
tests to fail the test suite, defaulting to ``False``. There's also a
|
||||
``xfail_strict`` ini option that can be used to configure it project-wise.
|
||||
Thanks `@rabbbit`_ for the request and `@nicoddemus`_ for the PR (`#1355`_).
|
||||
|
||||
* ``Parser.addini`` now supports options of type ``bool``. Thanks
|
||||
`@nicoddemus`_ for the PR.
|
||||
|
||||
* New ``ALLOW_BYTES`` doctest option strips ``b`` prefixes from byte strings
|
||||
in doctest output (similar to ``ALLOW_UNICODE``).
|
||||
Thanks `@jaraco`_ for the request and `@nicoddemus`_ for the PR (`#1287`_).
|
||||
|
||||
* give a hint on KeyboardInterrupt to use the --fulltrace option to show the errors,
|
||||
this fixes `#1366`_.
|
||||
Thanks to `@hpk42`_ for the report and `@RonnyPfannschmidt`_ for the PR.
|
||||
|
||||
* catch IndexError exceptions when getting exception source location. This fixes
|
||||
pytest internal error for dynamically generated code (fixtures and tests)
|
||||
where source lines are fake by intention
|
||||
|
||||
**Changes**
|
||||
|
||||
* **Important**: `py.code <https://pylib.readthedocs.io/en/latest/code.html>`_ has been
|
||||
merged into the ``pytest`` repository as ``pytest._code``. This decision
|
||||
was made because ``py.code`` had very few uses outside ``pytest`` and the
|
||||
fact that it was in a different repository made it difficult to fix bugs on
|
||||
its code in a timely manner. The team hopes with this to be able to better
|
||||
refactor out and improve that code.
|
||||
This change shouldn't affect users, but it is useful to let users aware
|
||||
if they encounter any strange behavior.
|
||||
|
||||
Keep in mind that the code for ``pytest._code`` is **private** and
|
||||
**experimental**, so you definitely should not import it explicitly!
|
||||
|
||||
Please note that the original ``py.code`` is still available in
|
||||
`pylib <https://pylib.readthedocs.io>`_.
|
||||
|
||||
* ``pytest_enter_pdb`` now optionally receives the pytest config object.
|
||||
Thanks `@nicoddemus`_ for the PR.
|
||||
|
||||
* Removed code and documentation for Python 2.5 or lower versions,
|
||||
including removal of the obsolete ``_pytest.assertion.oldinterpret`` module.
|
||||
Thanks `@nicoddemus`_ for the PR (`#1226`_).
|
||||
|
||||
* Comparisons now always show up in full when ``CI`` or ``BUILD_NUMBER`` is
|
||||
found in the environment, even when -vv isn't used.
|
||||
Thanks `@The-Compiler`_ for the PR.
|
||||
|
||||
* ``--lf`` and ``--ff`` now support long names: ``--last-failed`` and
|
||||
``--failed-first`` respectively.
|
||||
Thanks `@MichaelAquilina`_ for the PR.
|
||||
|
||||
* Added expected exceptions to pytest.raises fail message
|
||||
|
||||
* Collection only displays progress ("collecting X items") when in a terminal.
|
||||
This avoids cluttering the output when using ``--color=yes`` to obtain
|
||||
colors in CI integrations systems (`#1397`_).
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
* The ``-s`` and ``-c`` options should now work under ``xdist``;
|
||||
``Config.fromdictargs`` now represents its input much more faithfully.
|
||||
Thanks to `@bukzor`_ for the complete PR (`#680`_).
|
||||
|
||||
* Fix (`#1290`_): support Python 3.5's ``@`` operator in assertion rewriting.
|
||||
Thanks `@Shinkenjoe`_ for report with test case and `@tomviner`_ for the PR.
|
||||
|
||||
* Fix formatting utf-8 explanation messages (`#1379`_).
|
||||
Thanks `@biern`_ for the PR.
|
||||
|
||||
* Fix `traceback style docs`_ to describe all of the available options
|
||||
(auto/long/short/line/native/no), with `auto` being the default since v2.6.
|
||||
Thanks `@hackebrot`_ for the PR.
|
||||
|
||||
* Fix (`#1422`_): junit record_xml_property doesn't allow multiple records
|
||||
with same name.
|
||||
|
||||
|
||||
.. _`traceback style docs`: https://pytest.org/latest/usage.html#modifying-python-traceback-printing
|
||||
|
||||
.. _#1422: https://github.com/pytest-dev/pytest/issues/1422
|
||||
.. _#1379: https://github.com/pytest-dev/pytest/issues/1379
|
||||
.. _#1366: https://github.com/pytest-dev/pytest/issues/1366
|
||||
.. _#1040: https://github.com/pytest-dev/pytest/pull/1040
|
||||
.. _#680: https://github.com/pytest-dev/pytest/issues/680
|
||||
.. _#1287: https://github.com/pytest-dev/pytest/pull/1287
|
||||
.. _#1226: https://github.com/pytest-dev/pytest/pull/1226
|
||||
.. _#1290: https://github.com/pytest-dev/pytest/pull/1290
|
||||
.. _#1355: https://github.com/pytest-dev/pytest/pull/1355
|
||||
.. _#1397: https://github.com/pytest-dev/pytest/issues/1397
|
||||
.. _@biern: https://github.com/biern
|
||||
.. _@MichaelAquilina: https://github.com/MichaelAquilina
|
||||
.. _@bukzor: https://github.com/bukzor
|
||||
.. _@hpk42: https://github.com/hpk42
|
||||
.. _@nicoddemus: https://github.com/nicoddemus
|
||||
.. _@jab: https://github.com/jab
|
||||
.. _@codewarrior0: https://github.com/codewarrior0
|
||||
.. _@jaraco: https://github.com/jaraco
|
||||
.. _@The-Compiler: https://github.com/The-Compiler
|
||||
.. _@Shinkenjoe: https://github.com/Shinkenjoe
|
||||
.. _@tomviner: https://github.com/tomviner
|
||||
.. _@RonnyPfannschmidt: https://github.com/RonnyPfannschmidt
|
||||
.. _@rabbbit: https://github.com/rabbbit
|
||||
.. _@hackebrot: https://github.com/hackebrot
|
||||
67
doc/en/announce/release-2.9.1.rst
Normal file
67
doc/en/announce/release-2.9.1.rst
Normal file
@@ -0,0 +1,67 @@
|
||||
pytest-2.9.1
|
||||
============
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
Bruno Oliveira
|
||||
Daniel Hahler
|
||||
Dmitry Malinovsky
|
||||
Florian Bruhin
|
||||
Floris Bruynooghe
|
||||
Matt Bachmann
|
||||
Ronny Pfannschmidt
|
||||
TomV
|
||||
Vladimir Bolshakov
|
||||
Zearin
|
||||
palaviv
|
||||
|
||||
|
||||
Happy testing,
|
||||
The py.test Development Team
|
||||
|
||||
|
||||
2.9.1 (compared to 2.9.0)
|
||||
-------------------------
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
* Improve error message when a plugin fails to load.
|
||||
Thanks `@nicoddemus`_ for the PR.
|
||||
|
||||
* Fix (`#1178 <https://github.com/pytest-dev/pytest/issues/1178>`_):
|
||||
``pytest.fail`` with non-ascii characters raises an internal pytest error.
|
||||
Thanks `@nicoddemus`_ for the PR.
|
||||
|
||||
* Fix (`#469`_): junit parses report.nodeid incorrectly, when params IDs
|
||||
contain ``::``. Thanks `@tomviner`_ for the PR (`#1431`_).
|
||||
|
||||
* Fix (`#578 <https://github.com/pytest-dev/pytest/issues/578>`_): SyntaxErrors
|
||||
containing non-ascii lines at the point of failure generated an internal
|
||||
py.test error.
|
||||
Thanks `@asottile`_ for the report and `@nicoddemus`_ for the PR.
|
||||
|
||||
* Fix (`#1437`_): When passing in a bytestring regex pattern to parameterize
|
||||
attempt to decode it as utf-8 ignoring errors.
|
||||
|
||||
* Fix (`#649`_): parametrized test nodes cannot be specified to run on the command line.
|
||||
|
||||
|
||||
.. _#1437: https://github.com/pytest-dev/pytest/issues/1437
|
||||
.. _#469: https://github.com/pytest-dev/pytest/issues/469
|
||||
.. _#1431: https://github.com/pytest-dev/pytest/pull/1431
|
||||
.. _#649: https://github.com/pytest-dev/pytest/issues/649
|
||||
|
||||
.. _@asottile: https://github.com/asottile
|
||||
.. _@nicoddemus: https://github.com/nicoddemus
|
||||
.. _@tomviner: https://github.com/tomviner
|
||||
78
doc/en/announce/release-2.9.2.rst
Normal file
78
doc/en/announce/release-2.9.2.rst
Normal file
@@ -0,0 +1,78 @@
|
||||
pytest-2.9.2
|
||||
============
|
||||
|
||||
pytest is a mature Python testing tool with more than a 1100 tests
|
||||
against itself, passing on many different interpreters and platforms.
|
||||
|
||||
See below for the changes and see docs at:
|
||||
|
||||
http://pytest.org
|
||||
|
||||
As usual, you can upgrade from pypi via::
|
||||
|
||||
pip install -U pytest
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
Adam Chainz
|
||||
Benjamin Dopplinger
|
||||
Bruno Oliveira
|
||||
Florian Bruhin
|
||||
John Towler
|
||||
Martin Prusse
|
||||
Meng Jue
|
||||
MengJueM
|
||||
Omar Kohl
|
||||
Quentin Pradet
|
||||
Ronny Pfannschmidt
|
||||
Thomas Güttler
|
||||
TomV
|
||||
Tyler Goodlet
|
||||
|
||||
|
||||
Happy testing,
|
||||
The py.test Development Team
|
||||
|
||||
|
||||
2.9.2 (compared to 2.9.1)
|
||||
---------------------------
|
||||
|
||||
**Bug Fixes**
|
||||
|
||||
* fix `#510`_: skip tests where one parameterize dimension was empty
|
||||
thanks Alex Stapleton for the Report and `@RonnyPfannschmidt`_ for the PR
|
||||
|
||||
* Fix Xfail does not work with condition keyword argument.
|
||||
Thanks `@astraw38`_ for reporting the issue (`#1496`_) and `@tomviner`_
|
||||
for PR the (`#1524`_).
|
||||
|
||||
* Fix win32 path issue when putting custom config file with absolute path
|
||||
in ``pytest.main("-c your_absolute_path")``.
|
||||
|
||||
* Fix maximum recursion depth detection when raised error class is not aware
|
||||
of unicode/encoded bytes.
|
||||
Thanks `@prusse-martin`_ for the PR (`#1506`_).
|
||||
|
||||
* Fix ``pytest.mark.skip`` mark when used in strict mode.
|
||||
Thanks `@pquentin`_ for the PR and `@RonnyPfannschmidt`_ for
|
||||
showing how to fix the bug.
|
||||
|
||||
* Minor improvements and fixes to the documentation.
|
||||
Thanks `@omarkohl`_ for the PR.
|
||||
|
||||
* Fix ``--fixtures`` to show all fixture definitions as opposed to just
|
||||
one per fixture name.
|
||||
Thanks to `@hackebrot`_ for the PR.
|
||||
|
||||
.. _#510: https://github.com/pytest-dev/pytest/issues/510
|
||||
.. _#1506: https://github.com/pytest-dev/pytest/pull/1506
|
||||
.. _#1496: https://github.com/pytest-dev/pytest/issue/1496
|
||||
.. _#1524: https://github.com/pytest-dev/pytest/issue/1524
|
||||
|
||||
.. _@astraw38: https://github.com/astraw38
|
||||
.. _@hackebrot: https://github.com/hackebrot
|
||||
.. _@omarkohl: https://github.com/omarkohl
|
||||
.. _@pquentin: https://github.com/pquentin
|
||||
.. _@prusse-martin: https://github.com/prusse-martin
|
||||
.. _@RonnyPfannschmidt: https://github.com/RonnyPfannschmidt
|
||||
.. _@tomviner: https://github.com/tomviner
|
||||
82
doc/en/announce/release-3.0.0.rst
Normal file
82
doc/en/announce/release-3.0.0.rst
Normal file
@@ -0,0 +1,82 @@
|
||||
pytest-3.0.0
|
||||
============
|
||||
|
||||
The pytest team is proud to announce the 3.0.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 lot of bugs fixes and improvements, and much of
|
||||
the work done on it was possible because of the 2016 Sprint[1], which
|
||||
was funded by an indiegogo campaign which raised over US$12,000 with
|
||||
nearly 100 backers.
|
||||
|
||||
There's a "What's new in pytest 3.0" [2] blog post highlighting the
|
||||
major features in this release.
|
||||
|
||||
To see the complete changelog and 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:
|
||||
|
||||
AbdealiJK
|
||||
Ana Ribeiro
|
||||
Antony Lee
|
||||
Brandon W Maister
|
||||
Brianna Laugher
|
||||
Bruno Oliveira
|
||||
Ceridwen
|
||||
Christian Boelsen
|
||||
Daniel Hahler
|
||||
Danielle Jenkins
|
||||
Dave Hunt
|
||||
Diego Russo
|
||||
Dmitry Dygalo
|
||||
Edoardo Batini
|
||||
Eli Boyarski
|
||||
Florian Bruhin
|
||||
Floris Bruynooghe
|
||||
Greg Price
|
||||
Guyzmo
|
||||
HEAD KANGAROO
|
||||
JJ
|
||||
Javi Romero
|
||||
Javier Domingo Cansino
|
||||
Kale Kundert
|
||||
Kalle Bronsen
|
||||
Marius Gedminas
|
||||
Matt Williams
|
||||
Mike Lundy
|
||||
Oliver Bestwalter
|
||||
Omar Kohl
|
||||
Raphael Pierzina
|
||||
RedBeardCode
|
||||
Roberto Polli
|
||||
Romain Dorgueil
|
||||
Roman Bolshakov
|
||||
Ronny Pfannschmidt
|
||||
Stefan Zimmermann
|
||||
Steffen Allner
|
||||
Tareq Alayan
|
||||
Ted Xiao
|
||||
Thomas Grainger
|
||||
Tom Viner
|
||||
TomV
|
||||
Vasily Kuznetsov
|
||||
aostr
|
||||
marscher
|
||||
palaviv
|
||||
satoru
|
||||
taschini
|
||||
|
||||
|
||||
Happy testing,
|
||||
The Pytest Development Team
|
||||
|
||||
[1] http://blog.pytest.org/2016/pytest-development-sprint/
|
||||
[2] http://blog.pytest.org/2016/whats-new-in-pytest-30/
|
||||
26
doc/en/announce/release-3.0.1.rst
Normal file
26
doc/en/announce/release-3.0.1.rst
Normal file
@@ -0,0 +1,26 @@
|
||||
pytest-3.0.1
|
||||
============
|
||||
|
||||
pytest 3.0.1 has just been released to PyPI.
|
||||
|
||||
This release fixes some regressions reported in version 3.0.0, being a
|
||||
drop-in replacement. To upgrade:
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
Adam Chainz
|
||||
Andrew Svetlov
|
||||
Bruno Oliveira
|
||||
Daniel Hahler
|
||||
Dmitry Dygalo
|
||||
Florian Bruhin
|
||||
Marcin Bachry
|
||||
Ronny Pfannschmidt
|
||||
matthiasha
|
||||
|
||||
Happy testing,
|
||||
The py.test Development Team
|
||||
24
doc/en/announce/release-3.0.2.rst
Normal file
24
doc/en/announce/release-3.0.2.rst
Normal file
@@ -0,0 +1,24 @@
|
||||
pytest-3.0.2
|
||||
============
|
||||
|
||||
pytest 3.0.2 has just been released to PyPI.
|
||||
|
||||
This release fixes some regressions and bugs reported in version 3.0.1, being a
|
||||
drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Ahn Ki-Wook
|
||||
* Bruno Oliveira
|
||||
* Florian Bruhin
|
||||
* Jordan Guymon
|
||||
* Raphael Pierzina
|
||||
* Ronny Pfannschmidt
|
||||
* mbyt
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
27
doc/en/announce/release-3.0.3.rst
Normal file
27
doc/en/announce/release-3.0.3.rst
Normal file
@@ -0,0 +1,27 @@
|
||||
pytest-3.0.3
|
||||
============
|
||||
|
||||
pytest 3.0.3 has just been released to PyPI.
|
||||
|
||||
This release fixes some regressions and bugs reported in the last version,
|
||||
being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The 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
|
||||
* Huayi Zhang
|
||||
* Lev Maximov
|
||||
* Raquel Alegre
|
||||
* Ronny Pfannschmidt
|
||||
* Roy Williams
|
||||
* Tyler Goodlet
|
||||
* mbyt
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
29
doc/en/announce/release-3.0.4.rst
Normal file
29
doc/en/announce/release-3.0.4.rst
Normal file
@@ -0,0 +1,29 @@
|
||||
pytest-3.0.4
|
||||
============
|
||||
|
||||
pytest 3.0.4 has just been released to PyPI.
|
||||
|
||||
This release fixes some regressions and bugs reported in the last version,
|
||||
being a drop-in replacement. To upgrade::
|
||||
|
||||
pip install --upgrade pytest
|
||||
|
||||
The changelog is available at http://doc.pytest.org/en/latest/changelog.html.
|
||||
|
||||
Thanks to all who contributed to this release, among them:
|
||||
|
||||
* Bruno Oliveira
|
||||
* Dan Wandschneider
|
||||
* Florian Bruhin
|
||||
* Georgy Dyuldin
|
||||
* Grigorii Eremeev
|
||||
* Jason R. Coombs
|
||||
* Manuel Jacob
|
||||
* Mathieu Clabaut
|
||||
* Michael Seifert
|
||||
* Nikolaus Rath
|
||||
* Ronny Pfannschmidt
|
||||
* Tom V
|
||||
|
||||
Happy testing,
|
||||
The pytest Development Team
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user